PNG 文件结构与 IHDR 篡改隐写:CRC 反推图片尺寸

PNG 文件结构与 IHDR 篡改隐写:CRC 反推图片尺寸

本文字数:1305  阅读时长:3分钟

访客数:加载中... | 阅读量:加载中...

一、PNG 文件结构

一个 PNG 文件由文件头、若干数据块和文件尾组成:

text
文件头(89 50 4E 47 0D 0A 1A 0A)+ 数据块 + 数据块 + ... + 文件尾(00 00 00 00 49 45 4E 44 AE 42 60 82)
  • 文件头固定为 89 50 4E 47 0D 0A 1A 0A,即 ASCII 字符 .PNG 加上换行(0D 0A)、EOF 等标记;
  • 文件尾固定为 00 00 00 00 49 45 4E 44 AE 42 60 82,IEND 表示文件结束。

PNG 定义了两类数据块:

  • 关键数据块(critical chunk):标准数据块,PNG 文件必须包含,共 4 个(IHDR、PLTE、IDAT、IEND);
  • 辅助数据块(ancillary chunk):可选数据块。

每个数据块都有统一的结构,由 4 个部分组成:

text
长度(4 字节) + 数据块类型(4 字节) + 数据(长度字段指定的字节数) + CRC 校验值(4 字节)

IHDR(文件头数据块)

IHDR 是文件的第一个数据块,由第 11~32 字节组成(从 0 开始计数),包含 PNG 文件中存储的图像数据的基本信息:

  • 数据从第 16 字节开始,共 13 字节;
  • 前 8 字节分别用 4 个字节(大端序)规定了图片的宽和高,单位为像素。

IDAT(图像数据块)

IDAT 存储实际图像数据,在数据流中可包含多个连续顺序的图像数据块。它采用 LZ77 算法的派生算法进行压缩,可以用 zlib 解压缩。

二、IHDR 篡改图片宽高

IHDR 的前 8 字节规定了图片的宽和高,我们可以用十六进制编辑器修改它们,使图片显示不完整,从而达到隐藏信息的目的。此时图片的像素数据并没有变,在 Windows 图片查看器中,超过规定宽高的部分只是不再显示了。

然而在 Kali Linux 中,这样修改过的图片通常是打不开的,会提示 IHDR CRC error——因为每个数据块的最后 4 字节都有 CRC(循环冗余校验)值,用于检测数据是否被篡改。

篡改方法

用十六进制编辑器打开图片,定位到 IHDR 块中表示宽高的字段:

  • 数据块数据起始处的前 4 字节是宽度(十六进制);
  • 紧接着的后 4 字节是高度(十六进制)。

例如把宽度改为 0384(对应十进制 900)、高度改为 0096(对应十进制 150),图片就会按修改后的尺寸显示,其余像素被截断隐藏,从而起到隐藏信息的效果。

用 CRC 反推原始宽高

CRC 校验值是根据 IHDR 块的完整数据(含原始宽高)计算得到的。一旦宽高字段被篡改,CRC 就会对不上,解码器随即报 IHDR CRC error。反过来,我们可以枚举宽高,计算每种组合下的 CRC,与文件中已有的 CRC 值比对,一致时即为被隐藏的原始尺寸。

以下脚本根据图片中已有的 CRC 值,计算 PNG(其他图片格式似乎不适用)正确的宽度和高度:

python
import os
import binascii
import struct

crcbp = open("misc26.png", "rb").read()  # 打开图片
crc32frombp = int(crcbp[29:33].hex(), 16)  # 读取图片中的 CRC 校验值
print(crc32frombp)

for i in range(4000):  # 宽度 1-4000 枚举
    for j in range(4000):  # 高度 1-4000 枚举
        data = crcbp[12:16] + \
               struct.pack('>i', i) + struct.pack('>i', j) + crcbp[24:29]
        crc32 = binascii.crc32(data) & 0xffffffff
        if crc32 == crc32frombp:  # 计算当图片大小为 i:j 时的 CRC,与图片中的 CRC 相同则尺寸确定
            print(i, j)
            print('hex:', hex(i), hex(j))

脚本说明:

  • crcbp[29:33] 取出 IHDR 块末尾 4 字节的 CRC 校验值(IHDR 数据从第 16 字节开始共 13 字节,其后 4 字节即 CRC,位于 29~32 字节);
  • crcbp[12:16] 是 IHDR 四个字节的类型标识;
  • struct.pack('>i', i) 与 struct.pack('>i', j) 分别把当前枚举的宽度、高度写入大端序 4 字节;
  • crcbp[24:29] 是 IHDR 数据中除宽高外的其余 5 字节(位深、颜色类型、压缩方法、过滤方法、隔行扫描方式);
  • 将上述字段组合后计算 CRC32,与文件中的 CRC 值比对,一致即得到被隐藏的原始宽高。

得到原始尺寸后,用十六进制编辑器把宽高改回去,图片即可正常显示,被隐藏的内容也随之露出。