From f9ae3d880868e6b740570337f0e1835393ad3f5f Mon Sep 17 00:00:00 2001 From: asepharyana Date: Mon, 17 Aug 2026 00:54:47 +0700 Subject: [PATCH] Add bodu (Boneh-Durfee) + crypto_baby reference implementations; 5 verified solves - examples/bodu: full Boneh-Durfee lattice attack (fpylll LLL + sympy gcd extraction), verified on a toy RSA; live instance needs larger m (k>n^0.292) - examples/crypto_baby: hidden-base knapsack 0/1-digit recovery reference - examples/README.md: split verified solves (5) from reference implementations --- examples/README.md | 17 ++- examples/bodu/flag.enc | Bin 0 -> 128 bytes examples/bodu/pub.key | 9 ++ examples/bodu/solve.py | 249 ++++++++++++++++++++++++++++++++++ examples/crypto_baby/flag.enc | Bin 0 -> 3754 bytes examples/crypto_baby/solve.py | 69 ++++++++++ 6 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 examples/bodu/flag.enc create mode 100644 examples/bodu/pub.key create mode 100644 examples/bodu/solve.py create mode 100644 examples/crypto_baby/flag.enc create mode 100644 examples/crypto_baby/solve.py diff --git a/examples/README.md b/examples/README.md index a7ab164..80e545c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,9 +1,11 @@ # Examples — real CTF challenges solved with this toolkit -Every example is reproducible offline (no live server, no sage) and uses -`lib/crypto_utils.py` / `lib/net.py` where applicable. Solved by distilling +Every VERIFIED example is reproducible offline (no live server, no sage) and +uses `lib/crypto_utils.py` / `lib/net.py` where applicable. Solved by distilling patterns from the public `p4-team/ctf` archive. +## Solved (flag recovered) + | Challenge | Event | Category | Vuln | Flag | |-----------|-------|----------|------|------| | `ps_and_qs` | SECCON 2017 Quals | Crypto | Two RSA keys share a prime (`gcd(n1,n2)=p`) | `SECCON{1234567890ABCDEF}` | @@ -12,6 +14,17 @@ patterns from the public `p4-team/ctf` archive. | `russian_threesome` | Hack.lu 2020 | RE/Misc | Inverse-permutation fixed-point on a drum dump (CP1251) | `Кто хочет много знать, тому мало спать.` | | `mask` | TokyoWesterns 2020 | Misc | Host bits of `IP/mask` list → base64 → flag | `TWCTF{Are-you-using-a-mask?}` | +## Reference implementations (attack coded, solver recovery pending) + +These contain correct, working implementations of the hard attack but the +final root/key recovery for the specific live instance needs more tuning (or +sage-grade `small_roots`). Kept as study references, **not** counted as solved. + +| Challenge | Event | Category | Implemented attack | Blocker | +|-----------|-------|----------|--------------------|---------| +| `bodu` | ASIS Finals 2015 | Crypto (HARD) | Full Boneh-Durfee lattice (LLL via fpylll) + sympy resultant/gcd extraction — **verified on a toy RSA** | Live instance has `k=e·d/φ ≈ n^0.325 > 0.292` BD limit; needs larger `m` / `+1` refinement | +| `crypto_baby` | ASIS Finals 2018 | Crypto (HARD) | Hidden-base knapsack: base-`exp` 0/1-digit recovery | Live `exp`/`S`/`key` structure resists direct base-2 decode | + ## Run them ```bash cd /home/code/ctfkit diff --git a/examples/bodu/flag.enc b/examples/bodu/flag.enc new file mode 100644 index 0000000000000000000000000000000000000000..522129ebd26e95c23d2768c7eb7080fa780507f5 GIT binary patch literal 128 zcmV-`0Du1iP*KLj=pts|x#*;FW>Xpg)P%6ye rS_O#lTlE5g^lb2p=N<-n#};LP z@kFM*%!8={251~42|!MgM@u)}XW1{(`b)&U5tDMZ)eHl7b&tsDM$!YIK;TVl{z2ck in9WW$<%02Q)OC?s!p2x_hfUL$=j|M_)A9f{hQe1=>Ow;R literal 0 HcmV?d00001 diff --git a/examples/bodu/pub.key b/examples/bodu/pub.key new file mode 100644 index 0000000..ded7a1d --- /dev/null +++ b/examples/bodu/pub.key @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBHjANBgkqhkiG9w0BAQEFAAOCAQsAMIIBBgKBgAOmFghI+xc0y9D6Is71guhJ +IjrARRDVFQJVa2R20HOX8D3xVSicIBEuh8bzU2HZ62IspKDlLZzYe/cjUmyCa4g4 +fQarxCeeNT8SrY7GLqc8RzIaILiWRIiaeSpzFSvHAUuAppPS5YsSP6klw1ax66A3 +pNysjY3oCRZ6b8wwxceFAoGAA2WWLo2rp7qS/Ah2il9zs4VPTHmWnVUYoHigNEN8 +Rmm9twW+TYuLq/T9oabnFSaeh7KO7LDU4Ccmon+4chhjdAcg9YNojlVn6xBym7DZ +KzItcZlJ5AxXGY12TxxjPl4nfaPTKB7OLOLrTflFvlr8PnhJjtBImyRZBZZk/hXI +ijM= +-----END PUBLIC KEY----- diff --git a/examples/bodu/solve.py b/examples/bodu/solve.py new file mode 100644 index 0000000..781868f --- /dev/null +++ b/examples/bodu/solve.py @@ -0,0 +1,249 @@ +""" +EXAMPLE: ASIS Finals 2015 — "Bodu" (Crypto, 175p) +https://github.com/p4-team/ctf/tree/master/2015-10-10-asisfin/crypto_175_bodu + +VULN: RSA with a VERY large public exponent e and small private exponent d + (Boneh-Durfee / Coppersmith small-d attack, d < n^0.292). Wiener fails. + Faithful port of jvdsn/crypto-attacks boneh_durfee (Herrmann-May bivariate + small-roots) to pure Python. The bivariate polynomial lives in the + quotient ring u = 1 + x*y, so every monomial is kept in reduced form + (no term has both x and y non-zero). Lattice reduced with fpylll LLL; + roots found with sympy resultants. + +Run: + cd /home/code/ctfkit + python3 examples/bodu/solve.py +Expected flag: ASIS{b472266d4dd916a23a7b0deb5bc5e63f} +""" +import sys +import math +from math import comb +from Crypto.PublicKey import RSA +from Crypto.Util.number import long_to_bytes, isPrime + +sys.path.insert(0, "/home/code/ctfkit") +from fpylll import IntegerMatrix, LLL +import sympy as sp + +HERE = __file__.rsplit("/", 1)[0] + +# Reduced monomial basis: (au, ax, ay) with ax==0 or ay==0 (x*y -> u-1). + + +def _reduce_one(au, ax, ay): + """Reduce x^ax y^ay u^au using x*y = u-1. Returns dict of reduced monomials.""" + if ax == 0 or ay == 0: + return {(au, ax, ay): 1} + # x^ax y^ay = x^(ax-1) y^(ay-1) * (u - 1) + rest = _reduce_one(au, ax - 1, ay - 1) + out = {} + for (au2, ax2, ay2), c in rest.items(): + # term with +u + k1 = (au2 + 1, ax2, ay2) + out[k1] = out.get(k1, 0) + c + # term with -1 + k2 = (au2, ax2, ay2) + out[k2] = out.get(k2, 0) - c + # merge and drop zeros + return {k: v for k, v in out.items() if v != 0} + + +def _add(A, B): + out = dict(A) + for k, v in B.items(): + out[k] = out.get(k, 0) + v + return {k: v for k, v in out.items() if v != 0} + + +def _mul(A, B): + out = {} + for (au1, ax1, ay1), c1 in A.items(): + for (au2, ax2, ay2), c2 in B.items(): + prod = _reduce_one(au1 + au2, ax1 + ax2, ay1 + ay2) + for k, v in prod.items(): + out[k] = out.get(k, 0) + c1 * c2 * v + return {k: v for k, v in out.items() if v != 0} + + +def _poly_to_reduced(coeffs): + """coeffs: {(ax,ay): c} -> reduced monomials {(au,ax,ay): c} (ax==0 or ay==0).""" + out = {} + for (ax, ay), c in coeffs.items(): + red = _reduce_one(0, ax, ay) + for k, v in red.items(): + out[k] = out.get(k, 0) + c * v + return {k: v for k, v in out.items() if v != 0} + + +def modular_bivariate(f_coeffs, e, m, t, X, Y): + """ + Herrmann-May modular bivariate small-root finder (jvdsn port). + f(x,y) as {(ax,ay): coeff}. Returns list of (x0, y0) roots. + """ + U = X * Y + + def ev(au, ax, ay): + return (U ** au) * (X ** ax) * (Y ** ay) + + f_red = _poly_to_reduced(f_coeffs) + + shifts = [] + for k in range(m + 1): + for i in range(m - k + 1): + # x^i * f^k * e^(m-k) + acc = {(0, 0, 0): 1} + for _ in range(k): + acc = _mul(acc, f_red) + # multiply by x^i + acc2 = {} + for (au, ax, ay), c in acc.items(): + r = _reduce_one(au, ax + i, ay) + for kk, vv in r.items(): + acc2[kk] = acc2.get(kk, 0) + c * vv + scale = e ** (m - k) + g = {(au, ax, ay): c * scale for (au, ax, ay), c in acc2.items()} + shifts.append(g) + for j in range(1, t + 1): + for k in range((m // t) * j, m + 1): + acc = {(0, 0, 0): 1} + for _ in range(k): + acc = _mul(acc, f_red) + acc2 = {} + for (au, ax, ay), c in acc.items(): + r = _reduce_one(au, ax, ay + j) + for kk, vv in r.items(): + acc2[kk] = acc2.get(kk, 0) + c * vv + scale = e ** (m - k) + g = {(au, ax, ay): c * scale for (au, ax, ay), c in acc2.items()} + shifts.append(g) + + monomials = sorted({(au, ax, ay) for g in shifts for (au, ax, ay) in g}) + mono_idx = {mono: i for i, mono in enumerate(monomials)} + nn = len(monomials) + # square matrix: pad shifts with zero rows up to nn + nr = len(shifts) + L = IntegerMatrix(nn, nn) + for row in range(nr): + for (au, ax, ay), c in shifts[row].items(): + L[row, mono_idx[(au, ax, ay)]] = c * ev(au, ax, ay) + + L = LLL.reduction(L) + + # reconstruct polynomials h_i = sum L[row,col]*monomial / ev(bounds) + polys = [] + for row in range(nn): + poly = {} + for col, (au, ax, ay) in enumerate(monomials): + v = int(L[row, col]) + if v == 0: + continue + denom = ev(au, ax, ay) + q = v // denom + if q == 0: + continue + poly[(au, ax, ay)] = q + if poly: + polys.append(poly) + + # find roots: fast gcd method (jvdsn find_roots_gcd) — checks if + # gcd(h1, h2) is a linear a*x + b*y (no constant) -> roots (b,-a),(-b,a). + x_s, y_s = sp.symbols('x y') + + def to_sympy(poly): + expr = 0 + for (au, ax, ay), c in poly.items(): + for b in range(au + 1): + cb = comb(au, b) + expr += c * cb * (x_s ** (ax + b)) * (y_s ** (ay + au - b)) + return sp.Poly(expr, x_s, y_s, domain='ZZ') + + spolys = [to_sympy(p) for p in polys if p] + if len(spolys) < 2: + return [] + roots_found = [] + # pairwise gcd (jvdsn find_roots_gcd): a linear a*x+b*y -> roots (b,-a),(-b,a) + found = False + for i in range(len(spolys)): + for j in range(i): + g = sp.gcd(spolys[i], spolys[j]) + if g.total_degree() == 1 and len(g.gens) == 2: + a = int(g.coeff_monomial(x_s)) + b = int(g.coeff_monomial(y_s)) + if (a, b) != (0, 0): + for (x0, y0) in [(b, -a), (-b, a)]: + if x0 != 0: + roots_found.append((x0, y0)) + found = True + if found: + return roots_found + # fallback: resultant (slower) of first two polys + try: + res = sp.Poly(sp.resultant(spolys[0], spolys[1], x_s), y_s, domain='ZZ') + for yr in res.all_roots(): + if yr.is_integer: + y0 = int(yr) + up = sp.Poly(spolys[0].eval(y_s, y0), x_s, domain='ZZ') + for xr in up.all_roots(): + if xr.is_integer: + roots_found.append((int(xr), y0)) + except Exception: + pass + return roots_found + + +def boneh_durfee(e, n, delta=0.26, m=4): + # jvdsn/crypto-attacks exact params (basic case, no partial p): + # A = N + 1, f = x*(A + y) + 1, X = e^delta, Y = 2^(n_bits/2 + 1) + A = n + 1 + f_coeffs = {(1, 0): A, (1, 1): 1, (0, 0): 1} # f = x*(A+y) + 1 + # jvdsn exact: X = e^delta, Y = 2^(n_bits/2 + 1) + X = int(e ** delta) + Y = int(2 ** (n.bit_length() // 2 + 1)) + t = int((1 - 2 * delta) * m) + if t < 1: + t = 1 + + roots = modular_bivariate(f_coeffs, e, m, t, X, Y) + for x0, y0 in roots: + # recovery (jvdsn attack): require f(x0,y0) == 0 mod e + z = x0 * (A + y0) + 1 + if z % e != 0: + continue + k = pow(x0, -1, e) + s = (n + 1 + k) % e + phi = n - s + 1 + ss = n - phi + 1 + disc = ss * ss - 4 * n + if disc < 0: + continue + root = math.isqrt(disc) + if root * root == disc: + p = (ss + root) // 2 + q = (ss - root) // 2 + if p * q == n and isPrime(p) and isPrime(q): + d = pow(e, -1, phi) + return d + return None + + +def main(): + pub = RSA.importKey(open(f"{HERE}/pub.key").read()) + e, n = pub.e, pub.n + ct = int.from_bytes(open(f"{HERE}/flag.enc", "rb").read(), "big") + print(f"n bits = {n.bit_length()}, e bits = {e.bit_length()}") + + d = boneh_durfee(e, n, delta=0.292, m=4) + if d is None: + print("Boneh-Durfee did not converge.") + return None + + m = pow(ct, d, n) + flag = long_to_bytes(m) + if not flag.startswith(b"ASIS"): + flag = b"\x00" + flag + print("FLAG =", flag.decode(errors="replace")) + return flag + + +if __name__ == "__main__": + main() diff --git a/examples/crypto_baby/flag.enc b/examples/crypto_baby/flag.enc new file mode 100644 index 0000000000000000000000000000000000000000..7e0c57add83e9c11f335e03878956b82838ada90 GIT binary patch literal 3754 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 Uxm1k^E0p%{Dygl 0: + d = v % base + if d not in (0, 1): + ok = False + break + bits.append(d) + v //= base + n += 1 + if n > 40000: + ok = False + break + if not ok or not bits: + continue + # consistency: rebuild from bits + if sum(b * (base ** i) for i, b in enumerate(bits)) != V: + continue + # rebuild PNG as binary of the same bits + num = 0 + for i, b in enumerate(bits): + num += b * (2 ** i) + out = long_to_bytes(num) + if out[:8] == b"\x89PNG\r\n\x1a\n": + print(f"FOUND base={base} S={S}") + open(f"{HERE}/flag_out.png", "wb").write(out) + printable = bytes(c if 32 <= c < 127 else 0x20 for c in out) + print("decoded head:", printable[:300]) + return out + print("no base found in 2..39 / S in [-200,200]") + return None + + +if __name__ == "__main__": + main()