PNG Analysis
Deep-dive PNG structure analysis - chunk inspection, CRC manipulation, and hidden data extraction.
PNG Analysis
PNG files have a rich internal structure that CTF challenge authors exploit heavily. Flags can be hidden in chunk metadata, appended after IEND, embedded as different image dimensions, or encoded in color palette manipulation. Understanding PNG structure lets you find data that automated tools miss.
PNG File Structure
Every PNG consists of a magic header followed by chunks. Each chunk has the format: [4-byte length][4-byte type][data][4-byte CRC].
| Chunk | Description |
|---|---|
IHDR | Image header — width, height, bit depth, color type |
PLTE | Color palette (for indexed images) |
IDAT | Compressed image data |
IEND | End marker (length=0, data=empty) |
tEXt | Text metadata — keyword\0value |
iTXt | International text metadata |
zTXt | Compressed text metadata |
bKGD | Background color |
tIME | Last modification time |
Chunk types starting with a lowercase first letter are "ancillary" (optional). CTF challenges often insert custom ancillary chunks with hidden data.
Systematic Analysis
Validate the PNG structure
pngcheck -v suspicious.pngThis immediately flags CRC errors, truncated chunks, or invalid chunk sequences.
List all chunks
import struct
data = open('suspicious.png','rb').read()
pos = 8 # skip magic
while pos < len(data):
length = struct.unpack('>I', data[pos:pos+4])[0]
ctype = data[pos+4:pos+8].decode()
cdata = data[pos+8:pos+8+length]
crc = data[pos+8+length:pos+12+length].hex()
print(f'{ctype:8s} len={length:6d} offset=0x{pos:06x} crc={crc}')
pos += 12 + lengthCheck text chunks for embedded flags
Look specifically for tEXt, iTXt, zTXt chunks — these often contain the flag directly.
Check for data after IEND
data = open('suspicious.png','rb').read()
iend_pos = data.rfind(b'IEND')
after_iend = data[iend_pos + 8:]
if after_iend:
print(f"Data after IEND: {len(after_iend)} bytes")
open('after_iend.bin','wb').write(after_iend)Verify IHDR CRC — detect dimension manipulation
If pngcheck reports a CRC error in IHDR, the width/height may have been tampered with to hide rows.
LSB analysis
zsteg suspicious.png
zsteg -a suspicious.pngCommon CTF Hiding Places
Text metadata chunks often contain flags directly:
import struct, zlib
data = open('suspicious.png','rb').read()
pos = 8
while pos < len(data):
length = struct.unpack('>I', data[pos:pos+4])[0]
ctype = data[pos+4:pos+8].decode()
cdata = data[pos+8:pos+8+length]
if ctype in ('tEXt', 'iTXt'):
print(f"[{ctype}] {cdata[:200]}")
elif ctype == 'zTXt':
null = cdata.index(b'\x00')
keyword = cdata[:null].decode()
compressed = cdata[null+2:]
decompressed = zlib.decompress(compressed)
print(f"[zTXt] key={keyword}: {decompressed[:200]}")
pos += 12 + lengthExtracting and Decompressing IDAT
import struct, zlib
data = open('suspicious.png','rb').read()
idat_data = b''
pos = 8
while pos < len(data):
length = struct.unpack('>I', data[pos:pos+4])[0]
ctype = data[pos+4:pos+8].decode()
if ctype == 'IDAT':
idat_data += data[pos+8:pos+8+length]
pos += 12 + length
raw_pixels = zlib.decompress(idat_data)Stegsolve Equivalent in Python
from PIL import Image
import numpy as np
img = Image.open('suspicious.png').convert('RGB')
pixels = np.array(img)
# Extract all 24 bit planes
for channel, name in enumerate(['R','G','B']):
for bit in range(8):
plane = ((pixels[:,:,channel] >> bit) & 1) * 255
Image.fromarray(plane.astype('uint8')).save(f'{name}_bit{bit}.png')
print("Saved all 24 bit planes: R_bit0.png through B_bit7.png")Checklist
-
pngcheck -v suspicious.png - Parse all chunks — look for
tEXt/iTXt/zTXt - Check for data after IEND
- Verify IHDR CRC — if wrong, brute-force true width/height
- Open in Stegsolve / Aperisolve
- Check LSB in all channels with Python
-
binwalk -efor embedded files - Compare file size vs expected size for given dimensions
Last updated on