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 mode Exit 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 mode Exit 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 mode Exit 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 mode Exit 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 mode Exit 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 mode Exit 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 mode Exit 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 mode Exit 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 mode Exit fullscreen mode
$ exiftool challenge.png
Image Width    : 500
Image Height   : 500
Enter fullscreen mode Exit 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:

import zlib
chunk = b'IHDR' + bytes.fromhex(
    '000001f4'    # width  = 500
    '000001f4'    # height = 500
    '0802000000'  # bit depth, color type, compression, filter, interlace
)
crc = zlib.crc32(chunk) & 0xffffffff
print(crc.to_bytes(4, "big").hex())
# 44b448dd
Enter fullscreen mode Exit 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 mode Exit fullscreen mode

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

Result

feh challenge.png
Enter fullscreen mode Exit 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 mode Exit fullscreen mode

Key Techniques

# Technique Purpose
1 Reading raw bytes with hexdump/xxd and reasoning about endianness Identify that the magic-byte signature had been swapped for DEADBEEF
2 Knowing the fixed PNG signature (89 50 4E 47 0D 0A 1A 0A) Restore the container so parsers would even attempt to read it
3 Understanding IHDR chunk layout (width/height/bit depth/etc.) Locate and correct the zeroed height field
4 Recomputing CRC32 over type + data with zlib.crc32 Make the corrected chunk pass integrity validation
5 binwalk to confirm the actual image payload (zlib/IDAT) was intact Confirmed 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 mode Exit fullscreen mode

Top comments (0)