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

隐写与取证深入实战

完整实现 LSB 隐写的隐藏与提取、ZIP 伪加密的十六进制修改与修复、CRC32 短内容爆破。

14th Aug 2026

6 min read

一、现实背景:文件头与压缩包

文件真实类型由文件头决定。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
zstegLSB 自动检测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 题。