Write-Ups
8 min read

CA CTF 2022: Exploiting vulnerable Elliptic Curve parameters - MOVs Like Jagger

Exploiting vulnerable Elliptic Curve parameters, WizardAlfredo shares his write-up of MOVs Like Jagger from Cyber Apocalypse CTF 2022.

Jun 23, 2022
Hack The Box Article

In this write-up, we'll go over the solution for the medium difficulty crypto challenge MOVs Like Jagger that requires the exploitation of a vulnerable elliptic curve.

Challenge Description 📄

While following the breadcrumbs from the secret messages you managed to recover from Broider, Bonnie gathered more information from his well trusted "Tsirakia" and found out that the messages were about the destinations of a STS (Space Transportation Ship). The STS, named "Paketo", is a notoriously large spaceship that transports military supplies for the Golden Fang mercenary army. After spending some time observing the seemingly random trajectory of Paketo, you and Paulie manually plotted the delivery locations and realized that the STS's orbit forms an elliptic curve. Your spaceship's advanced technology GPS has calculated the exact parameters of the curve. Can you somehow predict Paketo's final destination and hijack it?

The application at-a-glance 🔍

If you visit the homepage of the application, you will see a GPS like system indicating the movement of the STS called "Paketo". The only information available to us is the current coordinates and the coordinates from which the ship started.

We can try entering random data into the secret coordinates field and see what happens when we press the travel button.

We see that we are redirected to a conversation between Klaus and Bonnie, in which Bonnie tells us that we have travelled to Longhir.


At this point, we need to start looking at the source code to understand how things work.

Analysing the source code 📖

There are 3 files available app.py, navigation.py, utils.py

The utils.py script, generates random locations if we enter the wrong coordinates, so we ignore it.

app.py

We can see that in app.py there is an API provided:

@app.route('/api/coordinates', methods=['GET']) def coordinates(): points = { 'departed_x': hex(Q.x()), 'departed_y': hex(Q.y()), 'present_x': hex(P.x()), 'present_y': hex(P.y()) } return points @app.route('/api/get_flag', methods=['POST']) def get_flag(): try: travel_result = checkDestinationPoint(request.json, P, nQ, E) location = generateLocation(travel_result) if travel_result: return {"location": location, "flag": FLAG} else: return {"location": location} except ValueError as error: return {"error": error} if __name__ == '__main__': Q, nQ, P, nP = calculatePointsInSpace() app.run(host='0.0.0.0', port=1337, debug=False, use_reloader=False)

We see that in order to get the flag, we must somehow set travel_result = True. The function that sets travel_result is called checkDestinationPoint() and is imported from navigation.py

navigation.py

The basic workflow of the script is:

  1. Initialise and define the parameters for the curve.

  2. Compute 2 key pairs bZNIGF7dCWcXeVYSz5Gb5jrFSEI3QB2VcFcnRxjkBL2zKS8PseQUnQtRnOmg-i2yUrV771OOKpx119HgsddabP35uziTB9bD4JE9zeVFstaQn3GEkarVgySdZ-XksRHeoYLGnfqmLNm6iL8BzA (the departed point) and 6ciorI0NQeWGyD2xtpwPidSlDMrdWoWyYgNIb_hlBd8ViDBUzy6p04T0LGbAHBbfjG9yrb5HB9vcnuuqHlSMI_B00Si0Zw4WyCuUdwXN482LHjF7e_Rma52WnHVUVCumijNrMLotYbkHPSZgiQ (the present point).

  3. Check if the coordinates given by the player are valid.

  4. The secret point (destination point) is generated ixv1B4W8poTzgCGXnf4ZWeZrtLiY3FDiijCWwDUNLvIGCP_wcL82yuA5dr0eznElXQ9V_ZFvTpq-dnw0lcmQn0fb0GZ9jrfHnZ5wQgID2lP7bISDi0WYF94iCQGxyw-fj4XRIQg6bTG1_mnF_A and compared with the point we have given. If they match, we get the flag.

Step 1 is translated into code:

from ecdsa import ellipticcurve as ecc from random import randint a = -35 b = 98 p = 434252269029337012720086440207 Gx = 16378704336066569231287640165 Gy = 377857010369614774097663166640 ec_order = 434252269029337012720086440208 E = ecc.CurveFp(p, a, b) G = ecc.Point(E, Gx, Gy, ec_order)

Step 2 is implemented with the calculatePointsInSpace() and generateKeyPair():

def generateKeyPair(): private = randint(1, 2**64) public = G * private return(public, private) def calculatePointsInSpace(): Q, nQ = generateKeyPair() P, nP = generateKeyPair() return [Q, nQ, P, nP]

Step 3 is implemented with the checkCoordintates() function. This is just a simple method to see if the points entered by the player are valid.

Step 4 is the win condition. If the secret_point and the point we entered match, we get the flag

secret_point = P * nQ same_x = destination_point.x() == secret_point.x() same_y = destination_point.y() == secret_point.y() if (same_x and same_y): return True else: return False

To summarise once again:
To get the flag, we need to make checkDestinationPoint() return True. To do this, we must somehow predict what the secret location 8Esn2Xofc4SSihSKjjgMmETIw7fKXePV6-InPgEQCnuzit2M7UFLZRS5EIP7rEtWrt2DGwJEDVhww6mamRITcQzFB-jmBamqksVHa86OoV5kS_bqSd_WtaITfvzD6WQHqvVRD-6DtjZcNwaAkQ  is.
Let’s search for the vulnerability now

Searching for the bugs 👾

One thing that should strike us when we look at the challenge is that the curve appears to use custom parameters. Let us examine these.

a = -35 b = 98 p = 434252269029337012720086440207

There are a lot of vulnerabilities we can check in custom elliptic curves, but really the title of the challenge is a big clue to the attack. If we search for something along the lines of "MOV attack elliptic curves" we will find a lot of resources.

Understanding the vulnerability 🔐

In ECC it is difficult to solve the discrete logarithm problem.

The basic idea of this attack is that the discrete logarithm problem can be moved from an elliptic curve defined over Ntxq7Dr9T5FBvjaNO1r5U7XmxNDspjq7LT9n9E8LrDrLxF6tyhcY-ujDMpBii23vVw3XVUn6_HgIr36_N5W49gdq30GCqEgzqi697nsonkwV-68TJE_a9_4hk_8CD9LlTMARhUpbkjoE9ayZyQ (finite field) to the multiplicative group of the finite field sK55woOhmOWpp1ufo2xbczqiGphXQ9eAL1Tk0MSNjuCXVEQ1jZ1Brd21l4BAIRB723zsnSImSMfqROlsbmRW6B7UM6LTQjKv_o7ftbYv_qzISe2pOA7F-xZsqNrloCJrSAPbcwEtNUtpA3JcKw when 0XQqQoJzO9Irh-A9kDo5s7feiHk4c9xLtPMLWH6KzUMW3X7-KH-KRBjHOidEwtp3xRVXZjHNbqQ_5bk3f5xcH0p6fdQ2yniN9B7WUfs6pGbfsPM0Xb3E8c0pRzubNvymEdGIhFDd_VdzqdeHzw is divided by IblJaE7tJT6dhBYsa6shSCRecwrBv6h2IQshr3MxRFQq0a944mktNSwVzuJZuUO9j7HnFvfliqObd61hMLv-HAB3zyaxPKHqVX4x19-xwuftJZfID-4JTBEw4Fe81xIdKqPzQ3l9iD2kb3uKXg . If QYQ3S9TWe5Arzh1AuaJ9Zaq7q2nJzH7wuYqIl6XWus_xV0q4cLDb8Jm9ixQsCEpiweaRGUBsn9pXJB8Ni7o_0SR-tEwFZ1B43jt3goo-HRAswFawKDRC2sCERxdW8EECX34DNFOtmjNA3UKErA is small enough (k < 6), the discrete log becomes easier to calculate than on the curve. Let us first check that t3BfjqGXAeNNwHHsIbm-6AfAaz6s5aCI5e3tYbUi7FP2fSz-AJ0G549ainPTp4Llg383l7mWKm1wIRUKDNGp7nHv5kgMVcX1gCF-5fr3468AM4PzCnZjQkigajmkuj3aRu1LCfhbh7vqIiay5g is small:

from sage.all import * a = -35 b = 98 p = 434252269029337012720086440207 Gx = 16378704336066569231287640165 Gy = 377857010369614774097663166640 E = EllipticCurve(GF(p), [a, b]) k = 1 while (p**k - 1) % E.order(): k += 1 print(k)

In our case _W4UdQ0rHXvq8_EBH1z8qtVF7Z6H1eRZ_yJ2tF65dtv48ygjuAG4Dp_qJo1HNEHwu_iQMrT9p5zCTIKAQRyM-sFyqqWMTMu_-L4F3ve-vDkBEuqhz3dGF_ul377UdSItPVEsMrDNUq6l7HKSLQ everything seems to be set.

┏━[/tmp] ┗━━ ■ python3 check_k.py 2

Exploitation 🔓

MOV attack

Analysing the source code in the above section, we concluded in step 5 that we must somehow find AoqsL_o2Z4D3e0laFJcC2ntYYgnzcZ0WEk4Y1RR7TN4qiY5kkYJes4gVnvU5NodbJBXIqTIRcMLtgzlZYm7BrZP0n6i9q41DIghvhqaKQhadYZWlTzVZIaLePWl1j7vX673Kw81BhmT3ceJ1SQ . g45ieVmVHGBMlk8ro6R4CZMlWkcJvy_kruq6g7a-FvKMX9dSkFZFs_qBee4nAZzK5i6DSNPwTARjXfe10KPicGuBLX8AvmYaDHHVv-M_Rv0FCc80tJtaV4ER4cu1eryXv_h2E6q-3hqZtcYNZg is known, but sENw7gEJQZILgtr0NHtMXrD838WjqTj2iguqKbuL9gMOTOvvmHkrdDY9ccUfrQNflQf44Xslqjk0oxK_CKiWzi0S5OrKihwuvVnuKXkAHkq6w_NVKosnO6LhH_p5tslXlnvTm3RMP9TBKnVv9w is not.

We know  that JVDzcYSfwg7qJvmZoTf079VoWr0kXi65343mNZQy1wgaiCAKVG1XP1ZlTSd99NIAdRjTTZWSK9W0kMjx6KfTvRqqh4-R4xQcLWgUNxUJwERMh-w3uOymmRedt4vYhLLQWaA5jL-bZ1CQPoUTiA , but in ECC it is really hard to solve this problem. If only we had a vulnerability... Oh, wait, we do! The MOV attack. Let us implement it.

A good abstraction of the attack algorithm can be found here.

Also a write-up that can help a lot of users with the implementation is this one.

The problem is solved for SDkXBU9axmOWVsFVEjvS0xE91yckI1WzUf2x9l92mr8UDLmksH3vPb4DHHM1MBU2pqbmxn3RHpHAYRnE3aJgvUL9mpitqIQWZpiQqfastPdsMnKi6-OfuNpnguGzvX_n_JARsGmtN2IloaQsVw , yiUsHrBbJ2cyDE5cW5kx_NklVWSz2LEJOTd7sVh4Qv2bRJTQMVoMJUiIqdQjx9FFB0gEnbsLvjKf7_1UCabcvnkxSvRE99iS98aAZIL45fCrVsq4LJ6bGKxxxOVdzkDzwJve_wtz_yOvdl5HDA , where Tjy6Tc_c2KSedO9iF4sRRwuzqX0oRApMeWynXIddPnnzN_Aw-EwVm49VrbUy2sDiEg2WBFGtTUX9sc5bjGoIjhttqqrgLXmeF51nLaZyNoR044jsSOh1NJtdXBSQZwqjAWgk_bAoALDA7GQkcw and nQ = randomint(1, 2**64)

We will start by working in bUOMmA51M-Xc1ap78UH1qyuOgJp1rIchqsh7Qm2KsNW2JJqrHrg8RQeY23EbII3X_t6bLG_E45vZ4q0M7Gw13Z0LQRvSNWMecJLkavPoFaj9FH7XhggKxBk3ZNWbMXjkpeZPKJ1zDFwhBp9qFA

Ee = EllipticCurve(GF(p**k, 'y'), [a, b])

We choose Q39ii1ETVbNwU41FrJqzXvv-lK1wYGyvNYchqHxF8SqKy_1slVCWshqF6zlC6DCP0X45UxGSB5KnsS2j-7Airj43cX9mPKjXzhbDg3JGZiQ3d0Fyz0FfI0w6pnWhtuJbUgiXgHEF90YoZPyp0Q such that Q39ii1ETVbNwU41FrJqzXvv-lK1wYGyvNYchqHxF8SqKy_1slVCWshqF6zlC6DCP0X45UxGSB5KnsS2j-7Airj43cX9mPKjXzhbDg3JGZiQ3d0Fyz0FfI0w6pnWhtuJbUgiXgHEF90YoZPyp0Q and SDkXBU9axmOWVsFVEjvS0xE91yckI1WzUf2x9l92mr8UDLmksH3vPb4DHHM1MBU2pqbmxn3RHpHAYRnE3aJgvUL9mpitqIQWZpiQqfastPdsMnKi6-OfuNpnguGzvX_n_JARsGmtN2IloaQsVw are linearly independent.

R = Ee.random_point() m = R.order() d = gcd(m, G.order()) B = (m // d) * R

After that we take the Weil pairing FegIZiBmnYWH8IPyVVAe6kV8sGEpG4U_2tWI1sxEPcGAAClF5DiYu7OGF_Ch3O6LiwxC5E66KINH9a68adOSLeJnEkUY8H8xWogZBNztGoRdcW8GqQKckF-fTRyaIgzYteNWHmFIiut2RgNO0g and WfjF0dQcg_jw1ZieWvtzFu7bK6b1hTEIP4tiD_WQWijRg9FlxkFOEqQJT8u7r7fcvj6mbaBpjn0zWGn5SQqjrROXI5obQm393o3cuyouDg_zwYg8AFZ01DWvCDNJywyPPTL6P5VMQTR3HV8nHA

Ge = Ee(G) Qe = Ee(Q) n = G.order() alpha = Ge.weil_pairing(B, n) beta = Qe.weil_pairing(B, n)

Finally we compute the discrete logarithm in this finite field to find the secret multiplier.

nQ = beta.log(alpha)

The complete function is:

def movAttack(G, Q): k = 1 while (p**k - 1) % E.order(): k += 1 Ee = EllipticCurve(GF(p**k, 'y'), [a, b]) R = Ee.random_point() m = R.order() d = gcd(m, G.order()) B = (m // d) * R assert G.order() / B.order() in ZZ assert G.order() == B.order() Ge = Ee(G) Qe = Ee(Q) n = G.order() alpha = Ge.weil_pairing(B, n) beta = Qe.weil_pairing(B, n) print('Computing log...') nQ = beta.log(alpha) return nQ

There are 2 ways to get the points and calculate the secret coordinates. We can use the UI or the API. I will provide the manual way for aesthetic reasons and then the automatic solver that uses the API.

Manual exploitation

Let’s start by defining the curve:

from sage.all import * a = -35 b = 98 p = 434252269029337012720086440207 Gx = 16378704336066569231287640165 Gy = 377857010369614774097663166640 E = EllipticCurve(GF(p), [a, b])

Let’s visit the website again.

Gather all of the points:

Px = 0x43984282fd683ce8a6ae3b3be Py = 0x566a38eb7dbd4af0ed45421cb Qx = 0x3b505638fdfbf59f732e44805 Qy = 0xb8823b17c3f4047dc20a3afa P = E(Px, Py) Q = E(Qx, Qy)

Run the attack for Q:

nQ = movAttack(G, Q) secret_point = nQ * P

The complete script is:

from sage.all import * a = -35 b = 98 p = 434252269029337012720086440207 E = EllipticCurve(GF(p), [a, b]) def movAttack(G, Q): k = 1 while (p**k - 1) % E.order(): k += 1 Ee = EllipticCurve(GF(p**k, 'y'), [a, b]) R = Ee.random_point() m = R.order() d = gcd(m, G.order()) B = (m // d) * R assert G.order() / B.order() in ZZ assert G.order() == B.order() Ge = Ee(G) Qe = Ee(Q) n = G.order() alpha = Ge.weil_pairing(B, n) beta = Qe.weil_pairing(B, n) print('Computing log...') nQ = beta.log(alpha) return nQ Gx = 16378704336066569231287640165 Gy = 377857010369614774097663166640 Px = 0x43984282fd683ce8a6ae3b3be Py = 0x566a38eb7dbd4af0ed45421cb Qx = 0x3b505638fdfbf59f732e44805 Qy = 0xb8823b17c3f4047dc20a3afa G = E(Gx, Gy) P = E(Px, Py) Q = E(Qx, Qy) nQ = movAttack(G, Q) secret_point = nQ * P print(hex(secret_point[0])) print(hex(secret_point[1]))

And the output:

┏━[/tmp] ┗━━ □ python3 exploit.py Computing log... 0x34b15c6cdfc3233e1a0490134 0x57832f1e17a296d194cceefc3

Entering them to the secret coordinates field.

Give us the flag.

Automated exploitation

The automated solver that uses the API is:

from sage.all import * import requests def movAttack(G, Q): k = 1 while (p**k - 1) % E.order(): k += 1 Ee = EllipticCurve(GF(p**k, 'y'), [a, b]) Ge = Ee(G) Qe = Ee(Q) R = Ee.random_point() m = R.order() d = gcd(m, G.order()) B = (m // d) * R assert G.order() / B.order() in ZZ assert G.order() == B.order() n = G.order() alpha = Ge.weil_pairing(B, n) beta = Qe.weil_pairing(B, n) print('Computing log...') nQ = beta.log(alpha) return nQ url = 'http://127.0.0.1:1337/' a = -35 b = 98 p = 434252269029337012720086440207 Gx = 16378704336066569231287640165 Gy = 377857010369614774097663166640 E = EllipticCurve(GF(p), [a, b]) print("Elliptic Curve defined...\n") coordinates = requests.get(url + 'api/coordinates') departed_x = coordinates.json()['departed_x'] departed_y = coordinates.json()['departed_y'] print("Got Departed point coordinates!!\n") print(f" X: {departed_x}") print(f" Y: {departed_y}\n") present_x = coordinates.json()['present_x'] present_y = coordinates.json()['present_y'] print("Got Present point coordinates!!\n") print(f" X: {present_x}") print(f" Y: {present_y}\n") G = E(Gx, Gy) P = E(present_x, present_y) Q = E(departed_x, departed_y) print("Calculating private key using MOV attack...\n") nQ = movAttack(G, Q) print("Private key found!!\n") print(f"nQ: {hex(nQ)}\n") secret_point = nQ * P print("Calculated Destination point!!\n") print(f"X: {hex(secret_point[0])}") print(f"Y: {hex(secret_point[1])}\n") payload = { 'destination_x': hex(secret_point[0]), 'destination_y': hex(secret_point[1]) } flag = requests.post(url + 'api/get_flag', json=payload) print("Sending coordinates...") flag = flag.json()['flag'] print("Got flag...\n")

And that’s a wrap for this challenge write-up!