oynix

于无声处听惊雷,于无色处见繁花

把代码藏到png里

之前有提到,最近因需求特殊,所以需要把代码藏起来,想着想着就盯上了资源图。为了把代码藏到png里,所以专门研究了png的文件格式。

宏观来看,文件都是以字节为单位存储在介质上,如电脑硬盘,手机ROM,而字节无非就是010101的形式,那么既然都是010101的格式,为什么有的文件是png,有的文件是jpg,还有的文件是webp呢?这里面,一定有着什么规则,就像是网络里的各种协议,是同样的道理。

要查看这些,首先就要把文件以二进制的形式打开,我用的是UltraEdit,也可以用其他的。

1. png文件结构

关于png文件结构,白皮书有着详细介绍,这里是官方文档。这个文档里面对png进行了十分详尽的介绍。总的来说,png文件的结构就是一个文件头,加上若干了chunk块组成,

1
[文件头][chunk][chunk][chunk][chunk][chunk]

2. 文件头

正如白皮书中所说,png文件头是固定的

1
89 50 4E 47 0D 0A 1A 0A

这是16进制格式,每个数字代表4个bit,所以每两个数字代表一个byte,文件头的长度是8个字节。第一个字节0x89是规定的,用一个超过ASCII字符范围的值,为的就是防止被当作文本文件处理。紧接着的3个字节,查一下ASCII表就知道了,50=P,4E=N,47=G,剩下的0D 0A 1A 0A是什么没说,文档上就一句话,这个签名表明是png图片

5.2 PNG signature
The first eight bytes of a PNG datastream always contain the following (decimal) values:

137 80 78 71 13 10 26 10
This signature indicates that the remainder of the datastream contains a single PNG image, consisting of a series of chunks beginning with an IHDR chunk and ending with an IEND chunk.

3. chunk结构

结构如下

1
[LENGTH][CHUNK TYPE][CHUNK DATA][CRC]

当chunk data的长度是0时,chunk的结构是这样的,就是去掉了DATA部分

1
[LENGTH][CHUNK TYPE][CRC]

其中,LENGHT的长度是4个字节,TYPE的长度也是4个字节,DATA的长度对于不同的TYPE是不同的,LENGTH的值就是DATA的长度,CRC的长度也是4个字节,下面一一自报家门。

4. chunk type

type表明了chunk块的类型,目前png一共定义了18个类型,然后又把这18个类型分为两大种,一个是关键chunks,另个是辅助chunks。关键chunks一共有4个,剩下的都是辅助类型,有些类型的位置是固定的,这些都写在白皮书的5.6小节。

关键chunks,

chunk name multiple allowed optinal order constraint
IHDR No No Shall be first
PLTE No Yes Before first IDAT
IDAT Yes No Multiple IDAT chunks shall be consecutive
IEND No No Shall be last

辅助chunks,所有辅助chunk都是可选的,意思即使png文件里可能有也可能没有

chunk name multiple allowed order constraint
cHRM No Before PLTE and IDAT
gAMA No Before PLTE and IDAT
iCCP No Before PLTE and IDAT. If the iCCP chunk is present, the sRGB chunk should not be present.
sBIT No Before PLTE and IDAT
sRGB No Before PLTE and IDAT. If the sRGB chunk is present, the iCCP chunk should not be present.
bKGD No After PLTE; before IDAT
hIST No After PLTE; before IDAT
tRNS No After PLTE; before IDAT
pHYs No Before IDAT
sPLT Yes Before IDAT
tIME No None
iTXt Yes None
tEXt Yes None
zTXt Yes None

5. chunk data

chunk的数据都存放在data里,data的长度,也就是字节数量,等于LENGTH的值。

6. chunk crc

crc存放的是该chunk块的校验码,采用的是crc32的计算方式,将type和data输入,输出的结果就是crc

1
chunk crc = crc32([CHUNK TYPE][CHUNK DATA])

白皮书里是这么介绍的,另外这个文档里有介绍png所使用的crc算法

1
2
3
4
5
6
7
8
5.5 Cyclic Redundancy Code algorithm
CRC fields are calculated using standardized CRC methods with pre and post conditioning, as defined by ISO 3309 [ISO-3309] and ITU-T V.42 [ITU-T-V42]. The CRC polynomial employed is

x32 + x26 + x23 + x22 + x16 + x12 + x11 + x10 + x8 + x7 + x5 + x4 + x2 + x + 1

In PNG, the 32-bit CRC is initialized to all 1's, and then the data from each byte is processed from the least significant bit (1) to the most significant bit (128). After all the data bytes are processed, the CRC is inverted (its ones complement is taken). This value is transmitted (stored in the datastream) MSB first. For the purpose of separating into bytes and ordering, the least significant bit of the 32-bit CRC is defined to be the coefficient of the x31 term.

Practical calculation of the CRC often employs a precalculated table to accelerate the computation. See Annex D: Sample Cyclic Redundancy Code implementation.

看完了计算原理之后,便开始自己实现算法,写着写着发现python有现成的lib提供crc32的算法,带入了几组数据之后发现,结果竟然都正确,既然如此,那我还吭哧吭哧的实现什么呢。

7. 关键chunk块,IHDR

IHDR块一般都紧跟在文件头那8个字节后面,其中chunk data部分每个字节存储的数据表示的含义都是固定的,如下

Name Bytes Length Description
width 4 bytes 宽度,单位像素
height 4 bytes 高度,单位像素
bit depth 1 byte 图像深度
color type 1 byte 颜色类型
compression method 1 byte 压缩方法
filter metho 1 byte 滤波器方法
interlace method 1 byte 隔行扫描

我找了个一个png图片的前几个字节,一起对着来看看

1
2
3
4
5
89 50 4E 47 0D 0A 1A 0A 
00 00 00 0D
49 48 44 52
00 00 02 D0 00 00 05 00 08 06 00 00 00
6E CE 65 3D

为了便于观察,我给它断成了几行,第一行就不用多说了,是png文件头,后面的都是IHDR chunk块。前4个字节00 00 00 0D代表着chunk length,计算出来是13,接下来的个4个字节,就是ASCII码,49=I,48=H,44=D,52=R。然后就是最长的一行,数一数可以发现,正好是13个byte,和chunk length的值是一致的,根据上表可知,00 00 02 D0为图片宽度,算出来是720像素,00 00 05 00是高度1280像素,后面的图像深度、颜色类型什么的,就不看了,直接看chunk crc部分的6E CE 65 3D,计算一下

1
2
3
4
5
import zlib
if __name__ == '__main__':
data = [0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x02, 0xD0, 0x00, 0x00, 0x05, 0x00, 0x08, 0x06, 0x00, 0x00, 0x00]
crc = zlib.crc32(bytearray(data))
print(hex(crc))

运行结果:0x6ece653d。可见,截止至此,一切看起来都是正常的。

7. 关键chunk块,IDAT、IEND

这两个的chunk块和IHDR是一样的,如果能明白上面的IHDR,这两个就也能明白。这里单独说一句IEND,因为这个chunk块没有chunk data部分,所以,所有png的IEND块都是一样的,一样的length,一样的type,以及一样的crc。

8. 插入代码

好了,有了上面的基础知识准备,开始干点正事。

png中,像素的数据存在连续的IDAT chunk中,一长串的IDAT数据,根据IHDR中width和height参数,再解析出来。

我开始的想法,是把代码藏在辅助chunk中,那么多可选的辅助类型,照片的地理位置、镜头、光圈等信息,都是存放在这些辅助chunk里,随便选几个,把数据插入到data部分,然后再计算一下crc不就完美了吗?冷静下来后,觉得此法尚有不妥之处,你想啊,一个png图片,辅助信息占了好大一个比例,但是这个辅助信息又不是正常的文本,或是数字,除了自己知道怎么解析,别人都看不懂,让人易生疑心:你这里面是不是藏了什么不能见人的东西?这浑身是嘴也说不清楚。

有个同事是把代码直接拼在了文件的末尾,因为width和height没变,中间的IDAT也没变,所以完全不影响png文件的读取,遇到IEDN就停了,即便你后面还有数据也不妨事。当代码很少时,难以看出端倪,但是代码一旦很多,画风就会很诡异:一个几十像素乘几十像素的图片,文件大小却足足有上百kB,这就好比,你E盘存储进度条都变红了,点进去一看,发现里面只有一集蜡笔小新的动画,还是渣画质,大小为20MB,你可以说这里一切正常,但要是有人信了,就当你没说。这个时候,只要一打开png文件,就会看到后面跟着的那条长长的尾巴。

思索再三,我还是决定把代码伪装成IDAT块,插入到最后一个IDAT和IEND之间,且,如果代码很大,就切成片段,插入到多个png图片中,以保证起码看上去显得很和谐。比如IHDR中width是720,height是1280,所以IDAT中,只有720x1280=921,600个像素会被显示出来,后面的IDAT是不会影响png解析的。但是为了以防万一,在插入之前,是需要把代码加密的,一般是和一个数字做异或操作,等到解析时,从png中取出代码,再与同一个数字做异或操作即可,这样一来,即便是被一些小机灵鬼发现端倪,找到隐藏的秘密,也不至于造成代码泄漏。

除此之外,还有一些简单的方式,比如直接把代码的.jar改成.ttf,伪装成字体文件,放在项目中,我觉得这种方式就是在靠天吃饭,看命,但凡遇到个勤劳的人,就会发现这个ttf并不简单,再顺着这个ttf找引用,顺藤摸瓜,就让人抄了老巢。

补充

最新发现,在开启了minifyEnabled后,drawable里的图片会被压缩,这个事是aapt在做,根绝结果来看,它是根据png IHDR里的长和宽计算出数据的总长度,然后把超出的IDAT切掉。如果不想被无情阉割,那么就要修改IHDR里的长和宽,让额外的IDAT成为png的一部分,但是这样的问题是,无法正常解析出额外的部分的像素值,折衷的策略是,找一个View引用这张图,但是这个View永远不会显示。还有个退而求其次的方式,那就是放到raw目录中,这个目录下的图片不会被阉割,都是完整的。

附录

https://www.cnblogs.com/esestt/archive/2007/08/09/848856.html
一个介绍crc算法原理的文章,写的非常详细,才看了一遍我就看明白了,虽然也没算出来,但我觉得这和文章没有关系,一定是我的问题,遇到问题要多反思自己。

最后,再推荐一个网站,用来压缩png图片,无损或接近无损,TinyPng。但是,每次上传下载效率很差,所以有不愿透露姓名的热心市民写了一个客户端,效率指数加倍,在这里

补充,Virgy 推荐的另一个处理图片的网站,支持JPEG和PNG,文件上限是50M:
Website Planet

------------- (完) -------------
  • 本文作者: oynix
  • 本文链接: https://oynix.com/2022/04/89bdf64eea68/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

欢迎关注我的其它发布渠道