ブログ未満のなにか

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

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/ptmxopsを書き換えたままなので、そのままだとroot権限のコマンドを1回実行したらカーネルパニックとなってしまう (実際になった)。 なので、不正に上書きしたopsを正常なものに書き戻しておく必要がある。 exploit内では、一度正常な状態のttyを保存してから不正な状態に書き換えている。 そして上手くroot権限が取れた後に、正常な状態に書き戻している。

gist.github.com

参照URL