Codegate CTF 2020 Finals Writeups

As a disclaimer, I didn't participate in this CTF. I just found this challenge online and thought it was fun.


We've been tricked, we've been backstabbed and we've been quite possibly, bamboozled.


#!/usr/bin/env sage
from secret import P1, Q1, a, b
from Crypto.Util.Padding import pad
from Crypto.Cipher import AES

P0 = P1 & ord('?')
Q0 = Q1 & ord('?')
assert is_prime(P0) and is_prime(P1)
assert is_prime(Q0) and is_prime(Q1)

class Chall:

    def __init__(self, p, q):
        self.p = p
        self.q = q
        self.n = p * q
        self.E  = EllipticCurve(Zmod(self.n), [a, b])
        self.E1 = EllipticCurve(Zmod(p), [a, b])

        # Not Implemented, but you get the point :D
        self.G = E.random_point()
        self.d = randint(1, 1 << 128) & (p >> 1)
        self.Q = self.d * self.G

    def expose(self):

    def getkey(self):
        return self.d

if __name__ == '__main__':
    s = Chall(P0, Q0)
    sd = s.getkey()

    l = Chall(P1, Q1)
    ld = l.getkey()

    size = 16
    flag = pad(open('flag.txt', 'rb').read(), size)

    key = int(sd + ld)
    key = key.to_bytes(size, byteorder='big')
    cipher = AES.new(key, AES.MODE_ECB)
    enc_flag = cipher.encrypt(flag).hex()



The challenge encrypts the flag with AES using a key derived from some large secret number. To recover the secret number, we need to solve the ECDLP. The challenge comes in two parts: recovering the unknown curve parameters, and solving the ECDLP.


The Chall class is initialised with two (prime) numbers pp and qq. It selects a random point GG on the curve E(Z/pqZ)E(\mathbb{Z}/pq\mathbb{Z}) and a random 128 bit number dd as the private key. We are given N=pqN = pq, the order of E(Fp)E(\mathbb{F}_p), the random point GG, and the point given by dGdG.

Part 1: Recovering the Unknown Curve Parameters

Points on the curve are related by the equation

y2x3+ax+b(modN)y^2 \equiv x^3 + ax + b \pmod N

We are given 2 points (x1,y1)(x_1, y_1) and (x2,y2)(x_2, y_2) on the curve, so we can solve for aa and bb:

a(y22x23+x13y12)(x2x1)1(modN)by12x13ax1(modN)\begin{aligned} a &\equiv (y_2^2 - x_2^3 + x_1^3 - y_1^2)(x_2 - x_1)^{-1} \pmod N \\ b &\equiv y_1^2 - x_1^3 - ax_1 \pmod N \end{aligned}

We notice that the order of E(Fp)E(\mathbb{F}_p) is rather smooth! If we can recover pp, we should be able to solve the ECDLP in E(Fp)E(\mathbb{F}_p).

So the goal for now is to recover pp. Curiously, we are given n=#E(Fp)n = \#E(\mathbb{F}_p). It turns out that this is actually a pretty good estimate for pp. In fact, on average the top 128 MSB of nn are the same as the top 128 MSB of pp. (Here pp is around 256 bits). We know that, as a general rule of thumb, whenever we have a good amount of bits of a number we're trying to find, we can usually apply Coppersmith's theorem to recover the number completely (given some other information, of course). We're actually also given the bottom 6 LSB of pp with the call to s.expose() since we can easily factor NN in that case and deduce that p & 0b111111 == 37. So we have just over half the bits of pp.

Let tt be the top ~128 bits of nn. We know that N=pqN = pq, and pt+37p \approx t + 37. Now, construct the univariate polynomial in Z/NZ\mathbb{Z}/N\mathbb{Z}

f(x)t+26x+37(modN)f(x) \equiv t + 2^{6}x + 37 \pmod N

ff will have a root δ<2122|\delta| < 2^{122} (modulo pp).

We can recover pp by computing

p=t+26δ+37p = t + 2^6 \delta + 37

Part 2: Solving the ECDLP

As mentioned before, the order of E(Fp)E(\mathbb{F}_p) is rather smooth. We are given this in the challenge output, so we are pretty much guided on how to proceed. This is the prime factorisation of the order n:

2^3 * 3^4 * 13 * 151 * 37277 * 63737 * 743689 * 14743331 * 20904431 * 3659465143 * 38635749385473505471502894387389

Most of the factors are small, except the large 105 bit factor. Luckily for us, the secret multiplier d is only 128 bits! Excluding the large 105 bit factor, the product of the other factors is more than 128 bits. We can solve the ECDLP in these subgroups and combine the results using the Chinese Remainder Theorem to get the secret multiplier. See here for details.

Grabbing the Flag

The value of sd is less than 37 >> 1 so we can trivially bruteforce it. All we need to do is decrypt the flag and check which decryption contains the flag format.

Solve script:

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

N = 5836992596022446937012188954528837967652088799787297418688161952734029742601918639776384293816907277293165804095447608755394244018171460874413413360601287
ZN = Zmod(N)
n = 97940012926710762153437884674079301076079191877203953722437921714333988067208
G = (4791064145174837833113077069599757584947381216841105432787931481123835537923996904590176334618000141035959257993847069760040827648845993882710813263422518, 2007135516277895026771627676893419200766568709594031697039637947675097596595809713825936430608820664600227626467013163201670055105153466868380086912003923)
Q = (2906660915459424515040277093002683642589488507112805139726386938933880929506501185082819430093812825540133325640097413100449877310669418449600698325701077, 3812143203765395705358551712573539116980648501774991245491977901798688330759954052153901303962483747022229555022370548381218346760417689877969168781021420)
ct = bytes.fromhex('f512c0de4f899ac8d8e6481f2f9b9df22f0cd05f50f9d42750be913156bb27ea5a141f014082853aa97341499ca74d84')

x1,y1 = G
x2,y2 = Q
a = ZN((y2^2 - x2^3 + x1^3 - y1^2)/(x2-x1))
b = ZN(y1^2 - x1^3 - a*x1)
print('[+] a recovered:', a)
print('[+] b recovered:', b)

P.<x> = PolynomialRing(ZN, implementation='NTL')
poly = (n - (n % 2^124)) + 2^6 * x + 37
poly = poly.monic()
delta = poly.small_roots(X=2^122, beta=0.5, epsilon=1/84)[0]
p = (n - (n % 2^124)) + 2^6 * delta + 37
print('[+] p recovered:', p)

E = EllipticCurve(GF(p), [a, b])
G = E(G)
Q = E(Q)

factors = (n//(2^3 * 3^4 * 38635749385473505471502894387389)).factor()
factors = [f for f,_ in factors]

K = []
for pi in factors:
    qi = int(n//pi)
    Pi = qi*G
    Qi = qi*Q
    print('[*] computing discrete log for subgroup of order', pi)
    K.append(discrete_log(Qi, Pi, operation='+'))
d = crt(K, factors)
print('[+] private key recovered:', d)

for sd in range(37 >> 1):
    key = int(sd + d)
    key = key.to_bytes(16, byteorder='big')
    cipher = AES.new(key, AES.MODE_ECB)
    flag = cipher.decrypt(ct)
    if b'CODEGATE' in flag:
        print(unpad(flag, 16).decode())

Flag: CODEGATE2020{Here_comes_the_crypto_genius}