别只当工具人!深入理解PNG/BMP/GIF文件结构,手撸Python脚本破解CTF图片题
2026/6/5 6:52:33 网站建设 项目流程

从二进制到CTF:Python实战解析PNG/BMP/GIF文件结构

在CTF竞赛中,图片类题目往往是最基础的题型,但也是最容易被忽视的"宝藏"。许多选手止步于使用现成工具进行简单分析,却错过了深入理解文件格式和编写自动化脚本的机会。本文将带你从二进制层面解析PNG、BMP和GIF文件结构,并通过Python实战案例展示如何破解典型CTF图片题。

1. 图片文件结构基础解析

1.1 PNG文件格式深度剖析

PNG(Portable Network Graphics)文件由多个数据块(chunk)组成,每个块都有特定的结构和功能。理解这些块的结构是解决CTF题目的关键。

PNG文件标准结构:

位置长度(字节)内容说明
0-78文件签名固定为\x89PNG\r\n\x1a\n
8-...可变数据块序列包含IHDR、PLTE、IDAT等块
...-EOF可变IEND块文件结束标记

关键数据块类型:

  • IHDR:包含图像宽度、高度、位深度等基本信息
  • PLTE:调色板数据(仅索引彩色图像使用)
  • IDAT:实际图像数据(可能被分割为多个块)
  • tEXt:可存储文本信息(常用于隐写)
  • IEND:图像结束标记
import struct def parse_png_header(data): """解析PNG文件头""" if data[:8] != b'\x89PNG\r\n\x1a\n': raise ValueError("不是有效的PNG文件") # IHDR块位于8-33字节 chunk_length = struct.unpack('>I', data[8:12])[0] chunk_type = data[12:16] if chunk_type != b'IHDR': raise ValueError("第一个块不是IHDR") width, height = struct.unpack('>II', data[16:24]) return width, height

1.2 BMP文件格式关键点

BMP文件结构相对简单,但有几个关键字段在CTF中经常被利用:

def parse_bmp_header(data): """解析BMP文件头""" if data[:2] != b'BM': raise ValueError("不是有效的BMP文件") file_size = struct.unpack('<I', data[2:6])[0] pixel_offset = struct.unpack('<I', data[10:14])[0] # 图像数据起始偏移量 width = struct.unpack('<i', data[18:22])[0] height = struct.unpack('<i', data[22:26])[0] return { 'file_size': file_size, 'pixel_offset': pixel_offset, 'width': width, 'height': height }

BMP文件关键偏移量:

  • 0x0A(10):像素数据起始偏移(常被修改隐藏数据)
  • 0x12(18):图像宽度(4字节)
  • 0x16(22):图像高度(4字节)

1.3 GIF文件特殊结构

GIF文件由多个帧组成,每帧都有自己的控制信息和图像数据。在CTF中常考察:

  • 帧延迟时间隐写
  • 多帧合成分析
  • 调色板修改
def parse_gif_header(data): """解析GIF文件头""" if data[:6] not in (b'GIF87a', b'GIF89a'): raise ValueError("不是有效的GIF文件") width = struct.unpack('<H', data[6:8])[0] height = struct.unpack('<H', data[8:10])[0] return width, height

2. CTF常见图片题型与破解方法

2.1 高度/宽度修改题

这是最常见的题型之一,出题人通过修改图片的高度或宽度值,隐藏部分图像数据。

PNG高度修改实战:

def modify_png_height(filename, new_height): """修改PNG文件高度""" with open(filename, 'rb') as f: data = bytearray(f.read()) # IHDR块中高度位于20-24字节 data[20:24] = struct.pack('>I', new_height) # 需要重新计算CRC32校验 crc_data = data[12:24] new_crc = zlib.crc32(crc_data) & 0xFFFFFFFF data[24:28] = struct.pack('>I', new_crc) with open('modified.png', 'wb') as f: f.write(data)

2.2 CRC32爆破宽高

当图片被修改但保留了原始CRC校验值时,可以通过爆破恢复原始尺寸:

import zlib def brute_force_png_size(filename, known_crc): """爆破PNG宽高""" with open(filename, 'rb') as f: data = f.read() ihdr_data = data[12:24] # IHDR块数据(不含长度和类型) for width in range(1, 2000): for height in range(1, 2000): # 替换宽高值 test_data = ihdr_data[:4] + struct.pack('>II', width, height) + ihdr_data[12:] if zlib.crc32(test_data) == known_crc: return width, height return None, None

2.3 IDAT块分析

PNG的IDAT块存储实际图像数据,常被用于隐藏信息:

def extract_idat_chunks(filename): """提取所有IDAT块数据""" with open(filename, 'rb') as f: data = f.read() pos = 8 # 跳过文件头 idat_data = bytearray() while pos < len(data): chunk_length = struct.unpack('>I', data[pos:pos+4])[0] chunk_type = data[pos+4:pos+8] if chunk_type == b'IDAT': idat_data += data[pos+8:pos+8+chunk_length] pos += 12 + chunk_length # 移动到下一个块 return bytes(idat_data)

3. 高级技巧与自动化脚本

3.1 自动化分析框架

构建一个基础的图片分析框架可以大幅提高解题效率:

class ImageAnalyzer: def __init__(self, filename): self.filename = filename with open(filename, 'rb') as f: self.data = bytearray(f.read()) self.file_type = self.detect_file_type() def detect_file_type(self): """检测文件类型""" if self.data.startswith(b'\x89PNG'): return 'PNG' elif self.data.startswith(b'BM'): return 'BMP' elif self.data.startswith((b'GIF87a', b'GIF89a')): return 'GIF' else: return 'UNKNOWN' def analyze(self): """执行完整分析""" if self.file_type == 'PNG': return self.analyze_png() elif self.file_type == 'BMP': return self.analyze_bmp() elif self.file_type == 'GIF': return self.analyze_gif() else: return {"error": "Unsupported file type"} def analyze_png(self): """分析PNG文件""" result = {} # 解析IHDR块 width, height = struct.unpack('>II', self.data[16:24]) result['dimensions'] = f"{width}x{height}" # 查找所有块 pos = 8 chunks = [] while pos < len(self.data): length = struct.unpack('>I', self.data[pos:pos+4])[0] chunk_type = self.data[pos+4:pos+8].decode('ascii') chunks.append({ 'type': chunk_type, 'position': pos, 'length': length }) pos += 12 + length result['chunks'] = chunks return result

3.2 GIF帧分析与隐写

GIF的多帧特性常被用于隐写:

def analyze_gif_frames(filename): """分析GIF各帧信息""" from PIL import Image img = Image.open(filename) frames = [] try: while True: frame_info = { 'size': img.size, 'duration': img.info.get('duration', 0), 'disposal': img.info.get('disposal', 0), 'offset': img.tell() } frames.append(frame_info) img.seek(img.tell() + 1) except EOFError: pass return frames

3.3 BMP偏移量隐写

利用BMP文件头的像素偏移量隐藏数据:

def extract_bmp_hidden_data(filename): """���取BMP偏移量隐藏数据""" with open(filename, 'rb') as f: data = f.read() pixel_offset = struct.unpack('<I', data[10:14])[0] header_size = struct.unpack('<I', data[14:18])[0] # 偏移量与头部大小之间的数据可能包含隐藏信息 hidden_data = data[14 + header_size : pixel_offset] return hidden_data

4. 实战案例解析

4.1 PNG高度隐藏flag

题目特征:图片显示不完整,flag隐藏在未显示区域

解决步骤:

  1. 检查IHDR块高度值
  2. 尝试增大高度值
  3. 可能需要调整CRC校验
def solve_png_height_challenge(filename): """解决PNG高度隐藏flag题目""" analyzer = ImageAnalyzer(filename) info = analyzer.analyze() original_height = int(info['dimensions'].split('x')[1]) # 尝试增加高度 for delta in [100, 200, 300, 500, 1000]: new_height = original_height + delta modify_png_height(filename, new_height) # 检查新图片是否显示flag try: from PIL import Image img = Image.open('modified.png') img.show() return f"尝试高度: {new_height}" except: continue return "未找到flag,请尝试其他方法"

4.2 GIF帧延迟隐写

题目特征:GIF动画中某些帧停留时间异常

解决步骤:

  1. 提取各帧延迟时间
  2. 将延迟时间转换为二进制数据
  3. 组合二进制数据得到flag
def solve_gif_delay_challenge(filename): """解决GIF帧延迟隐写题目""" frames = analyze_gif_frames(filename) binary_str = '' for frame in frames: if frame['duration'] > 50: # 假设长延迟代表1 binary_str += '1' else: binary_str += '0' # 将二进制转换为ASCII flag = '' for i in range(0, len(binary_str), 8): byte = binary_str[i:i+8] if len(byte) == 8: flag += chr(int(byte, 2)) return flag

4.3 BMP偏移量隐藏数据

题目特征:BMP文件大小与图像尺寸不匹配

解决步骤:

  1. 检查像素偏移量
  2. 提取偏移量前的隐藏数据
  3. 可能需要进一步分析提取的数据
def solve_bmp_offset_challenge(filename): """解决BMP偏移量隐藏数据题目""" hidden_data = extract_bmp_hidden_data(filename) # 尝试直接显示文本 try: text = hidden_data.decode('ascii') if 'ctfshow' in text.lower(): return text except: pass # 尝试binwalk分析 try: import binwalk for result in binwalk.scan(hidden_data, quiet=True): if result.valid: return f"发现隐藏数据: {result.description}" except: pass return "发现隐藏数据,但无法自动解析,请手动检查"

在CTF图片题实战中,理解文件结构只是第一步,更重要的是培养对异常数据的敏感度和系统化的分析思路。我曾遇到一个看似简单的PNG文件,常规检查一无所获,最后发现出题人将flag分散存储在多个tEXt块中,每个块只包含flag的一部分。这种题目考验的不仅是技术,更是耐心和细致。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询