Genigma

Contents

Genigma#

DISCLAIMER: our team did not solve this challenge during the live competition; we first solved it while preparing for this write-up.

Helpers#

import numpy as np
import os
import requests
import time
from dotenv import load_dotenv
from string import ascii_uppercase, digits

load_dotenv()
API_KEY = os.environ["API_KEY"]
URL_QUERY = "https://%s.crucible.dreadnode.io/%s"
URL_FLAG = "https://crucible.dreadnode.io/api/challenges/%s/submit-flag"
CHALLENGE = "genigma"
CHARS = ascii_uppercase + digits

def query(data, route="score"):
    response = requests.post(
        URL_QUERY % (CHALLENGE, route),
        headers={ "X-API-Key": API_KEY },
        json={ "data": data }
    )
    return response.json()

def submit(flag):
    response = requests.post(
        URL_FLAG % CHALLENGE,
        headers={ "X-API-Key": API_KEY },
        json={ "challenge": CHALLENGE, "flag": flag }
    )
    return False if response.status_code != 200 else response.json().get("correct")

Solution#

If we submit some random values to the endpoint we can see that it returns 16 logits.

data = np.zeros((100, 1))
query(data.tolist(), route="predict")["prediction"]
[[0.13794109654006212],
 [0.14017329184141292],
 [0.18642894700537402],
 [0.1637307606500884],
 [0.13963116830454997],
 [0.16077281815261674],
 [0.13729851190502865],
 [0.13969673425944382],
 [0.18523813603837574],
 [0.13947802411484708],
 [0.1383049461891841],
 [0.13842525959608593],
 [0.13832571431083493],
 [0.16387797054251438],
 [0.16311711035024368],
 [0.1879446894428261]]

For each of the 16 logits we can submit 100 different queries where each query contains 99 zeros and a single large value. If we take the index which maximizes the logit and then index into the alphabet (including digits) we can find the flag.

Note: for convenience, the below code only submits 36 different queries as it turns out that the logit is always maximized within the first 36 values.

flag_chars = []
for logit in range(16):
    max_value = 0.0
    max_option = 0
    for option in range(36):
        data = np.zeros((100, 1))
        data[option][0] = 100
        response = query(data.tolist(), route="predict")
        value = response["prediction"][logit][0]
        if value > max_value:
            max_value = value
            max_option = option
        time.sleep(0.1)
    flag_chars.append(CHARS[max_option])

flag = "".join(flag_chars)
print(flag)
print("Flag accepted:", submit(query(flag)["flag"]))
GEN0M1CINV3RS10N
Flag accepted: True