從我們知道的經(jīng)驗來看,大多都聽說過這些攻擊,但是很少幾個真的理解攻擊的具體機制,有些人有些模糊的印象,甚至有些人根本不知道越界攻擊是什么。還有些人認為這個屬于秘密的智慧和技能只有少數(shù)幾個專家才能掌握的。實際上,它只不過是由我們這些粗心的程序員制造的漏洞罷了。
C語言編寫的程序擁有高效的性能和很小的二進制代碼,卻最容易感染這種攻擊。事實上,在程序界,C語言以靈活和強大著稱,然而它也是諸多新手最頭痛的語言。它提供了基于直接指針的函數(shù)調(diào)用,這樣在一些文本字符串的處理庫上無法控制真正的內(nèi)存長度,因此容易導致內(nèi)存溢出訪問。
在介紹任何攻擊的機制之前,我們先熟悉一下幾個和程序執(zhí)行以及內(nèi)存管理切切相關(guān)的基本概念。
進程內(nèi)存空間
當一個程序被執(zhí)行的時候,它的各個編譯單元被映射到一個組織良好的內(nèi)存結(jié)構(gòu)上,如圖1所示:
圖. 1: 進程內(nèi)存空間
擴展:
text 段保護了基本的可執(zhí)行的程序代碼,data段包括了所有的全局變量,data段的長度在編譯的時候決定。在內(nèi)存空間的頂端是由stack和heap共享的地址段,他們都是在運行時分配。Stack用來保存函數(shù)調(diào)用的參數(shù),局部變量以及一些用來保存程序當前狀態(tài)的寄存器值。Heap分配給動態(tài)變量,比如malloc和new。
Stack用來干什么?
Stack是一個LIFO隊列(先進后出),由于stack是在函數(shù)的生命周期分配的,因此只有在此生命周期內(nèi)的變量存在在那,這一切的根源在于機構(gòu)化編程的本質(zhì),我們吧代碼分解為一個一個的函數(shù)代碼段。當程序在內(nèi)存里面運行的時候,它時而順序的調(diào)用函數(shù),時而從一個函數(shù)調(diào)用另外一個函數(shù),從而構(gòu)成了一個多層的調(diào)用鏈。當一個函數(shù)執(zhí)行完后。它需要去執(zhí)行緊接著它的下一個指令,當從一個函數(shù)調(diào)用另外一個函數(shù)的時候,它需要凍住(frozen)當前的變量狀態(tài),以便函數(shù)執(zhí)行完返回后恢復。Stack正好能實現(xiàn)這些需求。
函數(shù)調(diào)用
CPU順序執(zhí)行CPU的指令,使用一個擴展的EIP寄存器來維護執(zhí)行的順序。這個寄存器保存了下一個被執(zhí)行的指令地址。例如,運行一個jump或者call一個函數(shù),將會修改EIP寄存器。大家想如果把當前代碼的地址寫入EIP,會發(fā)生什么?
調(diào)用完該函數(shù)后需要執(zhí)行的下一個指令的地址叫返回地址(return address),當一個函數(shù)被調(diào)用的時候,我們需要把返回地址壓入堆棧。從攻擊者的角度來看,這個機制至為重要。如果攻擊者通過某種方法設法修改了保存在堆棧里面的返回地址,那么當函數(shù)執(zhí)行完的時候,這個地址將被加載到EIP,因此內(nèi)存溢出的代碼將被下一個執(zhí)行,而不是程序里面的代碼,下面的代碼可以用來解釋堆棧的工作原理。
Listing1
void f(int a, int b)
{
}
void main()
{
}
當進入 f(), 堆棧的內(nèi)容如圖2所示。
圖. 2 Behavior of the stack during execution of a code from Listing 1
擴展:
首先,函數(shù)的參數(shù)被壓入了堆棧的底部(C語言的規(guī)則如此),緊接著是返回地址。下面進入f()的執(zhí)行,它首先把當前的EBP寄存器壓入堆棧(后面解釋)并且給函數(shù)的局部變量分配空間。有兩件事值得注意:第一,stack是自頂部向下分配的,我們的記住下面這句匯編是增加了stack的大小,雖然這看起來有點容易迷惑,事實上就是ESP越大,堆棧越小。:
sub esp, 08h
第二,stack是32位對齊的,也就是說如果一個10字符的數(shù)組要占用12字節(jié)。
Stack如何工作?
有兩個CPU寄存器對于stack的功能至關(guān)重要,它是ESP和EBP。ESP保存stack的頂部地址,ESP可以被修改,可以被直接修改或者間接修改,直接操作的指令比如,add esp, 08h,將導致ESP縮小8個字節(jié)。間接的操作,比如壓棧和出棧操作。EBP寄存器指向堆棧的底部,更精確的說是包含了堆棧底部和可執(zhí)行代碼之間的距離。每次調(diào)用一個新函數(shù)的時候,當前EBP的值被首先壓入stack,然后新的ESP值將被移入EBP寄存器,現(xiàn)在EBP指向了當前函數(shù)的堆棧底部。[i]
由于ESP指向stack的頂部,它在程序執(zhí)行過程中不斷變化,用它作為偏移量寄存器很笨重,這就是為什么要有EBP的原因。
威脅
如何知道什么地方可能會被攻擊?我們現(xiàn)在只知道返回地址是保存在stack上面,同時函數(shù)變量也是在stack里面進行處理。后面我們將了解,在某些特定的環(huán)境下,正是由于這兩個特性導致返回地址可以被改變。帶著這個疑問,下面讓我們來看一段簡單的小程序。
Listing 2
#include
char *code = "AAAABBBBCCCCDDD"; //including the character '\0' size = 16 bytes
void main()
{
}
當執(zhí)行該程序的時候,該程序會提示“內(nèi)存訪問錯誤”[ii],為什么?因為當我們嘗試把一個16字節(jié)的字符串寫入一個8字節(jié)的空間(這個很少發(fā)生,因為缺乏必要的空間限制檢查)。因此分配的內(nèi)存空間已經(jīng)被超過,在stack底部的數(shù)據(jù)已經(jīng)被改寫。讓我們再回顧一下圖2,stack里面的重要的數(shù)據(jù):幀地址和返回地址都已經(jīng)被改寫了!因此,當函數(shù)返回的時候,一個錯誤的返回地址已經(jīng)被寫到EIP,這樣允許程序去執(zhí)行該地址指向的值,產(chǎn)生了一個stack操作錯誤。由此看來,在stack里面破壞返回地址不僅可行而且很平常。糟糕的程序或者含有bug的軟件給攻擊者提供了一個巨大的機會去執(zhí)行攻擊者設計的惡意代碼。
Stack overrun
現(xiàn)在我們該梳理一下所有這些知識了。我們已經(jīng)知道程序通過EIP寄存器控制代碼的執(zhí)行,我們還知道在調(diào)用函數(shù)的時候緊跟在函數(shù)后面的一句代碼的地址被壓入堆棧,在函數(shù)調(diào)用返回的時候從stack恢復并移到EIP寄存器。通過一種控制的方法進行內(nèi)存溢出寫入,我們可以弄清返回地址被保存的具體位置。這樣攻擊者就擁有了所有的信息可以去控制程序執(zhí)行他想執(zhí)行的代碼,創(chuàng)建有害的進程。簡單的來說,有效的進行內(nèi)存侵害的算法如下:
1. 找到一段存在內(nèi)存越界缺陷的代碼;
2. 探測需要多少字節(jié)才能修改返回地址;
3. 計算指向改變后代碼的地址;
4. 寫一段代碼用于被執(zhí)行;
5. 鏈接在一起進行測試。
下面的Listing 3是一段可以被利用的代碼示例:
Listing 3 – The victim’s code
#include
#define BUF_LEN 40
void main(int argc, char **argv)
{
}
這段代碼擁有所有的內(nèi)存溢出缺陷的特征:局部stack緩沖,一個不安全的函數(shù)會去改寫內(nèi)存,第一個命令行參數(shù)沒有進行長度檢查。
加上我們新學到的知識,讓我們來完成一個攻擊任務。我們已經(jīng)清楚,猜測一段代碼存在內(nèi)存溢出缺陷非常容易,如果有源代碼的話就更容易了。第一個方法就是尋找字符相關(guān)函數(shù),比如strcpy(),strcat()或者gets(),他們的共有的特性是都沒有長度限制的拷貝,直到發(fā)現(xiàn)NULL(code 0)為止。而且這些函數(shù)在局部緩沖上進行操作,有機會修改保存在局部緩沖上的函數(shù)的返回地址。另外一個方法是反復試探法,通過填充大批量的數(shù)據(jù),比如下面的例子:
victim.exe
AAAAAAAAAAAAAAAAAAAAAAAA
如果程序返回一個訪問沖突的錯誤,我們就可以向下一步了。
下一步,我們需要構(gòu)造一個大字符串,能夠破壞返回地址。這一步也非常簡單,還記得前面我們說過寫入stack都是以WORD對齊的么,我們可以構(gòu)造如下示例的字符串:
AAAABBBBCCCCDDDDEEEEFFFF
如果成功,這個字符串將導致程序crash,并彈出著名的錯誤對話框:
The instruction at ?0x4b4b4b4b” referenced memory at ?0x4b4b4b4b”. The memory could not be ?read”
我們知道,0x4b就是字符”K”的ASCII碼,返回地址已經(jīng)被“KKKK”改寫了。好了,下面我們可以進入步驟3了,找到當前buffer的開始地址不太容易。有很多方法進行這種“試探”,現(xiàn)在我們來討論其中一種,其它的后面在討論。我們可以通過跟蹤代碼的方式來獲得所需要的地址。首先通過debugger加載目標程序,然后開始單步執(zhí)行,不過令人頭痛的是開始執(zhí)行的時候會有一系列和我們代碼不相關(guān)的系統(tǒng)函數(shù)調(diào)用;蛘咴诔绦蜻\行時監(jiān)控程序的stack,跟蹤到出現(xiàn)我們輸入的字符串的下一句。不管用哪個方法,我們最終要找到類似于如下的代碼就算達到目的了:
:00401045 8A08 mov cl, byte ptr [eax]
:00401047 880C02 mov byte ptr [edx+eax], cl
:0040104A 40 inc eax
:0040104B 84C9 test cl, cl
:0040104D 75F6 jne 00401045
這個是我們所要尋找的strcpy函數(shù),進入函數(shù)后,首先讀入EAX指向的內(nèi)存的字節(jié),下一行代碼再寫入到EDX+EAX的地址去,通過讀寄存器,我們可以獲得這個緩存的地址是0x0012fec0。
寫一段shellcode也是一門藝術(shù)。不同的操作系統(tǒng)使用不同的系統(tǒng)函數(shù),就需要不同的方法達到我們的目的。最簡單的情況下,我們什么都不做,只是改寫返回地址,導致程序出現(xiàn)偏離預計的行為。事實上,攻擊者可以執(zhí)行任意的代碼,唯一的約束是可使用的空間大。ㄊ聦嵣线@一點也可以設法克服)和程序的訪問權(quán)限。在大部分情況下,緩沖溢出正是一種被用來獲得超級用戶權(quán)限、利用有缺陷的系統(tǒng)進行DOS攻擊的方法。例如,創(chuàng)建一段shellcode允許執(zhí)行命令行處理程序(WinNT/2000下的cmd.exe)。通過調(diào)用系統(tǒng)函數(shù)WinExec或者CreateProcess就可以實現(xiàn)這個目標。調(diào)用WinExec的代碼如下:
WinExec(command, state)
為了實現(xiàn)我們的目標,women需要傳遞這樣的參數(shù):
- 將我們需要傳入的參數(shù)字符串壓棧,也就是“cmd /c calc”.
- 將第二個參數(shù)壓棧,這兒我們不需要內(nèi)容,就壓入NULL(0)。(從右向左的參數(shù)調(diào)用規(guī)則,先壓入第二個參數(shù))
- 將剛剛壓入的“cmd /c calc”的地址作為第一個參數(shù)壓棧。
- 調(diào)用WinExec系統(tǒng)函數(shù).
下面的代碼是完成這個目標的一個實現(xiàn):
sub esp, 28h ; 3 bytes
jmp calling ; 2 bytes
par:
call WinExec ; 5 bytes
push eax ; 1 byte
call ExitProcess ; 5 bytes
calling:
xor eax, eax ; 2 bytes
push eax ; 1 byte
call par ; 5 bytes
.string cmd /c calc|| ; 13 bytes
關(guān)于代碼的一些解釋:
sub esp, 28h
在函數(shù)退出的時候會首先回收函數(shù)的局部變量的棧長度,剛剛寫入stack的部分代碼現(xiàn)在被聲明為無效了,這就意味著程序?qū)堰@部分stack分配給別的函數(shù)調(diào)用使用,從而破壞我們剛剛寫入的代碼,因此我們的第一個代碼就是將ESP減40個字節(jié)(相應的stack增長了40個字節(jié))。
jmp calling
下一行語句跳轉(zhuǎn)到WinExec函數(shù)參數(shù)壓棧的代碼。我們需要注意以下幾點:第一,NULL值必須通過精心構(gòu)造的方法獲得,因為如果我們直接寫一個0的話,將會在strcpy的時候被當成是字符串結(jié)尾而導致后面的代碼無法被寫入堆棧。因此只能把字符串放在最后。我們知道,調(diào)用call指令的時候,會自動將下一個指令的指針壓入stack作為返回地址,我們可以利用這個特性來把字符串和字符串的地址壓入堆棧。為此我們首先跳轉(zhuǎn)到calling語句的位置,將第二個參數(shù)壓入堆棧,然后調(diào)用call,將后面的地址壓入堆棧,接著開始順序調(diào)用WinExec和ExitProcess,下圖是調(diào)用順序,方便的計算各個變量的值。
Fig. 3 A sample shellcode
聯(lián)想:
我們看到,我們的例子沒有考慮EBP壓棧的大小,這是因為我們假設使用VC7編譯,該編譯器不向堆棧壓入EBP寄存器的內(nèi)容。
剩下的工作就是把上面的代碼轉(zhuǎn)換為二進制格式并完成程序進行測試了,下面是代碼:
Listing 4 – Exploit of a program victim.exe
char *victim = "victim.exe";
char *code = "\x90\x90\x90\x83\xec\x28\xeb\x0b\xe8\xe2\xa8\xd6\x77\x50\xe8\xc1\x90\xd6\x77\x33\xc0\x50\xe8\xed\xff\xff\xff";
char *oper = "cmd /c calc||";
char *rets = "\xc0\xfe\x12";
char par[42];
void main()
{
}
太棒了,它能夠工作了!這里需要從Listing 3代碼編譯的victim.exe放在該程序的當前目錄。如果一切順利,我們可以看到一個系統(tǒng)的計算器彈出來!
歡迎光臨 (http://m.zg4o1577.cn/bbs/) | Powered by Discuz! X3.1 |