隐藏在图片中的密钥

在客户端开发的时候,有时需要把密钥保存在本地。这时就会遇到密钥安全性的问题。要保证密钥安全性,无非就是混淆、隐藏、白盒等手段。本文以隐藏在图片中来阐述密钥的安全保存。

PNG图片

便携式网络图形(PNG)是一种无损压缩的位图图形格式,支持索引、灰度、RGB三种颜色方案以及Alpha通道等特性。

文件结构

PNG图像格式文件由一个8字节的PNG文件标识域和3个以上的后续数据块(IHDR, IDAT, IEND)组成。

十六进制 含义
89 用于检测传输系统是否支持8位的字符编码
50 4E 47 PNG每个字母对应的ASCII
0D 0A DOS风格的换行符
1A 在DOS命令下,用于阻止文件显示的文件结束符
0A Unix风格的换行符
PNG定义了两种类型的数据块:一种是PNG文件必须包含、读写软件也都必须要支持的关键块(critical chunk); 另一种叫做辅助块, PNG允许软件忽略它不认识的附加块。
关键数据块中的4个标准数据块:
- 文件头数据块IHDR:包含有图像基本信息,作为第一个数据块出现并只出现一次。
- 调色板数据块PLET:必须放在图像数据块之前。
- 图像数据块IDAT:存储实际图像数据。PNG数据允许包含多个连续的图像数据块。
- 图像结束数据IEND:放在文件尾部,表示PNG数据流结束。
每个数据块都由下表所示的4个域组成
名称 字节数 说明
length 4字节 指定数据块中数据域的长度,不超过2^31-1字节
Chunk Type Code(数据块类型) 4字节 数据块类型码由ASCII字母A-Za-z组成
Chunk Data(数据块数据) 可变长度 存储按照Chunk Type Code指定的数据
CRC(循环冗余检测) 4字节 存储用来检测是否有错误的循环冗余码,计算不包括length字段

例子

以下面这张图片为例,使用Hxd工具来看一下实际的数据。

test

我们用Hxd工具打开图片,首先看到的是89 50 4E 47 0D 0A 1A 0A,表示是一张PNG图片。

PNG标识域

紧接着的是IHDR。前面4字节表示长度,长度后面的4个字节是数据块类型,接着是数据块数据,最后4字节是CRC。

IHDR

再接着是PLTE调色板数据块,格式同上。

PLTE

然后我们会看到tEXt数据块,这个是可选数据块,里面可以放入图片的介绍说明,同时我们也可以将密钥放在其中。

tEXt

然后是图像数据块IDAT。

IDAT

最后是图像结束数据IEND。

IEND

CRC算法

CRC是一种根据网络数据包或电脑文件等数据产生简短固定位数校验码的一种散列函数,主要用来检测或校验数据传输或者保存后可能出现的错误。它是校验和的一种,是两个字节数据流采用二进制除法(没有进位,使用XOR来代替减法)相除所得到的余数。
PNG图片中的CRC算法为CRC32。其多项式表示为0x04C11DB7或者0xEDB88320(反转)。另外CRC计算值可以到在线网站比如ip33上计算得到。

下面是用代码实现的CRC32算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

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 < len; m++) {
CRC = (CRC >> 8) ^ CRC32Table[(CRC & 0xFF) ^ inStr[m]];
}

CRC ^= 0xFFFFFFFF;
return CRC;
}

图片中的密钥

tEXt隐藏

在图片中增加密钥,可以在非关键字段比如tEXt中进行。首先要去除原先图片中tEXt字段,然后填充自己要加入的tEXt字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
int generateNewPng() {
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("oldpng.png", "rb+")) == NULL) {
return 0;
}
if ((fpnew = fopen("newpng.png", "wb")) == NULL) {
return 0;
}
fseek(fp, 0, SEEK_END); //移动末尾
len = (int)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); //重置 0
fread(buf, 4, 1, fp); //读完文件流位置指针后移size * count。读的是长度
fwrite(buf, 4, 1, fpnew); //写入新png
ChunkLen = (buf[0]<<24)|(buf[1]<<16)|(buf[2]<<8)|buf[3];
fread(buf,4+ChunkLen,1,fp); //读的是数据块类型和内容
printf("[+]ChunkName:%c%c%c%c ",buf[0],buf[1],buf[2],buf[3]); //数据
if(strncmp((char *)buf, "tEXt", 4)==0) {
//过滤掉辅助数据块
printf("Ancillary Chunk\n");
fseek(fpnew, -4, SEEK_CUR);
j = 1;
} else {
printf("Palette Chunk\n");
fwrite(buf, 4+ChunkLen, 1, fpnew);
}
printf(" ChunkOffset:0x%08x \n",ChunkOffset);
printf(" ChunkLen: %10d \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 ",ChunkCRC32);
if(crc32!=ChunkCRC32)
printf("[!]CRC32Check Error!\n");
else {
printf("Check Success!\n\n");
if (j == 0) {
fwrite(buf, 4, 1, fpnew);
}
}
ChunkLen=(int)ftell(fp); //得到当前文件的偏移位置
if(ChunkLen==(len-12)) {
printf("\n----------------------------------------------------\n");
printf("Total Chunk:%d\n",i);
break;

}
}

char payload[] = "test";
unsigned char *tmpbuf;
int templen;
// int crc32;
templen = (int)strlen(payload);
tmpbuf = new unsigned char[templen+12];
tmpbuf[0] = templen>>24&0xff;
tmpbuf[1] = templen>>16&0xff;
tmpbuf[2] = templen>>8&0xff;
tmpbuf[3] = templen&0xff;
tmpbuf[4] = 't';
tmpbuf[5] = 'E';
tmpbuf[6] = 'X';
tmpbuf[7] = 't';
for (int j=0; j<len; j++) {
buf[j+8] = payload[j];
}
buf[len+8] = 0X88;
buf[len+9] = 0X1E;
buf[len+10] = 0XE2;
buf[len+11] = 0X27;
fwrite(buf, len+12, 1, fp);

fwrite(IEND, 12, 1, fpnew);
fclose(fp);
fclose(fpnew);
return 0;
}

填充完所要的数据后,接着在程序中可以解析新图片中tEXt字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int getPayload(const char *res) {
FILE *fp;
unsigned char *buf = NULL;
unsigned int len = 0;
if ((fp = fopen("newpng.png", "rb+")) == NULL) {
return 0;
}
fseek(fp, 0, SEEK_END); //移动末尾
len = (int)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");

for(int i=1;i<=len;i++) {
printf("%02X ",buf[i-1]);
if(i%16==0)
printf("\n");
}
const char *text = "tEXt";
std::string s((const char *)buf,len);
size_t start = s.find(text);
const char *iend = "IEND";
size_t end = s.find(iend);
std::string result = s.substr(start+4, end-start-12);
res = result.c_str();
printf("----------------------------------------------------\n");
printf("%s\n",result.c_str());
fclose(fp);
printf("\n");
return 0;
}

LBS隐藏

LBS隐藏是一种更加隐秘的隐藏手段。它通过改写IDAT数据中的RGB三通道数据的低3位,把密钥藏进去。因为只改写了低位数据,所以人眼往往很难区分出来。具体的实现可以参考cloacked-pixel。由于每个像素点最多隐藏3位,就会导致一个量的问题。当隐藏的数据比较多时,就会需要比较大的图片。

参考

PNG
CRC
隐写技巧——利用PNG文件格式隐藏Payload

Author: MrHook
Link: https://bigjar.github.io/2018/04/09/%E9%9A%90%E8%97%8F%E5%9C%A8%E5%9B%BE%E7%89%87%E4%B8%AD%E7%9A%84%E5%AF%86%E9%92%A5/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.