Challenge: Spoiled Cheese Pull · CTF: DalCTF 2026 · Category: Forensics · Difficulty: Medium
“My cheese got pulled and now I can’t eat it. Can you help me find out who did it?”
A short, silly prompt and a single file: chall.png. The whole challenge is a chain of file‑format trolls — the name “Spoiled Cheese Pull” is a hint in itself: the file is spoiled (corrupted on purpose), and we have to pull it back into shape. Let’s walk through it.
1. First contact — trust nothing about the extension
The file is named chall.png, but the first rule of forensics is to never trust an extension. Run file on it:
| |
Interesting. The extension says PNG, but file swears it’s a JPEG. And even the JPEG identification is nonsense:
JFIF standard 13.73— there is no JFIF version 13.73 (real ones are 1.00, 1.01, 1.02).density 17748x0— a zero density is invalid.thumbnail 3x42— garbage dimensions.
When file reports a “valid” type but every field inside it is impossible, that’s a giant red flag that the magic bytes were forged. The file is wearing a JPEG costume. Time to look at the actual bytes.
2. Reading the hex — a PNG hiding under a JPEG mask
| |
The first 11 bytes are a textbook JPEG opener:
| |
But look at what follows. Right after the fake JFIF header we see strings that almost look like PNG chunk names:
| Found in file | Should be | Note |
|---|---|---|
IHET | IHDR | PNG header chunk |
ISAD | IDAT | PNG image-data chunk |
And immediately after ISAD we find 78 9C — the unmistakable zlib “default compression” header. PNG image data (IDAT) is always a zlib stream, and 78 9C is its most common starting signature. There is no doubt now: this is a PNG, not a JPEG.
Someone took a normal PNG and:
- Replaced the 8‑byte PNG signature (
89 50 4E 47 0D 0A 1A 0A) with a fake JPEG/JFIF header. - Renamed the PNG chunk type names with subtle character swaps.
A nice detail: why the lengths still line up
Decode the IHDR data that follows the (broken) IHET name:
| |
Those are perfectly sane image dimensions (810 × 110, 8‑bit RGBA). The IHDR data block was left completely intact — only the chunk name was vandalized. The forgery is purely cosmetic: replace the signature, scramble the 4‑letter chunk identifiers, and file’s signature database gets fooled into matching the JPEG magic at offset 0.
Let’s also check the tail of the file before fixing anything:
| |
Two more trolls at the end:
SEND— the renamedIENDchunk (the PNG end-of-file marker).Nothing2SeeHereGoLookSomewhereElse— junk bytes appended after the image ends, pure misdirection. (Classic.)
So the full list of damage is:
| Original | Tampered |
|---|---|
| PNG signature | JPEG/JFIF header |
IHDR | IHET |
IDAT | ISAD |
IEND | SEND |
| (nothing) | trailing Nothing2See... junk |
3. Un-spoiling the cheese — rebuilding a valid PNG
The repair is mechanical. Restore the signature + IHDR header, fix the chunk names, and chop off the trailing junk at the end of the real IEND chunk:
| |
Why offset 16? A real PNG begins with the 8-byte signature, then
00 00 00 0D(IHDR length = 13), thenIHDR— that’s 16 bytes before the width field at offset0x10. The forger overwrote exactly that 16-byte region with the JPEG mask, keeping everything from the width onward intact. Restoring 16 clean bytes lines the whole file back up.
Verify:
| |
It opens cleanly. Rendering it gives a long, thin black-and-white image — clearly some kind of barcode:
| |
4. “Why so long?” — identifying the barcode
The image looks at first like a 1‑D barcode, so the instinct is to throw zbar at it:
| |
Nothing. Scaling, padding with a quiet zone, binarizing — still nothing. zbar simply doesn’t support this symbology.
So let’s actually look at the structure instead of guessing. Sample the image down onto its module grid (each module is 10 px):
| |
| |
Now the structure jumps out:
- A solid black finder running down the left edge (
#######). - Alternating timing patterns (
#.#.#.#) along the top and bottom rows. - Only 9 module rows tall but 81 modules wide — a very rectangular code.
A square code (QR, Aztec, Data Matrix) is ruled out by the aspect ratio. A normal stacked code wider than it is tall, with a corner finder and edge timing patterns — this is a rMQR Code (Rectangular Micro QR), a newer ISO symbology designed for exactly these long, narrow label spaces.
And that is the punchline of the challenge title and the flag: the code is “long” because it’s the rectangular QR variant. “Why so long?”
5. Decoding the rMQR
zbar can’t read rMQR, but ZXing (via the zxing-cpp Python bindings) can:
| |
| |
| |
There it is — the symbology is confirmed as rMQR Code, and the payload is the flag.
Flag
| |
Takeaways
filereads magic bytes, not reality. Forged signatures plus impossible field values (JFIF 13.73, density0) are a tell that the header was hand-edited. Always confirm with a hex dump.- PNG structure is easy to repair by hand. The signature is fixed, chunk names are fixed-length 4‑byte ASCII, and
IHDR/IDAT/IENDare unmissable. Spotting the78 9Czlib header right after a chunk name is a reliable “this is really a PNG” confirmation. - Trailing data after
IENDis almost always a decoy (or sometimes a hidden payload — worth checking, but here it was justNothing2SeeHere...). - When
zbarfails, the symbology may simply be unsupported. Inspect the module grid to identify the format, then reach for ZXing / zxing-cpp, which covers PDF417, Aztec, Data Matrix, MaxiCode, and the rectangular Micro QR (rMQR) used here.
Cheese successfully un-spoiled. 🧀