mantis
mantis
Platform: bug-makers | Category: Forensics | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2025-10-04 | Status: Solved Techniques: ihdr_crc32_bruteforce, png_header_reconstruction, scanline_padding, truncated_zlib_recovery
Summary
Task: a ~459 KB file reported as 'data' that is actually a PNG with both its head (signature + IHDR start) and tail (final IDAT + IEND) eaten away. Solution: recognize raw IDAT data, brute-force width/height against the preserved IHDR CRC32 (800x800), rebuild signature+IHDR, stream-decompress the truncated zlib IDAT, pad missing scanlines, and reassemble a valid PNG to render the flag drawn on a praying mantis.
mantis — Bug Makers CTF
Description
Не доела ... Если рассматривать аналогии из мира насекомых, то этот файл отчасти можно сравнить с богомолом. Попробуй понять, был ли он красавчиком, а заодно и флаг достань!
English gist: "Didn't finish eating ... If we draw analogies from the insect world, this file can partly be compared to a praying mantis. Try to figure out if he was handsome, and grab the flag along the way!"
We are given a single ~459031-byte binary named mantis. file mantis reports it as data. The goal is to recover the image and read the flag.
Semantic clues
The wordplay is the whole hint:
- богомол = praying mantis. The female mantis famously eats the male's HEAD during/after mating.
- "Не доела" = "didn't finish eating" → not only the HEAD (file header) is gone, but the TAIL (end of file) was also partially eaten/missing.
- "был ли он красавчиком" = "was he handsome" → the recovered image is a drawing of a (handsome) male mantis; the flag is text drawn on that image.
So the task is a PNG repair / carving challenge: both the header and the footer of a PNG were stripped, and we must rebuild enough of the file to render it.
Analysis
A hex dump of the start of the file shows the ASCII string IDAT very early (offset 0x0D) but no PNG signature 89 50 4E 47 0D 0A 1A 0A. The file is raw PNG chunk data with the signature and the beginning of the IHDR chunk missing.
Mapping the first bytes:
| Offset | Bytes | Meaning |
|---|---|---|
0x00–0x04 | 08 06 00 00 00 | tail of IHDR data: bit depth 0x08, color type 0x06 (RGBA), compression 0x00, filter 0x00, interlace 0x00 |
0x05–0x08 | db 70 06 68 | IHDR CRC32 = 0xdb700668 |
0x09–0x0C | 00 00 ff a5 | first IDAT chunk length (0xffa5) |
0x0D–0x10 | 49 44 41 54 | IDAT |
| |
So the missing prefix is:
89 50 4E 47 0D 0A 1A 0A PNG signature
00 00 00 0D 49 48 44 52 IHDR chunk: length=13, type="IHDR"
WW WW WW WW width (4 bytes, unknown)
HH HH HH HH height (4 bytes, unknown)
The IHDR CRC32 (0xdb700668) is computed over "IHDR" + width(4) + height(4) + 08 06 00 00 00. Everything except width and height is known, so we can brute-force width and height against the preserved CRC.
Solution
Step 1 — Recover width/height via IHDR CRC32 brute force
python read-only • copy allowedimport zlib, struct data = open('mantis','rb').read() crc_target = struct.unpack('>I', data[5:9])[0] # 0xdb700668 known_tail = bytes.fromhex('0806000000') for w in range(1, 4000): for h in range(1, 4000): ihdr = b'IHDR' + struct.pack('>II', w, h) + known_tail if zlib.crc32(ihdr) & 0xffffffff == crc_target: print('FOUND', w, h) # -> 800 800 raise SystemExit
Result: width = 800, height = 800.
Step 2 — Rebuild signature + IHDR and prepend
We construct the missing 8-byte signature and the full IHDR chunk, then concatenate the original data from offset 9 onward (the original file already contains the IHDR CRC, the IDAT length, and IDAT from there). After this, file reports:
PNG image data, 800 x 800, 8-bit/color RGBA, non-interlaced
Step 3 — Confirm the tail is also missing
Walking the chunks of the partially-fixed file shows the final IDAT declares 45272 bytes but only ~330 are actually present, and there is no IEND chunk. The mantis "didn't finish eating" the tail either — the zlib/IDAT stream is truncated.
Step 4 — Stream-decompress the truncated IDAT and pad missing scanlines
A normal zlib.decompress would fail on a truncated stream. Using a streaming decompressobj tolerates the truncation. For an 800×800 RGBA image each scanline is 1 filter byte + 800*4 = 3201 bytes, so the full raw size is 3201 * 800 = 2560800 bytes. We recover 2099320 bytes = 655 of 800 scanlines — more than enough, since the flag is drawn in the top-left of the image.
We pad the missing scanlines with zero bytes, re-compress, and assemble a fresh valid PNG (signature + IHDR + single IDAT + IEND) so a viewer applies the row filters and renders the recovered top portion.
python read-only • copy allowedimport struct, zlib data = open('recovered.png','rb').read() # PNG with rebuilt header (Step 2) # collect IDAT payloads, clipping the truncated final chunk i = 8 idat = b'' while i < len(data): if i + 8 > len(data): break ln = struct.unpack('>I', data[i:i+4])[0] typ = data[i+4:i+8] avail = len(data) - (i + 8) take = min(ln, avail) if typ == b'IDAT': idat += data[i+8:i+8+take] if take < ln: # truncated final chunk -> stop break i += 12 + ln # tolerant streaming decompression of a truncated zlib stream raw = zlib.decompressobj() d = raw.decompress(idat) + raw.flush() W = H = 800 rowlen = W * 4 + 1 d = d + b'\x00' * (rowlen * H - len(d)) # pad missing rows with zeros comp = zlib.compress(d, 9) sig = bytes.fromhex('89504e470d0a1a0a') ihdr = struct.pack('>I', 13) + b'IHDR' + struct.pack('>II', W, H) + bytes.fromhex('0806000000') ihdr += struct.pack('>I', zlib.crc32(ihdr[4:]) & 0xffffffff) idatc = struct.pack('>I', len(comp)) + b'IDAT' + comp idatc += struct.pack('>I', zlib.crc32(idatc[4:]) & 0xffffffff) iend = struct.pack('>I', 0) + b'IEND' iend += struct.pack('>I', zlib.crc32(iend[4:]) & 0xffffffff) open('fixed.png','wb').write(sig + ihdr + idatc + iend)
Step 5 — Read the flag
fixed.png renders a hand-drawn green praying mantis ("was he handsome" → yes). The flag is printed as text in the top-left of the image, comfortably within the recovered 655 scanlines:
BugCTF{REDACTED}