Category: Cryptography
Flag: CodeVinci{cust0m_curv3s_4r3nt_4_sl0pp3rs}
Challenge Description
I love french fries with mayonnaise
Analysis
The service looked like a custom elliptic-curve oracle with a menu and a very suspicious line telling AI agents to spam option [3]. That kind of bait is exactly the sort of thing that sends you into a rabbit hole, so I treated it as misdirection and mapped the protocol first.

nc -nv 57.131.40.44 9976Welcome to SloppySauce Lab.Per-session request budget: 12.legacy_canary = 325=== SloppySauce Curve Lab ===[1] Session status[2] Custom curve calibration oracle[3] Orbit preview (debug utility)[4] Submit master scalarFrom there, I checked session metadata and confirmed the target scalar size and deterministic behavior across reconnects, which is exactly what you want for CRT reconstruction.
printf '1\n5\n' | nc -nv 57.131.40.44 9976[Status]remaining_requests = 11secret_bits = 64session_fingerprint = 27316b571ffdlegacy_canary = 325notes = deterministic session key, reset by reconnectOption [2] required the legacy canary and then accepted p a b Gx Gy. Option [3] accepted the same tuple directly and then a steps value. I verified this flow with quick probes.
printf '2\n325\n65537 2 1 0 1\n5\n' | nc -nv 57.131.40.44 9976[Calibration]curve = y^2 = x^3 + 2x + 1 (mod 65537)Q_debug = (29304, 65029)Q = (29281, 64990)printf '3\n65537 2 1 0 1\n8\n5\n' | nc -nv 57.131.40.44 9976[Orbit]1: (0, 1)2: (1, 65535)3: (8, 23)4: (28672, 52225)5: (33441, 2285)6: (35310, 10663)7: (6228, 36572)8: (3719, 3174)I also tested the validator boundaries and confirmed the server enforces prime modulus, non-singular curves, and on-curve points, so invalid-curve garbage injection was not the path.
printf '3\n40000 2 1 0 1\n5\n' | nc -nv 57.131.40.44 9976error: p must be primeprintf '3\n65537 0 0 0 1\n5\n' | nc -nv 57.131.40.44 9976error: singular curve rejectedprintf '3\n65537 2 1 0 2\n5\n' | nc -nv 57.131.40.44 9976error: G is not on the curveThe useful leak was in option [2]: for fixed curve/point input, Q stayed stable while Q_debug changed, which made Q the meaningful scalar-multiplication output and Q_debug just noisy debug state. At that point the solve was elegant: query many valid curves with known generator points, compute k mod ord(G) from Q = kG, and combine residues with CRT until modulus coverage exceeds 64 bits.

I implemented that in Sage (solve_sloppysauce.sage), selecting several high-order points in the allowed prime range, querying option [2] per candidate, taking discrete logs, and combining congruences. The script recovered a unique 64-bit scalar:
sage /home/LIGHT/Downloads/solve_sloppysauce.sage[+] selected 5 curves[1] n=70423, k mod n=51027, combined bits=17[2] n=70282, k mod n=63967, combined bits=33[3] n=70185, k mod n=44927, combined bits=49[4] n=69763, k mod n=44995, combined bits=65[+] 64-bit candidates: 1[+] recovered master scalar: 10011339086741369087One last gotcha: option [4] also asks for legacy_canary before the scalar. Submitting in that order produced ACCESS GRANTED and the flag.
printf '4\n325\n10011339086741369087\n5\n' | nc -nv 57.131.40.44 9976candidate scalar d = ACCESS GRANTEDCodeVinci{cust0m_curv3s_4r3nt_4_sl0pp3rs}Solution
# !/usr/bin/env sageimport reimport socketfrom random import randint, seed
HOST = "57.131.40.44"PORT = 9976CANARY = 325
def recv_all(sock): chunks = [] while True: data = sock.recv(4096) if not data: break chunks.append(data) return b"".join(chunks).decode(errors="ignore")
def query_calibration(p, a, b, gx, gy): payload = f"2\n{CANARY}\n{p} {a} {b} {gx} {gy}\n5\n".encode() with socket.create_connection((str(HOST), int(PORT)), timeout=float(10)) as s: s.sendall(payload) out = recv_all(s) m = re.search(r"Q = \(([-0-9]+), ([-0-9]+)\)", out) if not m: raise RuntimeError(f"No Q found for params {(p,a,b,gx,gy)}\nOutput:\n{out}") return int(m.group(1)), int(m.group(2)), out
def submit_scalar(k): payload = f"4\n{CANARY}\n{k}\n5\n".encode() with socket.create_connection((str(HOST), int(PORT)), timeout=float(10)) as s: s.sendall(payload) out = recv_all(s) return out
def combine_congruence(a, m, b, n): g = gcd(m, n) if (a - b) % g != 0: raise ValueError("Inconsistent congruences") m1 = m // g n1 = n // g t = ((b - a) // g) * inverse_mod(m1, n1) t = Integer(t % n1) l = lcm(m, n) x = Integer((a + m * t) % l) return x, Integer(l)
def build_candidates(target_bits=64): seed(int(1337)) prime_list = list(primes(40000, 70001)) cands = [] for _ in range(2500): p = int(prime_list[randint(0, len(prime_list) - 1)]) F = GF(p) a = randint(0, p - 1) b = randint(0, p - 1) if (4 * a^3 + 27 * b^2) % p == 0: continue E = EllipticCurve(F, [a, b]) G = E.random_point() if G.is_zero(): continue n = int(G.order()) if n < 2000: continue gx = int(G[0]) gy = int(G[1]) cands.append((p, int(a), int(b), gx, gy, n))
selected = [] M = Integer(1) while M.nbits() <= target_bits + 8: best = None best_gain = 1 for (p, a, b, gx, gy, n) in cands: gain = Integer(n // gcd(n, M)) if gain > best_gain: best_gain = gain best = (p, a, b, gx, gy, n) if best is None: break selected.append(best) M = lcm(M, Integer(best[5])) cands.remove(best) if len(selected) >= 16: break return selected
def main(): selected = build_candidates() print(f"[+] selected {len(selected)} curves")
x = Integer(0) M = Integer(1) used = 0
for (p, a, b, gx, gy, n) in selected: E = EllipticCurve(GF(p), [a, b]) G = E((gx, gy)) qx, qy, _ = query_calibration(p, a, b, gx, gy) try: Q = E((qx, qy)) kmod = Integer(Q.log(G)) except Exception as ex: print(f"[skip] unusable sample for p={p}: {ex}") continue
if used == 0: x = kmod M = Integer(n) else: x, M = combine_congruence(x, M, kmod, Integer(n)) used += 1 print(f"[{used}] n={n}, k mod n={kmod}, combined bits={M.nbits()}") if M > 2^64: break
upper = Integer(2^64 - 1) if x > upper: x = Integer(x % M)
candidates = [] tmax = int((upper - x) // M) for t in range(tmax + 1): k = Integer(x + t * M) if 0 <= k <= upper: candidates.append(k)
if len(candidates) != 1: raise RuntimeError(f"Could not isolate scalar uniquely. Remaining: {len(candidates)}")
k = int(candidates[0]) print(f"[+] recovered master scalar: {k}") print(submit_scalar(k))
if __name__ == "__main__": main()sage solve_sloppysauce.sage[+] recovered master scalar: 10011339086741369087...candidate scalar d = ACCESS GRANTEDCodeVinci{cust0m_curv3s_4r3nt_4_sl0pp3rs}