Difficult CTF...
hyper (hxp CTF 2020)
Good luck! :-)
#!/usr/bin/env sage
import struct
from random import SystemRandom
p = 10000000000000001119
R.<x> = GF(p)[]; y=x
f = y + prod(map(eval, 'yyyyyyy'))
C = HyperellipticCurve(f, 0)
J = C.jacobian()
class RNG(object):
def __init__(self):
self.es = [SystemRandom().randrange(p**3) for _ in range(3)]
self.Ds = [J(C(x, min(f(x).sqrt(0,1)))) for x in (11,22,33)]
self.q = []
def clk(self):
self.Ds = [e*D for e,D in zip(self.es, self.Ds)]
return self.Ds
def __call__(self):
if not self.q:
u,v = sum(self.clk())
rs = [u[i] for i in range(3)] + [v[i] for i in range(3)]
assert 0 not in rs and 1 not in rs
self.q = struct.pack('<'+'Q'*len(rs), *rs)
r, self.q = self.q[0], self.q[1:]
return r
def __iter__(self): return self
def __next__(self): return self()
flag = open('flag.txt').read().strip()
import re; assert re.match(r'hxp\{\w+\}', flag, re.ASCII)
text = f"Hello! The flag is: {flag}"
print(bytes(k^^m for k,m in zip(RNG(), text.encode())).hex())
a0955c882185b50a69d9d19a24778519d6da23894e667d7130b495b645caac72163d242923caa00af845f25890
Solution
The flag is encrypted by XORing it with outputs of the hyperelliptic curve based RNG. We have some known plaintext, so we can recover some bytes of the RNG output. The task is to use this bit of known output to get the next few outputs.
Background
A hyperelliptic curve of genus over a field is given by the equation
where , and , and .
There is no way to define an operation that gives a group structure, but we can use a related object called the Jacobian of , denoted , which can be equipped with a group law. See here and here for an introduction to hyperelliptic curves.
Essentially, every element can be uniquely represented as a pair of polynomials in . This is the Mumford representation of . The polynomials satisfy the properties:
a) is monic
b) divides
c)
These properties will come in handy soon!
Analysis
In the challenge, the curve is of genus 3 and has equation
(i.e. and ).
The RNG generates three large random numbers and , as well as three constant elements . To generate random bytes, the RNG computes
and converts the coefficients of and to bytes (excluding the coefficient of in which is since is monic).
Since we have 24 bytes of known plaintext, we can completely recover . To recover the next outputs of the RNG, we'll need to somehow determine .
Solving the challenge
From the properties of elements in listed above, we have
so if is a root of (over the algebraic closure of ), then
which implies that is a point on .
In the challenge, , so we have
Now, remember that we have , and we want to recover . It turns out we have just enough information to do that! is of degree , so over the algebraic closure of , we can find three roots and . We just argued that the points , and lie on the curve , so we have some candidates of and (namely and so on). To recover the polynomial , we can use Lagrange interpolation.
Solve script:
import itertools
import struct
p = 10000000000000001119
R.<x> = GF(p)[]; y=x
f = y + prod(map(eval, 'yyyyyyy'))
C = HyperellipticCurve(f, 0)
J = C.jacobian()
Ds = [J(C(x, min(f(x).sqrt(0,1)))) for x in (11,22,33)]
enc = bytes.fromhex('a0955c882185b50a69d9d19a24778519d6da23894e667d7130b495b645caac72163d242923caa00af845f25890')
known_pt = 'Hello! The flag is: hxp{'.encode()
rng_output = bytes(e^^m for e,m in zip(enc, known_pt))
blocks = [rng_output[i:i+8] for i in range(0, len(rng_output), 8)]
ui = [int.from_bytes(r, 'little') for r in blocks]
u = x^3 + ui[2]*x^2 + ui[1]*x + ui[0]
L = GF(p).algebraic_closure()
roots = [r[0] for r in u.change_ring(L).roots()]
RR.<zz> = PolynomialRing(L)
v = RR.lagrange_polynomial([(xi, f(xi).sqrt()) for xi in roots])
vi = [v.coefficients()[i].as_finite_field_element()[1] for i in range(3)]
vi = [(int(-c), int(c)) for c in vi]
for rs in itertools.product(*vi):
q = struct.pack('<'+'Q'*len(rs), *rs)
flag = bytes(k^^m for k,m in zip(rng_output+q, enc))
print(flag)
Flag: hxp{ez_P4rT_i5_ez__tL0Cm}