在CTF竞赛中,PNG图片隐写术是常见的考察点。许多新手第一次遇到被修改了宽高的PNG图片时,往往会感到无从下手。本文将以BUUCTF平台上的"Findme"题目为例,带你从零开始编写Python脚本修复图片宽高,并深入理解背后的技术原理。
PNG文件由多个数据块(chunk)组成,其中最关键的是IHDR块。这个块包含了图片的基本信息,包括宽度、高度、位深度等。当我们用十六进制编辑器查看PNG文件时,IHDR块总是位于文件开头附近。
IHDR块的结构如下:
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| Length | 4 | IHDR数据块的长度,固定为13 |
| Chunk Type | 4 | 固定为"IHDR"(0x49 0x48 0x44 0x52) |
| Width | 4 | 图片宽度(大端序) |
| Height | 4 | 图片高度(大端序) |
| Bit depth | 1 | 位深度 |
| Color type | 1 | 颜色类型 |
| Compression | 1 | 压缩方法 |
| Filter | 1 | 过滤方法 |
| Interlace | 1 | 隔行扫描方法 |
| CRC | 4 | IHDR块的CRC校验值 |
当CTF题目修改了PNG的宽高但忘记更新CRC校验值时,就会出现图片无法正常显示的情况。我们可以利用CRC校验的这一特性,通过暴力破解找出正确的宽高值。
在开始编写修复脚本前,我们需要确保Python环境已准备好必要的库:
bash复制pip install pillow # 用于验证修复后的图片
核心工具库Python已内置:
zlib:用于计算CRC32校验值struct:用于处理二进制数据提示:建议使用Python 3.6+版本,本文所有代码均基于Python 3.8测试通过
下面我们分步骤构建完整的修复脚本,每个部分都有详细解释:
python复制import zlib
import struct
def repair_png_width_height(filename):
# 以二进制模式读取文件
with open(filename, 'rb') as f:
png_data = bytearray(f.read())
# IHDR块从第12字节开始(跳过8字节签名和4字节长度)
ihdr_start = 12
ihdr_data = png_data[ihdr_start:ihdr_start+17] # 13字节数据+4字节CRC
# 提取原始CRC值(最后4字节)
original_crc = ihdr_data[-4:]
crc32key = int.from_bytes(original_crc, byteorder='big', signed=False)
print(f"原始CRC值: 0x{crc32key:08X}")
这段代码完成了:
python复制 # 准备IHDR数据部分(不包含长度和块类型)
ihdr_content = png_data[ihdr_start+4:ihdr_start+4+13]
# 设置合理的宽高搜索范围
max_dimension = 4096 # 假设图片宽高不会超过4096像素
for width in range(max_dimension):
# 将宽度转换为4字节大端序格式
width_bytes = struct.pack('>i', width)
for height in range(max_dimension):
# 将高度转换为4字节大端序格式
height_bytes = struct.pack('>i', height)
# 替换宽高数据
new_ihdr = bytearray(ihdr_content)
new_ihdr[0:4] = width_bytes # 替换宽度
new_ihdr[4:8] = height_bytes # 替换高度
# 计算CRC32校验值
crc32result = zlib.crc32(new_ihdr)
if crc32result == crc32key:
print(f"找到匹配的宽高: {width}x{height}")
print(f"CRC32: 0x{crc32result:08X}")
# 更新原始PNG数据中的宽高值
png_data[ihdr_start+8:ihdr_start+12] = width_bytes
png_data[ihdr_start+12:ihdr_start+16] = height_bytes
# 保存修复后的文件
new_filename = filename.replace('.png', '_fixed.png')
with open(new_filename, 'wb') as f:
f.write(png_data)
return width, height
print("未找到匹配的宽高组合")
return None, None
这段代码的核心逻辑是:
将上述代码组合起来,我们得到完整的修复脚本:
python复制import zlib
import struct
def repair_png_width_height(filename):
with open(filename, 'rb') as f:
png_data = bytearray(f.read())
ihdr_start = 12
ihdr_data = png_data[ihdr_start:ihdr_start+17]
original_crc = ihdr_data[-4:]
crc32key = int.from_bytes(original_crc, byteorder='big', signed=False)
ihdr_content = png_data[ihdr_start+4:ihdr_start+4+13]
max_dimension = 4096
for width in range(max_dimension):
width_bytes = struct.pack('>i', width)
for height in range(max_dimension):
height_bytes = struct.pack('>i', height)
new_ihdr = bytearray(ihdr_content)
new_ihdr[0:4] = width_bytes
new_ihdr[4:8] = height_bytes
crc32result = zlib.crc32(new_ihdr)
if crc32result == crc32key:
print(f"找到匹配的宽高: {width}x{height}")
png_data[ihdr_start+8:ihdr_start+12] = width_bytes
png_data[ihdr_start+12:ihdr_start+16] = height_bytes
new_filename = filename.replace('.png', '_fixed.png')
with open(new_filename, 'wb') as f:
f.write(png_data)
return width, height
print("未找到匹配的宽高组合")
return None, None
if __name__ == '__main__':
import sys
if len(sys.argv) != 2:
print("使用方法: python repair_png.py <filename.png>")
sys.exit(1)
width, height = repair_png_width_height(sys.argv[1])
if width is not None:
print(f"修复成功! 保存为 {sys.argv[1].replace('.png', '_fixed.png')}")
使用方法:
bash复制python repair_png.py corrupted.png
上面的基础脚本虽然能用,但在处理大尺寸图片时效率较低。我们可以进行以下优化:
python复制import multiprocessing
def check_dimensions(args):
width, ihdr_content, crc32key = args
width_bytes = struct.pack('>i', width)
for height in range(4096):
height_bytes = struct.pack('>i', height)
new_ihdr = bytearray(ihdr_content)
new_ihdr[0:4] = width_bytes
new_ihdr[4:8] = height_bytes
if zlib.crc32(new_ihdr) == crc32key:
return width, height
return None
def repair_png_fast(filename):
# ... (前面的读取代码相同)
with multiprocessing.Pool() as pool:
args = [(w, ihdr_content, crc32key) for w in range(4096)]
results = pool.imap_unordered(check_dimensions, args)
for result in results:
if result is not None:
width, height = result
# ... (后面的修复代码相同)
break
对于更高级的优化,我们可以利用CRC的数学性质,通过已知的CRC值和部分数据,直接计算出宽度或高度,而不需要暴力破解。这种方法需要更深入的数学知识,但可以将破解时间从几分钟缩短到几秒钟。
在BUUCTF的"Findme"题目中,修复图片宽高只是第一步。根据原始题目描述,后续还需要:
这类题目通常考察的是综合的隐写分析能力。修复宽高后,建议使用以下工具进行进一步分析:
注意:在CTF比赛中,时间就是分数。建议先快速尝试常见的隐写方法,如宽高修复、LSB隐写、文件尾附加数据等,再逐步深入分析。
在编写和运行修复脚本时,可能会遇到以下问题:
问题1:脚本运行很久都没结果
print输出当前尝试的宽高,确认脚本在运行问题2:修复后的图片仍然无法显示
89 50 4E 47 0D 0A 1A 0A)pngcheck工具验证文件完整性问题3:找到多个匹配的宽高组合
调试时可以添加以下代码检查数据:
python复制print(f"IHDR内容: {ihdr_content.hex(' ')}")
print(f"CRC32键: {crc32key:08X}")
掌握了PNG宽高修复后,可以进一步学习以下内容:
PNG文件格式详解:
CTF隐写术进阶:
推荐工具:
exiftool:查看和修改图片元数据stegoveritas:自动化隐写分析工具zsteg:专门检测PNG和BMP中的隐写在BUUCTF平台上,类似的题目还有:
修复图片宽高只是CTF隐写术的入门技能,但理解了这个过程,你就掌握了分析文件格式和二进制数据的基本方法。