ブログ未満のなにか

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

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も無効なのだから直接ユーザ空間内でもよかった)

gist.github.com

f:id:hama7230:20181118154849p:plain

参考URL