N
Back to writeup archive
Hack@10 International Capture The Flag Pre-Eliminary Round 2026

baby crypto

Hex stream reconstruction and ordered pair mapping from the encrypted output.

CryptoMarkdown + PDFEasysolved2 minread·505words
route
/TryN3rr0r/writeups/hack10-pre-eliminary-2026/baby-crypto
archive index
2 / 15
read mode
parsed markdown
Writeup loaded.

baby crypto

challenge brief
target profile, scoring, and execution stack
verified solve
target
chal.py, output
category
Crypto
points
494
difficulty
Medium
flag format
hack10{flag_here}
tools used
Python 3xxdcat
final flag
hack10{a88dacd5fb88dc4973bb3a56fff9be940bb1f1b83c2b82f3f6daa256267c9786f4cdc70255079e3cfaea9956211e615fe78ee9d5a95a832afff2f09b05c39db4}
reveal and copy from section 5

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.

PDF evidence frame 01

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:

bash12 lines
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:

bash4 lines
1$ xxd -p output | head -n 32609f87e8252266d2da91565659b0068c053da6194aebae11c565b4b4a249392f743be89b586691bd124f40d4e41eacde166e7edf6a657febd8ad913aa4bc9bf832440dcebf0d306eff67592e7147739b1a0512d695c11c611484e9
PDF evidence frame 02

3. 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.

PDF evidence frame 03

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:

python38 lines
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!

PDF evidence frame 04

5. Flag

captured flag
hack10{a88dacd5fb88dc4973bb3a56fff9be940bb1f1b83c2b82f3f6daa256267c9786f4cdc70255079e3cfaea9956211e615fe78ee9d5a95a832afff2f09b05c39db4}

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.