$ cat writeup.md…
$ cat writeup.md…
broncoctf2026
Task: a corrupted challenge.png that no viewer can open, with a wiped PNG signature, zeroed IHDR height, and invalid chunk CRCs. Solution: restore the 8-byte magic signature, derive the missing height (200) from the inflated IDAT scanline geometry, recompute every chunk CRC32, then OCR the repaired image for the flag.
I rubbed this lamp and out came challenge.png. But, I can't open it for some reason. Help me out and maybe I'll grant you a wish (flag).
A file challenge.png is provided but cannot be opened by any image viewer. The
goal is to repair the corrupted PNG and read the flag rendered inside it.
The title "Magic Ways" is a semantic clue pointing at PNG magic bytes
(the file signature). The flag bronco{REDACTED} confirms this.
file challenge.png reports only data, not a PNG image, so the header is
broken. A hex dump of the first bytes reveals the exact damage:
$ xxd -l 64 challenge.png
00000000: dead beef 0000 0000 0000 000d 4948 4452 ............IHDR
00000010: 0000 01f4 0000 0000 0802 0000 00... ................
Findings:
de ad be ef 00 00 00 00
instead of the correct 89 50 4e 47 0d 0a 1a 0a. The deadbeef marker is a
classic "I was corrupted on purpose" tell.00 00 00 0d = 13, type IHDR.
00 00 01 f4 = 500.00 00 00 00 = 0 (zeroed — this is invalid, a PNG cannot have
height 0).02 (truecolor RGB).pngcheck still complains.00 00 19 3d = 6461 bytes, zlib header 78 9c.So three things must be repaired: the signature, the IHDR height, and every chunk CRC.
The width (500) is known; only the height is missing. Concatenating and inflating all IDAT streams with zlib yields exactly 300,200 bytes.
For an 8-bit RGB (3 bytes/pixel) non-interlaced PNG, each scanline is:
1 filter byte + width * 3 = 1 + 500 * 3 = 1501 bytes
Therefore the number of rows is 300200 / 1501 = 200. Cross-checking, every
inferred scanline starts with a valid PNG filter byte (0–4), confirming the
geometry is 500 × 200, so the missing height is 200.
89 50 4e 47 0d 0a 1a 0a.chunk-type + chunk-data and rebuild
the file with correct CRCs.#!/usr/bin/env python3 import binascii import struct data = open("challenge.png", "rb").read() offset = 8 # skip the (broken) 8-byte signature chunks = [] while offset + 12 <= len(data): length = struct.unpack(">I", data[offset:offset + 4])[0] kind = data[offset + 4:offset + 8] payload = bytearray(data[offset + 8:offset + 8 + length]) chunks.append((kind, payload)) offset += 12 + length if kind == b"IEND": break # The IDAT inflates to exactly 200 scanlines of (filter + 500 RGB pixels). ihdr = chunks[0][1] ihdr[4:8] = struct.pack(">I", 200) # fix zeroed height out = bytearray(b"\x89PNG\r\n\x1a\n") # restore magic bytes for kind, payload in chunks: out += struct.pack(">I", len(payload)) + kind + payload out += struct.pack(">I", binascii.crc32(kind + payload) & 0xffffffff) open("repaired.png", "wb").write(out)
The result validates cleanly:
$ file repaired.png
repaired.png: PNG image data, 500 x 200, 8-bit/color RGB, non-interlaced
$ pngcheck repaired.png
OK: repaired.png (500x200, 8-bit/color RGB, non-interlaced)
The image renders the flag as text. Extract it with OCR:
$ tesseract repaired.png stdout
bronco{REDACTED}
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar