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 0000000..522129e Binary files /dev/null and b/examples/bodu/flag.enc differ 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 0000000..7e0c57a Binary files /dev/null and b/examples/crypto_baby/flag.enc differ diff --git a/examples/crypto_baby/solve.py b/examples/crypto_baby/solve.py new file mode 100644 index 0000000..d5ad9bc --- /dev/null +++ b/examples/crypto_baby/solve.py @@ -0,0 +1,69 @@ +""" +EXAMPLE: ASIS Finals 2018 — "Made by baby" (Theory, 141p / 29 solves) +https://github.com/p4-team/ctf/tree/master/2018-11-24-asis-finals/crypto_baby + +VULN: hidden-base knapsack. The plaintext `num` (a PNG) has binary bits b_i +(LSB-first). The ciphertext is + C = sum_i b_i*(exp^i + (-1)^i) = (sum_i b_i*exp^i) + S +where S = sum_i b_i*(-1)^i is a tiny correction. So (C - S) written in base +`exp` has digits exactly equal to b_i (each 0/1). Once we recover b_i we rebuild +the PNG as num = sum_i b_i * 2^i (binary). Only the low bytes of the PNG are +disturbed by the unknown <=512-bit `key`; the header survives. + +Run: + cd /home/code/ctfkit + python3 examples/crypto_baby/solve.py +""" +from Crypto.Util.number import bytes_to_long, long_to_bytes + +HERE = __file__.rsplit("/", 1)[0] + + +def main(): + data = open(f"{HERE}/flag.enc", "rb").read() + C = bytes_to_long(data) + print(f"C bits = {C.bit_length()}") + + for base in range(2, 40): + for S in range(-200, 201): + V = C - S + if V <= 0: + continue + # digits of V in `base`, LSB-first, all must be 0/1 + bits = [] + v = V + ok = True + n = 0 + while v > 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()