Ever-broken
Ever-broken
Platform: hackerlab | Category: Forensics | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-04-05 | Status: Solved Techniques: ihdr_crc_recalculation, ocr_extraction, png_dimension_bruteforce, png_signature_repair
Summary
Task: recover a corrupted PNG stored as an untyped data file where both the signature and IHDR metadata are damaged. Solution: repair the magic bytes and CRC, brute-force valid image dimensions from decompressed scanlines, then OCR the restored image to read the flag.
Ever-broken — Hackerlab
Description
Ever-broken
English summary: the challenge provided a file named task that no longer opened as an image. The goal was to recover the original PNG, repair enough metadata for a viewer to parse it correctly, and extract the CODEBY{...} flag from the restored picture.
Analysis
Reconnaissance
The first clue was that the file did not identify cleanly as a known format:
bash read-only • copy allowed$ file task task: data $ strings task IHDR pHYs IDATx IEND
Even though file only said data, the chunk names in strings strongly suggested a broken PNG container.
Broken PNG signature
Dumping the first bytes confirmed that the PNG signature was almost correct, but the last byte was wrong:
bash read-only • copy allowed$ xxd -l 64 task 00000000: 8950 4e47 0d0a 1aa0 0000 000d 4948 4452 .PNG........IHDR 00000010: 0000 0200 0000 0300 0800 0000 00a4 5824 ..............X$
Valid PNG magic bytes should be:
text read-only • copy allowed89 50 4e 47 0d 0a 1a 0a
but the file had:
text read-only • copy allowed89 50 4e 47 0d 0a 1a a0
So the first repair step was simply restoring the last signature byte from a0 to 0a.
IHDR inconsistency and wrong dimensions
After fixing the signature, the next problem was the IHDR chunk. Parsing it showed width and height values of 512 x 768, grayscale, 8-bit, but the CRC no longer matched:
text read-only • copy allowedwidth=512, height=768, bit_depth=8, color_type=0 stored CRC = a45824bf calc CRC = 77648092
Recomputing the IHDR CRC fixed that part, but pngcheck still rejected the file with invalid row-filter values in the decompressed image stream. That is a classic sign that the raw IDAT bytes are being split into scanlines at the wrong boundaries, so the image dimensions in IHDR are also corrupted.
Because the PNG was 8-bit grayscale, each row should be:
text read-only • copy allowed1 filter byte + width bytes of pixel data
If the dimensions are correct, then every scanline must begin with a valid filter byte in the range 0..4. Brute-forcing plausible width / height pairs against the decompressed IDAT stream quickly reveals the correct geometry.
The correct dimensions were:
- width = 750
- height = 1000
That also explained the damaged original fields: the header contained rounded or zeroed values 512 x 768 instead of the real 750 x 1000.
After updating those two fields and recomputing the IHDR CRC again, the file validated successfully with:
bash read-only • copy allowed$ pngcheck -vp restored.png 750 x 1000 image, 8-bit grayscale, non-interlaced No errors detected
The restored image visibly contained the flag, and OCR made it easy to read reliably.
Solution
- Run
file taskand see onlydata. - Run
strings taskand noticeIHDR,pHYs,IDAT, andIEND, which identify the file as a damaged PNG. - Inspect the first bytes with
xxd -l 64 taskand repair the final byte of the PNG signature. - Recompute the
IHDRCRC. - Test the partially repaired file with
pngcheck; invalid row-filter values indicate the dimensions are still wrong. - Decompress the
IDATpayload and brute-force dimensions so every scanline starts with a legal filter byte. - Replace the
IHDRwidth and height with750 x 1000and recompute the CRC again. - Validate the final file with
pngcheck -vp restored.png. - Open the image or run
tesseract restored.png stdoutto recover the flag text.
python read-only • copy allowed#!/usr/bin/env python3 import binascii import struct import zlib PNG_SIG = b"\x89PNG\r\n\x1a\n" def parse_chunks(data: bytes): off = 8 chunks = [] while off < len(data): length = struct.unpack(">I", data[off:off + 4])[0] ctype = data[off + 4:off + 8] cdata = data[off + 8:off + 8 + length] crc = struct.unpack(">I", data[off + 8 + length:off + 12 + length])[0] chunks.append([off, length, ctype, cdata, crc]) off += 12 + length if ctype == b"IEND": break return chunks def crc32_png(ctype: bytes, cdata: bytes) -> int: return binascii.crc32(ctype + cdata) & 0xFFFFFFFF def rebuild_png(chunks): out = bytearray(PNG_SIG) for _, length, ctype, cdata, _ in chunks: out += struct.pack(">I", len(cdata)) out += ctype out += cdata out += struct.pack(">I", crc32_png(ctype, cdata)) return bytes(out) with open("task", "rb") as f: data = bytearray(f.read()) # Step 1: fix signature data[:8] = PNG_SIG chunks = parse_chunks(data) ihdr = bytearray(chunks[0][3]) # Existing IHDR says: 512 x 768, 8-bit grayscale old_width, old_height, bit_depth, color_type, comp, filt, interlace = struct.unpack(">IIBBBBB", ihdr) print(f"Original IHDR: {old_width}x{old_height}, bit_depth={bit_depth}, color_type={color_type}") assert bit_depth == 8 and color_type == 0 and comp == 0 and filt == 0 # Step 2: inflate concatenated IDAT stream idat = b"".join(cdata for _, _, ctype, cdata, _ in chunks if ctype == b"IDAT") raw = zlib.decompress(idat) # Step 3: brute-force dimensions candidates = [] for height in range(1, 5000): if len(raw) % height != 0: continue rowlen = len(raw) // height width = rowlen - 1 # grayscale: 1 filter byte + width bytes if width <= 0: continue ok = True for y in range(height): if raw[y * rowlen] > 4: ok = False break if ok: candidates.append((width, height)) print("Candidates:", candidates[:20]) # Correct pair from the valid scanline layout width, height = 750, 1000 chunks[0][3] = struct.pack(">IIBBBBB", width, height, bit_depth, color_type, comp, filt, interlace) # Step 4: rebuild PNG with corrected CRCs restored = rebuild_png(chunks) with open("restored.png", "wb") as f: f.write(restored) print("Wrote restored.png") print("Flag: CODEBY{REDACTED}")
Useful validation and extraction commands:
bash read-only • copy allowedxxd -l 64 task strings task pngcheck -vp restored.png tesseract restored.png stdout