Difficulty
easyCategories
cryptoDescription
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.gzService
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}