0CTF 2017 Finals cred_jar writeup
はじめに
一人writeup advent calendarの6日目です。 1日1問分のwriteupを目標に頑張っていきます。 6日目の問題は、0CTF 2017 Finalsで出題された「cred_jar」。 race conditionを起点としたkernel exploit問題で初めての人にオススメです。
カーネルの情報 (セキュリティ機構など)
カーネルのバージョンが4.12で、smep, kaslrが有効になっている。
kernel exploitでよく使われる/dev/ptmx
が存在しない。
% cat boot.sh #!/bin/bash qemu-system-x86_64 -initrd rootfs -kernel bzImage -append 'console=hvc0 root=/dev/ram rdinit=/linuxrc oops=panic panic=1 quiet kaslr' -m 128M -chardev stdio,id=k33n -device virtio-serial -device virtconsole,chardev=k33n -display none -monitor none -smp cores=1,threads=1 -cpu kvm64,+smep [in qemu] / $ uname -a Linux (none) 4.12.0-rc1 #25 SMP Fri May 26 16:51:03 CST 2017 x86_64 GNU/Linux / $ cat /proc/cpuinfo | grep flags flags : fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx lm constant_tsc nopl cpuid pni cx16 hypervisor smep / $ ls /dev/ console cred_jar
解析
攻略対象のデバイスは/dev/cred_jar
。
デバイスに対する操作は以下のように定義されている。
struct file_operations cred_jar_fops = {
.open = cred_jar_open,
.release = cred_jar_release,
.read = cred_jar_read,
.write = cred_jar_write,
.ioctl = cred_jar_ioctl
};
cred_jar_ioctl()がメインとなっている箇所で、指定するcmdによって下の構造体jarに関する操作が行える。 行える処理は、0x7401:alloc、0x7402:get、0x7403:get id、0x7404:change id、の4つである。 file構造体のprivate_dataで、jarへのポインタを保存している。
allocは、指定したサイズとjar構造体の大きさの合計でkamlloc()して領域を確保する。 確保したら、メンバを適切に初期化して、リンクドリストに繋ぎ、private_dataにポインタを入れる。
id指定で、jarを選択する。指定したidを持つjarが存在したら、private_dataにポインタを入れる。 一度getすると、他のjarを選び直すことはできない。 read/writeは、選択したjarのbufを読み書きできる (bofはない)。
get idとchange idはそのままの処理。get idは選択しているjarのidを得られる。change idは選択しているjarのidを変更できる。
refcountはそのjarへの参照数である。 allocした時点で、参照数は1。 getされたら参照数は増えて、jarを選択しているfdがclose()されたら参照数は減る。
struct jar { int refcount; int id; struct jar *next; struct jar *prev; unsigned long size; char buf; };
exploit
getはget_cred_jar_ctx()が実行され、close()するときにはput_cred_jar_ctx()が実行される。
struct jar* get_cred_jar_ctx(int id) { struct jar* ptr; mutex_lock(&ctx_lock); if ( ctx_list != &ctx_list ) { ptr = ctx_list; while(1) { if (ptr == &ctx_list) break; if (ptr->id == id ) { _InterlockedIncrement(ptr->count); break; } ptr = ptr-next; } } ptr = 0LL; mutex_unlock(&ctx_lock); return ptr; } int put_cred_jar_ctx(struct jar *jar) { if (jar) { if (!_InterlockedDecrement(jar->count)) { mutex_lock(&ctx_lock); // ここにunlinkの処理 mutex_unlock(&ctx_lock); kzfree(a1); } } return 0; }
put_cred_jar_ctx()は、refcountをデクリメントし0になった場合 (他からも参照されていない状態)、kfree()を実行する。 しかし、refcountの確認をした後にmutex_lock()が走るので、race conditionとなる可能性がある。
対してget_cred_jar_ctx()は、まずmutex_lock()が走る。 ロック後に、指定したidを持つjarをリストを辿って検索する。 指定のidを持つjarが見つかったら、refcountをインクリメントする。 そしてreturnする前にmutex_unlock()が走る。
put_cred_jar_ctx()とget_cred_jar_ctx()を上手く使うことで、参照している状態にあるjarをkfree()することができる。
race conditionが発生してkUAFとなる具体的な流れを詳しく説明する。
ここで、get_cred_jar_ctx()にて目的のidを持つjar X
を探索している途中に、jar X
を参照するfdがclose()されてput_cred_jar_ctx()が実行されたと仮定する。
jar X
はclose()したfdだけが参照している状態、つまりrefcountは1であるとする。
get_cred_jar_ctx()でmutex_lock()が実行されているが、put_cred_jar_ctx()のmutex_lock()は_InterlockedDecrement()後にあるため、jar X
を参照するrefcountのデクリメント自体は問題なく実行される。jar X
のrefcountは元々1だったので、デクリメントされて0になる。jar X
をリンクドリストから外しkfree()を実行する方向へ分岐が進むが、mutex_lock()があるため、get_cred_jar_ctx()の処理が終わるまで、put_cred_jar_ctx()を実行しているスレッドは待機する。
get_cred_jar_ctx()ではjar X
を見つけて、mutex_unlock()が走り、file->private_dataにjar X
へのポインタが格納される。
もう片方のput_cred_jar_ctx()では、mutex_lock()の停止から復帰してkfree()が実行される。
しかしjar X
への参照はget側で残ったままなので、kUAFとなる。
雑に説明すると、同じjarに対して、スレッドAでget_cred_jar_ctx()、スレッドBでput_cred_jar_ctx()を実行すると、スレッドAでgetしたjarがなぜか知らないうちにkfree()されているという感じ。
SLUBアロケータでは、kfree()したチャンクの先頭8byteはfree済みのチャンクを繋ぐためのポインタになっている。 そのためkUAFが起きた場合、idには設定したものと異なる値が入っているので、idを確認して違う値だった場合はrace conditionに成功してkUAFが起きたと判断できる。
race conditionを起こしてkUAFが起こせるので、別のオブジェクトをラップさせてroot権限を取っていく。
まずはカーネルのアドレスのリークを狙う。
今回のカーネルは、/dev/ptmx
がいないので、共有メモリのshmget()を呼んだときに割り当てられるfile構造体を使用した。
file構造体はkmalloc-256から取られるので、alloc_cred_jar()に渡すサイズは256-0x20にする。(関連する部分のコードは次のリンク先に Linux source code: ipc/shm.c (v4.12) - Bootlin, Linux source code: fs/file_table.c (v4.12) - Bootlin)_
file構造体のf_opからカーネルのベースアドレスを求めることができ、さらにf_opを書き換えれば制御の奪取が可能である。 smapは無効なので、ユーザ空間に偽のfile_operationsを用意できる。
struct file { union { struct llist_node fu_llist; struct rcu_head fu_rcuhead; } f_u; struct path f_path; struct inode *f_inode; /* cached value */ const struct file_operations *f_op; // このポインタの値を知ればカーネルのベースアドレスが求まるし、書き換えれば制御が奪える。 以下省略
偽のfile_operationsのioctlに、ユーザ空間へとstackをpivotするガジェットを入れてropした。 pivot先に目的のrop chainを用意しておきcommit_creds(prepare_kernel_commit(0))を実行して、root権限を取った。