PNG_challenge
字数 2117 2025-08-30 06:50:35
PNG文件结构与CTF攻防技术详解
一、PNG文件基础结构
1. PNG文件签名
PNG文件以固定的8字节签名开头,用于标识文件类型:
89 50 4E 47 0D 0A 1A 0A
这8个字节是PNG文件的标识符,任何合法的PNG文件都必须以此开头。
2. 数据块(Chunks)结构
PNG文件由多个数据块组成,每个数据块包含以下4个部分:
- 长度(Length):4字节,大端序存储,指定数据段的字节数(不包括长度、类型和CRC)
- 块类型(Chunk Type):4字节ASCII码,标识数据块的功能(如IHDR、IDAT等)
- 数据(Chunk Data):长度指定的数据内容
- CRC校验(CRC):4字节,循环冗余校验码,确保数据块完整性
注意:CRC计算范围只包括块类型和数据,不包括长度字段
二、关键数据块详解
1. IHDR块(文件头数据块)
- 每个PNG图片有且只有一个IHDR块
- 固定长度:13字节
- 结构内容:
- 宽度(4字节,大端序)
- 高度(4字节,大端序)
- 位深度(1字节):1、2、4、8、16
- 颜色类型(1字节):
- 0=灰度
- 2=RGB
- 3=索引颜色
- 4=灰度+Alpha
- 6=RGB+Alpha
- 压缩方法(1字节):通常为0(deflate)
- 滤波方法(1字节):通常为0
- 隔行扫描方法(1字节):0=非隔行,1=Adam7隔行
示例分析:
00 00 00 0D 49 48 44 52 00 00 05 DA 00 00 08 8D 08 02 00 00 00 1C 4C 11 E6
- 长度:00 00 00 0D → 13字节
- 类型:49 48 44 52 → "IHDR"
- 数据:
- 宽度:00 00 05 DA → 1498像素
- 高度:00 00 08 8D → 2189像素
- 位深度:08 → 8位
- 颜色类型:02 → RGB模式
- 压缩方式:00 → deflate
- 滤波方式:00
- 隔行扫描:00
- CRC:1C 4C 11 E6
2. IDAT块(图像数据块)
- 存储压缩后的图像像素数据,使用deflate算法
- 一张PNG可能有多个IDAT块,解码时按顺序拼接
- 常见IDAT块大小限制为65536字节(64KB),但理论上最大可达2GB
- DEFLATE算法:
- LZ77算法:查找和替换重复的数据序列
- Huffman编码:对符号进行变长编码
- 预处理过滤器(5种类型):
- 类型0:无过滤
- 类型1:Sub(当前值减前一个像素值)
- 类型2:Up(当前值减上一行对应位置值)
- 类型3:Average(当前值减前一个和上一行像素的平均值)
- 类型4:Paeth预测器
3. IEND块(文件尾数据块)
- 固定结构:
00 00 00 00 49 45 4E 44 AE 42 60 82
- 数据长度为0,类型为"IEND",CRC固定为AE 42 60 82
三、辅助数据块
1. PLTE块(调色板)
- 定义调色板(索引颜色模式)的颜色表
- 每3字节表示一个颜色(RGB),最多256个颜色(768字节)
- 必须在IDAT块之前出现
- 仅当颜色类型为3时必须存在
2. 其他辅助块
- 图像显示:tRNS、bKGD、sRGB、pHYs
- 颜色管理:cHRM、gAMA、iCCP、sRGB、sBIT
- 元数据:tEXt、zTXt、iTXt、tIME
- 调色板优化:hIST、sPLT
四、PNG攻击技术汇总
1. 文件插入字符信息
- 直接附加:在IEND块后添加任意数据
- 使用辅助块隐藏信息:
- tEXt块:存储未压缩文本,格式
[关键字]\0[文本内容] - zTXt块:存储压缩文本,格式
[关键字]\0[压缩标志]\0[压缩数据] - iTXt块:支持国际化文本,格式
[关键字]\0[压缩标志]\0[压缩方式]\0[语言标签]\0[翻译关键字]\0[文本] - 自定义辅助块:块类型为小写字母开头
- tEXt块:存储未压缩文本,格式
2. 结构缺失/混乱攻击
- 多个IHDR块:可能隐藏多个图像
- 异常数据块顺序:可能导致解析异常
- 解决方法:使用工具分析并重组合法结构
3. IDAT块隐写
- 异常IDAT块大小或数量
- 隐藏额外数据在IDAT块中
- 工具:TweakPNG分析处理
4. CRC宽高爆破
- IHDR块宽高被修改但CRC未更新
- 爆破原理:已知CRC和部分数据,爆破宽高值
- 脚本示例:
import zlib
import struct
def crc32(data):
return zlib.crc32(data) & 0xffffffff
def brute_force_crc(png_file):
with open(png_file, 'rb') as f:
data = f.read()
# 定位IHDR块
ihdr_pos = data.find(b'IHDR') - 4
ihdr_data = data[ihdr_pos:ihdr_pos+29]
original_crc = struct.unpack('>I', ihdr_data[21:25])[0]
# 常见分辨率组合
common_sizes = [(800, 600), (1024, 768), (1920, 1080), (1280, 720)]
for width, height in common_sizes:
# 构造IHDR数据
new_data = ihdr_data[8:12] # IHDR
new_data += struct.pack('>I', width)
new_data += struct.pack('>I', height)
new_data += ihdr_data[20:21] # 剩余字段
# 计算CRC
crc = crc32(new_data)
if crc == original_crc:
return width, height
return None
5. 颜色通道隐写
LSB隐写(最低有效位)
- 修改像素通道的最低位(第0位)
- 人眼几乎无法察觉变化
- 工具:stegsolve、zsteg
MSB隐写(最高有效位)
- 修改像素通道的最高位
- 变化较明显但仍有隐蔽性
通道组合爆破
- 当隐写通道未知时,可爆破所有可能的通道组合
- 示例脚本:
from PIL import Image
import itertools
def extract_from_channels(image_path, dict_words):
img = Image.open(image_path)
pixels = img.load()
width, height = img.size
# 所有可能的通道和位组合
channels = ['R', 'G', 'B']
bits = range(8)
for combo in itertools.product(channels, bits):
channel, bit = combo
extracted = []
for y in range(height):
for x in range(width):
pixel = pixels[x, y]
if channel == 'R':
val = pixel[0]
elif channel == 'G':
val = pixel[1]
else:
val = pixel[2]
extracted.append(str((val >> bit) & 1))
binary = ''.join(extracted)
# 检查是否包含字典中的关键词
for word in dict_words:
if word in binary:
return combo, binary
return None
五、实用工具推荐
- 010 Editor:强大的十六进制编辑器
- TweakPNG:分析处理PNG数据块
- exiftool:提取PNG元信息
- stegsolve:分析颜色通道隐写
- zsteg:检测LSB隐写数据
六、开发脚本示例
1. PNG数据块分析脚本
import struct
def analyze_png_chunks(png_file):
with open(png_file, 'rb') as f:
data = f.read()
# 检查PNG签名
if data[:8] != b'\x89PNG\r\n\x1a\n':
print("Not a valid PNG file")
return
pos = 8 # 跳过签名
while pos < len(data):
# 读取块长度
length = struct.unpack('>I', data[pos:pos+4])[0]
pos += 4
# 读取块类型
chunk_type = data[pos:pos+4]
pos += 4
# 读取块数据
chunk_data = data[pos:pos+length]
pos += length
# 读取CRC
crc = struct.unpack('>I', data[pos:pos+4])[0]
pos += 4
print(f"Chunk: {chunk_type.decode()}")
print(f"Length: {length}")
print(f"CRC: {hex(crc)}")
# 特殊处理IHDR块
if chunk_type == b'IHDR':
width, height = struct.unpack('>II', chunk_data[:8])
print(f"Image size: {width}x{height}")
print("---")
2. 自定义IDAT块生成脚本
import zlib
import struct
from PIL import Image
def create_png_with_custom_idat(output_path, width, height, idat_sizes):
# 创建空白图像
img = Image.new('RGB', (width, height), (255, 255, 255))
# 准备IHDR数据
ihdr_data = struct.pack('>IIBBBBB', width, height, 8, 2, 0, 0, 0)
ihdr_crc = zlib.crc32(b'IHDR' + ihdr_data) & 0xffffffff
# 准备IDAT数据
raw_data = img.tobytes()
compressed = zlib.compress(raw_data)
# 分割IDAT数据
idat_chunks = []
pos = 0
for size in idat_sizes:
chunk_data = compressed[pos:pos+size]
pos += size
if not chunk_data:
continue
idat_chunks.append({
'length': len(chunk_data),
'type': 'IDAT',
'data': chunk_data,
'crc': zlib.crc32(b'IDAT' + chunk_data) & 0xffffffff
})
# 构建PNG文件
with open(output_path, 'wb') as f:
# 写入签名
f.write(b'\x89PNG\r\n\x1a\n')
# 写入IHDR
f.write(struct.pack('>I', 13))
f.write(b'IHDR')
f.write(ihdr_data)
f.write(struct.pack('>I', ihdr_crc))
# 写入IDAT块
for chunk in idat_chunks:
f.write(struct.pack('>I', chunk['length']))
f.write(chunk['type'].encode())
f.write(chunk['data'])
f.write(struct.pack('>I', chunk['crc']))
# 写入IEND
f.write(struct.pack('>I', 0))
f.write(b'IEND')
f.write(struct.pack('>I', 0xAE426082))
七、总结
PNG文件结构复杂但规范,为CTF比赛提供了丰富的出题点。掌握PNG的核心结构和常见攻击技术,能够有效解决大多数PNG相关的CTF题目。关键点包括:
- 理解PNG数据块结构和校验机制
- 熟悉IHDR、IDAT等关键块的解析方法
- 掌握CRC宽高爆破、IDAT隐写等攻击技术
- 熟练使用相关分析工具和脚本开发
通过系统学习和实践,可以逐步掌握PNG文件的深入分析和攻击技术。