← Back to Writeups
HTBEasyForensics

Magic Ways

D3v0o0Nu119/2/20264 min read
#forensics#htb#easy#chunk_crc_recalculation#ihdr_height_recovery#ocr#png_signature_repair

Magic Ways

Platform: Broncoctf2026 | Category: Forensics | Type: Challenge | Difficulty: Easy | OS: NA | Author: D3v0o0Nu11 | Date: 2026-07-11 | Status: Solved Techniques: chunk_crc_recalculation, ihdr_height_recovery, ocr, png_signature_repair

Summary

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.

Recon

Port scan

bash
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: broncoctf2026 | ID: 20260711_broncoctf2026_magic_ways
  • Tags: png, magic_bytes, image_recovery, crc32, corrupted_png, ihdr, idat
  • Indicators: file reports 'data' not PNG, signature replaced with de ad be ef 00 00 00 00, IHDR height field zeroed, chunk CRC fields zeroed, title references 'magic
  • Source: 20260711_broncoctf2026_magic_ways.md

Foothold

Vulnerability / Misconfiguration

  1. chunk_crc_recalculation
  2. ihdr_height_recovery
  3. ocr
  4. png_signature_repair
bash
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

bash
sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
bash
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • chunk_crc_recalculation
  • ihdr_height_recovery
  • ocr
  • png_signature_repair
  • Tags: png, magic_bytes, image_recovery, crc32, corrupted_png, ihdr, idat

Original Writeup

<details><summary>Click to expand original content</summary>

Description

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.

Analysis

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:

  • PNG signature destroyed — the first 8 bytes are 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.
  • IHDR chunk otherwise intact — length 00 00 00 0d = 13, type IHDR.
    • Width = 00 00 01 f4 = 500.
    • Height = 00 00 00 00 = 0 (zeroed — this is invalid, a PNG cannot have height 0).
    • Bit depth = 8, color type = 02 (truecolor RGB).
  • Chunk CRCs zeroed / invalid — the IHDR CRC and other chunk CRC fields were wiped, so even after fixing the signature pngcheck still complains.
  • First IDAT length = 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.

Recovering the height from IDAT geometry

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.

Solution

  1. Restore the 8-byte PNG signature to 89 50 4e 47 0d 0a 1a 0a.
  2. Parse the chunk stream (each chunk = length + type + data + CRC).
  3. Patch the IHDR height field to 200.
  4. Recompute the CRC32 of every chunk over chunk-type + chunk-data and rebuild the file with correct CRCs.
python
#!/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}
</details>

signed by XESXOR