Flag: zdk{4_F3w_BL7S_per_51gna7uR3_SlNKs_tHE_key}
TL;DR
The server's ECDSA nonce leaks its top 10 bits through a publicly computable function of the message. A handful of signatures is enough to recover the private key via a lattice-based Hidden Number Problem (HNP) attack, forge a signature on a message the server refuses to sign directly, and unlock the flag.
Target
siren-268feb4305c4.chals.z0d1ak.org:1337 (TLS), secp256k1 ECDSA signer.
Vulnerability
Each nonce k used to sign a message m is constructed as:
k = pitch(m) << 246 | rand(246 bits)
pitch(m) = SHA256(SONG_ID + ":" + m) >> 246 # top 10 bits
SONG_ID is handed out alongside the public key, so pitch(m) — and
therefore the top 10 bits of every nonce — is computable by anyone, before a
signature is even requested. The server will verify a signature on the
restricted message PRIV_MSG, but refuses to sign it directly — the
opening for a forgery.
Exploit
Classic HNP reduction. From the ECDSA signing equation
s = k⁻¹(z + r·d) mod N:
x ≡ a + b·d (mod N)
b = s⁻¹r mod N
a = s⁻¹z − pitch(m)·2²⁴⁶ mod N
x = unknown low 246 bits of k, 0 ≤ x < 2²⁴⁶
Lattice construction (dimension m+1, one row per signature sample plus a
target row), normalized against the first sample (cᵢ = bᵢ·b₁⁻¹ mod N):
row 0: (1, c₂, ..., c_m, 0)
row i: N·eᵢ for i = 2..m
row target: (−a₁', ..., −a_m', S) Kannan embedding, S = 2²⁴⁵ (centered)
LLL-reduce the basis and scan the reduced rows for one ending in ±S. That
row's leading coordinate gives x₁ directly, and the private key follows
algebraically: d = (x₁ − a₁)·b₁⁻¹ mod N.
Key implementation detail: never embed d itself as a lattice
coordinate — it's full-size (~256 bit) and swamps the norm. A first attempt
that scaled d into the basis failed to reduce even with 150 signatures and
BKZ. Solving only for the small residual x and recovering d afterward is
what makes the lattice converge with plain LLL.
Results
| Setting | Signatures | Reduction | Result |
|---|---|---|---|
| Local validation (known ground-truth key) | n=30 | plain LLL | 5/5 fresh keys recovered, 100% |
| Live target | n=45 | plain LLL | Solved end-to-end in ~18s |
With the recovered private key, forged a valid signature on
unlock:release-the-tide offline and submitted it via the unlock endpoint
to obtain the flag.
Tools & Files
solve/siren_client.py— TLS/JSON protocol clientsolve/hnp_solve.py— lattice construction + LLL solversolve/attack.py— orchestrator (gather sigs → solve → forge → unlock)solve/run_local_server.py,multi_trial.sh— local validation harness- fpylll / sagemath (LLL), Python
cryptography/ecdsafor signing primitives
Takeaway
Any construction that ties nonce bits to a value derivable before the signature request turns "how many signatures do you need" into a pure lattice-dimension question — 10 leaked bits per signature is more than enough for HNP to bite at n≈30–45.