SEC-T CTF gh0st writeup
はじめに
一人writeup advent calendarの2日目です。 1日1問分のwriteupを目標に頑張っていきます。 2日目の問題は、SEC-T CTF 2018で出題された「gh0st」。 out-of-boundsを起点としたkernel exploit問題で初めての人にオススメです。
カーネルの情報 (セキュリティ機構など)
配布ファイルを以下の通り。 READMEを読むと、kaslr, smep, smapがオフになっているらしい。 initは圧縮されたcpioファイルなので展開する。 展開するとgh0st.koが存在する。 このモジュールが攻略対象。
% ls README gdb.cmd* init run.sh* tiny.kernel* vmlinux.bin* % cat README First complete your exploit locally using the given qemu environment, it comes with symbols etc. ./run.sh -debug gdb target remote :1234 file ./vmlinux.bin Then pwn the remote system by connecting to the service and f.ex. dump your exploit bin via gzip/b64. NX is ON, KASLR/SMEP/SMAP is OFF!
解析
gh0st.koでは、簡易バイトコードを実行できる。 バイトコードそのものはカーネルモジュールのbss領域にbytecodeとして保持される。 また、バイトコード実行の管理領域として、stack_ptrなどがbss領域に存在する。
/dev/gh0st に対する主な挙動は以下の通りであった。
write: bytecodeへユーザ空間から0x1000byteのコピー
ioctl(0x1337B4B3): writeを呼び出してbytecodeに0x1000byte書き込み、bytecodeの内容がフォーマットに合っているか確認する。フォーマット通りであった場合、指定したサイズでkmalloc()を呼び出し、結果をstack_ptrとorig_stack_ptrへ書き込む。bytecodeのフォーマットは以下の通り。
- header: 4byte。"BFBF"である必要がある。
- size: 4byte。kmalloc()で呼び出すサイズの指定。スタックの大きさ。
- out: 0x100byte。命令用のバッファ。
- in: 0x100byte。命令用のバッファ。
- inst: 0x418byte。命令のバイトコード。使用できる命令は下述の6つ。
ioctl(0xAC1DC0DE): 先に0x1337B4B3を呼び出す必要がある。bytecodeの命令に従って処理を実行する。命令は0x418回実行されると終了する。実行できる命令は以下の6つ。out_counter、in_counterは0で初期化されてから実行される。
- '+', '>': stack_ptrをインクリメント
- '-', '<': stack_ptrをデクリメント
- ',': stack_ptrが指す先へout[out_counter]を書き込む。その後out_counterをインクリメントする。out_counterが0x100まで行ったら処理終了
- '.': stack_ptrが指す先からin[in_counter]を読み込む。その後in_counterをインクリメントする。in_counterが0x100まで行ったら処理終了
exploit
stack_ptrの操作に制限がないため、out-of-boundsで読み書きができる。 読みはできるが、ユーザ空間へ渡す方法が (簡単には) ないので、書き込みだけを使っていく。 stack_ptrが指す先はkmalloc()で取得された領域である。 そのため、その前後には同様にkmalloc()を用いて確保されたオブジェクトが存在している。
今回のカーネルはSLOBアロケータを使用しているため、近いサイズのオブジェクトは隣接して配置される可能性が高い。 具体的には、256byte未満、256byte以上1024byte未満、1024byte以上の3種類に分けられ、それぞれは別の領域でアロケートされる。 事前にターゲットとなるオブジェクトを配置しておき、ioctl(0x1337B4B3)でkmalloc()を呼び出す。 もしターゲットのオブジェクトとkmalloc()で取得された領域が連続している場合、stack_ptrをズラすことでターゲットのオブジェクトへ任意の書き込みが行える。
ターゲットとするオブジェクトは、ファイルを展開した際に使用されるfile構造体にした(Linux source code: include/linux/fs.h (v4.17) - Bootlin)。 理由は単純でメンバのf_opは関数テーブルを指しているので、これを上書きすれば制御を奪えるからである。 具体的な流れは、stack_ptrを適用な回数デクリメントさせて、真上に存在する(であろう)file構造体のf_opを指す。 書き込みを用いてf_opをこちらが制御できるアドレスで上書きする。
実際に書き込むアドレスは、gh0stのbytecodeの中にした。 自由に制御できるうえに、今回はKASLRが無効なのでbytecodeのアドレスは固定となっている。 あらかじめbytecode中に偽のfile_operationsを用意しておく。 ユーザ空間からread()を実行すると、f_op->read()が実行されるので、readの位置に呼び出したいアドレスを書いておく。 file構造体を狙うため、適当なファイルを複数回開いてからgh0stでkmalloc()を呼べば隣接する確率が高くなる(0x100回もすれば100%上手くいった)。
root権限を取るまでの流れだが、基本に忠実にcommit_creds(prepare_kernel_commit(0));を呼び出す。SMAPが無効なのでユーザ空間内にstackをpivotさせてROPを実行する。 (あとから考えるとSMEPも無効なのだから直接ユーザ空間内でもよかった)