png文件格式分析


png文件格式分析

写在前面

在写这个东西写到一半的时候,突然发现CTFWiki已经有PNG隐写这篇相对正规的文章了。对PNG文件格式的分析网上相对比较多,所以分析的比较菜,表哥们轻喷。

PNG文件结构

PNG文件格式

PNG文件格式很简单,对于一个PNG文件来说,主要是开头固定的字节(又叫做文件头,文件署名域,标识符等等)和三组以上的PNG数据块按照特定的顺序组成,其中,最基本的PNG至少包含以下部分:

文件头		IHDR		IDAT		IEND

其中,文件头的hex为:

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

这一部分主要是考察对各类媒体类型(mime type)的识别比较多。重要的是,正确判断一个图片的文件格式十分重要,如 GIF 里面有帧信息,而JPG 里面却没有,PNG 图片有通道信息,而 JPG 也没有。单凭后缀进行错判会导致处理的时候文件报错。(补充:常见文件格式的文件头)
同时由于文件种类多而复杂,通过背诵文件头实现对应图片文件格式便显得十分困难。根据网上资料可知,python库中存在imghdr模块可以直接识别文件,同目录下命令行里敲入指令:
python -m imghdr file1
即可识别,结果会返回文件格式。

PNG数据块(Chunk)

PNG定义了两种类型的数据块:关键数据块(critical chunk),是标准的数据块;辅助数据块(ancillary chunks),是可选的数据块。关键数据块定义了四个标准数据块。分别是IHDR、PLTE、IDAT、IEND,后面我们会分别提一下这四块。其中PLTE仅与索引彩色图像有关,在黑白PNG图片是作为可选数据块存在的,但是本身是标准数据块之一。
对于每一个数据块,其数据结构又是统一的,每个数据块由Length(4bytes,指定数据块数据域的长度),Chunk Type Code(4bytes,数据块类型码,由ASCII字母组成), Chunk Data(数据块数据,储存按照CTC指定的数据),CRC(4byte,循环冗余检测,检测是否有错误的循环冗余码)组成。CRC计算是通过CTC和CD得到的。CRC通过触发以及余数原理进行错误侦测。附:《CRC(循环冗余校验码)简介与实现解析》

同时这里提供一下正常图片CRC校验值的计算方法:

import zlib

with open('d0430db27b8c4d3694292d9ac5a55634.png','rb') as image_data:
    bin_data=image_data.read()
#截取待计算的字符串即IHDR中去除前四字节(length)和后四字节(CRC)
data = bytearray(bin_data[12:29])
#使用函数计算
crc32key = zlib.crc32(data)
print(hex(crc32key))

PNG数据块结构

IHDR

文件头数据块IHDR(header chunk):包含PNG文件中储存的图像数据的基本信息。位于文件头之后的第一个数据块。文件头数据块是由13个字节组成的:

域名称 字节数 说明
Width 4bytes 图像宽度,以像素为单位
Height 4bytes 图像高度,以像素为单位
Bit depth 1byte 图像深度
Color Type 1byte 颜色类型
Compression method 1byte 压缩方法(LZ77派生算法)
Filter method 1byte 滤波器方法
Interlace method 1byte 隔行扫描方法

图像深度:
索引彩色图像:1,2,4或8
灰度图像:1,2,4,8或16
真彩色图像:8或16
颜色类型:
0:灰度图像, 1,2,4,8或16
2:真彩色图像,8或16
3:索引彩色图像,1,2,4或8
4:带α通道数据的灰度图像,8或16
6:带α通道数据的真彩色图像,8或16
隔行扫描方法:
0:非隔行扫描
1: Adam7(由Adam M. Costello开发的7遍隔行扫描方法)

其中我们关注的是前8字节内容,也就是Width和Height。通过对图片高度和宽度的修改隐藏原本应该显示的flag是最为常见的方法。比较常见的就是CRC爆破和简单计算猜测原图片宽度,在下面我可能会提一下。

PLTE

调色板数据块 PLTE(palette chunk):它包含有与索引彩色图像(indexed-color image)相关的彩色变换数据,它仅与索引彩色图像有关,而且要放在图像数据块(image data chunk)之前。

颜色 字节 意义
red 1 0 = 黑色, 255 = 红
green 1 0 = 黑色, 255 = 绿
blue 1 0 = 黑色, 255 = 蓝

由于三原色的原因,调色板数据块的长度应该是三的倍数,否则就是一个非法调色板。真彩色的 PNG 数据流也可以有调色板数据块,目的是便于非真彩色显示程序用它来量化图像数据,从而显示该图像。这一块考察的相对较少,所以暂时没啥要提的。
但是也不是不能提一下。我们可以通过修改PLTE数据块修改图片的颜色。不过我搜到的资料中很少是对调色板修改,更多是对数据中的RGB进行修改。我个人偏向于这种修改是对IDAT块的修改,而不是PLTE层面。

IDAT

图像数据块IDAT(image data chunk),这部分储存实际的数据,而且在数据流中可以多个连续存在。

  • 储存图像像数数据
  • 数据留种可以包含多个连续顺序的IDAT
  • 采用LZ77算法的派生算法进行压缩
  • 可以用zlib解压缩
  • 只有单个数据块充满时,才会填充下一个数据块
    相对重要的时最后两条,尤其是最后一条。IDAT单个数据块不充满表示此处的数据块出现错误,而错误大多数时隐写导致的,这也是我们获取flag的一个小小的突破口。
    关于zlib解压缩这一部分,PNG文件是“可选”zlib解压缩,不代表只有zlib一种。zlib不是一个压缩算法,我们说到“zlib压缩数据”更多的一是是指代一种压缩数据的存放格式。我们可以使用python提供的zlib库对其进行处理。我尝试了zlib字符串的解压缩,在这里提供代码以及一篇文章《关于zlib的解压》
#导入zlib包
import zlib
#目标字符串
message = 'abcd1234'
#字符串压缩
compressed = zlib.compress(message)
#字符串解压
decompressed = zlib.decompress(compressed)

print 'original:', repr(message)
print 'compressed:', repr(compressed)
print 'decompressed:', repr(decompressed)

这一块牵扯比较多的就是LSB隐写相关的,还有数据插入。一般来说数据插入说明这个只是一个跳板,下面还有其他隐写……这两块我们稍微提一下。
LSB 全称 Least Significant Bit,最低有效位。PNG 文件中的图像像数一般是由 RGB 三原色(红绿蓝)组成,每一种颜色占用 8 位,取值范围为 0x00 至 0xFF,即有 256 种颜色,一共包含了 256 的 3 次方的颜色,即 16777216 种颜色。
而人类的眼睛可以区分约 1000 万种不同的颜色,意味着人类的眼睛无法区分余下的颜色大约有 6777216 种。
LSB 隐写就是修改 RGB 颜色分量的最低二进制位(LSB),每个颜色会有 8 bit,LSB 隐写就是修改了像数中的最低的 1 bit,而人类的眼睛不会注意到这前后的变化,每个像素可以携带 3 比特的信息。
我们假设一个像素块的RGB是(11011010,10010110,10010101),那么,我们修改它末尾的bit的时候,人眼看来色调基本上是没有变化的。说白了,就是人类视觉冗余没有办法识别相近的色素块导致信息的隐藏,LSB隐写见过的两个比较常见的方向有藏二维码和ASCII码。本人能力不足,在这里附上lsb隐写教程

IEND

图像结束数据 IEND(image trailer chunk):它用来标记 PNG 文件或者数据流已经结束,并且必须要放在文件的尾部。文件结尾的十二个字符看起来应该是:
00 00 00 00 49 45 4E 44 AE 42 60 82
IEND 数据块的长度总是 00 00 00 00,数据标识总是 IEND 49 45 4E 44,因此,CRC 码也总是 AE 42 60 82
这个地方遇到的是图片叠图片,也就是很粗暴的将两张图片的数据块叠在一起,这样的文件只能打开,在工具中是报错的。但是在读取数据的时候读取到IEND读取停止,此时只显示第一张图片。这样相对粗暴的完成了隐写的目的。

PNG图片隐写

文件类型判断

CRC爆破

在这里CRC爆破我选用的是BUUOJ 大白。tweakpng中提示校验码与实际校验码出现差别,一般这种情况下是擅自修改宽高没有修改校验码导致的。这个时候我们以原本校验码为准,进行CRC爆破。

  import struct
  import binascii
  from Crypto.Util.number import bytes_to_long
  
  img = open("dabai.png", "rb").read()
  
  for i in range(0xFFFF):
  
      stream = img[12:20] + struct.pack('>i', i) + img[24:29]
      crc = binascii.crc32(stream)
     #crc = binascii.crc32(stream) & 0xFFFF
     #if crc == 0x8e14dfcf:
     if crc == bytes_to_long(img[29:33]):
         print(hex(i))

(友情提醒,crypto库安装后运行代码大概率还会出现无crypto库的白给情况,减少白给人人有责)
高度遍历 0~0xFFFF。此外,因为img[12:20]等输出的结果是bytes, 所以这里需要利用struct对整型数据格式化, 将其打包为字节流。另外格式符i意味着4字节的整型。默认小端输出加'改为大端输出。
得出结果在010editor修改即可。
不过这种题目制作的时候或多或少有一点点bug,就是有的时候凭手感直接输入高度或者宽度也可以观察到隐藏的那部分图片,这样看来好像不用很正规的办法也能解出来(?

未填充满数据块

隐藏数据块与二维码还有一些删除错误数据块就可以了的习题。

LSB隐写

题目示例我选用的是BUUOJ的LSB,这道题比较简单,而且是牵扯到二维码。这个地方我们是stegsolve工具,这里附上Stegsolve用法
使用stegsolve打开,在Red plane 0、Grenn plane 0、Blue plane0通道发现图片的上方好有东西,Analyse->Data Extract稍微调整至一下,发现这是一张图片,Save Bin保存为flag.png。打开发现是二维码,扫码出flag。关于stegsolve通道问题

IEND文件叠文件

misc8是文件叠加文件的典型。我们先用binwalk进行检查,发现其中叠加了一个png文件。分离图片文件分为两种,一种是用foremost或者binwalk直接分离,另外一种是WinHex手动分离。
第一种:在Linux中通过指令binwalk misc8.png查看,发现存在PNG文件。offset是3892,也就是说跳过前方3892块偏移后,后方为我们所要的。
通过dd指令:
$ dd if=misc8.png of=misc8a.png skip=3892 bs=1
其中,if是目标文件,of是输出文件,skip是跳过偏移数目,bs设置每次读写块的大小为1字节 。
第二种:直接肉眼手动分离了。有些费时费力,不太建议用这种方法。png文件的IEND应该是00 00 00 00 49 45 4E 44 AE 42 60 82,我们在WinHex中通过肉眼发现确实是有多的png。通过16进制文本定位,在offset F2C处已经出现上述字符块,而到结尾也存在同样的字符块。我们选中后半部分,编辑->复制选块->至新文件,将文件后缀填好。打开发现flag。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM