Sharif CTF 2018 kdb writeup
はじめに
一人writeup advent calendarの4日目です。1日1問分のwriteupを目標に頑張っていきます。 4日目の問題は、Sharif CTF 2018で出題された「kdb」。kUAF (kernel Use After Free) を起点としたkernel exploit問題で初めての人にオススメです。
カーネルの情報 (セキュリティ機構など)
配布ファイルは以下の通り。 run.shを中身を見てみると、smep、kaslrが有効になっている。
% ls bzImage rootfs.cpio run.sh* % cat run.sh #!/bin/sh qemu-system-x86_64 -cpu kvm64,+smep -m 64M -kernel ./bzImage -initrd ./rootfs.cpio -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" -smp cores=2,threads=1,sockets=1 -monitor /dev/null -nographic 2>/dev/null
解析
攻略対象は/dev/kdb
。
/dev/kdb
に対する操作はopenやreleaseなどが定義されているが、ioctl()がメインの処理となっている。
ioctl(fd, cmd, args);と言った感じでユーザ空間から呼んでやると以下の処理を実行する。
cmdによって実行する内容が異なっている。
0x13371338: alloc_buf()を呼んでbufを新規作成。argsは先頭20byteは作成されたbufのnameに書き込まれる。次の8byteはsizeで、kmalloc()を呼ぶ時の引数となっている。
0x13371339: find_cbuf()でbufの探索。argsは先頭20byteで指定した文字列と合致するnameを持つbufを探す。argsの次の8byteは使われない。そのまた次の8byteがユーザ空間のポインタとなっている。copy_to_user()で見つけたbufのptrの中身をsize分だけ書き出す。
0x1337133a: 0x13371339の逆の処理で、copy_from_user()で書き込む。
- 0x1337133d: find_cbuf()で探索したbufが存在した場合、free_buf()で解放する。
- 0x1337133f: argsにはname[0x20]; user_ptr; len;ように30byte分設定しておく。find_cbuf()でbufを探索し、見つかったbufのsizeが、lenより小さい場合、kfree(ptr)してから新しくkmalloc(len)で領域を確保する。その後copy_from_user()で書き込む。lenが超えていない場合は、元のptrにそのままcopy_from_user()で書き込む。
使用される構造体は以下の通り。双方向のリンクドリストとなっている。
struct buf { char name[0x20]; char* ptr; long size; struct buf *next; struct buf *prev; };
exploit
ioctl(0x1337133F)には、kfree()した後にポインタを適切に処理せずに終了するパスが存在する。 つまり、ioctl()を呼ぶ時の引数を工夫することで、kUAFがおこせる。 具体的には、まず引数で設定するサイズはオブジェクトのサイズよりも大きくしてkfree()を実行させる。 その後、書き込む元のユーザランドのポインタとサイズの合計がユーザ空間内に収まっているかチェックが行われる。 このチェックに引っかかるとkfree()したポインタはそのままでretrunするためkUAFとなる。
この問題環境はkaslrが有効なので、まずはカーネル空間のアドレスをリークする必要がある。
取得した領域は初期化されず、さらに取得した領域から自由に読み込みができるので、未初期化の空間からカーネル空間のアドレスを特定できそうなアドレスを得る。
具体的には、/dev/ptmx
のオープン時に作成されるtty_struct構造体がもつメンバであるopsのアドレスからカーネル空間に関するリークができる。
opsはptm_unix98_opsを指しており、これはカーネル空間のdata(?)を指している。
そのため、そのアドレスからカーネルがマップされているベースのアドレスが計算できる。
リーク後は、ropでcommit_creds(prepare_kernel_commit(0));を実行後にswapgs+iretqでユーザ空間へ戻り、シェルを起動する。 このropの流れは、今までのwriteupのものと同じなので詳細については省略する。