$cat code-jumbler.mdReversingHackultet 2026
Code Jumbler
A custom file "jumbler" runs four layers of reversible scrambling, each seeded from the same CRC32, which makes the whole thing fully invertible from the scrambled file alone, no key needed.
$man code-jumbler
What this one teaches:
- Probing an unknown binary with crafted inputs (single bytes, sequential runs, distinct blocks) before reading any disassembly.
- Recognising common building blocks in assembly, an LCG, a CRC32 table, a Fisher-Yates shuffle, a substitution box.
- Why a transformation seeded only from data already present in the output (a checksum of the plaintext, stored in the header) is deterministically reversible without any secret key.
Task description
I jumbled my payload, can you dejumble it?
The original was in Croatian; this is my translation.
Starting point
The challenge hands over a stripped, statically linked x86-64 binary (jumbler) and a scrambled file, flag.jmbl, in a custom format. Before touching a disassembler, running the jumbler on small, crafted test inputs (a single repeated byte, a sequential run, a file of visibly distinct 256-byte blocks) showed the output’s structure directly: a fixed-size header, then the data split into 256-byte blocks that had clearly been reordered and transformed, with a few extra blocks appended that didn’t correspond to anything in the input.
The file format
The header is 38 bytes:
| Offset | Size | Field |
|---|---|---|
| 0 | 5 | Magic, CJMB2 |
| 5 | 1 | Version |
| 6 | 8 | Original file size |
| 14 | 4 | Block size (always 256) |
| 18 | 4 | Number of real blocks |
| 22 | 4 | Number of padding blocks |
| 26 | 4 | CRC32 of the original data |
| 30 | 8 | A fixed constant |
Everything after the header is (real blocks + padding blocks) * 256 bytes of transformed data.
The four layers
Reading the disassembly (no decompiler, just objdump, working through it by hand) turned up four transformations applied in sequence, each with its seed derived from the stored CRC32:
- Block shuffling. The input is split into 256-byte blocks, and a few extra, randomly filled padding blocks are mixed in (their count depends on the file size and whether it looks like a known binary format). Every block, real and padding, gets reordered using a Fisher-Yates shuffle driven by an LCG seeded from
CRC32 XOR a fixed constant XOR the file size. - Byte substitution. A 256-entry substitution table is built the same way, a Fisher-Yates shuffle over
[0..255]seeded fromCRC32 XOR the fixed constant XOR another constant, and every byte in every block is replaced through that table. - XOR with a static key. A 12-byte key, embedded in the binary at a fixed address, is XORed cyclically across every byte.
- Bit rotation. Each byte at position
iis rotated left by((i * 31) % 11) + 1bits.
Why no key is needed to reverse it
Every seed in this chain traces back to the CRC32 of the original, unscrambled data, and that CRC32 is stored in plaintext in the header of the scrambled file itself. That means every PRNG sequence, every substitution table, every shuffle permutation used to produce flag.jmbl can be recomputed from flag.jmbl alone: read the stored CRC32, rebuild the same LCG, rebuild the same substitution table, and run the whole pipeline backwards. The CRC32 does double duty as both a verification checksum and the seed for every pseudo-random step, which is exactly what makes the scrambling fully self-contained and reversible.
Reversing in the opposite order from how they were applied:
# 1. Undo bit rotation: rotate each byte right instead of left.
# 2. Undo the XOR: XOR is its own inverse, so XOR with the same key again.
# 3. Undo the substitution: rebuild the same table, then apply its inverse permutation.
# 4. Undo the block shuffle: rebuild the same LCG sequence, replay the same
# Fisher-Yates shuffle to get the permutation, then place each block back
# at its original index, dropping the padding blocks (marked with index -1).
After each step, recomputing the CRC32 of the recovered data and checking it against the one stored in the header confirms the chain was undone correctly.
Running it
The recovered file, once fully de-jumbled, is itself a small statically linked ELF binary. Running it is the last step: it simply prints the flag.
$ python3 dejumbler.py flag.jmbl flag_decoded
orig=866344, blocks=3385, padding=11, total=3396, crc=0xe5ce137e
CRC: MATCH
$ chmod +x flag_decoded && ./flag_decoded
CTF2026[960584536674]
What made this tractable
Testing the jumbler as a black box first, before any disassembly, meant the file format and the broad shape of the transformation (block-based, reordered, substituted) were already known going in; the disassembly work was then about confirming which specific primitives (an LCG with a recognisable multiplier constant, the standard CRC32 polynomial 0xedb88320, a Fisher-Yates loop) were in play, rather than reconstructing the algorithm from nothing.
$cat flag.txt reveal
CTF2026[960584536674]