NCSTISC 2018 babydriver writeup
はじめに
一人writeup advent calendarの3日目です。1日1問分のwriteupを目標に頑張っていきます。 3日目の問題は、NCSTISC CTF 2018で出題された「babydriver」。kUAF (kernel Use After Free) を起点としたkernel exploit問題で初めての人にオススメです。
カーネルの情報 (セキュリティ機構など)
配布されるファイルは以下の通り。boot.shが起動スクリプトで、起動するとqemuが立ち上がる。 boot.shの中身を見ると、smepが有効になっている。また、起動してカーネルのバージョンを確認すると、4.4系であった。
% ls boot.sh* bzImage* rootfs.cpio* % cat boot.sh #! /bin/sh qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic -smp cores=1,threads=1 -cpu kvm64,+smep [In qemu] / $ uname -a Linux (none) 4.4.72 #1 SMP Thu Jun 15 19:52:50 PDT 2017 x86_64 GNU/Linux / $ cat /proc/cpuinfo | grep flags flags : fpu vme 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 pni cx16 x2apic hypervisor smep
解析
rootfs.cpioを展開すると、/lib/modules/4.4.72/babydriver.ko
に攻略対象となるカーネルモジュールがある。
babydriver_init()がモジュールの初期化関数で、/dev/babydev
を作成し登録している。
デバイスに対する動作は、以下の通りとなっている。
babyopen : ユーザから
/dev/babydev
をopen()すると呼ばれる。カネールモジュールのbss領域にあるbabydev_structを初期化する (構造は以下の通り)。device_bufにkmem_cache_alloc_trace()で得られたポインタを入れ、device_buf_lenに0x40を入れる。kmem_cache_alloc_trace()で取得されるチャンクのサイズは0x40となっている。babyrelease :
/dev/babydev
をclose()すると呼ばれる。kfree(babydev_struct.device_buf)を呼ぶ。babywrite :
/dev/babydev
にwrite()すると呼ばれる。device_buf_lenがwrite()で書き込むサイズよりも大きい場合、copy_from_user(device_buf, user_buf, len)が呼ばれる。babyread :
/dev/babydev
にread()すると呼ばれる。device_buf_lenがread()で読み込むサイズよりも大きい場合、copy_to_user(user_buf, device_buf, len)が呼ばれる。babyioctl :
/dev/babydev
にioctl()すると呼ばれる。cmdが0x10001で呼び出すと処理が実行される。ioctl(fd, 0x10001, size)といった感じで呼ぶと、元々のbabydev_struct.device_bufをkfree()して、新しくkmalloc(size)で取得されたポインタを入れる。device_buf_lenにはsizeが入る。
struct babydev_struct { char* device_buf; long device_buf_len; }
exploit
babydev_strudtはモジュールのbssで管理されており、/dev/babydev
を複数回開いてそれぞれのfd経由で呼び出しても同じ箇所を使用する。
そこで/dev/babydev
を2度open()し、片方をclose()する。
すると、close()する際にbabyrelease()が呼ばれて、babydev_struct.device_bufはkfree()されるが、もう片方のfd経由ではbabydev_struct.device_bufを参照している状態にあり、kUAFとなる。
kUAFを起こせるで、適当なオブジェクトを被せて制御を奪っていく。
今回は/dev/ptmx
が存在するので、これを使う。
/dev/ptmx
を開いた時、tty_struct構造体用の領域がkmalloc()で取得される。
このtty_struct構造体にはopsというメンバを持っており、これはtty_operationsで定義される関数テーブルとなっている。opsを書き換えることで、制御が奪える。
tty_struct構造体は、kmalloc-256に該当するオブジェクトなので、ioctl()で事前にサイズを256byteに変更しておく。
// https://elixir.bootlin.com/linux/v4.4.72/source/include/linux/tty.h#L259 struct tty_struct { int magic; struct kref kref; struct device *dev; struct tty_driver *driver; const struct tty_operations *ops; // これを書き換えたい int index; あとは省略 }
今回はsmapが無効なので、偽のtty_operationsをユーザ空間に用意できる。
kUAFで重複している/dev/ptmx
が指すopsメンバを、ユーザ空間に用意した偽のものを指すように書き換える。
root権限を取るまでの流れは、基本に忠実にropで実行してく。 ユーザ空間に予め偽のstackとrop chainを用意して、そこへpivotさせる。 ropでは、commit_creds(prepare_kernel_cred(0))を呼び出し、swapgs+iretqでユーザ空間へ復帰し、シェルを起動する。 pivotする先は、0x01740100で固定なのであらかじめmmapでその近辺を確保しておく。
kUAFで重複している/dev/ptmx
のopsを書き換えたままなので、そのままだとroot権限のコマンドを1回実行したらカーネルパニックとなってしまう (実際になった)。
なので、不正に上書きしたopsを正常なものに書き戻しておく必要がある。
exploit内では、一度正常な状態のttyを保存してから不正な状態に書き換えている。
そして上手くroot権限が取れた後に、正常な状態に書き戻している。