PostECDSA
- Nombre: PostECDSA
- Categoría: Crypto
- Dificultad: Easy
- Puntaje: 285
Descripción
El reto expone un servicio remoto que genera una única firma ECDSA sobre un mensaje conocido y muestra la flag cifrada con AES. No existe interacción adicional con el servidor ni posibilidad de solicitar más firmas. El objetivo es recuperar la flag explotando una debilidad criptográfica en la implementación de ECDSA.
Análisis del Código Proporcionado
El servidor genera una clave privada ECDSA d y firma el mensaje fijo:
sig = ecdsa_sign('Stay at home kiddo !', privkey)Posteriormente, la flag se cifra usando AES en modo ECB con una clave derivada directamente de la clave privada:
key = sha256(str(d).encode()).digest()[:16]El punto crítico se encuentra en la generación del nonce dentro del proceso de firma:
nonce = ((h // 2**128) * 2**128) + dDonde:
hes el hash SHA-256 del mensaje.des la clave privada ECDSA.
Esto implica que el nonce depende directamente de la clave privada.
Vulnerabilidad
La seguridad de ECDSA depende completamente de que el nonce k sea impredecible e independiente de la clave privada. En este caso:
k = A + ddonde:
A = (h // 2^128) * 2^128Dado que:
- el mensaje es conocido,
- el hash
hes conocido, Apuede calcularse,
el nonce filtra información lineal sobre la clave privada.
Este error invalida completamente la seguridad de ECDSA y permite recuperar la clave privada usando una sola firma.
Explotación Matemática
Las ecuaciones fundamentales de ECDSA son:
r = (kG).x mod qs = k⁻¹ (h + r·d) mod qSustituyendo k = A + d:
s(A + d) = h + r·d (mod q)Desarrollando:
sA + s·d = h + r·dAgrupando términos con d:
d(s − r) = h − sADespejando la clave privada:
d = (h − sA) · (s − r)⁻¹ mod qTodos los valores del lado derecho son conocidos, por lo que la clave privada puede recuperarse directamente.
Recuperación de la Clave Privada
A partir de la salida del servidor se obtienen:
- el mensaje
msg, - los valores de la firma
rys, - el orden de la curva
q.
Pasos:
- Calcular
h = SHA256(msg). - Calcular
A. - Calcular el inverso modular de
(s − r). - Recuperar la clave privada
d.
Descifrado de la Flag
Una vez recuperada la clave privada d, se deriva la clave AES exactamente como en el servidor:
key = sha256(str(d).encode()).digest()[:16]La flag se descifra usando AES en modo ECB y se elimina el padding PKCS#7, obteniendo el texto plano.
Script de Explotación
from pwn import *import jsonfrom hashlib import sha256from Crypto.Cipher import AESfrom Crypto.Util.Padding import unpadfrom Crypto.Util.number import inversefrom ecdsa.ecdsa import generator_256
HOST = "3ae0cd25013a53f6.chal.ctf.ae"PORT = 443
io = remote(host=HOST, port=PORT, ssl=True, sni=HOST)
data = io.recvall(timeout=5).decode()
sig_line = Noneenc_flag = None
for line in data.splitlines(): if line.startswith("sig ="): sig_line = line.replace("sig = ", "").strip() if line.startswith("enc_flag ="): enc_flag = line.replace("enc_flag = ", "").strip().strip("'")
sig = json.loads(sig_line.strip("'"))
G = generator_256q = G.order()
msg = sig["msg"]r = int(sig["r"])s = int(sig["s"])
h = int(sha256(msg.encode()).hexdigest(), 16)A = (h // 2**128) * 2**128
d = ((h - s * A) * inverse(s - r, q)) % q
key = sha256(str(d).encode()).digest()[:16]aes = AES.new(key, AES.MODE_ECB)
flag = unpad(aes.decrypt(bytes.fromhex(enc_flag)), 16)print(flag.decode())
io.close()Flag
flag{506f6c796d65726f5761734865726521}Conclusión
Este reto ilustra un error crítico en el uso de ECDSA: derivar el nonce a partir de la clave privada. Incluso una dependencia parcial entre el nonce y la clave privada es suficiente para comprometer completamente el esquema criptográfico.
La lección principal es clara: ECDSA no tolera errores en la generación del nonce. Una sola firma mal implementada puede llevar a la recuperación total de la clave privada y a la ruptura completa del sistema.