$ cat writeup.md…
$ cat writeup.md…
HackTheBox
The challenge provides a server implementing Diffie-Hellman key exchange with a menu system to get parameters, reset parameters, and retrieve an encrypted flag.
I received this lovely letter but the sender put a seal on it. She said that can be opened only at our place, Rhome.
The challenge provides a server implementing Diffie-Hellman key exchange with a menu system to get parameters, reset parameters, and retrieve an encrypted flag.
The server implements a custom Diffie-Hellman class with a critical vulnerability:
class DH: def gen_params(self): self.r = getPrime(512) while True: self.q = getPrime(42) # VULNERABILITY: q is only 42 bits! self.p = (2 * self.q * self.r) + 1 if isPrime(self.p): break while True: self.h = getPrime(42) self.g = pow(self.h, 2 * self.r, self.p) # g has order q if self.g != 1: break self.a = randint(2, self.p - 2) self.b = randint(2, self.p - 2) self.A, self.B = pow(self.g, self.a, self.p), pow(self.g, self.b, self.p) self.ss = pow(self.A, self.b, self.p) def encrypt(self, flag_part): key = sha256(long_to_bytes(self.ss)).digest()[:16] cipher = AES.new(key, AES.MODE_ECB) ct = cipher.encrypt(pad(flag_part, 16)).hex() return f"encrypted = {ct}"
Prime Structure: p = 2 * q * r + 1 where:
r is a 512-bit primeq is only a 42-bit prime (extremely small!)Generator Order: The generator g = h^(2*r) mod p has order q, not p-1:
g^q = h^(2*r*q) = h^(p-1) = 1 mod p (by Fermat's Little Theorem)qSmall Subgroup Attack: With q being only 42 bits:
p-1 to extract the small prime qg^q = 1 mod p to confirm generator ordera where g^a = A mod p using Baby-step Giant-stepss = B^a mod p#!/usr/bin/env python3 """ Rhome - HackTheBox Crypto Challenge Small Subgroup Attack on Diffie-Hellman The vulnerability: generator g operates in a subgroup of order q, where q is only 42 bits. This makes discrete log feasible via BSGS. """ from pwn import * from Crypto.Util.number import long_to_bytes from Crypto.Cipher import AES from Crypto.Util.Padding import unpad from hashlib import sha256 from sympy import factorint from math import isqrt import re def baby_giant(g, h, p, n): """ Baby-step Giant-step algorithm for discrete logarithm. Finds x such that g^x = h mod p, where x < n. Time complexity: O(sqrt(n)) Space complexity: O(sqrt(n)) """ m = isqrt(n) + 1 # Baby step: compute g^j for j in [0, m) # Store in hash table for O(1) lookup table = {} val = 1 for j in range(m): table[val] = j val = (val * g) % p # Giant step: compute h * (g^(-m))^i for i in [0, m) # Looking for collision with baby step table factor = pow(g, -m, p) # g^(-m) mod p gamma = h for i in range(m): if gamma in table: x = i * m + table[gamma] return x gamma = (gamma * factor) % p return None def solve(): # Connect to server r = remote('94.237.121.111', 56547) # Get DH parameters r.sendlineafter(b'> ', b'1') data = r.recvuntil(b'Choose').decode() p = int(re.search(r'p = (\d+)', data).group(1)) g = int(re.search(r'g = (\d+)', data).group(1)) A = int(re.search(r'A = (\d+)', data).group(1)) B = int(re.search(r'B = (\d+)', data).group(1)) log.info(f"p = {p}") log.info(f"g = {g}") log.info(f"A = {A}") log.info(f"B = {B}") # Get encrypted flag r.sendlineafter(b'> ', b'3') data = r.recvuntil(b'Choose').decode() ct = bytes.fromhex(re.search(r'encrypted = ([0-9a-f]+)', data).group(1)) log.info(f"Ciphertext: {ct.hex()}") r.close() # Factor p-1 to find the small prime q # p = 2*q*r + 1, so p-1 = 2*q*r log.info("Factoring p-1 to find small prime q...") factors = factorint((p - 1) // 2, limit=2**50) log.info(f"Factors of (p-1)/2: {factors}") # Find the small 42-bit prime q = None for factor in factors: if 40 <= factor.bit_length() <= 50: q = factor break if q is None: log.error("Could not find small prime factor!") return log.success(f"Found q = {q} ({q.bit_length()} bits)") # Verify g has order q assert pow(g, q, p) == 1, "g^q != 1 mod p - unexpected!" log.success("Verified: g^q = 1 mod p (generator has order q)") # Compute discrete log using Baby-step Giant-step log.info(f"Computing discrete log (BSGS with ~{isqrt(q)} operations)...") a = baby_giant(g, A, p, q) if a is None: log.error("BSGS failed to find discrete log!") return log.success(f"Found private key a = {a}") # Verify: g^a should equal A assert pow(g, a, p) == A, "Verification failed: g^a != A" log.success("Verified: g^a = A mod p") # Compute shared secret ss = pow(B, a, p) log.info(f"Shared secret: {ss}") # Derive AES key and decrypt key = sha256(long_to_bytes(ss)).digest()[:16] cipher = AES.new(key, AES.MODE_ECB) plaintext = unpad(cipher.decrypt(ct), 16) log.success(f"FLAG: {plaintext.decode()}") if __name__ == "__main__": solve()
For those who prefer SageMath, the discrete log can be computed directly:
# In SageMath F = GF(p) g_sage = F(g) A_sage = F(A) # discrete_log automatically uses Pohlig-Hellman + BSGS a = discrete_log(A_sage, g_sage)
When the group order n factors as n = p1^e1 * p2^e2 * ... * pk^ek, the discrete log problem can be solved by:
pi^eiIn this challenge:
p - 1 = 2 * q * rg has order q (not p-1)qq is 42 bits, BSGS needs only ~2^21 operationsFor a group of order n:
For n = 2^42: sqrt(n) = 2^21 ~ 2 million operations (seconds on modern CPU)
p = 2q + 1 where q is also prime (Sophie Germain prime)p-1 (or q for safe primes)$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar