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 = 11
secret_bits = 64
session_fingerprint = 27316b571ffd
legacy_canary = 325
notes = 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/rei/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 GRANTED
CodeVinci{cust0m_curv3s_4r3nt_4_sl0pp3rs}Solution
# solve_sloppysauce.sage
#!/usr/bin/env sage
import re
import socket
from random import randint, seed
HOST = "57.131.40.44"
PORT = 9976
CANARY = 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 GRANTED
CodeVinci{cust0m_curv3s_4r3nt_4_sl0pp3rs}