baby crypto
chal.py, outputhack10{flag_here}1. Challenge Overview
We received a cryptography challenge consisting of two files: chal.py, which is the encryption script, and output, a binary file containing the encrypted data. The objective is to reverse the encryption procedure to recover the original hack10{...} formatted flag by parsing the given custom cipher stream.
2. Initial Reconnaissance
The original Python script (chal.py) outlines the custom encryption scheme snippet. We initially explored the source logic and the binary output contents.
To analyze the output file, we generated a hex dump using the xxd tool:
1$ cat chal.py2import os, hashlib, random, binascii3flag = b'hack10{REDACTED}'4encrypted = ""5for i in range(0,len(flag),2):6 a = random.randint(90, 128)7 b = random.randint(1,15)8 cipher = hashlib.sha512(flag[i:i+2]).hexdigest()9 encrypted += binascii.hexlify(os.urandom(random.randint(0, 31))).decode('utf-8')10 encrypted += cipher[b:a]11with open("output", "wb") as f:12 f.write(bytes.fromhex(encrypted))To take a peek at the initial hex bytes of the output file:
1$ xxd -p output | head -n 32609f87e8252266d2da91565659b0068c053da6194aebae11c565b4b4a249392f743be89b586691bd124f40d4e41eacde166e7edf6a657febd8ad913aa4bc9bf832440dcebf0d306eff67592e7147739b1a0512d695c11c611484e93. Analysis / Forensics Path
Analyzing chal.py revealed a weakness in the encryption mechanism:
The script processes the flag sequentially in 2-byte chunks (flag[i:i+2]).
For each chunk, it computes the SHA512 hash and gets its 128-character hexadecimal representation (cipher).
It appends random hex padding (0 to 31 bytes -> up to 62 hex characters).
Most crucially, it appends a slice of the SHA512 hash string: cipher[b:a], where b is random between 1 and 15, and a is random between 90 and 128.
Because b <= 15 and a >= 90, the sliced hash string always includes the substring cipher[15:90]. This 75-hex-character substring acts as an unmistakable hash "signature" for that specific 2-byte combination.
Since there are only $256 \\times 256 = 65,536$ possible 2-byte chunks (and 256 possible 1-byte chunks at the very end of the flag), we can compute the SHA512 hashes for all potential chunks offline. By extracting their respective cipher[15:90] signatures, we can search the entire output hex string for matches without worrying about the prepended random bytes or the variance in a and b.
4. Exploitation / Recovery
To exploit this structure flaw, we implement a solver script that scans a dictionary of potential signatures and searches their indexes within the sequence:
1import hashlib2# Read the hex representation of the output output stream exactly as written 3with open('output', 'rb') as f:4 encrypted = f.read().hex()5candidates = []6print("Searching 2-byte chunks...")7for i in range(256):8 for j in range(256):9 chunk = bytes([i, j])10 h = hashlib.sha512(chunk).hexdigest()11 # The guaranteed static substring from indices 15 through 9012 sig = h[15:90]13 idx = 014 while True:15 idx = encrypted.find(sig, idx)16 if idx == -1:17 break18 candidates.append((idx, len(chunk), chunk))19 idx += 120print("Searching 1-byte chunks (if the flag length is odd)...")21for i in range(256):22 chunk = bytes([i])23 h = hashlib.sha512(chunk).hexdigest()24 sig = h[15:90]25 idx = 026 while True:27 idx = encrypted.find(sig, idx)28 if idx == -1:29 break30 candidates.append((idx, len(chunk), chunk))31 idx += 132# Sort the matching pairs strictly by their location mapped in the encrypted string block33candidates.sort()34flag = b""35for c in candidates:36 flag += c[2]37print("Found flag:")38print(flag.decode('ascii', errors='ignore'))Executing the solver script sequentially extracts and constructs the original payload to decode the final flag string!
5. Flag
6. Summary of Approach & Key Takeaways
Methodology Recap:
Identified the block-wise processing of the flag loop.
Determined the minimum bounds for the randomized slice (a and b), finding that the indices between 15 and 90 of the hash digest are always leaked unconditionally.
Precomputed SHA512 signatures for all 2-byte (and potential 1-byte final) segments and extracted their static 75-character subsets.
Mapped these subsets efficiently back into their spatial order in the encrypted hex string utilizing offset indexing.
Concatenated the corresponding original plain text blocks sequentially.
Key Takeaway: Short block sizes coupled with deterministic components present severe vulnerability risks, regardless of surrounding "randomized" padding. Because cryptographic digest collisions on 75-character strings are astronomically rare, the static slice becomes an exact fingerprint, reducing the cryptographic strength to simple dictionary lookups.