一篇浅析png-filter的水文
前言
PNG(Portable Network Graphics,便携式网络图形)作为常见的图像文件存放格式之一,具有「无损压缩」、「支持透明通道」、「可移植」等特点,广泛运用于互联网及其他方面。总之对其filter做一篇浅析,在后文的介绍中,都是围绕CTF隐写点做介绍。
PNG和其他主流格式比较
PNG是一种光栅图形文件格式,支持无损数据压缩,PNG是作为GIF改进的。而JPEG是一种常用的有损压缩数字图像的方法,通常实现 10:1 压缩,图像质量几乎没有可察觉的损失。因此在常见的隐写中,一般不会在使用stegsolve对JPEG图片进行分析,因为其有损的原因,除了一些软件去对JPEG做LSB隐写,几乎不会直接在stegsolve中提取出可用的信息。
PNG结构
PNG分为signature和chunk
signature
其中signature即常说的文件头,PNG的文件头为x89x50x4ex47x0dx0ax1ax0a
,通过文件头来判断文件类型是最常见的一种判断方式 。一般情况下,出现这8个字节,就代表后面的数据包含一个PNG图像,且直到IEND结束(PNG的"文件尾",字节为x00x00x00x00x49x45x4ex44xaex42x60x82
)
chunk
接下来是数据块,每个数据块都是由Length
,Chunk type
,data
,CRC
组成的。
其中Length是4字节的无符号数,范围是0~2^31-1。
chunk type为块的类型,例如PNG的数据块其chunk type就为IDAT。
data为该块的大小,该值可以为空
CRC是根据Chunk type
+data
计算出来的,不包含Length
,采用循环冗余码算法。详细见参考链接3
总结如下
数据块字段 | 长度 | 特点 |
---|---|---|
uint32 Length | 4 bytes | 最大0xFFFFFFFF |
Type | 4 bytes | 每个字节限制在[a-zA-Z] |
data | 不定字节,最小为0 | 可为0 |
uint32 CRC | 4 bytes | 计算时不包括Length |
chunk Type命名规则
第一个字母: 大写表示渲染图片所必需的块,小写表示用于渲染图像的辅助块,即非必须。
第二个字母:大写表示公共标准块,而小写表示专有或非标准。
第三个字母:始终是大写字母。
第四个字母:小写字母表示如果生成新的 PNG 则可以安全复制。否则制作衍生PNG文件时,要么重新生成它,要么将其删除。
例如:
带有PLTE的图像
IHDR
对于chunk来说,IHDR
、IDAT
、IEND
是必须的。这里只讲一手IHDR,因为后面开始解析IDAT
IHDR头即块中Type字段为IDAT
,IHDR应该是PNG数据流中的第一个块且唯一。其data包含如下信息
字段 | 长度 | 特点 |
---|---|---|
Width(宽) | 4 bytes | uint32 |
Height(高) | 4 bytes | uint32 |
Bit depth(位深) | 1 bytes | 不同类型允许的位深度不同 |
Colour type(颜色类型) | 1 bytes | 0,2,3,4,6五个值 |
Compression method(压缩方式) | 1 bytes | 值只有0,deflate算法 |
Filter method(过滤器类型) | 1 bytes | 值只有0,包含5种类型 |
Interlace method(扫描方式) | 1 bytes | 逐行扫描(0),Adam7隔行扫描(1) |
其中 颜色类型如下:
类型值 | 类型 | 描述 |
---|---|---|
0 | 灰度 | 灰度图像 |
2 | 真彩色 | 常见的RGB图,包含R,G,B三种通道 |
3 | 索引色 | 索引图,每个像素都是一个调色板索引,会出现PLTE块 |
4 | 带alpha灰度色 | 灰度+alpha |
6 | 带alpha真彩色 | RGB+A |
解析IDAT内容
解压缩
IDAT中的data是采用zlib库的DEFLATE算法进行压缩的,在python中直接使用zlib库解压缩
import zlib
f = open('file.txt','rb').read() #file.txt为一段压缩后的数据
print(zlib.decompress(f))
过滤
在之前提到Filter method只有一种类型,但是包含5种算法,分别是表中内容
表1:定义变量
c | b |
---|---|
a | x |
x为当前字节,a为x左边那一像素值,b为上一条扫描线对应的像素值,c为左上
或者这个
表2:
类型 | 名称 | 过滤函数 | 重构函数 |
---|---|---|---|
0 | None | F(x) = O(x) | Rec(x) = F(x) |
1 | Sub | F(x) = O(x) - O(a) | Rec(x) = F(x) + Rec(a) |
2 | Up | F(x) = O(x) - O(b) | Rec(x) = F(x) + Rec(b) |
3 | Average | F(x) = O(x) - (O(a)+O(b)) / 2 | Rec(x) = F(x) + (Rec(a) + Rec(b)) / 2 |
4 | Paeth | F(x) = O(x) - PaethPredictor(O(a), O(b), O(c)) | Rec(x) = O(x) + PaethPredictor(O(a), O(b), O(c)) |
其中 PaethPredictor函数定义如下:
p = a + b - c
pa = abs(p - a)
pb = abs(p - b)
pc = abs(p - c)
if pa <= pb and pa <= pc then Pr = a
else if pb <= pc then Pr = b
else Pr = c
return Pr
❝
注意:
1.如果在计算完之后超出了255或者小于了0,如果没有uint,那么就需要手动&0xFF才行
2.对于average中的除以2,使用>>1即可,例如X[j] = (X[j] - ((A[j] + B[j])>>1)) & 0XFF
对于Sub,第一个值为标准值,不做任何过滤 对于Up,如果是第一行那么也不做任何过滤 对于average,如果是第一行,则第一个值不做任何过滤,其他值则计算X[j] = (X[j] - (A[j]>>1)) & 0XFF 如果是第一列,则计算X[j] = (X[j] - (B[j]>>1)) & 0XFF 对于Paeth,如果是第一行,则同Sub;如果是第一列,则与上一行作差 ❞
针对Filter进行隐写
根据filter一共有5种类型,出一道指定filter的题目,其中包含5种类型的filter,构造4进制,用None类型进行分割
可以手写出5种filter类型的Filter Function,自己调用并修改字节,最后进行压缩,构造好chunk并添加signature IHDR IEND
例如构造一张10*5的图片,其filter类型分别为0 1 2 3 4 5,原图的RGB值全为128 128 128
from PIL import Image
pic = Image.new('RGB',(10,5),(128,128,128))
pic.save('tmps.png')
通过zlib库解压缩可以看一下得到的字节
import zlib
import struct
f = open('tmps.png','rb').read()
ind = f.index(b'IDAT')
length, = struct.unpack('>L',f[ind-4:ind])
data = f[ind+4:ind+4+length] #只有一个IDAT块
print(zlib.decompress(data))
#b'x01x80x80x80x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x02x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x02x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x02x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x02x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00'
接下来,有多种方法实现,例如提取RGB值,重构整个data。
先用数组存储一下pixel
from PIL import Image
img = Image.open('tmps.png')
w,h = img.size
pixel = []
for i in range(h):
tmp = []
for j in range(w):
tmp.append(img.getpixel((j,i)))
pixel.append(tmp)
print(pixel)
接下来,为了演示的方便,所以写的比较直接,没有任何优化,甚至不能直接拿去用
哦,是多么好看的字节
要注意的是,在使用的时候是拿像素的值去做加减,而不是拿字节
然后再用zlib库进行压缩,最后重新写进去,得到这样
❝
78 9C 63 68 C0 0B 18 81 98 01 37 60 C2 23 07 04 CC 0E 0E 0E 78 A4 59 F0 EB 06 00 14 0E 11 4B 可以自己去inflate看看
❞
简直一模一样(因为本来就一模一样) 这样就可以实现自己指定filter类型 实现隐写咯
不放脚本,是真的因为写的太垃圾了,只用了PIL去处理
题目试炼
既然文章写的那么短,要不再给大伙出个简单题,见下图图一与图二,flag格式mumuzi{}
题目名称:GURA
题目下载:https://pan.baidu.com/s/1eC7-XRK9RXnnF6-u9n2Gdw 提取码(GAME)
题目试练地址:https://www.ichunqiu.com/battalion?t=1&r=70900
参考链接
1.https://en.wikipedia.org/wiki/Portable_Network_Graphics: Portable Network Graphics.
2.https://en.wikipedia.org/wiki/JPEG: JPEG
3.https://en.wikipedia.org/wiki/Cyclic_redundancy_check : CRC
4.https://www.w3.org/TR/PNG/:Information technology — Computer graphics and image processing — Portable Network Graphics (PNG): Functional specification. ISO/IEC 15948:2003 (E)
致谢
特别感谢**Rightp4th(TRPH)**的倾囊相授与对文章错误的修改
原文始发于微信公众号(i春秋):浅析png-filter
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论