Difficulty
easy
Categories
crypto
Description
Do you have a park with dinosaurs of your own? Then make sure back your dinos up regularly! You never know when the next mass extinction event will happen, so better safe than sorry. We encrypt all your dinosaur data so that you need not worry that anyone is able to copy your designs. Our encrypted designs are uploaded as well for anyone to verify that we use dinosaur-grade cryptography!
Author
Freakness109
Attachments
dino-vault.tar.gz
Service
Challenge has a remote instance.
LLM Usage
I used ChatGPT to generate a reverse 'to_dna' function.

Solution

We get an app.py implementing a dataclass for a dino (RSA variables were renamed from generic names from the handout version):

import os
import socketserver
from dataclasses import dataclass
from Crypto.Util.number import getPrime, long_to_bytes, bytes_to_long

FLAG = os.getenv("FLAG", "fake_flag")
print(FLAG)

primesize = 2048

@dataclass
class Dino:
    name: str
    dna: str
    q: int

    @staticmethod
    def to_dna(dinosaur_information: str):
        lookup = ["A", "T", "G", "C"]
        dna = []
        for c in dinosaur_information:
            c = ord(c)
            for _ in range(4):
                dna.append(lookup[c & 3])
                c >>= 2
        return "".join(dna)

    def get_encrypted_dna(self):
        p = getPrime(primesize)
        public_n = p * self.q
        public_e = 2**16 + 1
        resampled_dna = pow(bytes_to_long(self.dna.encode()), public_e, public_n)
        encrypted_dna = long_to_bytes(resampled_dna).hex()
        return encrypted_dna, public_n

class DinoVaultServer(socketserver.StreamRequestHandler):
    def write(self, string):
        self.wfile.write(f"{string}\n".encode())

    def read(self):
        return self.rfile.readline().rstrip().decode("utf-8")

    def read_int(self):
        line = self.read()
        try:
            ret = int(line)
            return ret
        except ValueError:
            return 0
        return 0

    def prepare_dinos(self):
        self.dinos = [
            Dino(name="Vexillum Rex",  dna=Dino.to_dna(f"Has a crown and {FLAG} written on its back"), q=getPrime(primesize)),
            Dino(name="Pedosaurus",    dna=Dino.to_dna("Has giant feet"), q=getPrime(primesize)),
            Dino(name="Despotiraptor", dna=Dino.to_dna("Slightly sus ethics"), q=getPrime(primesize)),
            Dino(name="Planosaurus",   dna=Dino.to_dna("Is floored when nothing goes to plan"), q=getPrime(primesize)),
        ]

    def create(self):
        self.write("What is your dinosaur called?")
        name = self.read()
        self.write(f"Please give me all the information about {name}")
        info = self.read()
        self.write("Thanks, we have now sequenced your dinosaur.")
        q = getPrime(primesize)
        self.write(f"To access your dino, you will need this key: {q}")
        self.dinos.append(Dino(name=name, dna=Dino.to_dna(info), q=q))

    def view(self):
        self.write("We have the following selection of dinosaurs available:")
        for dino in self.dinos:
            self.write(f"- {dino.name}")

    def download(self):
        self.write("Which encrypted dinosaur-DNA do you want to download?")
        name = self.read()
        for dino in self.dinos:
            if name == dino.name:
                break
        else:
            self.write("Could not find the dino you were looking for")
            return
        self.write("Here is the encrypted DNA you wanted:")
        enc_dna, mod_index = dino.get_encrypted_dna()
        self.write(enc_dna)
        self.write("Use your vault key alongside the modulation index to access the DNA:")
        self.write(mod_index)

    def menu(self):
        self.write("")
        self.write("You can:")
        self.write(" 1. Create your own Dino")
        self.write(" 2. View all available dinos")
        self.write(" 3. Download the DNA of a dino")
        self.write(" 4. Exit")

    def welcome(self):
        self.write("Hello and welcome to the")
        self.banner()
        self.write("")
        self.write("Here, you can give us your 🦕 and we extract and store DNA samples for all your cloning needs.")
        self.write("Make sure to back up your creations before the next apocalypse!")

    def banner(self):
        self.write("""
                   """)
        self.write("Vault")

    def handle_choice(self, choice):
        match choice:
            case 1:
                self.create()
                return True
            case 2:
                self.view()
                return True
            case 3:
                self.download()
                return True
            case 4:
                self.write("See you soon at the")
                self.banner()
                return False
            case _:
                self.menu()
                return True

    def handle(self):
        self.prepare_dinos()
        self.banner()
        self.menu()

        choice = self.read_int()
        while self.handle_choice(choice):
            self.write("What do you want to do now?")
            choice = self.read_int()


if __name__ == "__main__":
    HOST, PORT = "0.0.0.0", 5000
    with socketserver.ThreadingTCPServer((HOST, PORT), DinoVaultServer) as server:
        server.serve_forever()

The interesting part is, that every time when we call the get_encrypted_dna function, we get the same data encrypted with another q value.

This means, we can request the Vexillum Rex twice, take the greatest common divisor (p) and recover the RSA private key.

All that’s left after that is to undo the DNA encoding that is applied:

from pwn import *
from Crypto.Util.number import long_to_bytes
from math import gcd

def from_dna(dna: str):
    lookup = {"A": 0, "T": 1, "G": 2, "C": 3}
    out = []
    for c in range(0, len(dna), 4):
        chunk = dna[c:c+4]
        char=0
        for j, b in enumerate(chunk):
            char |= lookup[chr(b)] << (j * 2)
        out.append(chr(char))
    return "".join(out)

r = remote('<chall>', 31337, ssl=True)
r.sendline(b"")

def get_dino(name):
    r.recvuntil(b"What do you want to do now?")
    r.sendline(b"3")
    r.sendlineafter(b"Which encrypted dinosaur-DNA do you want to download?", name)
    r.recvuntil(b"Here is the encrypted DNA you wanted:\n")
    ct = int(r.recvline(),16)
    r.recvuntil(b"Use your vault key alongside the modulation index to access the DNA:\n")
    n = int(r.recvline())
    return ct,n


ct1, n1 = get_dino(b"Vexillum Rex")
ct2, n2 = get_dino(b"Vexillum Rex")

# common p factor
p = gcd(n1, n2)
success(f"{p=}")

e =  2**16 + 1
q = n1 // p
phi = (p-1)*(q-1)
d = pow(e, -1, phi)
success(from_dna(long_to_bytes(pow(ct1, d, n1))))

Flag:

dach2026{R5A_a_d1n0s5r_0f_1ts_0wn_5b533da361e9}