Crypto Party 2
A UUID4 used as an ECDSA nonce has ~106 unpredictable bits, recovered with a 168-dimension lattice.
NNS{bu7_uu1ds_4r3_r4nd0m!!_64c7593130} Challenge summary
A remote service holds an ECDSA (NIST P-256) private key secret_key,
uses it to sign up to 6 attacker-chosen messages (“friend names”), and
encrypts the flag with AES-ECB under key = long_to_bytes(secret_key, 32).
Each signature’s nonce is:
k = bytes_to_long(str(uuid.uuid4())[:32].encode())
i.e. NOT a proper random 256-bit scalar — it’s the literal ASCII bytes of the first 32 characters of a random UUID4 string.
Vulnerability
A UUID4 string has the fixed layout xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
(y in {8,9,a,b}). Restricting to the first 32 characters and reading the
big-endian byte value at each position:
- positions 8, 13, 18, 23 = literal ’-’ (0x2D)
- position 14 = literal ‘4’ (0x34, UUID version)
- position 19 = one of {‘8’,‘9’,‘a’,‘b’} (UUID variant)
- the other 26 positions = an independent random ASCII hex digit, i.e. one of 16 values in {0x30..0x39} u {0x61..0x66}
So k = a known constant (from the fixed bytes) + a sum of 26 (+1 small)
per-position “offset” terms, each bounded to a small range and multiplied
by a known place-value weight (a power of 256). Effectively only ~106 of
k’s 256 bits are unpredictable — this is a biased/partially-known ECDSA
nonce, recoverable via the Hidden Number Problem (HNP) and lattice
reduction (LLL), using signatures over messages we chose ourselves (so we
get h = SHA256(m) for free).
Per-signature relation: s*k = h + r*d (mod n) => k = A + B*d (mod n)
with A = h*s^-1 mod n, B = r*s^-1 mod n computable from each captured
(m, r, s).
Attack
- Captured a batch of 6 signatures from one live connection.
- Modeled each nonce’s 26 unknown hex-digit bytes (and the 1 variant byte) as small non-negative integer offsets from a known base — using the interval relaxation [0,54] per hex-digit slot (not the exact 16-value alphabet) was precise enough; no extra case-splitting needed.
- Eliminated the shared unknown
dby taking pairwise differences of the 6 per-signature HNP relations against a reference signature (signature 0):B_0 * k_a - B_a * k_0 = B_0*A_a - B_a*A_0 (mod n)for a=1..5. This leaves only the small per-signature nonce offsets as unknowns (6 nonces * 27 small unknowns = 162 total), no more direct dependence on the (much larger, ~256-bit-range) secret key inside the lattice target. - Built an LLL lattice of dimension 168 (162 “offset” coordinates scaled
to read off directly + 5 “equation” coordinates scaled up to force
them to exactly 0 + 1 Kannan-embedding coordinate), reduced it with
python-flint’s
fmpz_mat.lll(). The short vector containing the embedding coordinate at +-weight directly gives all 162 offsets. - Reconstructed nonce k_0 from the recovered offsets, then
d = (k_0 - A_0) * B_0^{-1} mod n. - Sanity-checked every recovered k_i decodes to a structurally valid 32-byte UUID-prefix (dashes/version/variant in the right spots) before trusting the recovered key.
key = long_to_bytes(d, 32), AES-ECB-decryptct-> flag.
(Note: an earlier attempt that kept d as an explicit ~256-bit unknown
directly in the lattice, rather than eliminating it via pairwise
differencing first, failed — the embedding target ended up essentially
as large as the Gaussian-heuristic shortest-vector estimate, leaving LLL
no safety margin. Removing d algebraically before building the lattice
was the key fix.)
Lesson
“Looks random” isn’t the same as “is random” — a UUID4 has ~122 bits of real entropy inside a 128-bit-looking, 36-character string, and truncating
- using its ASCII form directly as a nonce leaks the fixed format bytes outright and constrains every other byte to a 16-value alphabet. Any structured/low-entropy ECDSA nonce, even one that “looks like enough bits” at a glance, is a lattice attack waiting to happen once you can collect a handful of signatures.