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
This commit is contained in:
+15
-2
@@ -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
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,9 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBHjANBgkqhkiG9w0BAQEFAAOCAQsAMIIBBgKBgAOmFghI+xc0y9D6Is71guhJ
|
||||
IjrARRDVFQJVa2R20HOX8D3xVSicIBEuh8bzU2HZ62IspKDlLZzYe/cjUmyCa4g4
|
||||
fQarxCeeNT8SrY7GLqc8RzIaILiWRIiaeSpzFSvHAUuAppPS5YsSP6klw1ax66A3
|
||||
pNysjY3oCRZ6b8wwxceFAoGAA2WWLo2rp7qS/Ah2il9zs4VPTHmWnVUYoHigNEN8
|
||||
Rmm9twW+TYuLq/T9oabnFSaeh7KO7LDU4Ccmon+4chhjdAcg9YNojlVn6xBym7DZ
|
||||
KzItcZlJ5AxXGY12TxxjPl4nfaPTKB7OLOLrTflFvlr8PnhJjtBImyRZBZZk/hXI
|
||||
ijM=
|
||||
-----END PUBLIC KEY-----
|
||||
@@ -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()
|
||||
Binary file not shown.
@@ -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()
|
||||
Reference in New Issue
Block a user