新手也能玩转的CTF隐写术:手把手教你用Python脚本修复PNG图片宽高(附BUUCTF Findme实战)

张开发
2026/6/15 9:28:45 15 分钟阅读
新手也能玩转的CTF隐写术:手把手教你用Python脚本修复PNG图片宽高(附BUUCTF Findme实战)
从零破解CTF图片隐写Python修复PNG宽高实战指南第一次参加CTF比赛时我盯着那道需要修复PNG图片的题目发呆了半小时。直到一位前辈递来三行Python代码我才恍然大悟——原来那些看似神秘的隐写术不过是文件格式与编程基础的精妙结合。本文将带你用开发者的视角重新理解CTF中最常见的PNG宽高修复问题。1. 为什么修改PNG宽高能隐藏信息每张PNG图片开头都藏着一个名为IHDR的数据块它像身份证一样记录了图像的元信息。其中最关键的是4字节宽度和4字节高度值它们决定了图像查看器如何解析后续像素数据。当我们故意修改这些数值时会导致两种有趣的现象图像显示不全若将实际500x500的图片宽高改为300x300查看器只会渲染前300行和列图像显示错乱当宽高值与实际数据不匹配时像素排列会像错位的拼图般产生异常视觉效果# 典型PNG文件结构示例 00-07: 文件头签名 (89 50 4E 47 0D 0A 1A 0A) 08-20: IHDR块 (包含宽/高/位深/颜色类型等) 21-24: IHDR的CRC校验码提示CRC校验就像IHDR的指纹任何对IHDR内容的修改都必须同步更新CRC值否则文件将被视为损坏在BUUCTF的Findme题目中出题人正是利用这个特性——他们将真实flag藏在完整的图片数据里却通过修改IHDR中的宽高值让常规查看器只能显示部分内容。我们的任务就是找到正确的宽高组合。2. 逆向破解PNG宽高的关键技术2.1 定位IHDR关键数据段用十六进制编辑器打开1.png你会看到如下结构偏移量长度含义示例值0x008PNG文件签名89 50 4E 47 0D 0A 1A 0A0x084IHDR块长度00 00 00 0D0x0C4块类型标识49 48 44 52 (IHDR)0x104宽度值被修改的数值0x144高度值被修改的数值0x185其他参数08 06 00 00 000x1D4CRC32校验码C4 ED 3C 002.2 CRC32校验原理与爆破CRC校验就像一道数学题的解它由IHDR块中从IHDR到最后一个参数的所有字节计算得出。我们可以利用这个特性反向推导提取原始CRC值示例中为0xC4ED3C00保持其他参数不变枚举所有可能的宽高组合当某个组合的CRC与原始值匹配时即为正确答案import zlib import struct def brute_force_png_dimensions(file_path, original_crc): with open(file_path, rb) as f: data bytearray(f.read()) # 提取IHDR数据段 (从IHDR标识开始到最后一个参数) ihdr_data data[12:29] for width in range(1, 4096): for height in range(1, 4096): # 替换宽高值 temp_data bytearray(ihdr_data) temp_data[4:8] struct.pack(i, width) # 大端序编码 temp_data[8:12] struct.pack(i, height) # 计算CRC if zlib.crc32(temp_data) original_crc: return width, height return None, None注意struct.pack(i, value)中的表示大端序网络字节序这是PNG标准要求的格式3. 完整实战Findme题目破解让我们用Python脚本实际破解BUUCTF的题目准备阶段下载题目附件1.png用十六进制编辑器查看CRC值位于0x1D-0x20编写爆破脚本import zlib import struct def fix_png_dimensions(input_path, output_path): with open(input_path, rb) as f: original_data bytearray(f.read()) # 从文件中提取原始CRC (小端序存储需转换) original_crc int.from_bytes(original_data[29:33], byteorderbig) # IHDR数据段 (类型标识宽高其他参数) ihdr_chunk original_data[12:29] print(f开始爆破CRC: 0x{original_crc:08X}...) for width in range(1, 2000): width_bytes struct.pack(i, width) for height in range(1, 2000): height_bytes struct.pack(i, height) # 替换宽高值 test_data bytearray(ihdr_chunk) test_data[4:8] width_bytes test_data[8:12] height_bytes if zlib.crc32(test_data) original_crc: print(f找到匹配的宽高: {width}x{height}) # 修复文件 fixed_data bytearray(original_data) fixed_data[16:20] width_bytes fixed_data[20:24] height_bytes with open(output_path, wb) as f: f.write(fixed_data) return print(未找到匹配的宽高组合) # 使用示例 fix_png_dimensions(1.png, 1_fixed.png)进阶技巧——优化爆破速度先确定合理范围如Findme实际为666x666使用多线程加速注意GIL限制from concurrent.futures import ThreadPoolExecutor def check_dimensions(args): width, height, ihdr_template, target_crc args test_data bytearray(ihdr_template) test_data[4:8] struct.pack(i, width) test_data[8:12] struct.pack(i, height) return (width, height) if zlib.crc32(test_data) target_crc else None def parallel_brute_force(workers4): # ...省略初始化代码 with ThreadPoolExecutor(max_workersworkers) as executor: args [(w, h, ihdr_chunk, original_crc) for w in range(1, 1000) for h in range(1, 1000)] for result in executor.map(check_dimensions, args): if result: return result4. 常见问题与调试技巧4.1 为什么我的脚本找不到正确宽高可能原因及解决方案字节序错误PNG要求大端序但某些系统默认小端序确认使用struct.pack(i, value)CRC计算范围错误必须包含IHDR标识到最后一个参数示例中为原始数据[12:29]字节数值范围不足有些CTF题目会设置非常规尺寸可先尝试0-5000范围4.2 修复后图片仍显示异常典型问题排查流程用xxd 1_fixed.png | head -n 30检查修复结果确认IHDR块后是否存在其他异常IDAT块缺失如Findme题目辅助块损坏# 使用pngcheck工具诊断 $ pngcheck -v 1_fixed.png4.3 更隐蔽的隐写变种进阶CTF题目可能会修改多个IHDR参数如位深度使用非标准CRC多项式在IDAT块中做手脚# 检查所有关键参数 params { width: (16, 20), height: (20, 24), bit_depth: (24, 25), color_type: (25, 26) }5. 从解题到精通理解PNG结构本质真正掌握PNG隐写术需要理解其底层结构。推荐使用010 Editor的PNG模板分析文件关键数据块IHDR图像头部信息PLTE调色板数据IDAT实际图像数据IEND图像结束标记手工修复示例 当自动脚本失效时可以# 手工修补IDAT标识如Findme题目 with open(1_fixed.png, rb) as f: f.seek(0x24) # 定位到第二个块 f.write(bIDAT) # 修正块标识创意利用在IEND后追加数据多数查看器会忽略利用PNG扫描行过滤器隐藏信息修改gamma值进行视觉隐写在最近一次线下赛中我遇到一道需要同时修复宽高和颜色类型的题目。通过结合CRC爆破和逐块分析最终在图像alpha通道找到了flag。这种深度理解远比单纯解题更有价值——它让你能创造自己的隐写方法而不仅是破解他人的设计。

更多文章