373 words
2 minutes
CryptoNite CTF 2026 - An Image? - Forensics Writeup

Category: Forensics Flag: TACHYON{change_the_header?}

Challenge Description#

I tried to transfer this from my old drive, but the file was corrupted during the move. The image viewer says it’s an invalid format, but I know the data is still in there…

Analysis#

This one immediately smelled like header corruption, so I started by asking the file what it thought it was.

file "chall.png"
chall.png: OpenPGP Public Key

A PNG being identified as an OpenPGP key usually means the magic bytes at the start are broken, while the rest of the structure might still be intact. A quick hex peek confirmed that: the first 8 bytes were wrong, but right after that I could already see IHDR, which is the first mandatory PNG chunk.

rsxxd "chall.png" | head -3
00000000: 9a61 5f58 0d0a 1a0a 0000 000d 4948 4452  .a_X........IHDR
00000010: 0000 027b 0000 027b 0103 0000 00a1 fac5  ...{...{........
00000020: d400 0000 0173 5247 4200 aece 1ce9 0000  .....sRGB.......

So instead of brute-forcing random repairs, I only restored the PNG signature bytes (89 50 4E 47 0D 0A 1A 0A) and left everything else untouched.

from pathlib import Path

d = bytearray(Path("chall.png").read_bytes())
d[:8] = b"\x89PNG\r\n\x1a\n"
Path("chall_fixed.png").write_bytes(d)
print("wrote", len(d), "bytes")
wrote 1172 bytes

Then I validated whether the repaired file was truly a valid PNG and not just “openable by luck.”

file "chall_fixed.png" && pngcheck -v "chall_fixed.png"
chall_fixed.png: PNG image data, 635 x 635, 1-bit colormap, non-interlaced
File: chall_fixed.png (1172 bytes)
  chunk IHDR at offset 0x0000c, length 13
    635 x 635 image, 1-bit palette, non-interlaced
  chunk sRGB at offset 0x00025, length 1
    rendering intent = perceptual
  chunk gAMA at offset 0x00032, length 4: 0.45455
  chunk PLTE at offset 0x00042, length 6: 2 palette entries
  chunk pHYs at offset 0x00054, length 9: 3779x3779 pixels/meter (96 dpi)
  chunk IDAT at offset 0x00069, length 1047
    zlib: deflated, 32K window, maximum compression
  chunk IEND at offset 0x0048c, length 0
No errors detected in chall_fixed.png (7 chunks, 97.7% compression).

At that point the image was structurally clean, so I checked for machine-readable hidden content. QR scan immediately returned a Base64 payload, which was delightfully direct.

smile

zbarimg -q "chall_fixed.png"
QR-Code:dGFjaHlvbntjaGFuZ2VfdGhlX2hlYWRlcj99

Decoding that payload produced the flag text in lowercase prefix form.

import base64

print(base64.b64decode("dGFjaHlvbntjaGFuZ2VfdGhlX2hlYWRlcj99").decode())
tachyon{change_the_header?}

Given the challenge prefix requirement, the final normalized flag is TACHYON{change_the_header?}.

Solution#

file "chall.png"
chall.png: OpenPGP Public Key
from pathlib import Path

d = bytearray(Path("chall.png").read_bytes())
d[:8] = b"\x89PNG\r\n\x1a\n"
Path("chall_fixed.png").write_bytes(d)
zbarimg -q "chall_fixed.png"
QR-Code:dGFjaHlvbntjaGFuZ2VfdGhlX2hlYWRlcj99
import base64

print(base64.b64decode("dGFjaHlvbntjaGFuZ2VfdGhlX2hlYWRlcj99").decode())
tachyon{change_the_header?}
TACHYON{change_the_header?}
CryptoNite CTF 2026 - An Image? - Forensics Writeup
https://blog.rei.my.id/posts/72/cryptonite-ctf-2026-an-image-forensics-writeup/
Author
Reidho Satria
Published at
2026-03-10
License
CC BY-NC-SA 4.0