# WCTF 2018 - klist Writeup
###### tags: `ctf` `pwn` `linux kernel exploitation`
# 概要
この記事は,[CTF Advent Calendar 2018](https://adventar.org/calendars/3210) の7日目の記事です.
6日目は私の「[最近のLinuxカーネルexploit問に対するテクニック集3](https://hackmd.io/s/ByA_8LeEQ)」でした.
今回は
- WCTF 2018 - klist Writeup
について書きます.12/3~6の記事で紹介した内容を幾つか使います.
# はじめに
大前提として,Linuxのカーネルexploit(権限昇格)についてある程度知っている方向けの内容となります.
全く知らないよ!って方は,[nokia31337の解説資料](https://speakerdeck.com/bata_24/katagaitai-ctf-number-3)で図を入れて解説したので,先にそちらで精進したほうが良いかと思います(2014年当時のARMにおけるLinuxカーネルExploit問です).
また私はLinuxカーネルのソースを全て読んだことはなく,必要に応じてチラ見した程度なので,そこまで深い知識は持っていません.もし記事の内容に間違いがありましたら,優しく教えてください.マサカリ投げるのは禁止!
# WCTF 2018 - klist
問題をもらって解いたので,Write-upを載せておきます.
バイナリはもう公開されていないので,ここからDLしてください.https://goo.gl/nf5k8P
# 問題概要
root権限を奪取する問題ですね.脆弱なLKM(Loadable Kernel Module,`*.ko`のこと)が読み込まれています.
同梱の`list.ko`が多分それなので,バグを探して突いて,rootになればOKですね.
同梱の起動スクリプト(`run.sh`)や起動した後の情報(`/proc/cpuinfo`)から,SMEP有効,SMAP無効,KPTI有効であることが分かります.
# 初動調査
アイテム管理をするようなLKMです.定義されている関数をざっと読みます.
- `/dev/klist`に対する`open()`, `read()`, `write()`, `close()`, `ioctl()`
- `list_open()`

- `list_read()`

- `list_write()`

- `list_release()`

- `list_ioctl()`

- `ioctl()`から,内部的に`add_item()`, `select_item()`, `remove_item()`, `list_head()`が呼べる
- `add_item()`

- `select_item()`

- `remove_item()`

- `list_head()`

- `ref_incl()`と`ref_decl()`
- `ref_incl()`

- `ref_decl()`

- 各種構造体

- `struct Item`は可変長構造体で,最後のメンバ(`content`)は,`char[size]`であることに注意(`size`は`Item->size`のこと)
- `union ARG`は共用体で,`ADD_ARG`,`LIST_HEAD_ARG`,`SELECT_ARG`のいずれかを取る
# 調査メモ
調査時のメモです.
- アイテムについて
- `add_item()`: addしたアイテムは、ヘッダ分のサイズ24byte(=`sizeof(struct Item)`)を加算して`kmalloc()`で確保され、`g_list`から辿ったリンクリストの先頭に追加される
- `remove_item()`: removeするとリンクリストから外される
- 各アイテムは`kref`を持っており、参照カウントが0になって初めて`kfree()`される
- `kfree()`は`ref_decl()`の内部で行われる
- `select_item()`: selectしたアイテムは`file->private_data`にポインタが保存される
- このアイテムに対してread/write出来る
- `list_head()`: `g_list`の先頭にあるアイテムをダンプ
- kheapリークとraceがある
- kheapリーク: `struct Item`全体を`copy_to_user()`するため`next`のポインタが見える(24はヘッダサイズ)
- race: `ref_decl()`がロックされてない

- 脆弱性考察
- 参照カウントに着目すると,以下のようになっている
- `add_item()`: `kref=1`(1アイテムに対して最初の一度のみ)
- `select_item()`: 新たに保持する対象が`kref++`,これまで保持していた対象が`kref--`
- `remove_item()`: `kref--`(1アイテムに対して最後の一度のみ)
- `list_head()`: `kref++`した後`kref--`
- つまり最初に1つだけ`add_item()`しておき(`item1`とする)、あとは2スレッドで、以下の様にするのが良さそう
- threadA: `add_item()` -> `remove_item()` -> ...のように`item2`を繰り返し確保解放
- threadB: `list_head()`(内部では`kref++`して`kref--`) ->...繰り返し
- Race詳細
- threadBで`list_head()`が,`g_list`先頭の`item2->kref`をインクリメント
- ロックのあるクリティカルセクションを抜けたタイミングでコンテキストスイッチ
- threadAで`remove_item()`が`item2`を`g_list`から外す
- リンクリストからは外されるが`item2->kref=1`なので`kfree()`はされない
- threadBで`list_head()`が再開し,`g_list`先頭の`item1->kref`をデクリメント
- `item1->kref=0`となり`kfree()`されるが、`g_list`からは特に外されない
- 以後も`g_list`から辿れるのでkUAF
# 攻略方針
`/tmp`は書込不可であり,`/home/list/`のみが書込可能だったため,`modprobe_path`には`/home/list/aaaa`を仕込むことにしました.16バイトの書き込みが必要なので(`/tmp/aa`が使えるなら8バイトなのでunlinkで書き換え完了だったのですが),ROPに持ち込んで`mov qword [rcx], rax ; ret`みたいなことを2回やる必要があります.
1. Race Conditionを使ってkUAF(`userfaultfd`は潰されており使えないため,確率的に頑張る)
2. `struct shmid_kernel`と重ね合わせ
3. 上手く重なればアイテムのサイズ値は上書きされ巨大になり,カーネルのヒープを大量に読むことでアドレスリークが可能
4. `struct shmid_kernel`のunlinkを悪用,偽造した`n_tty_ops`を`tty_ldiscs[0]`に書き込む
5. 偽造した関数テーブル経由でRIPを奪い,ROPに持ち込んで`modprobe_path`を書き換え,無限ループに突入させる
6. 別スレッドで,`call_modprobe()`を呼び出す一連のコマンドを実行し,シェルを開く
という感じで,知ってるテクニックの総力戦になりました.
step1~step2の成功率は結構低く,体感では20%~30%くらいです.失敗したらqemuを再起動しなければなりません(自動的にリブートするので手間はあまりかかりませんが).
# exploitコード
{%gist bata24/44b7a0fb2c8e747ce4163278c3f5f808 %}
# 終わりに
カーネルexploitも,慣れれば大して難しく有りません.
ユーザランドのexploitとは大きく異なるので,最初の一歩を踏み出すことに少し躊躇するかもしれませんが,所詮はアセンブリの塊です.気合でなんとかなるので,精進していきましょう.
精進の結果,何か得たテクニックがもしあれば,ぜひ記事にしてください.そういうみんなのノウハウが貯まれば,日本勢ももっと強くなると思いますし,攻撃手法がカーネル開発者に伝われば,セキュリティの改善につながると思います.
明日は[@hama7230](https://twitter.com/hama7230)さんの[最近のglibc heap問で使うテクニックについて](https://hama.hatenadiary.jp/entry/2018/12/08/142437)です.