← Z0D1AK writeups
Crypto ✓ independently re-verified

SIREN

Breaking ECDSA via a Hidden Number Problem lattice attack on a biased nonce.

Flag`zdk{4_F3w_BL7S_per_51gna7uR3_SlNKs_tHE_key}`

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

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.

Solve scripts:
attack.pyhnp_solve.pysiren_client.py