隐写与取证深入实战:LSB 隐写与 ZIP 伪加密

完整实现 LSB 隐写的隐藏与提取、ZIP 伪加密的十六进制修改与修复、CRC32 短内容爆破。
一、现实背景:文件头与压缩包
文件真实类型由文件头决定。ZIP 文件头是 50 4B 03 04,其内部结构里有"加密标志位",出题人可以把标志位置 1 制造"伪加密"(数据其实没加密)。图片取证中,LSB 隐写把秘密藏在像素最低位。本文完整实现这两种技术。
二、环境准备
apt install file binutils binwalk
pip3 install pillow
gem install zsteg
三、核心技能 1:LSB 隐写(完整实现)
3.1 原理
像素每个通道 1 字节(0-255)。把字节最低位替换成秘密比特,值最多变化 1,肉眼不可察觉:
原来: 11001000 10010110 01100100
写入: 11001001 10010110 01100101 ← 最低位写入 1,0,1
3.2 隐藏(出题人视角)
目的:理解"把秘密写进像素最低位"的实现细节,才能反过来写出提取脚本。
from PIL import Image
# PIL 是 Python 图像处理库,Image 模块负责打开、新建、保存图片。
img = Image.new('RGB', (8, 8), (200, 150, 100))
# Image.new(mode, size, color):新建图片。mode='RGB',size=(8,8),color 是初始色。
message = "flag{lsb_steganography}"
data = message.encode() + b'\x00'
# encode():字符串转字节;追加 b'\x00' 作为结束符(提取时遇到 0 停止)。
bits = ''.join(f'{b:08b}' for b in data)
# 每个字节转成 8 位二进制字符串后拼接成一条长比特串。
pixels = list(img.getdata())
# getdata():取出所有像素,每项是 (r,g,b)。
new_pixels = []
i = 0
for r, g, b in pixels:
if i < len(bits):
r = (r & 0xfe) | int(bits[i]); i += 1
# r & 0xfe:最低位清零;| int(bits[i]):写入第 i 个秘密比特。
if i < len(bits):
g = (g & 0xfe) | int(bits[i]); i += 1
# 绿色通道,同样处理。
if i < len(bits):
b = (b & 0xfe) | int(bits[i]); i += 1
# 蓝色通道。三个通道依次消耗比特,直到消息写完。
new_pixels.append((r, g, b))
img.putdata(new_pixels)
# putdata():把修改后的像素写回图片。
img.save('hidden.png')
# save():保存为 PNG。
print("已生成 hidden.png")
r & 0xfe 清零最低位(0xfe = 11111110),| bit 写入。
3.3 提取(解题视角)
目的:把像素最低位收集起来,每 8 位一组还原成字节,得到隐藏消息。
思路:读取像素 → 依次取 R、G、B 最低位 → 8 位拼 1 字节 → 遇到 0 结束。
from PIL import Image
img = Image.open('hidden.png').convert('RGB')
# Image.open:打开图片;convert('RGB'):统一为 RGB 模式。
bits = []
for r, g, b in img.getdata():
bits += [r & 1, g & 1, b & 1]
# r & 1:取最低位(0 或 1),顺序与隐藏时一致。
data = bytearray()
for i in range(0, len(bits) - 7, 8):
# 每 8 个比特一组,步长为 8。
byte = 0
for bit in bits[i:i+8]:
byte = (byte << 1) | bit
# 左移 1 位再或上当前比特,等价于二进制拼接。
if byte == 0:
# 字节 0 是隐藏时追加的结束符。
break
data.append(byte)
print(data.decode())
# decode():字节串解码成字符串。
实际输出:
flag{lsb_steganography}
3.4 工具验证
zsteg -a hidden.png
预期输出中包含 flag{lsb_steganography}。
四、核心技能 2:ZIP 伪加密(完整实现)
目的:理解 ZIP 的"加密标志位"结构,学会修复伪加密。
思路:先看正常 zip 的十六进制,找到标志位位置;把加密位置 1 模拟伪加密(此时解压报"需要密码");再把位置 0 修复,验证能正常读出内容。
4.1 ZIP 结构
本地文件头:
偏移 字段
0x00 签名 50 4B 03 04
0x04 版本(2 字节)
0x06 通用标志位(2 字节,第 0 位 = 加密位)
中央目录头(签名 50 4B 01 02)中标志位位于偏移 0x08。
4.2 生成 zip 并查看结构
目的:建立"正常 zip"的基线,确认本地文件头与中央目录头中标志位的位置。
import zipfile
with zipfile.ZipFile('flag.zip', 'w') as z:
z.writestr('flag.txt', 'ctf{zip}')
data = open('flag.zip', 'rb').read()
print(' '.join(f'{b:02x}' for b in data[:60]))
实际输出:
50 4b 03 04 14 00 00 00 00 00 d8 73 0e 5d b0 75 c1 31 08 00 00 00 08 00 00 00 08 00 00 00 66 6c 61 67 2e 74 78 74 63 74 66 7b 7a 69 70 7d 50 4b 01 02 14 03 14 00 00 00 00 00 d8 73
解释:
50 4b 03 04 ← 本地文件头签名
14 00 ← 版本
00 00 ← 通用标志位(00 00 = 未加密)
...
50 4b 01 02 ← 中央目录头签名
14 03 14 00 ← 版本信息
00 00 ← 中央目录标志位(未加密)
4.3 设置加密位(伪加密)
思路:只改数据不加密,而是把"加密标志位"置 1,让解压程序误以为需要密码。
data = bytearray(open('flag.zip', 'rb').read())
# 本地文件头:偏移 6 处标志位置位
idx = data.find(b'PK\x03\x04')
data[idx + 6] |= 1
# 中央目录头:偏移 8 处标志位置位
idx2 = data.find(b'PK\x01\x02')
data[idx2 + 8] |= 1
open('flag_enc.zip', 'wb').write(data)
修改后的 hexdump:
50 4b 03 04 14 00 01 00 00 00 d8 73 ... ← 本地标志位 01
50 4b 01 02 14 03 14 00 01 00 00 00 d8 73 ... ← 中央标志位 01
尝试读取:
import zipfile
try:
with zipfile.ZipFile('flag_enc.zip') as z:
print(z.read('flag.txt'))
except Exception as e:
print(type(e).__name__, e)
实际输出:
RuntimeError File 'flag.txt' is encrypted, password required for extraction
程序要求密码——但数据根本没加密,这就是伪加密。
4.4 修复
思路:把两处标志位最低位清零(& 0xfe),恢复成未加密状态。
data = bytearray(open('flag_enc.zip', 'rb').read())
idx = data.find(b'PK\x03\x04')
data[idx + 6] &= 0xfe # 清除本地头加密位
idx2 = data.find(b'PK\x01\x02')
data[idx2 + 8] &= 0xfe # 清除中央目录加密位
open('flag_fixed.zip', 'wb').write(data)
with zipfile.ZipFile('flag_fixed.zip') as z:
print(z.read('flag.txt'))
实际输出:
b'ctf{zip}'
实战提示:很多伪加密题只改本地头或中央目录头之一,binwalk -e 常常直接绕过标志位提取,先试它再手工改。
五、核心技能 3:CRC32 爆破
目的:当压缩包内文件内容很短、又拿不到明文时,用校验值反推内容。
思路:ZIP 记录每个文件的 CRC32(明文校验和)。枚举所有短内容,算 CRC 与记录值比对,相等即明文。
ZIP 中央目录记录每个文件的 CRC32。当文件内容很短(1-4 字符)时,枚举内容匹配校验值:
import zipfile, zlib, itertools
# zipfile:读写 zip;zlib:提供 crc32 校验函数;itertools:组合枚举。
z = zipfile.ZipFile('flag.zip')
target = z.getinfo('flag.txt').CRC
# getinfo('flag.txt'):读取该文件的目录信息;.CRC 属性是它的 CRC32 值。
print("目标 CRC:", target)
charset = 'ctf123{}_abcdefghijklmnopqrstuvwxyz'
# 题目语境下的候选字符集,缩小枚举空间。
for length in range(1, 5):
for tup in itertools.product(charset, repeat=length):
# product(charset, repeat=length):生成长度为 length 的所有组合。
s = ''.join(tup).encode()
# ''.join(tup):组合元组拼成字符串;encode() 转字节。
if zlib.crc32(s) & 0xffffffff == target:
# zlib.crc32 返回有符号整数,& 0xffffffff 转成无符号再比对。
print("爆破结果:", s)
exit()
实际输出(目标内容 ctf1):
目标 CRC: 961973094
爆破结果: b'ctf1'
注意 zlib.crc32 返回有符号整数,需要 & 0xffffffff 归一化。
六、必学工具
| 工具 | 核心功能 | 命令 |
|---|---|---|
| file | 看类型 | file flag.png |
| xxd | 看文件头与结构 | xxd -l 16 flag.png、xxd flag.zip | head |
| binwalk | 扫描/提取隐藏文件 | binwalk flag.png、binwalk -e flag.png |
| zsteg | LSB 自动检测 | zsteg -a flag.png |
七、小 CTF 实战:综合题
目的:把前三节串成一道完整题目。
思路:先排除简单情况(file/strings/binwalk),再怀疑 LSB 提取;若提取结果是 zip 头(PK),保存后检查伪加密。
题目:flag.png。
第一步:
file flag.png # PNG image data
strings flag.png | grep -i flag # 无输出
binwalk flag.png # 无隐藏文件
第二步:怀疑 LSB,运行提取脚本:
flag{lsb_steganography}
变体:若提取出的字节不是文本而是 PK 开头,把它保存为 zip,再检查是否伪加密:
with open('extracted.zip', 'wb') as f:
f.write(data)
然后按第四节流程检查并修复标志位。
八、小结
取证主线:file 确认类型 → strings/binwalk 排除简单情况 → LSB 提取 → 压缩包伪加密修复。这三板斧可覆盖大部分入门与中级 Misc 题。