Justice

Another nice chall by the great bebo07 in ASCWG 2025

Handouts


from hashlib import sha1,sha256
from Crypto.Util.number import bytes_to_long, long_to_bytes,inverse
from random import randint
from sage.all import *


FLAG = b'ASCWG{TESTTEST}'
a = 'redacted'
b = 'redacted'
q= 11722926242306411491117245929733953191095935082498997045653937809623622288511133179535069277231629076589496415875947858138184916449452869671412741618610351

E = EllipticCurve(GF(q), (a,b))

p = E.order
G = E.gen()
d=bytes_to_long(FLAG)
pub = d * G   

def sign(msg,d):
	hsh=bytes_to_long(sha1(msg).digest())
	while True:
		k=randint(1,2**450); print(f"🔑 Random k: {k}")
		r=int((k*G)[0])
		if r== 0:
			continue
		s=inverse(k,p)*(hsh+r*d)
		s=s%p
		if s==0:
			continue
		break
	return r,s
def verify(msg,r,s):
	hsh = bytes_to_long(sha1(msg.encode()).digest())
	u1=int((hsh*inverse(s,p))%p)
	u2 = int((r*inverse(s,p))%p)
	P=u1*G+u2*pub
	if P == E(0):
		return False
	return r == int(P[0])%p




def banner():
    print("="*50)
    print("🔐 ECDSA Signing & Verification Tool 🔐")
    print("="*50)
    x, y = G.xy()
    print(f"📍 Generator Point G:\n  x = {x}\n  y = {y}")
    print("-" * 50)
    px, py = pub.xy()
    print(f"🔑 Public Key Point P = d * G:\n  x = {px}\n  y = {py}")
    print("="*50)


def menu():
    print("\n1. Sign a message")
    print("2. Verify a signature")
    print("3. Exit")
    return input("> ")

def main():
    banner()
    while True:
        choice = menu()
        if choice == '1':
            msg = input("Enter the message to sign: ").encode()
            r, s = sign(msg, d)
            print(f"✅ Signature:\n r = {r}\n s = {s}")
        elif choice == '2':
            msg = input("Enter the message: ")
            
            r = int(input("Enter r: "))
            s = int(input("Enter s: "))
            if verify(msg, r, s):
                    print("✅ Signature is valid.")
            else:
                    print("❌ Invalid signature.")
            
        elif choice == '3':
            print("👋 Bye!")
            break
        else:
            print("❗ Invalid choice. Try again.")

if __name__ == "__main__":
    main()

This was originally a sage script but I made it into a python one.

Analysis & Solver

First thing, we don't have params, but we get 2 points and we got the prime, so easily recoverable.

I also hardcoded the order and G cz it takes a while to find each run.

Essentially, the vuln is in the bias of the generation of the nonce, we can assume zero MSB because it,s generated between 1 and 2^450 and both prime and order are 512 bits long giving us 62 bits of 0 MSB, reducing it to HNP given enough signatures and large enough zero MSB.

siki=zi+rid(modp)s_ik_i = z_i + r_id \pmod p

ki(si1ri)d+(si1zi)=0(modp)k_i − (s ^{−1}_i r_i)d + (−s^{ −1}_ i z_i) = 0 \pmod p

Lattice Construction

We collect ( N ) ECDSA signatures ((ri,si))((r_i, s_i)) on messages (mi)( m_i ), and define:

  • ( hi=SHA1(mi)h_i = \text{SHA1}(m_i) )

  • ( si1si1modps_i^{-1} \equiv s_i^{-1} \mod p )

  • ( ui=risi1modpu_i = r_i \cdot s_i^{-1} \mod p )

  • ( ti=hisi1modpt_i = h_i \cdot s_i^{-1} \mod p )

From the ECDSA signature equation:

si=ki1(hi+dri)modpki=si1(hi+dri)modps_i = k_i^{-1}(h_i + d \cdot r_i) \mod p \quad \Rightarrow \quad k_i = s_i^{-1}(h_i + d \cdot r_i) \mod p

Assuming the nonces ( k_i ) are biased and thus small, we construct a lattice ( \mathcal{L} \in \mathbb{Z}^{(N+1) \times (N+1)} ) with basis matrix:

M=[p00000p00000p00000p0u0u1u2uN11]\mathbf{M} = \begin{bmatrix} p & 0 & 0 & \cdots & 0 & 0 \\\\ 0 & p & 0 & \cdots & 0 & 0 \\\\ 0 & 0 & p & \cdots & 0 & 0 \\\\ \vdots & \vdots & \vdots & \ddots & \vdots & \vdots \\\\ 0 & 0 & 0 & \cdots & p & 0 \\\\ u_0 & u_1 & u_2 & \cdots & u_{N-1} & 1 \\ \end{bmatrix}

We define the target vector:

t=[t0t1tN10]\mathbf{t} = \begin{bmatrix} t_0 \\\\ t_1 \\\\ \vdots \\\\ t_{N-1} \\\\ 0 \\ \end{bmatrix}

After applying the LLL algorithm to ( \mathbf{M} ), we obtain a short vector ( \mathbf{v} \in \mathcal{L} ) such that:

v=t+db\mathbf{v} = \mathbf{t} + d \cdot \mathbf{b}

for some ( \mathbf{b} \in \mathbb{Z}^{N+1} ). The private key ( d ) is then recovered from:

d=(vt)Nd = -(\mathbf{v} - \mathbf{t})_{N}

Feedback

Tbh? amazing challenge, learned alot about HNP.

Last updated