一. PNG 文件结构#
一个 PNG 文件格式为:
Copy
文件头(89 50 4E 47 0D 0A 1A 0A) + 数据块 + 数据块 + 数据块…… + 文件尾(00 00 00 00 49 45 4E 44 AE 42 60 82)
PNG 定义了两种类型的数据块,一种是称为关键数据块,这是标准的数据块,另一种叫做辅助数据块,这是可选的数据块。关键数据块定义了4个标准数据块,每个 PNG 文件都必须包含它们。
每个数据块都有着统一的结构,由 4 个部分组成:
其中CRC域中的值是对数据块类型码和数据进行计算得到的,CRC具体算法定义在ISO 3309和ITU-T V.42中,其值按CRC码生成多项式进行计算
我们可以用 pngcheck 去查看PNG文件数据块信息。 PS:结构什么的看看就行,最好能掌握,掌握不了也没关系,咱可以做中学
IHDR(文件头数据块)#
第一块是文件头数据块(IHDR),它由第11——32字节组成(从0开始),包含有 PNG 文件中存储的图像数据的基本信息,数据从第 16字节开始,有13个字节,其前8字节分别用4个字节规定了图片的宽和高(十六进制,以像素为单位)。
IDAT(图像数据块)#
它存储实际的数据,在数据流中可包含多个连续顺序的图像数据块。它采用 LZ77 算法的派生算法进行压缩,可以用 zlib 解压缩。
二. IHDR 篡改图片宽高#
IHDR 的前8字节规定了图片的宽和高,我们可以用十六进位文件编辑器更改它们以使得这张图片显示不完整,从而达到隐藏信息的目的。此时它的图片数据并没有变,在Windows图片查看器中超过规定的图片宽高的部分只是不显示了。
细心的同学会发现在 Kali Linux 中是打不开这张图片的,会提示 IHDR CRC error,这是因为在每个数据块的最后4字节都有CRC(循环冗余检测)用来检测是否有错误和被篡改。聪明的同学自然就想到是不是可以用CRC反推图片原来的宽和高,答案是肯定的,我们可以利用 python 脚本反推图片原宽高,然后用十六进制编辑器打开图片修改图片宽高得到原图片。
Copy
#使用 python [脚本文件名] [图片文件名] import zlib import struct import sys filename = sys.argv[1] with open(filename, 'rb') as f: all_b = f.read() crc32key = int(all_b[29:33].hex(),16) data = bytearray(all_b[12:29]) n = 4095 for w in range(n): width = bytearray(struct.pack('>i', w)) for h in range(n): height = bytearray(struct.pack('>i', h)) for x in range(4): data[x+4] = width[x] data[x+8] = height[x] crc32result = zlib.crc32(data) if crc32result == crc32key: print("宽为:",end="") print(width) print("高为:",end="") print(height) exit(0)
三. LSB 隐写#
PNG 文件中的图像像数一般是由 RGB 三原色(红绿蓝)组成(有的图片还包含A通道表示透明度),每一种颜色占用8位,取值范围为0x00至0xFF。LSB 隐写就是修改 RGB 颜色分量的最低二进制位(LSB),它修改了每个像数颜色的最低的1 bit,而人类的眼睛不会注意到这前后的变化,这样每个像素可以携带3比特的信息。
如果是要寻找这种 LSB 隐藏痕迹的话,有一个工具 Stegsolve 是个神器,可以来辅助我们进行分析。通过下方的按钮观察每个通道的信息,我们可以捕捉异常点,抓住 LSB 隐写的蛛丝马迹(这玩意儿很难说,一般就是一看就感觉奇怪的n行或n列颜色块),进而利用 Stegsolve --> Analyse --> Data Extract 功能指定通道进行提取。
对于PNG和BMP文件中的 LSB 等常见的隐写方式,我们也可以使用 **zsteg **工具直接进行自动化的识别和提取。
四. IDAT 隐写#
IDAT 块只有当上一个块充满(正常length最大65524)时,才会继续一个新的块。程序读取图像的时候也会在第一个未满的块停止(查了下W3C标准,其实是PNG图片在压缩的时候会在最后一个块的标记位标明这是最后一个数据块)。所以如果某一块没有满但后面却还有 IDAT 块则说明后面的块是“假”的。
我们可以用 pngcheck -v [文件名]
去查看PNG文件数据块信息,然后利用 python zlib 解压多余IDAT块的内容,此时注意剔除长度、数据块类型及末尾的CRC校验值。
Copy
import zlib import binascii IDAT = " ".decode('hex') #双引号中填IDAT数据 result = binascii.hexlify(zlib.decompress(IDAT)) print(result)
例题#
找了半天没找齐合适的例题,自己写了一个,大家凑合做
例题链接:百度网盘 请输入提取码 提取码:6666
大家可以自己先尝试一下,一共有4个flag(有两个flag1,我打错了),如果有困难再去看下面的题解。
题解#
日常先打开编辑器看一下,没发现明显问题。枚举一波,先试试 LSB 隐写,打开 Stegsolve,查看不同轨道,发现在Red/Green/Blue plane 0的上方均有明显隐写痕迹(那一条奇怪的黑白码)。
打开 Stegsolve --> Analyse --> Data Extract ,选中那三个有痕迹的轨道,先点 Preview,确实有PNG文件藏在里面,点击 Sava Bin 保存文件为 1.png。( Stegsolve 具体怎么用可能有缘写~,现在就这样用也差不多)
打开 1.png,是个二维码,用 QR Reserch 扫描得到 Flag1。
还剩下3个 flag,把图片扔到 Kali Linux 里,发现图片无法查看,说明 IHDR 被篡改,用上面的脚本爆破 IHDR 得到宽高。
用 Winhex 打开,找到 IHDR 中的宽高,更改一下(上面的“\x00”代表一个字节,第四个是符号是因为他刚好属于可打印字节,把它打到右边就行),保存,确定更新。
打开图片,看到 flag1(其实是 flag3 ,打错了)。
还有两个 flag,我们把更改过的图片扔到 Kali Linux 里,惊奇地发现(真的很惊奇,以前没见过没这种情况)还是没有预览,但是在图片查看器里可以正常显示,在火狐浏览器中无法显示却不报错,用 binwalk 分析一下,有个 zlib 文件,只能说很正常,没啥问题,再用 pngcheck 检查一下,发现异常,第二个 IDAT 块只有9895的长度,在没有满的情况一般来说是不会出现下一个块的,说明最后一个块是“假块”(那个 CRC ERROR 请忽略,只是我出题时懒得算CRC而已,真正做题是不会给你留这个 bug 的)。
我们重新用 Winhex 打开图片,搜索“IDAT”找到最后一个 IDAT 块,把里面的数据(只要数据,不要长度位、“IDAT”和那4位 CRC )复制到新文件里(CRC 是我瞎写的,所以上面会报错),保存成1.txt(没错,为了偷懒我压根没压缩成 zlib,你用 binwalk 是分离不出来的)。
得到一串奇葩文字,搜索得知这是与佛论禅加密。
打开在线解密工具,解密得到 flag4。
最后还剩一个 flag,还记得我们扫出 flag1 的那个二维码吗?我们用 binwalk -e 扫描提取,发现能检测到 zip 尾,但是提取不出来。。。改用 foremost 提取,得到一个 zip 文件,打开看到里面有一个名字乱码的文本文件(无语),打开又是一堆迷惑代码。
这回是符号所以不太好搜索,只能靠经验得知这是 Brainfuck 代码,利用在线解密工具“Brainfuck to text”解密得到 flag2。
顺便说一下为什么“我们刚才用 binwalk -e 扫描提取,发现能检测到 zip 尾,但是却提取不出来”,用 WinHex 打开,发现在PNG文件尾与ZIP文件尾之间有“脏东西”,导致 binwalk 找不到文件头提取失败。(说实话,这个地方还能再出个 flag,我上一期说的两个同类型文件 夹在一块也是这种情况,如果你们遇到类似的情况也要注意看一下头尾之间有没有藏信息)
本期就先说到这里,主要讲了讲隐写分析中的 PNG 图片隐写,写的不太好的地方还请包涵并提出您宝贵的建议,我们下期再见。(顺便说一下,我发现在Winhex 里,“粘贴”是插入,“写入”是覆盖后面的)