图片分析简介
图像文件有多种复杂的格式,可以用于各种涉及到元数据、信息丢失和无损压缩、校验、隐写或可视化数据编码的分析解密,都是 Misc 中的一个很重要的出题方向。涉及到的知识点很多(包括基本的文件格式,常见的隐写手法及隐写用的软件),有的地方也需要去进行深入的理解。
元数据(Metadata)[¶]
元数据(Metadata),又称中介数据、中继数据,为描述数据的数据(Data about data),主要是描述数据属性(property)的信息,用来支持如指示存储位置、历史数据、资源查找、文件记录等功能。
元数据中隐藏信息在比赛中是最基本的一种手法,通常用来隐藏一些关键的 Hint
信息或者是一些重要的如 password
等信息。
这类元数据你可以 右键 --> 属性
去查看, 也可以通过 strings
命令去查看,一般来说,一些隐藏的信息(奇怪的字符串)常常出现在头部或者尾部。
接下来介绍一个 identify
命令,这个命令是用来获取一个或多个图像文件的格式和特性。
-format用来指定显示的信息,灵活使用它的
-format` 参数可以给解题带来不少方便
[format各个参数具体意义](https://www.imagemagick.org/script/escape.php)
PNG数据块(Chunk)
1.数据块构成结构
2.PNG数据块
数据块格式(统一数据结构)
数据块类型
PNG定义了两种类型的数据块:
- 关键数据块(critical chunk),这是标准的数据块
- 辅助数据块(ancillary chunks),这是可选的数据块。(可有可没有)
关键数据块定义了4个标准数据块,每个PNG文件都必须包含它们,PNG读写软件也都必须要支持这些数据块
数据块中有 4 个关键数据块:
- 文件头数据块 IHDR(header chunk):包含有图像基本信息,作为第一个数据块出现并只出现一次。
- 调色板数据块 PLTE(palette chunk):必须放在图像数据块之前。
- 图像数据块 IDAT(image data chunk):存储实际图像数据。PNG 数据允许包含多个连续的图像数据块。
- 图像结束数据 IEND(image trailer chunk):放在文件尾部,表示 PNG 数据流结束。
一.PNG图像标识符
PNG文件头
(根据PNG文件的定义来说,其文件头位置总是由位固定的字节来描述的)
十进制数 | 137 80 78 71 13 10 26 10 |
---|---|
十六进制数 | 89 50 4E 47 0D 0A 1A 0A |
一堆(n个)数据块就是一个数据流
以下是四个关键数据块
二.IHDR数据块(13个字节)
文件头数据块IHDR(header chunk)
-
包含有PNG文件中存储的图像数据的基本信息
-
作为第一个数据块出现在PNG数据流中,且一个PNG数据流中只能有一个文件头数据块。
文件头数据块由13字节组成,它的格式如下表所示。
域的名称 | 字节数 | 说明 |
---|---|---|
Width | 4 bytes | 图像宽度,以像素为单位 |
Height | 4 bytes | 图像高度,以像素为单位 |
Bit depth | 1 byte | 图像深度: 索引彩色图像:1,2,4或8 灰度图像:1,2,4,8或16 真彩色图像:8或16 |
ColorType | 1 byte | 颜色类型:0:灰度图像, 1,2,4,8或16 2:真彩色图像,8或16 3:索引彩色图像,1,2,4或8 4:带α通道数据的灰度图像,8或16 6:带α通道数据的真彩色图像,8或16 |
Compression method | 1 byte | 压缩方法(LZ77派生算法) |
Filter method | 1 byte | 滤波器方法 |
Interlace method | 1 byte | 隔行扫描方法 |
隔行扫描方法
0:非隔行扫描
1: Adam7(由Adam M. Costello开发的7遍隔行扫描方法)
三.PLTE(调色板数据块)
调色板数据块PLTE(palette chunk)包含有与索引彩色图像(indexed-color image)相关的彩色变换数据,它仅与索引彩色图像有关,而且要放在图像数据块(image data chunk)之前。
PLTE数据块是定义图像的调色板信息,PLTE可以包含1~256个调色板信息,每一个调色板信息由3个字节组成:
颜色 | 字节 | 意义 |
---|---|---|
Red | 1 byte | 0 = 黑色, 255 = 红 |
Green | 1 byte | 0 = 黑色, 255 = 绿色 |
Blue | 1 byte | 0 = 黑色, 255 = 蓝色 |
因此,调色板的长度应该是3的倍数,否则,这将是一个非法的调色板。
对于索引图像,调色板信息是必须的,调色板的颜色索引从0开始编号,然后是1、2……,调色板的颜色数不能超过色深中规定的颜色数(如图像色深为4的时候,调色板中的颜色数不可以超过2^4=16),否则,这将导致PNG图像不合法。
真彩色图像和带alpha通道数据的真彩色图像也可以有调色板数据块,目的是便于非真彩色显示程序用它来量化图像数据,从而显示该图像。
四.IDAT(图像数据块)
图像数据块IDAT(image data chunk):它存储实际的数据,在数据流中可包含多个连续顺序的图像数据块。
IDAT存放着图像真正的数据信息,因此,如果能够了解IDAT的结构,我们就可以很方便的生成PNG图像。
-
储存图像像素数据
-
在数据流中可包含多个连续顺序的图像数据块
-
采用 LZ77 算法的派生算法进行压缩
-
可以用 zlib 解压缩
eg:如下图
-
00 00 00 D3 数据长为211字节(211(只是第三个的)+12(头、标&校验))
211---16*n+R
-
49 44 41 54 IDAT标识(想找IDAT的位置就find十六进制49 44 41 54,无空格也行)
- 78 9C…… 压缩的数据,LZ77派生压缩方法
- DA 12 06 A5 CRC校验
-
** IDAT 块只有当上一个块充满时,才会继续一个新的块。
------->用 pngcheck
去查看此 PNG 文件
λ .\pngcheck.exe -v sctf.png
File: sctf.png (1421461 bytes)chunk IHDR at offset 0x0000c, length 131000 x 562 image, 32-bit RGB+alpha, non-interlacedchunk sRGB at offset 0x00025, length 1rendering intent = perceptualchunk gAMA at offset 0x00032, length 4: 0.45455chunk pHYs at offset 0x00042, length 9: 3780x3780 pixels/meter (96 dpi)chunk IDAT at offset 0x00057, length 65445zlib: deflated, 32K window, fast compressionchunk IDAT at offset 0x10008, length 65524
...chunk IDAT at offset 0x150008, length 45027chunk IDAT at offset 0x15aff7, length 138chunk IEND at offset 0x15b08d, length 0
No errors detected in sctf.png (28 chunks, 36.8% compression).
可以看到,正常的块的 length 是在 65524 的时候就满了,而倒数第二个 IDAT 块长度是 45027,最后一个长度是 138,很明显最后一个 IDAT 块是有问题的,因为他本来应该并入到倒数第二个未满的块里.
利用 python zlib
解压多余 IDAT 块的内容,此时注意剔除 长度、数据块类型及末尾的 CRC 校验值。
import zlib
import binascii
IDAT = "789...667".decode('hex')
result = binascii.hexlify(zlib.decompress(IDAT))
print result
五.IEND(文件尾)(12个字符)
图像结束数据IEND(image trailer chunk):它用来标记PNG文件或者数据流已经结束,并且必须要放在文件的尾部。
如果我们仔细观察PNG文件,我们会发现,文件的结尾12个字符看起来总应该是这样的:
00 00 00 00 49 45 4E 44 AE 42 60 82
不难明白,由于数据块结构的定义,IEND数据块的长度总是0(00 00 00 00,除非人为加入信息),数据标识总是IEND(49 45 4E 44),因此,CRC码也总是AE 42 60 82。
3*4 长度+数据标识+CRC码
辅助数据块
(除关键数据块外都是辅助数据块)
- 背景颜色数据块 bKGD(background color)
- 基色和白色度数据块 cHRM(primary chromaticities and white point),所谓白色度是指当
R=G=B=最大值
时在显示器上产生的白色度 - 图像 γ 数据块 gAMA(image gamma)
- 图像直方图数据块 hIST(image histogram)
- 物理像素尺寸数据块 pHYs(physical pixel dimensions)
- 样本有效位数据块 sBIT(significant bits)
- 文本信息数据块 tEXt(textual data)
- 图像最后修改时间数据块 tIME (image last-modification time)
- 图像透明数据块 tRNS (transparency)
- 压缩文本数据块 zTXt (compressed textual data)
图像示例
1.PNG文件头标识
2.IHDR数据块
-
00 00 00 0D 说明IHDR头块长为13
-
49 48 44 52 IHDR标识
-
00 00 01 00 图像的宽,256像素
-
00 00 01 00 图像的高,256像素
-
04 色深,2^4=16,即这是一个16色的图像(也有可能颜色数不超过16,当然,如果颜色数不超过8,用03表示更合适)
-
03 颜色类型,索引图像
-
00 PNG Spec 规定此处总为0(非0值为将来使用更好的压缩方法预留),表示使压缩方法(LZ77派生算法)
-
00 同上总为0
-
00 非隔行扫描
-
36 21 A3 B8 CRC校验
CRC校验原理
划分为组,每组kbit
设数据M=101001(此时k=6)
用二进制的模二运算进行2^n*M的运算(即在M后面添加n个0)
除数P 商Q 余数R
CRC校验代码如下:
import java.util.zip.CRC32;public class CrcTest {public static void main(String[] args) {byte[] checkData = new byte[]{0x49,0x48,0x44,0x52,0x00,0x00,0x00, 0x08,0x00,0x00,0x00, 0x08,0x04,0x03,0x00,0x00,0x00};CRC32 crc32 = new CRC32(); crc32.update(checkData);long value = crc32.getValue();byte[] intToBytes = longToBytes(value);String bytesToHexString = bytesToHexString(intToBytes);System.out.println(bytesToHexString);}public static byte[] longToBytes(long value) { byte[] src = new byte[4]; src[0] = (byte) ((value>>24) & 0xFF); src[1] = (byte) ((value>>16)& 0xFF); src[2] = (byte) ((value>>8)&0xFF); src[3] = (byte) (value & 0xFF); return src; } //将字节数组按16进制输出public static String bytesToHexString(byte[] src){StringBuilder stringBuilder = new StringBuilder("");if (src == null || src.length <= 0) {return null;}for (int i = 0; i < src.length; i++){int v = src[i] & 0xFF;String hv = Integer.toHexString(v);if (stringBuilder.length() != 0) {stringBuilder.append(",");}if (hv.length() < 2) {stringBuilder.append(0);}stringBuilder.append(hv);}return stringBuilder.toString();}}
3.可选数据块
可选数据块sBIT,颜色采样率,RGB都是256(2^8=256)
4.PLTE调色板数据块
这里是调色板信息
- 00 00 00 27 说明调色板数据长为39字节,即13个颜色数(从0开始)
- 50 4C 54 45 PLTE标识
- B7 00 34 颜色0
- FF 99 00 颜色1
- …… ……
- FF FF 00 最后一个颜色,12
- 48 29 75 2C CRC校验
()其他示例:
5.IDAT数据部分
-
00 00 00 D3 数据长为211字节(211(只是第三个的)+12(头、标&校验))
211---16* n+R--- 16*13+3
-
49 44 41 54 IDAT标识(想找IDAT的位置就find十六进制49 44 41 54,无空格也行)
-
78 9C…… 压缩的数据,LZ77派生压缩方法
-
DA 12 06 A5 CRC校验
6.IEND文件尾
IEND数据块,这部分正如上所说,通常都应该是
00 00 00 00 49 45 4E 44 AE 42 60 82
至此,我们已经能够从一个PNG文件中识别出各个数据块了。由于PNG中规定除关键数据块外,其它的辅助数据块都为可选部分,因此,有了这个标准后,我们可以通过删除所有的辅助数据块来减少PNG文件的大小。(当然,需要注意的是,PNG格式可以保存图像中的层、文字等信息,一旦删除了这些辅助数据块后,图像将失去原来的可编辑性。)
删除了辅助数据块后的PNG文件,现在文件大小为147字节,原文件大小为261字节,文件大小减少后,并不影响图像的内容。参考:打造自由换色的png图片类。
<文件不同,校验码也不一样>
CRC校验例题
脚本(不知道是不是所有脚本都一样)
import os
import binascii
import structmisc = open("misc4.png","rb").read()for i in range(1024):data = misc[12:16] + struct.pack('>i',i)+ misc[20:29]crc32 = binascii.crc32(data) & 0xffffffffif crc32 == 0x932f8a6b:print i
pngcheck
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 比特的信息。
stepic用法
Stepic - aldeid
JPG数据段
段结构
-
JPG是由一个一个段构成的
-
所有数据都是高位在前
- JPEG 是有损压缩格式,将像素信息用 JPEG 保存成文件再读取出来,其中某些像素值会有少许变化。在保存时有个质量参数可在 0 至 100 之间选择,参数越大图片就越保真,但图片的体积也就越大。一般情况下选择 70 或 80 就足够了
- JPEG 没有透明度信息
段标识 | 段类型 | 段长度 | 段数据 |
---|---|---|---|
1字节 | 1字节 | 2字节 | (段长度-2) |
FF | |||
(一定存在) | (一定存在) | (不包含段标识、段类型) |
- 有些段没有长度描述也没有内容,只有段标识和段类型。文件头和文件尾均属于这种段。
- 段与段之间无论有多少
FF
都是合法的,这些FF
称为「填充字节」,必须被忽略掉。
常见段类型
段结构包括:
段标识|段类型|段长度|端数据
段标识
1字节,固定为FFH,H表示16进制
段类型
1字节
文件头SOI
名称 SOI 标记码 D8
文件尾EOI
名称 EOI 标记码 D9
帧开始(标准JPEG)
名称 SOF0 标记码 C0
名称 SOF1 标记码 C1
C0与C1在一个文件里只会存在一种,下面以C0为例,C1同样操作。
FF C0H段数据分析
偏移位置 | 字节长度 | 含义 |
---|---|---|
0x00 | 1 | 标志(固定08H) |
0x01~0x02 | 2 | Y轴分辨率 |
0x03~0x04 | 2 | X轴分辨率 |
0x05 | 1 | 组件数量(固定03H) |
0x06~0x14 | 9 | 组件数据 |
我们查找
FF C0
后面两个字节00 11表示段长度,将十六进制转为十进制,可以知道后面有15个字节的数据(段长度占了2字节,并且不包括段标识和段类型)
那么我们可以看到后面的数据
标志08
1个字节
这个标志固定是08
y轴分辨率
2字节
计算一下,
与实际的分辨率(高度)一样
x轴分辨率
2字节
这两字节表示x轴分辨率,计算一下
与实际上文件的属性是一样的
组件数量03
这1个字节固定为03
从03之后的9个字节,都为组件数据,且每个组件数据为3个字节。
组件大小和数量都为固定的。
定义Huffman表(霍夫曼表)DHT
名称 DHT 标记码 C4
固定4个FF C4,通常用来表示图片的亮度和色度,其中2个记录亮度,2个记录色度
通常一个图片中有四个霍夫曼表段:亮度的直流量(DC)、亮度的交流量(AC)、色度的直流量(DC)、色度的交流量(AC)
FF C4H段数据结构分析
从其他图片复制霍夫曼表,会导致图片颜色或亮度错误
FF DA -- 数据区的开始
字节
这个红色框框里的两个数表示一个字节
高位在前
什么是高位在前?
例如现实生活中1234,高位在前则表示12 34
而winhex的都是低位在前,在后面会讲到段长度,如00 10,低位在前则表示10 00
12 34 56
56 34 12
例子:更改宽度和高度来恢复图片
GIF
块
文件头
固定长6个字节
逻辑屏幕描述符
7字节
宽 高 压缩字节
-
1C 02 宽
-
C8 01 高
在GIF文件格式中,所有的多字节值均以小端格式(little-endian format,从最后一个字节开始往前保存)存储。比如在我们看来 0A 00,这个值实际写作 000A,也就是十进制的10。
宽0X1C02是2*16^3+ 0 *16^2+12 *16+1=8385
-
F7 压缩字节 -----》转成二字节 1111 0111
- 第一个bit 标志位 ——表示全局颜色列表是否存在(1存在 0不存在)
- 接下来三个bit ——表示图像调色板中每个颜色的原色所占用的Bit数(颜色分辨率)
(011表示占用4个Bit,111占用8个Bit)——即最多只能占用8个bit
调色板最多只包含由24-Bit颜色中选出的256个颜色(实际有很多优化方案能提高颜色分辨率,如加入局部调色板)
-
第五个Bit为标志位 ——表示颜色列表排序方式。
若为1,表示颜色列表是按照颜色在图像中出现的频率降序排列。0则相反
-
最后三个bit ——表示全局颜色列表大小
计算方法是2^N+1 ,其中N为这三个Bit的二进制数值。
eg: 111 2**(7+1)=256
011 2**(3+1)=16
- FF ——背景色在全局颜色列表中的索引
若无全局颜色列表则此字节无效。
在GIF的图像数据中,没有被指定颜色的像素会被背景色填充
- 00 ——像素的宽高比
大多数时候这个值都是0,若值为N, 则图像的宽高比:
aspectRatio = (pixelAspectRatio + 15) / 64
全局颜色列表
由前面的逻辑屏幕描述符可知,全局颜色列表的大小是256,每个颜色占三个字节,按照RGB排列,所以它占有256*3个字节。
GIF格式可以拥有global color table,或用于针对每个子图片集,提供local color table。每个color table由一个RGB(就像通常我们见到的(255,0,0)红色 那种)列表组成。
正如之前说到的,global color table的长度为2 ^ (N + 1),因此这个表占用 3 * 2 ^ (N + 1)个字节。
数据流中,颜色是按照列表中的索引存储的。
应用程序扩展21 FF
开头21 FF
GIF中扩展块都以0x21开始,后一个字节是扩展标签,标识扩展用途。
应用程序扩展的标签是0xFF,它包含有应用程序的标识信息和应用程序数据。
其中 Netscape 应用程序扩展常用于控制GIF的动画循环次数。Netscape 扩展长19个字节,前14个是应用程序的ACSII信息,后四个是数据子块,用于指定GIF的循环次数, 按无符号整型存储,0表示无限循环。
图像控制扩展21 F9
用于指定透明度以及控制动画
图形控制扩展块属于”89a”版本的定义。
它在一个图像数据块的最前端,用来指定图像的透明度与动画属性。
图形控制扩展的开端两字节是0x21F9,其中0x21表示这是一个扩展,F9表示扩展用于图形控制。
第三个字节是块的大小(它到结束符之间的数据)。
第四个字节是压缩字段,前三个Bit保留,四到六Bit是disposal method(处理方法),倒数第二个位是user input flag(用户输入标志),最后一位transparent color flag(透明色标志)
第四、五个字节是图像控制扩展后面的图像的动画时间(delay time),以无符号整型(unsigned格式)存储。
第六个字节是透明色索引,之后是块结束符0x00。
图像描述符
每个image都以一个image descriptor block(图像描述块)作为开头,这个块固定为10字节。
图像描述符位于GIF中每一个图像数据的前端,由0x2C开始,长度固定为10个字节。
第一个字节是图像描述符的标识0x2c
后面八个字节表示图像的frame(left, top, width, height),用来在动画中局部更新图像。每个值都是2字节的,无符号小端格式。
最后一个是压缩字节,主要是关于局部颜色列表的信息,其中第二个Bit表示图像的存储方式是交织还是连续。
首位是local color table flag(局部颜色表标志)。设置为1表示允许你指定图片数据使用一个不同的颜色表(跟在后面)以取代全局颜色表。
第二个位是interlace flag(隔行扫描标志)。隔行扫描方式会改变图像渲染到屏幕上的方式,从而减少烦人的视觉闪烁(类似垂直同步)。隔行扫描在显示器上的效果是,第一轮扫描先立即模糊地显示图像,随后再一轮将其填充锐化。这种方式会让人们感觉更舒服,因为它可以让人们模糊地意识到即将显示的东西是什么,而不是等待像素点被一行一行地填充绘制。要支持这种显示方式,图片的扫描行需要以一种特定的顺序来存储,需要分为4个部分,每个部分都是一个完整的模糊显示,通过4次显示加成使得图片越来越清晰,最终完全呈现。
局部颜色列表
如果上面的局部颜色列表标志位为1,那么局部颜色列表会排列在图像描述符后面,它只对紧跟在它之后的图像数据有效。如果局部颜色列表标志位为0,那么图像数据将使用全局颜色列表索引颜色。局部颜色列表的大小计算方法和像素颜色格式与全局颜色列表相同。
图像数据
GIF的图像数据是经过LZW压缩的二进制流,通过解码可以将其按照颜色列表中的颜色进行像素填充。第一个字节是LZW最小编码大小,用来进行数据解码。第二个字节是图像数据的大小,之后的都是图像数据,直到块结束符。