m0leCon CTF 2020 Teaser — King Exchange

task title

The given zip-archive contains two files: a python script and its output.

server.py:

from Crypto.Util.number import long_to_bytes
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import random
from hashlib import sha256
from secret import flag, p


def add_points(P, Q):
    return ((P[0]*Q[0]-P[1]*Q[1]) % p, (P[0]*Q[1]+P[1]*Q[0]) % p)


def multiply(P, n):
    Q = (1, 0)
    while n > 0:
        if n % 2 == 1:
            Q = add_points(Q, P)
        P = add_points(P, P)
        n = n//2
    return Q


def gen_key():
    g = (0x43bf9535b2c484b67c68cb98bace14ae9526d955732e2e30ac0895ab6ba, 0x4a9f13a6bd7bb39158cc785e05688d8138b05af9f1e13e01aaef7c0ab94)
    sk = random.randint(0, 2**256)
    pk = multiply(g, sk)
    return sk, pk


a, A = gen_key()
b, B = gen_key()
print(A)
print(B)

shared = multiply(A, b)[0]
key = sha256(long_to_bytes(shared)).digest()
aes = AES.new(key, AES.MODE_ECB)
ciphertext = aes.encrypt(pad(flag.encode(), AES.block_size))
print(ciphertext.hex())

output.txt:

(70584838528566138057920558091160583247156394376694509226477175997005624, 47208562635669790449305203114934717034939475647594168392271311241505021)
(28274152596231079767179933954556001021066477327209843622539706192176128, 99565893173481261433550089673695177934890207483997197067732588009694082)
aaa21dce78ef99d23aaa70e5d263719de9245f33b8a9e2a0a63c8847dba61296c5a1f56154b062d3a347faa31b8d8030

The encryption algorithm is straightforward: it uses AES-ECB with unknown key and prints out a ciphertext.

aaa21dce78ef99d23aaa70e5d263719de9245f33b8a9e2a0a63c8847dba61296c5a1f56154b062d3a347faa31b8d8030

We need to recover the key, let’s find out how it was generated.

a, A = gen_key()           # A = multiply(g, a)
b, B = gen_key()           # B = multiply(g, b)
shared = multiply(A, b)[0] # shared = multiply(multiply(g, a), b)[0]

The challenge performs some kind of Diffie-Hellman key exchange with unknown modulus $p$ (we will back to this later). It means that server generates two secret values $a$ and $b$, multiplies the generator $g$ by these secret values and prints out resulting $A = g \cdot a$ and $B = g \cdot b$.

g = (0x43bf9535b2c484b67c68cb98bace14ae9526d955732e2e30ac0895ab6ba,
     0x4a9f13a6bd7bb39158cc785e05688d8138b05af9f1e13e01aaef7c0ab94)

A = (70584838528566138057920558091160583247156394376694509226477175997005624,
     47208562635669790449305203114934717034939475647594168392271311241505021)

B = (28274152596231079767179933954556001021066477327209843622539706192176128,
     99565893173481261433550089673695177934890207483997197067732588009694082)

The shared key (the same key for AES encryption) is calculated as

$$ S = A \cdot b = B \cdot a $$

We need to obtain $a$ or $b$, that means we need to solve a well-known discrete logarithm problem.

The main mystery there is which group we’re working in? Let’s take a closer look on functions multiply and add_points.

def multiply(P, n):
    Q = (1, 0)
    while n > 0:
        if n % 2 == 1:
            Q = add_points(Q, P)
        P = add_points(P, P)
        n = n//2
    return Q

multiply(P, n) is a simple double-and-add algorithm for faster $P \cdot n$ multiplication.

def add_points(P, Q):
    return ((P[0]*Q[0]-P[1]*Q[1]) % p, (P[0]*Q[1]+P[1]*Q[0]) % p)

This $P + Q$ operation is not so obvious (at least at the first sight), but it reminded me the addition law on Edwards elliptic curves:

$$ (x_1, y_1) + (x_2, y_2) = \left( \dfrac{x_1y_2 + x_2 y_1}{1 + dx_1x_2y_1y_2}, \dfrac{y_1y_2 - x_1x_2}{1 - dx_1x_2y_1y_2} \right) $$

It brought me to a solution. The $d$ parameter in denominator is a curve parameter which has the form:

$$ x^2 + y^2 = 1 + dx^2y^2 $$

If we set $d = 0$ in the fractions below then we will get our addition algorithm — notice that the both numerators of fractions are equal to our add_points result. But what will happen if we set $d = 0$ in Edwards curve equation?

That’s right! With $d = 0$ it becomes just a circle. That case is also mentioned in Wikipedia article:

circle addition

Ok, now we’ve got the curve we’re working on. $A$, $B$ and $g$ are points on the circle.

Remember unknown modulus $p$? Now we are able to recover it. From circle equation we get:

$$ A_0^2 + A_1^2 \equiv 1 \pmod{p} $$ $$ B_0^2 + B_1^2 \equiv 1 \pmod{p} $$

Move $1$ to the left side and rewrite the module condition:

$$ A_0^2 + A_1^2 - 1 \equiv k_1 \cdot p $$ $$ B_0^2 + B_1^2 - 1 \equiv k_2 \cdot p $$

Now $p$ is the common divisor (probably, greatest) of the left sides of the equation. Using Euclidean algorithm we could recover it.

from math import gcd

p = gcd(A[0]^2 + A[1]^2 - 1, B[0]^2 + B[1]^2 - 1)
# p = 108848362000185157098908557633810357240367513945191048364780883709439999

The given $p$ is prime, so we’re lucky, it is our modulus! But we still need to solve discrete logarithm.

After some searching I’ve found a question on StackExchange, where the author is asking why we use elliptic curves instead of other curves (such a circle) in cryptography. The answer gives a link to paper with a proof that the discrete logarithm in the circle group is no stronger than the discrete logarithm over the underlying field.

There was suggested a map (paragraph 3) from a circle group to $F_p[W]/(W^2 + 1)$ (where $F_p = GF(p)$):

bijective map

So I’ve tried to rewrite it in sage and run standart discrete_log function.

F = GF(p)
R.<w> = PolynomialRing(F)
K.<w> = F.extension(w^2 + 1)

g_K = g[0] + g[1]*w
B_K = B[0] + B[1]*w
b = discrete_log(B_K, g_K)

print(b)
print(multiply(g, b) == B)
# 84409516282208830711822012296755714894563394060577082225590467549929629
# True

We’re lucky again, after some seconds it was printed the secret $b$! In the general case we would need to check smoothness of the field order and other parameters, but here it’s not required — sage did its job.

Now we easily can calculate shared key and decrypt the flag:

shared = multiply(A, b)[0]
key = sha256(long_to_bytes(shared)).digest()
aes = AES.new(key, AES.MODE_ECB)
plaintext = aes.decrypt(bytes.fromhex(ciphertext))
print(plaintext)

The flag is ptm{c1rcl3s_r_n0t_4s_53cur3_4s_ell1ps3s}.