DEV Community

Cover image for BroncoCTF : Magic Ways Writeup
Yogeshwar Peela
Yogeshwar Peela

Posted on • Originally published at exploitnotes.hashnode.dev

BroncoCTF : Magic Ways Writeup

Executive Summary

challenge.png was a PNG file that had been deliberately mangled at the byte level so that neither the OS, exiftool, nor pngcheck would recognize it as a valid image. The fix required manually repairing the file's binary structure in three passes: restoring the correct PNG magic-byte signature, correcting a zeroed-out height field in the IHDR chunk, and recomputing/patching the IHDR chunk's CRC32 checksum so PNG parsers would trust the corrected header. Once valid, the file opened cleanly and displayed the flag as plain text rendered on a solid background.

Root cause / challenge design: the PNG format's own self-describing structure (magic bytes → chunk length → chunk type → chunk data → CRC) was used as the puzzle itself — each field had to be identified, understood, and hand-repaired in sequence, with each fix only revealing the next thing that was still broken.

Flag:bronco{wh4t_ar3_mag1c_byt3s}


Initial Triage

Every standard tool refused to touch the file, which was the first clue this wasn't image corruption — it was intentional tampering:

$file challenge.png
challenge.png: data
$exiftool challenge.png
Error : File format error
$pngcheck -v challenge.png
this is neither a PNG or JNG image nor a MNG stream
$binwalk challenge.png
41 0x29 Zlib compressed data, default compression
Enter fullscreen modeExit fullscreen mode

That last line was the tell: binwalk found valid zlib-compressed data (the IDAT chunk's payload) sitting at a normal-looking offset inside the file. If the compressed image data was intact, the surrounding PNG container was what had been broken — not the image itself.

Reading the Raw Bytes

$hexdump challenge.png
0000000 adde efbe 0000 0000 0000 0d00 4849 5244
Enter fullscreen modeExit fullscreen mode

hexdump's default output groups bytes as little-endian 16-bit words, which reads backwards from the actual byte order. Untangling it, the first 8 bytes on disk were:

DE AD BE EF 00 00 00 00
Enter fullscreen modeExit fullscreen mode

DEADBEEF, a classic placeholder/joke value, sitting exactly where the PNG magic-byte signature belongs. The real signature is a fixed 8-byte constant every valid PNG must start with:

89 50 4E 47 0D 0A 1A 0A
Enter fullscreen modeExit fullscreen mode

Every tool's refusal to parse the file made sense immediately: this signature is the very first thing any PNG-aware library checks before reading anything else.

Fix 1 — Magic Bytes

Patched the first 8 bytes to the correct signature (via hex editor):

DE AD BE EF 00 00 00 00 → 89 50 4E 47 0D 0A 1A 0A
Enter fullscreen modeExit fullscreen mode

Re-running exiftool afterward showed real progress — the file was now recognized as a PNG, with a plausible IHDR chunk:

File Type : PNG
Image Width : 500
Image Height : 0
Enter fullscreen modeExit fullscreen mode

Recognized, but not correct — height was 0, which meant the image had no pixel rows to decode.

Fix 2 — IHDR Height Field

The IHDR chunk layout (after the 4-byte length + IHDR type tag) is:

width (4 bytes) | height (4 bytes) | bit depth | color type | compression | filter | interlace
Enter fullscreen modeExit fullscreen mode
$xxd -g1-l 32 challenge.png
00000010: 00 00 01 f4 00 00 00 00 08 02 00 00 00 00 00 00
└─ width=0x1F4 (500) ─┘ └─ height=0 ─┘
Enter fullscreen modeExit fullscreen mode

Width was correctly 0x000001F4 (500px), but height had been zeroed. Since the image was expected to be square (500×500, a reasonable assumption confirmed once it rendered), the height field was patched to match the width:

printf'\x00\x00\x01\xf4' | dd of=challenge.png bs=1 seek=20 conv=notrunc
Enter fullscreen modeExit fullscreen mode
$exiftool challenge.png
Image Width : 500
Image Height : 500
Enter fullscreen modeExit fullscreen mode

Correct dimensions now — but the chunk still wouldn't validate, because editing the IHDR data invalidated its trailing CRC32 checksum.

Fix 3 — IHDR CRC32

Every PNG chunk ends with a CRC32 computed over its type tag + data (not including the length field). Recomputed it in Python to match the corrected IHDR contents:

importzlibchunk=b'IHDR'+bytes.fromhex('000001f4'# width = 500
'000001f4'# height = 500
'0802000000'# bit depth, color type, compression, filter, interlace
)crc=zlib.crc32(chunk)&0xffffffffprint(crc.to_bytes(4,"big").hex())# 44b448dd
Enter fullscreen modeExit fullscreen mode

Patched the 4 CRC bytes immediately following the IHDR data (offset 29):

printf'\x44\xb4\x48\xdd' | dd of=challenge.png bs=1 seek=29 conv=notrunc
Enter fullscreen modeExit fullscreen mode

At this point the file was a fully valid, standards-compliant PNG.

Result

feh challenge.png
Enter fullscreen modeExit fullscreen mode

Opened cleanly — a 500×500 image with a solid blue banner containing the flag rendered as plain white text:

bronco{wh4t_ar3_mag1c_byt3s}
Enter fullscreen modeExit fullscreen mode

Key Techniques

#TechniquePurpose
1Reading raw bytes with hexdump/xxd and reasoning about endiannessIdentify that the magic-byte signature had been swapped for DEADBEEF
2Knowing the fixed PNG signature (89 50 4E 47 0D 0A 1A 0A)Restore the container so parsers would even attempt to read it
3Understanding IHDR chunk layout (width/height/bit depth/etc.)Locate and correct the zeroed height field
4Recomputing CRC32 over type + data with zlib.crc32Make the corrected chunk pass integrity validation
5binwalk to confirm the actual image payload (zlib/IDAT) was intactConfirmed the fix only needed to target the container, not the pixel data

Lessons / Takeaways

  • File format "magic bytes" and per-chunk checksums exist specifically to let parsers fail fast and safely on malformed input — which also makes them a clean, minimal way to build a forensics puzzle: break the smallest number of bytes needed to make every standard tool refuse the file.
  • binwalk is a good first move on "this file won't open" challenges — it fingerprints embedded structures (like a valid zlib stream) independent of whether the outer container parses, which tells you whether you're repairing a container or recovering lost data.
  • PNG CRCs are computed over chunk type + chunk data, not the 4-byte length prefix — an easy mistake to make when hand-rolling the checksum.

Attack Chain

challenge.png (fails file/exiftool/pngcheck)
│
├── binwalk ──► valid zlib/IDAT data found ──► container is broken, not the image
│
├── hexdump first bytes ──► DE AD BE EF 00 00 00 00 (fake signature)
│ │
│ └── patch ──► 89 50 4E 47 0D 0A 1A 0A (real PNG signature)
│
├── exiftool ──► Height = 0
│ │
│ └── patch IHDR height (offset 20) ──► 000001F4 (500)
│
├── pngcheck still invalid ──► IHDR CRC mismatch
│ │
│ └── recompute CRC32(type+data) ──► patch offset 29 ──► 44 B4 48 DD
│
▼
Valid PNG ──► feh challenge.png
│
▼
FLAG: bronco{wh4t_ar3_mag1c_byt3s}
Enter fullscreen modeExit fullscreen mode

Top comments (0)