cryptofreehard

crypto

pingctf

Task: ECDH on NIST P-192 with MAC oracle, flag encrypted using private key. Solution: Invalid-curve attack exploiting missing point validation to recover private scalar via Pohlig-Hellman on smooth-order invalid curves, then CRT reconstruction.

$ ls tags/ techniques/
pohlig_hellmancrt_reconstructioninvalid_curve_attackmac_oraclesubgroup_confinement

$ cat /etc/rate-limit

Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.

crypto — pingCTF

Description

ECDH key exchange server on NIST P-192. The flag is encrypted with AES-CTR using a key derived from the server's private scalar. The server accepts arbitrary public points and returns a MAC of the shared secret.

Given: server.py (server implementation), ECDH.py (elliptic curve library), docker-compose.yml, DOCKERFILE.

Remote: nc 178.104.42.20 55137

Authors: mixer + Wintoch/Kubson

Analysis

Server behavior

  1. Server generates a random private scalar b_priv on NIST P-192
  2. Flag is encrypted with AES-CTR using sha256(str(b_priv).encode()).digest() as the key
  3. Server prints nonce + ciphertext in hex
  4. Server accepts arbitrary public points R_A as JSON [x, y]
  5. Server computes shared_secret = ECDH.ecdh_responder(R_A, b_priv) and returns MAC = sha256(shared_secret).hexdigest()
# server.py - key derivation aes_key = hashlib.sha256(str(b_priv).encode()).digest() cipher = AES.new(aes_key, AES.MODE_CTR) ciphertext = cipher.encrypt(flag) # server.py - MAC oracle shared_secret, R_B = ECDH.ecdh_responder(R_A, b_priv) mac = hashlib.sha256(shared_secret).hexdigest() print(f"MAC: {mac}")

The vulnerability

The critical flaw is in ECDH.py. The point_add and scalar_mult functions use only the curve parameters p (field prime) and a (curve coefficient), but never validate that the input point lies on the intended curve and never check subgroup membership:

# ECDH.py - point_add only uses p and a, not b! def point_add(P: Point, Q: Point, curve: Curve = NIST_P192) -> Point: p = curve.p # ... if P == Q: m = (3 * x1 * x1 + curve.a) * pow(2 * y1, -1, p) % p # Only uses 'a' else: m = (y2 - y1) * pow(x2 - x1, -1, p) % p # ...

This enables an invalid-curve attack: we can send points from curves with the same a and p but different b values. The server will happily compute b_priv * R_A on whatever curve R_A actually belongs to.

...

$ grep --similar

Similar writeups