PNG¶
File Format¶
For a PNG file, its file header is always described by fixed bytes, and the remaining part is composed of 3 or more PNG data chunks (Chunks) arranged in a specific order.
File header 89 50 4E 47 0D 0A 1A 0A + Data chunk + Data chunk + Data chunk……
Data Chunk (Chunk)¶
PNG defines two types of data chunks. One is called critical chunks, which are standard data chunks, and the other is called ancillary chunks, which are optional data chunks. Critical chunks define 4 standard data chunks that every PNG file must contain, and PNG read/write software must support these data chunks.
| Chunk Symbol | Chunk Name | Multiple | Optional | Position Restriction |
|---|---|---|---|---|
| IHDR | Header Chunk | No | No | First chunk |
| cHRM | Primary Chromaticities and White Point Chunk | No | Yes | Before PLTE and IDAT |
| gAMA | Image Gamma Chunk | No | Yes | Before PLTE and IDAT |
| sBIT | Significant Bits Chunk | No | Yes | Before PLTE and IDAT |
| PLTE | Palette Chunk | No | Yes | Before IDAT |
| bKGD | Background Color Chunk | No | Yes | After PLTE and before IDAT |
| hIST | Image Histogram Chunk | No | Yes | After PLTE and before IDAT |
| tRNS | Transparency Chunk | No | Yes | After PLTE and before IDAT |
| oFFs | (Specialized Public Chunk) | No | Yes | Before IDAT |
| pHYs | Physical Pixel Dimensions Chunk | No | Yes | Before IDAT |
| sCAL | (Specialized Public Chunk) | No | Yes | Before IDAT |
| IDAT | Image Data Chunk | Yes | No | Consecutive with other IDAT chunks |
| tIME | Image Last Modification Time Chunk | No | Yes | No restriction |
| tEXt | Textual Data Chunk | Yes | Yes | No restriction |
| zTXt | Compressed Textual Data Chunk | Yes | Yes | No restriction |
| fRAc | (Specialized Public Chunk) | Yes | Yes | No restriction |
| gIFg | (Specialized Public Chunk) | Yes | Yes | No restriction |
| gIFt | (Specialized Public Chunk) | Yes | Yes | No restriction |
| gIFx | (Specialized Public Chunk) | Yes | Yes | No restriction |
| IEND | Image End Data | No | No | Last data chunk |
Each data chunk has a uniform data structure, consisting of 4 parts:
| Name | Bytes | Description |
|---|---|---|
| Length | 4 bytes | Specifies the length of the data field in the data chunk, not exceeding (231-1) bytes |
| Chunk Type Code | 4 bytes | The chunk type code consists of ASCII letters (A - Z and a - z) |
| Chunk Data | Variable length | Stores data as specified by the Chunk Type Code |
| CRC (Cyclic Redundancy Check) | 4 bytes | Stores the cyclic redundancy code used to detect errors |
The value in the CRC (Cyclic Redundancy Check) field is calculated from the data in the Chunk Type Code field and the Chunk Data field.
IHDR¶
The Header Chunk IHDR: It contains the basic information about the image data stored in the PNG file, consists of 13 bytes, and must appear as the first data chunk in the PNG data stream. There can only be one header chunk in a PNG data stream.
The first 8 bytes are what we focus on:
| Field Name | Bytes | Description |
|---|---|---|
| Width | 4 bytes | Image width, in pixels |
| Height | 4 bytes | Image height, in pixels |
We often modify the height or width of an image to make it display incompletely, thereby achieving the purpose of hiding information.

Here you can notice that this image cannot be opened in Kali, with the prompt IHDR CRC error, while the built-in image viewer in Windows 10 can open it. This reminds us that the IHDR chunk has been tampered with, and we can try modifying the image height or width to discover the hidden string.
Example¶
WDCTF-finals-2017¶
By observing the file, we can notice that the file header and width are abnormal.
00000000 80 59 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 |.YNG........IHDR|
00000010 00 00 00 00 00 00 02 f8 08 06 00 00 00 93 2f 8a |............../.|
00000020 6b 00 00 00 04 67 41 4d 41 00 00 9c 40 20 0d e4 |k....gAMA...@ ..|
00000030 cb 00 00 00 20 63 48 52 4d 00 00 87 0f 00 00 8c |.... cHRM.......|
00000040 0f 00 00 fd 52 00 00 81 40 00 00 7d 79 00 00 e9 |....R...@..}y...|
...
It's important to note that the file width cannot be modified arbitrarily. The width needs to be brute-forced based on the CRC value of the IHDR chunk, otherwise the image will display incorrectly and you won't be able to obtain the flag.
import os
import binascii
import struct
misc = open("misc4.png","rb").read()
for i in range(1024):
data = misc[12:16] + struct.pack('>i',i)+ misc[20:29]
crc32 = binascii.crc32(data) & 0xffffffff
if crc32 == 0x932f8a6b:
print i
After obtaining the width value of 709, restore the image to get the flag.

PLTE¶
The Palette Chunk PLTE: It contains color transformation data related to indexed-color images. It is only related to indexed-color images and must be placed before the image data chunk. True color PNG data streams can also have palette chunks, the purpose being to facilitate non-true-color display programs to quantize image data and display the image.
IDAT¶
The Image Data Chunk IDAT: It stores the actual data and can contain multiple consecutive sequential image data chunks in the data stream.
- Stores image pixel data
- Can contain multiple consecutive sequential image data chunks in the data stream
- Compressed using a derivative algorithm of the LZ77 algorithm
- Can be decompressed with zlib
It is worth noting that an IDAT chunk will only continue to a new chunk when the previous chunk is full.
Use pngcheck to view this PNG file:
λ .\pngcheck.exe -v sctf.png
File: sctf.png (1421461 bytes)
chunk IHDR at offset 0x0000c, length 13
1000 x 562 image, 32-bit RGB+alpha, non-interlaced
chunk sRGB at offset 0x00025, length 1
rendering intent = perceptual
chunk gAMA at offset 0x00032, length 4: 0.45455
chunk pHYs at offset 0x00042, length 9: 3780x3780 pixels/meter (96 dpi)
chunk IDAT at offset 0x00057, length 65445
zlib: deflated, 32K window, fast compression
chunk IDAT at offset 0x10008, length 65524
...
chunk IDAT at offset 0x150008, length 45027
chunk IDAT at offset 0x15aff7, length 138
chunk IEND at offset 0x15b08d, length 0
No errors detected in sctf.png (28 chunks, 36.8% compression).
As we can see, normal chunks are full at a length of 65524, while the second-to-last IDAT chunk has a length of 45027, and the last one has a length of 138. It's obvious that the last IDAT chunk is problematic, because it should have been merged into the second-to-last chunk which was not full.
Use python zlib to decompress the content of the extra IDAT chunk. Note that you need to remove the length, chunk type code, and the trailing CRC checksum value.
import zlib
import binascii
IDAT = "789...667".decode('hex')
result = binascii.hexlify(zlib.decompress(IDAT))
print result
IEND¶
The Image Trailer Chunk IEND: It is used to mark the end of the PNG file or data stream and must be placed at the end of the file.
00 00 00 00 49 45 4E 44 AE 42 60 82
The length of the IEND data chunk is always 00 00 00 00, the data identifier is always IEND 49 45 4E 44, and therefore the CRC code is always AE 42 60 82.
Other Ancillary Data Chunks¶
- Background Color Chunk bKGD (background color)
- Primary Chromaticities and White Point Chunk cHRM (primary chromaticities and white point). The so-called white point refers to the white chromaticity produced on the display when
R=G=B=maximum value - Image Gamma Chunk gAMA (image gamma)
- Image Histogram Chunk hIST (image histogram)
- Physical Pixel Dimensions Chunk pHYs (physical pixel dimensions)
- Significant Bits Chunk sBIT (significant bits)
- Textual Data Chunk tEXt (textual data)
- Image Last Modification Time Chunk tIME (image last-modification time)
- Transparency Chunk tRNS (transparency)
- Compressed Textual Data Chunk zTXt (compressed textual data)
LSB¶
LSB stands for Least Significant Bit. The image pixels in PNG files are generally composed of three primary colors RGB (Red, Green, Blue). Each color occupies 8 bits with a value range from 0x00 to 0xFF, meaning there are 256 shades per color, containing a total of 256 to the power of 3 colors, which is 16,777,216 colors.
The human eye can distinguish approximately 10 million different colors, meaning there are about 6,777,216 colors that the human eye cannot distinguish.
LSB steganography modifies the least significant binary bit (LSB) of the RGB color components. Each color has 8 bits, and LSB steganography modifies the lowest 1 bit of a pixel. The human eye will not notice the change, and each pixel can carry 3 bits of information.

If you are looking for traces of LSB hiding, there is an essential tool called Stegsolve that can assist us in analysis.
By using the buttons at the bottom, you can observe the information in each channel. For example, viewing the information in the lowest bit plane (bit plane 8) of the R channel.

When examining each channel with Stegsolve for LSB information, you must carefully capture anomalies and look for clues of LSB steganography.
Example¶
HCTF - 2016 - Misc
In this challenge, the information is hidden in the least significant bits of the RGB three channels. Using Stegsolve-->Analyse-->Data Extract, you can specify channels for extraction.

You can find a zip header. Save it as a compressed archive using save bin, then open and run the ELF file inside to obtain the final flag.
For more research on LSB, you can check here.