Flag: lactf{t1m3_c0m3s_f4r_u_4all}
Challenge Overview
This challenge implements a Diffie-Hellman key exchange over a “clock” group, where points satisfy the equation (the unit circle). The goal is to recover the shared secret and decrypt the flag.
Given Information
- Challenge source code with a missing prime
p - Alice’s public key:
- Bob’s public key:
- Encrypted flag:
d345a465538e3babd495cd89b43a224ac93614e987dfb4a6d3196e2d0b3b57d9
Step 1: Recover the Prime p
The clock group consists of points satisfying . For valid points, must be divisible by .
base_x = 13187661168110324954294058945757101408527953727379258599969622948218380874617base_y = 5650730937120921351586377003219139165467571376033493483369229779706160055207alice_x = 13109366899209289301676180036151662757744653412475893615415990437597518621948alice_y = 5214723011482927364940019305510447986283757364508376959496938374504175747801bob_x = 1970812974353385315040605739189121087177682987805959975185933521200533840941bob_y = 12973039444480670818762166333866292061530850590498312261363790018126209960024
base_sum = base_x**2 + base_y**2 - 1alice_sum = alice_x**2 + alice_y**2 - 1bob_sum = bob_x**2 + bob_y**2 - 1
p = gcd(gcd(base_sum, alice_sum), bob_sum)Result:
p = 13767529254441196841515381394007440393432406281042568706344277693298736356611Verification: is prime and all given points satisfy the curve equation.
Step 2: Analyze the Group Structure
For the clock group over where :
- The group order is
- The group is isomorphic to the multiplicative subgroup of of order
Mapping: A point maps to the element , where .
Step 3: Factor the Group Order
group_order = p + 1# Factorization: 2^2 × 39623 × 41849 × 42773 × 46511 × 47951 × 50587 × 50741 × 51971 × 54983 × 55511 × 56377 × 58733 × 61843 × 63391 × 63839 × 64489Since the group order factors into small primes, the Pohlig-Hellman algorithm is applicable.
Step 4: Solve Discrete Logarithm
Using Pohlig-Hellman combined with Baby-Step Giant-Step (BSGS) for each prime power:
def discrete_log_ph(base, target, order, factors, p): # Pohlig-Hellman algorithm results = [] moduli = []
for q, e in factors.items(): qe = q ** e cofactor = order // qe
# Reduce to subgroup of order q^e base_q = fp2_pow(base, cofactor, p) target_q = fp2_pow(target, cofactor, p)
# Solve in subgroup using BSGS x = solve_prime_power(base_q, target_q, q, e, p)
results.append(x) moduli.append(qe)
# Combine using CRT return crt(results, moduli)
# Find Alice's secret keyalice_secret = discrete_log_ph(base_fp2, alice_fp2, group_order, factors, p)# Result: 13194345531092541282960468403774854038006010561489947009251825743361373440013Step 5: Compute Shared Secret and Decrypt
def clockadd(P1, P2, p): x1, y1 = P1 x2, y2 = P2 x3 = (x1 * y2 + y1 * x2) % p y3 = (y1 * y2 - x1 * x2) % p return (x3, y3)
def scalarmult(P, n, p): if n == 0: return (0, 1) if n == 1: return P Q = scalarmult(P, n // 2, p) Q = clockadd(Q, Q, p) if n % 2: Q = clockadd(P, Q, p) return Q
# Compute shared secretshared_secret = scalarmult(bob_public, alice_secret, p)# (12908236301563930913711454458378166788650310064605426549082577875696820848112,# 6746606992515589635357750300060959445352892536721721582298343132524571854099)
# Derive AES keykey = md5(f"{shared_secret[0]},{shared_secret[1]}".encode()).digest()
# Decrypt flagcipher = AES.new(key, AES.MODE_ECB)plaintext = cipher.decrypt(encrypted_flag)Key Takeaways
-
Recovering hidden parameters: When the prime is missing but points are given, use the curve equation to derive it via GCD.
-
Clock group structure: The clock curve over (with ) has order and can be mapped to a subgroup of .
-
Pohlig-Hellman attack: When the group order has only small prime factors, discrete logarithms become computationally feasible.
Full Solver Script
from hashlib import md5from Crypto.Cipher import AESimport math
# Given database_point = ( 13187661168110324954294058945757101408527953727379258599969622948218380874617, 5650730937120921351586377003219139165467571376033493483369229779706160055207)alice_public = ( 13109366899209289301676180036151662757744653412475893615415990437597518621948, 5214723011482927364940019305510447986283757364508376959496938374504175747801)bob_public = ( 1970812974353385315040605739189121087177682987805959975185933521200533840941, 12973039444480670818762166333866292061530850590498312261363790018126209960024)encrypted_flag = bytes.fromhex("d345a465538e3babd495cd89b43a224ac93614e987dfb4a6d3196e2d0b3b57d9")
# Recover pbase_x, base_y = base_pointalice_x, alice_y = alice_publicbob_x, bob_y = bob_public
base_sum = base_x**2 + base_y**2 - 1alice_sum = alice_x**2 + alice_y**2 - 1bob_sum = bob_x**2 + bob_y**2 - 1
def gcd(a, b): while b: a, b = b, a % b return a
p = gcd(gcd(base_sum, alice_sum), bob_sum)print(f"p = {p}")
# Group ordergroup_order = p + 1
# Factorizationfactors = {2: 2, 39623: 1, 41849: 1, 42773: 1, 46511: 1, 47951: 1, 50587: 1, 50741: 1, 51971: 1, 54983: 1, 55511: 1, 56377: 1, 58733: 1, 61843: 1, 63391: 1, 63839: 1, 64489: 1}
# F_p2 operationsdef fp2_mul(a, b, p): a_real, a_imag = a b_real, b_imag = b real = (a_real * b_real - a_imag * b_imag) % p imag = (a_real * b_imag + a_imag * b_real) % p return (real, imag)
def fp2_pow(a, n, p): result = (1, 0) base = a while n > 0: if n & 1: result = fp2_mul(result, base, p) base = fp2_mul(base, base, p) n >>= 1 return result
def point_to_fp2(point): x, y = point return (y, x)
base_fp2 = point_to_fp2(base_point)alice_fp2 = point_to_fp2(alice_public)
# BSGSdef bsgs(base, target, order, p): m = int(math.isqrt(order)) + 1 baby_steps = {} current = (1, 0) for j in range(m): if current not in baby_steps: baby_steps[current] = j current = fp2_mul(current, base, p)
base_m_inv = fp2_pow(base, order - m, p) gamma = target for i in range(m): if gamma in baby_steps: return (i * m + baby_steps[gamma]) % order gamma = fp2_mul(gamma, base_m_inv, p) return None
# Pohlig-Hellmandef discrete_log_ph(base, target, order, factors, p): results = [] moduli = []
for q, e in factors.items(): qe = q ** e cofactor = order // qe base_q = fp2_pow(base, cofactor, p) target_q = fp2_pow(target, cofactor, p)
x = 0 cur_base = base_q cur_target = target_q
for i in range(e): base_reduced = fp2_pow(cur_base, q ** (e - i - 1), p) target_reduced = fp2_pow(cur_target, q ** (e - i - 1), p) xi = bsgs(base_reduced, target_reduced, q, p) x += xi * (q ** i)
if i < e - 1: inv = fp2_pow(cur_base, xi, p) inv = (inv[0], (-inv[1]) % p) cur_target = fp2_mul(cur_target, inv, p) cur_base = fp2_pow(cur_base, q, p)
results.append(x) moduli.append(qe)
# CRT def extended_gcd(a, b): if b == 0: return a, 1, 0 g, x1, y1 = extended_gcd(b, a % b) return g, y1, x1 - (a // b) * y1
def crt(rems, mods): x = 0 M = 1 for m in mods: M *= m for ri, mi in zip(rems, mods): Mi = M // mi _, si, _ = extended_gcd(Mi, mi) x = (x + ri * Mi * si) % M return x
return crt(results, moduli)
# Get Alice's secretalice_secret = discrete_log_ph(base_fp2, alice_fp2, group_order, factors, p)print(f"Alice secret: {alice_secret}")
# Compute shared secretdef clockadd(P1, P2): x1, y1 = P1 x2, y2 = P2 x3 = (x1 * y2 + y1 * x2) % p y3 = (y1 * y2 - x1 * x2) % p return (x3, y3)
def scalarmult(P, n): if n == 0: return (0, 1) if n == 1: return P Q = scalarmult(P, n // 2) Q = clockadd(Q, Q) if n % 2: Q = clockadd(P, Q) return Q
shared_secret = scalarmult(bob_public, alice_secret)print(f"Shared secret: {shared_secret}")
# Decryptkey = md5(f"{shared_secret[0]},{shared_secret[1]}".encode()).digest()cipher = AES.new(key, AES.MODE_ECB)plaintext = cipher.decrypt(encrypted_flag)print(f"Flag: {plaintext}")