Spooky Safebox
9 minutos de lectura
Se nos proporciona el código fuente del servidor en Python. Este archivo es app.py
:
#!/usr/bin/env python3
import secrets
import os, sys, hmac
import cryptod
from proofofwork import challenge_proof_of_work
FLAG = os.environ.get("FLAG", "flag{FAKE_FLAG}") if "flag" in os.environ.get("FLAG","") else "flag{FAKE_FLAG}"
def main():
print("Welcome to the Spooky Safebox!")
if not challenge_proof_of_work():
return
kpriv, kpub = cryptod.make_keys()
order = cryptod.get_order()
encrypted_flag = cryptod.encrypt(kpub, FLAG)
print("Here is the encrypted flag:", encrypted_flag)
print("You've got 9 signatures, try to recover Satoshi's private key!")
for i in range(9):
msg_ = input("Enter a message to sign: >")
msg = hmac.new(cryptod.int_to_bytes(kpub.point.x() * i), msg_.encode(), "sha224").hexdigest()
checksum = 2**224 + (int(hmac.new(cryptod.int_to_bytes(kpriv.secret_multiplier) , msg_.encode(), "sha224").hexdigest(), 16) % (order-2**224))
nonce = secrets.randbelow(2 ** 224 - 1) + 1 + checksum
sig = kpriv.sign(int(msg, 16) % order, nonce)
print("Signature",(cryptod.int_to_bytes(int(sig.r)) + bytes.fromhex("deadbeef") + cryptod.int_to_bytes(int(sig.s))).hex())
print("Goodbye!")
if __name__ == '__main__':
try:
main()
except EOFError:
pass
except KeyboardInterrupt:
pass
Y este es cryptod.py
:
import ecdsa, ecdsa.ecdsa
from cryptography.hazmat.primitives.kdf.kbkdf import (
CounterLocation, KBKDFHMAC, Mode
)
from cryptography.hazmat.primitives import hashes
import secrets
from Crypto.Cipher import ChaCha20_Poly1305
def get_order(): return ecdsa.NIST256p.generator.order()
def encrypt_sym(input_bytes: bytes, key:bytes):
cipher = ChaCha20_Poly1305.new(key=key)
ciphertext, tag = cipher.encrypt_and_digest(input_bytes)
return ciphertext + tag + cipher.nonce
def derive_symkey(inp:bytes):
kdf = KBKDFHMAC(
algorithm=hashes.SHA3_256(),
mode=Mode.CounterMode,
length=32,
rlen=4,
llen=4,
location=CounterLocation.BeforeFixed,
label=b"safu",
context=b"funds are safu",
fixed=None,
)
return kdf.derive(inp)
def make_keys():
gen = ecdsa.NIST256p.generator
secret = secrets.randbelow(gen.order()-1) + 1
pub_key = ecdsa.ecdsa.Public_key(gen, gen * secret)
priv_key = ecdsa.ecdsa.Private_key(pub_key, secret)
return priv_key, pub_key
def int_to_bytes(n: int) -> bytes:
return n.to_bytes((n.bit_length() + 7) // 8, 'big') or b'\0'
def encrypt(kpub_dest:ecdsa.ecdsa.Public_key, msg:str):
gen = ecdsa.NIST256p.generator
r = secrets.randbelow(gen.order()-1) + 1
R = gen * r
S = kpub_dest.point * r
key = derive_symkey(int_to_bytes(int(S.x())))
cp = encrypt_sym(msg.encode(), key).hex()
return cp + "deadbeef" + R.to_bytes().hex()
Análisis del código fuente
El código es difícil de leer, pero después de un poco de tiempo, podemos descubrir qué hace. El servidor usa ECC y ECDSA en la curva P256.
La flag está cifrada con ChaCha20 y la clave se deriva de un punto
Todos estos conceptos están presentes en cryptod.py
. Los datos cifrados son el texto cifrado por Chacha20 y el punto
En main.py
, después de pasar la prueba de trabajo, se nos dan los datos cifrados y se nos permite ingresar 9 mensajes que se firmarán con ECDSA de una manera extraña:
for i in range(9):
msg_ = input("Enter a message to sign: >")
msg = hmac.new(cryptod.int_to_bytes(kpub.point.x() * i), msg_.encode(), "sha224").hexdigest()
checksum = 2**224 + (int(hmac.new(cryptod.int_to_bytes(kpriv.secret_multiplier) , msg_.encode(), "sha224").hexdigest(), 16) % (order-2**224))
nonce = secrets.randbelow(2 ** 224 - 1) + 1 + checksum
sig = kpriv.sign(int(msg, 16) % order, nonce)
print("Signature",(cryptod.int_to_bytes(int(sig.r)) + bytes.fromhex("deadbeef") + cryptod.int_to_bytes(int(sig.s))).hex())
Nuestro mensaje de entrada se pasa a un HMAC usando
Después de eso, este hash se usa para generar un valor de checksum
usando otro SHA224-HMAC y
Entonces, el nonce checksum
, que es un entero de 225 bits. Por lo tanto, los nonces son enteros de 225 bits.
Y finalmente, el servidor firma con ECDSA y salidas
Implementación del ECDSA
El código para el ECDSA es casi correcto (más información aquí). Tenemos esta ecuación para
Donde
El fallo de seguridad
El problema aquí es que el servidor genera los nonces
Como resultado, podemos usar un ataque basado en retículo (lattice) y LLL para resolver el Hidden Number Problem. Encontré esta técnica en esta página web, que referencia a la sección 4 de este artículo.
Hidden Number Problem
El HNP generalmente se define como:
Donde
Podemos definir un retículo que contenga la solución del HNP. En la definición, podemos usar el retículo formado por las columnas de:
Donde
Nótese que
Si aplicamos este procedimiento en las ecuaciones de ECDSA que tenemos, trataremos de encontrar el vector
Encontrar la clave pública
Para aplicar el HNP a esta situación de biased nonces, necesitamos saber los hashes
Es posible extraer la clave pública
Donde
Implementación
Comencemos por definir algunas funciones auxiliares:
def sign(m: bytes) -> Tuple[int, int]:
io.sendlineafter(b'Enter a message to sign: >', m)
io.recvuntil(b'Signature ')
r, s = map(lambda x: int(x, 16), io.recvline().decode().split('deadbeef'))
return r, s
def decrypt_sym(ct: bytes, key: bytes, nonce: bytes, tag: bytes) -> bytes:
return ChaCha20_Poly1305.new(key=key, nonce=nonce).decrypt_and_verify(ct, tag)
Ahora, pasemos la prueba de trabajo (el código fuente del reto ya incluye un archivo proofofwork.py
para resolver esto):
io = get_process()
io.recvuntil(b'Please provide a string that starts with ')
challenge = io.recvuntil(b' and whose sha256 hash starts with', drop=True).decode()
prefix = io.recvline().decode().strip()
result = solve_pow(challenge, prefix)
io.sendlineafter(b'POW: >', result.encode())
Luego, firmemos el primer mensaje e intentemos obtener la clave pública
io.recvuntil(b'Here is the encrypted flag: ')
enc_data = bytes.fromhex(io.recvline().decode())
m0 = b'asdf0'
r0, s0 = sign(m0)
h0 = int(hmac.new(b'\0', m0, 'sha224').hexdigest(), 16)
m1 = b'asdf1'
r1, s1 = sign(m1)
for P in E.lift_x(Fp(r0), all=True):
Q = pow(r0, -1, n) * (s0 * P - h0 * G)
h1 = int(hmac.new(long_to_bytes(int(Q[0])), m1, 'sha224').hexdigest(), 16)
if (pow(s1, -1, n) * (h1 * G + r1 * Q))[0] == r1:
break
else:
io.failure('Failed to find public ECDSA key')
exit(1)
io.info(f'Public ECDSA key: {Q}')
h, r, s = [h0, h1], [r0, r1], [s0, s1]
Ahora que tenemos la clave pública
h, r, s = [h0, h1], [r0, r1], [s0, s1]
for i in range(2, 9):
mi = f'asdf{i}'.encode()
ri, si = sign(mi)
hi = hmac.new(long_to_bytes(int(Q[0]) * i), mi, 'sha224').hexdigest()
h.append(int(hi, 16))
r.append(ri)
s.append(si)
En este punto, podemos modelar y resolver el HNP usando LLL de la siguiente manera (
p = n
a = list(map(lambda s_i, h_i: pow(s_i, -1, p) * h_i % p, s, h))
t = list(map(lambda r_i, s_i: pow(s_i, -1, p) * r_i % p, r, s))
X = 2 ** 225
raw_matrix = []
for i in range(len(a)):
raw_matrix.append([0] * i + [p] + [0] * (len(a) - i + 1))
raw_matrix.append(t + [X / p, 0])
raw_matrix.append(a + [0, X])
M = Matrix(QQ, raw_matrix)
L = M.LLL()
Una vez que tenemos la base reducida del retículo, necesitamos encontrar vector objetivo. Podemos hacer esto mirando si la cota superior
enc_flag, R_bytes = enc_data.split(b'\xde\xad\xbe\xef')
R_mpz = ellipticcurve.Point.from_bytes(NIST256p.curve, R_bytes)
R = E(R_mpz.x(), R_mpz.y())
ct, tag, nonce = enc_flag[:-28], enc_flag[-28:-12], enc_flag[-12:]
for row in L.rows():
if row[-1] == X:
k = list(map(int, row[:-2]))
x = (s[0] * k[0] - h[0]) * pow(r[0], -1, p) % p
io.info(f'Private key: {x}')
S = x * R
key = derive_symkey(long_to_bytes(int(S[0])))
try:
io.success(f'Flag: {decrypt_sym(ct, key, nonce, tag).decode()}')
exit(0)
except ValueError:
io.failure('MAC check failed')
continue
Flag
Si ejecutamos el script, obtendremos la flag:
$ python3 solve_safebox.py flu.xxx 10030
[+] Opening connection to flu.xxx on port 10030: Done
[*] Public ECDSA key: (2632898350580737258169039806927386431504104360833563771125317252526522550904 : 77026766777862121462475060111493786243486512461424268224672289571158997563209 : 1)
[*] Private key: 103023634815621571102628370470165979788796885143680181972239572475026982389573
[+] Flag: flag{s4tosh1s_Funds_4re_safu_safeB0x_isnt}
[*] Closed connection to flu.xxx port 10030
El script completo se puede encontrar aquí: solve.py
.