之前有提到,最近因需求特殊,所以需要把代码藏起来,想着想着就盯上了资源图。为了把代码藏到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 | 5.5 Cyclic Redundancy Code algorithm |
看完了计算原理之后,便开始自己实现算法,写着写着发现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 | 89 50 4E 47 0D 0A 1A 0A |
为了便于观察,我给它断成了几行,第一行就不用多说了,是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 | import zlib |
运行结果: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