棧溢出漏洞利用(繞過ASLR)
原文:https://sploitfun.wordpress.com/2015/05/08/bypassing-aslr-part-iii
- 原文內容比較精煉,只闡述關鍵思路,沒有具體到一些細節;
- 原文提供的利用腳本,很多數值需要根據實際環境修改,才能成功執行;
- 原文文字內容可以直接通過鏈接看到,但其中的圖片,是鏈接到google的,如果訪問不到,可以對照附件中的pdf文件一起看。
另外需要說明一下,原文只是一個系列(https://sploitfun.wordpress.com/2015/06/26/linux-x86-exploit-development-tutorial-series/)中的一篇文章:

學習這篇之前,最好先把前面的都搞明白,本文的細節,只針對Part III using GOT overwrite and GOT dereference這篇。
漏洞程序
// vuln.c#include <stdio.h>#include <string.h>#include <stdlib.h> int main (int argc, char **argv) { char buf[256]; int i; seteuid(getuid()); if(argc < 2) { puts("Need an argument\n"); exit(-1); } strcpy(buf, argv[1]); printf("%s\nLen:%d\n", buf, (int)strlen(buf)); return 0;}
準備工作:
# 編譯vuln.c,并修改vuln可執行文件屬性 sudo gcc -fno-stack-protector -o vuln vuln.c sudo chown root vuln sudo chgrp root vuln sudo chmod +s vuln# 打開ASLR sudo sh -c 'echo 2 > /proc/sys/kernel/randomize_va_space'
程序說明:
行9:setuid(getuid())
由于以上步驟,通過chmod +s vuln,給vuln可執行文件添加了粘滯位,這就使普通用戶(比如:jmpcall)執行vuln,在剛進入main()函數時,擁有vuln所屬用戶的權限(即root權限),setuid(getuid())就是將權限改回執行者本身的權限,其實就是給利用增加難度,期望的是,即使被攻擊者拿到shell,也只是個普通的shell,不具有root權限(就等同于沒給vuln添加粘滯位)。
行14:strcpy(buf, argv[1])
將命令行參數內容,拷貝到buf[256],沒有限制拷貝長度,從而存在棧溢出漏洞。
問題分解
面對一個復雜的問題時,通常需要自上而下、以大化小、分而治之,直接上圖:

換個方式來說明最終目標的話,就是要讓vuln執行這樣一段代碼:
char *p = "/bin/sh";char **argv = { p, NULL }; setuid(0);execve(p, argv, NULL);
并且僅僅是通過構造輸入數據達到這個目的,而不是修改vlun.c源碼,重新編譯出新的vlun可執行文件。
為了方便描述,先假設可以構造出如下堆棧布局:

這顯然可以使vuln按照上述的期望進行執行,但問題是我們無法向棧內構造這樣的數據,原因如下:
- 棧中的數據,全都來自于命令行參數,然后由vuln進程strcpy()進去的,所以當中不可能夾雜著'\0'字符;
- 由于開啟了ASLR,棧的地址和libc.so映射到vuln進程空間的位置,是隨機的,相應的,棧中的"/bin/sh"字符串地址、argv[]數組地址,以及setuid()、execve()函數地址,無法根據上次啟動的信息,事先計算出來。
不過,這些問題都可以化解:
- 對于0值,可以通過輸入數據,構造strcpy()執行邏輯(向棧中構造strcpy()函數地址及其所需的參數,后續簡稱"strcpy()構造邏輯"),從有0的地方復制;
- 對于"/bin/sh"、argv[]數組內容,可以通過strcpy()構造邏輯,將它們寫到一個固定的地方,后續就可以將這些固定的地址值,作為setuid()、execve()參數。關于這個固定地方的選擇,只要保證每次啟動vuln程序,這塊進程空間總是可寫,修改它的內容也不會導致程序掛掉即可,.bss、.data段加載所占的一塊進程空間,就符合這個條件,并且每次加載的位置不受ASLR影響:

- 對于setuid(),由于vuln.c本身調用過它,編譯器會在vuln可執行文件中生成setuid@PLT這個"中間跳轉函數",通過它也能執行setuid()函數,并且反編譯vuln看到的setuid@PLT地址,就是它運行時的地址。PLT的原理,我之前發過一個帖子詳細介紹過:32位elf格式中的10種重定位類型;
- 對于execve(),可以通過GOT overwrite的方法執行,它依據的原理是,vuln進程執行到攻擊者構造的邏輯時,GOT[getuid]這個內存單元,存儲的一定已經是getuid()函數的加載地址,另外,execve與getuid加載地址的偏移,和它們在libc.so動態庫文件中的偏移,是一樣的,這樣,通過輸入數據構造邏輯,將這個偏移加到GOT[getuid],并觸發getuid@PLT執行,即可執行execve()。GOT的原理,32位elf格式中的10種重定位類型同樣介紹的很清楚。
# 查看execve-getuid偏移ldd vulnobjdump -S /lib/i386-linux-gnu/libc.so.6 | grep "getuid"objdump -S /lib/i386-linux-gnu/libc.so.6 | grep "execve"
上述這些,明顯還沒有將問題徹底分解,它們都依賴"通過輸入數據構造的邏輯",比如,具體如何實現:GOT[getuid]+=(execve-getuid)?
由于gcc編譯時沒有加"-z execstack"選項,即棧所在的內存區間,沒有可執行權限,無法使用往棧里構造shellcode的方法,這種情況下可以使用ROP的方法,從代碼中尋找一些以"ret"指令結束的代碼片段,拼湊出需要的完整邏輯,還是直接上圖:

目標①:GOT[getuid] += (execve-getuid)
這個目標需要一條add指令,將execve-getuid(可以根據libc.so事先計算),加到GOT[getuid]所在的內存單元,作者從而找到gadget:"addl %eax, 0x5D5B04C4(%ebx); ret;",將目標轉換成②③;
目標②:ebx = GOT[getuid]-0x5d5b04c4
GOT[getuid]運行時地址,根據vuln可執行文件就可以得到:
objdump -R vuln
從而可以事先計算出ebx的靜態值:ebx = 0xaaa99b40,因此,作者找一個gadget: "popl %ebx; ret;",這樣,通過往棧里構造0xaaa99b40和這個gadget的地址即可達到目的;
目標③:eax = execve-getuid
如果能找到gadget: "popl %eax; ...; ret;",再通過往棧里構造execve-getuid(可以根據libc.so事先計算)和這個gadget的地址即可,也就不需要后續的目標④⑤⑥⑦了,可惜的是沒能找到,所以作者就找到圖中的代碼片段,也能實現向eax寄存器賦值的目的,但是"mov 0x34(%esp),%eax"到"ret"指令之間,有call和jne指令,為了跳轉到的代碼片段,別再次修改eax,又分化出2個新目標:目標④保證call指令調用到一個不影響eax值的函數,目標⑤保證jne指令不執行跳轉,因為當前這個代碼片段后續的指令,是不影響eax值的;
目標④:-0xe0(%ebx,%esi,4) = 0x0804861c
作者發現,如果上述的call指令,調用_fini函數,可以達到不修改eax的目的,也就是要讓-0xe0(%ebx,%esi,4)這個地址值(注意它前面有*號),存儲的是_fini函數地址0x0804861c:
objdump -sj .dynamic vuln | grep '1c860408'
通過這樣執行objdump命令,可以發現0x8049f3c這個地址處,存的是0x0804861c,從而又將目標轉換成:-0xe0(%ebx,%esi,4) = 0x0804861c,這可以由目標⑦實現。另外,_fini函數中又會執行call指令,不過call的目標地址是確定的,由目標⑥保證再次call到的函數不修改eax即可;
目標⑤:edi = esi+1
目標⑥:(0x804a028) = 0x01
作者發現,_fini再次call到的函數中,又有條jne指令,如果這條jne指令執行跳轉的話,就可以保證不修改eax,所以找到gadget:"movb $0x00000001, 0x0804A028; addl $0x04, %esp; popl %ebx; popl %ebp; ret;",保證jne跳轉;
目標⑦:esi = 0x01020101, ebx = 0x8049f3c -(0x01020101*0x4) + 0xe0 = 0x3fc9c18
通過往棧里構造0x01020101和gadget:"popl %ebx; ret;",往棧里構造0x3fc9c18和gadget:"popl %esi; popl %edi; popl %ebp; ret;",可以滿足:-0xe0(%ebx,%esi,4) = 0x0804861c(ebx可以選其它值,只要保證它本身,以及滿足目標等式的esi值不包含'\0'字節即可)。另外,選擇gadget:"popl %esi; popl %edi; popl %ebp; ret;",是為了一舉兩得,因為它里面包含了"popl %edi"指令,可以順便讓目標⑤的條件得到滿足。
堆棧布局結果
不管是寫一段程序,還是學習一段程序,先有一個概括性的圖,往往能事半功倍,所以我給棧的布局,畫了一張高清大圖:

根據上述的問題分解過程,再去理解為什么要這樣布局,難度就不大了,接下來可以順著這個布局,看一下具體的執行過程:
gadget7
指向代碼片段"movb $0x00000001, 0x0804A028; addl $0x04, %esp; popl %ebx; popl %ebp; ret;",并且gadget7所在位置,是main()函數返回地址的存儲位置,這是通過寫溢出覆蓋的,那么,main()函數返回時,就會執行這段指令。使用這個代碼片段,其實是有點“迫不得已”的,真正需要的其實只是"movb $0x00000001, 0x0804A028; ret;",但是沒能在vuln中直接找到。
gadget7執行后:
*(0x0804A028) = 0x00000001
另外,"addl $0x04, %esp; popl %ebx; popl %ebp;"使esp向上移動了4*3個字節,所以利用腳本在構造輸入數據時,需要在gadget7后面安排4*3字節的dummy數據,保證gadget6距離gadget7 4*3字節,進而保證gadget7中的"ret"指令,能繼續執行到gadget6,后續還有很多小的代碼片段,都是這樣連續在一起執行的,從而完成一個完整的邏輯,這也正是ROP的原理,以及gadget為什么要以"ret"指令結束的原因。
gadget6
指向代碼片段"popl %esi; popl %edi; popl %ebp; ret;",執行gadget7的"ret"指令時,esp指向圖中存儲gadget6的位置,而gadget7的"ret"指令,實際上是將gadget6 pop到eip寄存器,所以執行gadget6時,esp指向的是圖中存儲esi的位置,因此,gadget6執行后:
esi = 0x01020101edi = 0x01020102
和gadget7同樣的道理,由于沒能在vuln中找到"popl %esi; popl %edi; ret;",所以“迫不得已”使用了"popl %esi; popl %edi; popl %ebp; ret;",為了能繼續執行到gadget5,填充了4個字節的dummy。
gadget5
就不再啰嗦了,執行結果:
ebx = 0x3fc9c18
gadget4
執行結果:
eax = 0xfffff530
指向代碼片段"mov 0x34(%esp),%eax; ...; ret;",問題分解階段,已經說明過,需要gadget4~7,是由于沒能找到類似"popl %eax; ...; ret;"這樣的gadget。這里需要注意的是,gadget4不是用pop指令給eax賦值,而是將距離esp上方0x34字節處的值mov給eax,所以利用腳本也必須遵循這一點安排0xfffff530的位置,另外,gadget4指向的是一個函數的內部,相當于跳過了函數頭部對esp的減操作,因此gadget4后續的pop指令,會使esp上移4*11字節,所以要在gadget2之前,需要填充4*11字節的dummy數據。
gadget2
gadget4~7,相當于迂回實現了gadget3的作用,與gadget2一起,服務于gadget1,gadget2執行結果:
ebx = 0xaaa99b40
但是,由于上方正好遇到服務于gadget4的0xfffff530,所以特地讓gadget2指向代碼片段"popl %ebx; popl %ebp; ret;",保證esp能夠跳過0xfffff530的存儲位置,到達gadget1的存儲位置。
gadget1
指向代碼片段"addl %eax, 0x5D5B04C4(%ebx); ret;",此時,eax = execve-getuid,0x5D5B04C4(%ebx)=&GOT[getuid],從而達到將GOT[getuid]修改為execve()加載地址的目的。
不過,到這個時候,離最終目標,還有另外一半路程:觸發setuid(0)和execve()函數的執行。

根據上半程的"經驗"+棧的詳細布局圖,不難看出,后續邏輯,是找了另外一塊地盤,按照上圖布局,構造了一個遷移棧(兩個布局中的①~⑨標號,是一一對應的"邏輯-結果"),保證"/bin/sh"地址、{ "/bin/sh"地址, NULL }數組地址,都是固定的,從而繞過ASLR保護機制。
最后說明2點:
為了遷移到新棧幀,溢出棧的末尾布局如下:
pr_addr # leave; ret;cust_base_esp # 遷移棧地址lr_addr # popl %ebp; ret;
leave指令等于:
movl %ebp, %esp; # 將esp遷到cust_base_esppopl %ebp; # esp上移4字節,指向setuid()地址存儲位置
"/bin/sh"的復制
vuln可執行文件中,無法找到一個現成的"/bin/sh"字符串,提供給利用腳本往遷移棧復制,所以只能一個碎片一個碎片的復制,比如先復制個"/bin",再向緊接著的位置復制"/sh",作者提供的利用腳本,是一個字符一個字符復制的。
我想說明的是怎么去找這些字符,比如我的環境存在這樣的情況:

"/\00"找不到,但單獨的'/'可以找到,而strcpy()要遇到'\0'字符才會停止復制,所以盡量用能盡快遇到'\0'的那個:

另外,如果只能找到"h",找不到"h\x00",要額外再復制個'\0',保證"/bin/sh"以0結束(復制getuid@PLT、setuid@PLT地址的過程,同理)。
利用腳本執行結果演示
