ブログ未満のなにか

ブログなのか誰にも分からない

HCTF 2018 the end, baby printf ver2, Heapstorm zero writeup

the end

バイナリは小さく単純である。最初にsleep()関数のアドレスを教えてくれる。標準出力と標準エラーを閉じた後に、任意アドレスへの1byteずつの書き込みを5回行える。

int main(void) {
    char* buf;
    sleep(0);
    printf("here is a gift %p, good luck ;)\n", &sleep);
    close(1); close(2);
    for (int i=0; i<=4; i++) {
        read(0, &buf, 8);
        read(0, buf, 1);
    }
    exit(1337);
}

exploit

libcのアドレスを最初に得られるため、ライブラリ内への書き込みが可能となっている。 exit()を実行する際に呼ばれる処理を追っていくと、__GI_exit() -> __run_exit_handlers() -> _dl_fini() 経由で rtld_lock_default_lock_recursive()rtld_lock_default_unlock_recursive()が呼び出される。2つの関数はldのrwな領域に登録されており、これを書き換えると制御を奪える。ldとlibcのオフセットは固定なので、同じ環境を揃えてオフセットを求めた。問題バイナリに使用されたgccや配布されたlibcから、問題サーバーの環境をubuntu16.04と推測した。同じ環境を用意しオフセットを求めると、libc_base + 0x5f0f48であり、このアドレスを書き換えると制御を奪えた。rtld_lock_default_lock_recursive()system()に書き換え、呼び出し時にrdiが指している領域へshを書き込んでおくことで、system("sh")が呼び出されシェルを取れる。

HCTF 2018 the end · GitHub

baby printf ver2

最初にバイナリがマッピングされているアドレスを得られる。 自明なfsbがあるが、__printf_chk()を使用しているため任意オフセットからの読み出しや書き込みは出来ない。また_printf_chk()呼び出し時に、スタックが0xdeadbbefで埋め尽くされるためアドレスのリークも行えない。入力は、0x200byteがdata領域に保存される。オーバーフローしており、stdoutを指すポインタを書き換えることができる。 入力後に、stdoutのvtableが入力前と同じか確認している。元のstdoutのvtableをローカル変数に保存し、書き換わっていた場合元の値に上書きしている。

# ./babyprintf_ver2
Welcome to babyprintf_v2.0
heap is too dangrous for printf :(
So I change the buffer location to 0x564d0930d010
Have fun!
%p.%p
0xdeadbeef.0xdeadbeef
int main(void) {
    puts("Welcome to babyprintf_v2.0");
    puts("heap is too dangrous for printf :(");
    __printf_chk(1LL, "So I change the buffer location to %p\n", bin_base + 0x202010);

    while (1) {
        void* vtable = stdout.vtable;

        // 本当は1byteずつ読み込んでいるが面倒なので省略
        read(0, bin_base + 0x202010, 0x200);

        if (vtable != stdout.vtable)
            stdout.vtable = vtable;
            
        // スタックを0xdeadbeefで埋める処理がある
        __printf_chk(1, bin_base + 0x202010);
    };
}

exploit

バイナリがマッピングされているアドレスが分かるので、偽のstdoutを用意し、stdoutが指す先を偽の方へ向ける。vtableは改竄されていた場合元に戻されるので適当な値にしておいてよい。 フラグやバッファのアドレスを任意に制御できるので、任意箇所の読み書きが可能である。FILE構造体を利用した諸々のテクニックについては、217のangelboyのスライドで詳細に解説されている(Play with FILE Structure - Yet Another Binary Exploit Technique)。シェルを取るまでの流れは、libcのアドレスをリークして__free_hookへone gadget RCEを書き込んでおき、最後に__printf_chk()内でmallocが呼ば出せばシェルが取れる。

HCTF 2018 baby printf ver2 · GitHub

heapstorm zero

競技中に解けなかった。 note管理系のアプリで、動作は以下の通りである。

  • Allocate: 指定したサイズでcalloc()が実行され、確保された領域へ指定サイズ分の書き込みが行える。指定できるサイズは、0x1 <= size <= 0x38で、fastbinに該当する大きさだけである(とてもしんどい)。off-by-oneがあり、1byteだけnull byteで上書きできる。
  • View: 指定したインデックスの中身をputsで表示する。 存在しないチャンクを見るなどのバグはない。
  • Delete: 指定したインデックスが存在する場合freeを行う。 ポインタはクリアされるためUAFはない。
# ./heapstorm_zero
ooooo   ooooo   .oooooo.   ooooooooooooo oooooooooooo
`888'   `888'  d8P'  `Y8b  8'   888   `8 `888'     `8
 888     888  888               888       888
 888ooooo888  888               888       888oooo8
 888     888  888               888       888    "
 888     888  `88b    ooo       888       888
o888o   o888o  `Y8bood8P'      o888o     o888o

===== (fake) HEAP STORM ZERO =====

1. Allocate
2. View
3. Delete
4. Exit
Choice:

exploit

上記の動作だけからでは、シェルを取るところまでは到達できない。 実は動作の選択時の入力にはscanf("%d")が呼ばれており、入力を工夫することでscanf()内でmalloc/freeを呼び出すことができる。scanf()では、1度の入力のサイズが大きいと一時的なバッファを確保するために、malloc()を呼び出す。バッファは使用後にはfree()される。scanf("%d")に対して、'1'*0x400を入力すると内部では0x800サイズのチャンクが確保されて解放される。 これを利用すると、__malloc_consolidateを呼び出すことができ、fastbinチャンクをまとめてsmallbin/largebinへ繋ぐことができる。 あとは、チャンクのPREV_INUSEをクリアしてチャンクをオーバーラップさせれば、いつも通りのheap系の問題になる。シェルをとるまでの流れは、unsotedbin attackをstdinの_IO_buf_endに対して行い、オーバーフローが起きるようにする。最後に__malloc_hookを書き換えてシェルを起動した。

HCTF 2018 heapstorm zero · GitHub

参考リンク

Hctf 2018 heapstorm zero | Ne0’s blog