Difficulty
medium
Categories
crypto
Description
Look at this punky hash I found. Can you crack it?
Author
berndoJ
Attachments
punkhash.tar.gz
LLM Usage
I used ChatGPT in the browser to get to the general idea of LLL and the CVP approach. The exploit was written by myself.

Solution

The challenge itself has a really small source code:

#!/usr/bin/env python3

import json
import random

from Crypto.Cipher import AES
from Crypto.Util.number import getPrime
from Crypto.Util.Padding import pad

FLAG = "dach2026{xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}"

pbits = 137
p = getPrime(pbits)
key_bits = 16

# Generate hash of the key
factors = [random.SystemRandom().randint(0, p-1) for _ in range(key_bits)]
preimage = [random.SystemRandom().randint(0, 0xff) for _ in range(key_bits)]
hash = sum([factors[i] * preimage[i] % p for i in range(key_bits)]) % p

# Encrypt flag
key = bytes(preimage)
cipher = AES.new(key, AES.MODE_ECB)
enc = cipher.encrypt(pad(FLAG.encode(), 16)).hex()

data = {
    "p": p,
    "factors": factors,
    "hash": hash,
    "enc_flag": enc
}

with open("data.json", "w") as f:
    json.dump(data, f, indent=4)

After a lot of “wtf”, I found out, that we’re dealing with a knapsack -> hidden subset sum problem here.

We only know the factors output, not the preimage values, where each value is an element of range(0xff).

Simplified, this looks like this, but with longer primes:

123456 = sum(
    [
        [ 148 * randint(0, 0xff) % 123 ],
        [ 250 * randint(0, 0xff) % 123 ],
        [ 947 * randint(0, 0xff) % 123 ],
        [ 147 * randint(0, 0xff) % 123 ],
        [ 573 * randint(0, 0xff) % 123 ],
    ] % 123
)

Now, without understanding what a lattice even is, we can steal these Team EU bootcamp slides from 2024, to learn about practical lattice reduction.

We use fpylll, as this seems to be the simplest library to work with.

We need to arrange all of our values onto the matrix like this and add the main diagonal:

pff,f*12,1W**,5WW*W11111111111983131223868838637114037548615178743725561539771372200534998523079223077867934025172159354169567032101561672096565120131493616037663046838718183521025621237872655136491892016208417563223029638982007390764847067264320487595359582310685547828272863607622662296979927767647943261133980621246011945726875745246981657602485201188030612575682888429485933290693759768453010668599028770753055466602532620759324260283988405260528418415013848341056043262522922040856328764505045162212723847596935200697603854942096368387978563753001822891426005922634326873861498498945540079149173587690433306816193109204121212453174724998553110419979453484164738292133394627206476479438585145455673015473111944354644428145662442208000082060000000000000000001000000000000000001000000000000000001000000000000000001000000000000000001000000000000000001000000000000000001000000000000000001000000000000000001000000000000000001000000000000000001000000000000000001000000000000000001000000000000000001000000000000000001000000000000000001

W (the Weight) was just randomly chosen. It is required, to make balance things and my script didn’t work without it. From the slides:

How to determine weights: Wing it and hope for the best :)

After calling LLL.reduction, the matrix looks quite a bit different:

----1111112222222400000000000000000----1-11--113809391288643162-49544566411922832-----21-111----298780-6043537-7323536189153449456----11-2-11--1956541753359596160304133759171188--111-----2-21904864931393557432541535326585886---11--1-13211-858682730235-37382452083050835117------111212-11271815812060543414812276337656766-----111-1-2212321249375415-31273689016800737343--2-1--171562235515517360083056265077723----1-1111111111111135962163850498409412576490479514-----21121-111---984492263270576190358683734243346--1--12-1---111--1521349065344158529754390439885146------1-11-11-11-222349504414837380786197223450954------11111-11-9-68615823204926206224228226939586----111211211813924134118131160907569549828168------11-12212-11-2-18626820941955644596041159598587966-----1-1-21-1-169212341--815538976071473482488388

We now need to find the closest vector (or something like that, I’m still just following simillar writeups and the slides) to the result of the hash.

Thankfully, that is just one call of CVP.closest_vector with another row of the matrix:

has66h6m6*8a8W3g39i98c811881133999944110033115511117733996688003388331133994477993344669988228800Wpr1e22i30mage11[62090]1172017122022102030120140120231120151202451202071201961208012108912400122106120

After that, all that is left to do is to reconstruct the key and AES decrypt the flag.

Full script:

#!/usr/bin/env python3

import json
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from fpylll import LLL, IntegerMatrix, CVP

# stolen from generate.py
pbits = 137
n = 16

# load data
data = json.load(open("./data.json", "r"))
p = data["p"]
factors = data["factors"]
h = data["hash"]
ct = bytes.fromhex(data["enc_flag"])

# good guess
W = 120

M = IntegerMatrix(n+1, n+1)
M[0,0] = W * p
for i in range(1,n+1):
    M[i,i ] = 1 # diagonal
    M[i, 0] = W * factors[i-1]

LLL.reduction(M)

target = [W * h] + [W] * (n)
key = bytearray(CVP.closest_vector(M, target)[1:])
cipher = AES.new(key, AES.MODE_ECB)
flag = unpad(cipher.decrypt(ct), 16).decode()
print(flag)

Flag:

dach2026{r3duc3_m3_b4by_0n3_m0r3_t1m3_5a3d66ae5ef12bb5}