图片隐写
笔者:ILOVCTRY
分析流程
拿到图片后,可以按下面顺序检查:
- 确认文件类型、文件头和文件尾,必要时修改扩展名。
- 查看文件大小、宽高、帧数、颜色模式和元数据。
- 用
strings、binwalk、foremost等工具查找附加数据。 - 根据图片格式选择隐写分析方法:JPEG 重点看 EXIF、F5、JPHide;PNG 重点看宽高、LSB 和 CRC;GIF 重点看分帧。
- 最后再检查 Base64、倒写数据、二维码和像素编码等题目特征。
相关笔记:[[MISC/隐写/图片/GIF]]、[[MISC/隐写/图片/像素]]、[[MISC/隐写/图片/QR二维码]]
文件格式与文件头
用 file 或 WinHex/010 Editor 查看实际文件类型,再结合文件头判断是否需要修改扩展名。
| 格式 | 文件头 | 文件尾或特征 |
|---|---|---|
| JPEG | FF D8 FF(SOI 通常为 FF D8) |
FF D9(EOI) |
| PNG | 89 50 4E 47 0D 0A 1A 0A |
49 45 4E 44 AE 42 60 82(IEND) |
| GIF | 47 49 46 38(GIF87a/GIF89a) |
3B |
| BMP | 42 4D(BM) |
文件大小位于文件头偏移 0x02,小端 4 字节 |
| TIFF | 49 49 2A 00(小端)或 4D 4D 00 2A(大端) |
- |
常用检查命令
file image
exiftool image
strings -n 5 image
binwalk image
foremost image -o output
分离已知偏移处的数据:
dd if=file_path of=3.zip skip=70658 bs=1
binwalk -e file_name.jpg 可尝试自动提取;Linux 下若权限不足再考虑 --run-as=root。Windows 环境也可以使用 foremost 或随波逐流、CTF 编码工具中的文件分离功能。
JPEG
EXIF 信息
EXIF 可能包含拍摄时间、设备型号、GPS 经纬度、缩略图和评论等信息:
exiftool cat.jpg
重点查看 Comment、Description、UserComment、GPS 和缩略图字段。
提取字符串
strings hex.jpg
也可以查看 JPEG 尾部是否存在超过 FF D9 的附加数据。
F5 隐写
使用 CTF-All-In-One 等工具尝试 F5 隐写分析。F5 通常针对 JPEG 的 DCT 系数,不能仅凭文件扩展名判断是否存在隐藏数据。

binwalk或foremost提取
CTF 编码工具通常集成 binwalk、foremost 等提取功能。

JPHide / JPHSwin
JPHSwin 是 JPHS(JPEG Hide and Seek)的 Windows 图形界面版本:
- 打开 JPEG 文件。
- 提取数据时选择 Seek,输入密码并指定输出路径。
- 写入数据时选择 Hide,设置密码和待嵌入文件,再另存为新的 JPEG。
OutGuess
# 隐藏
outguess -k "my secret key" -d hidden.txt demo.jpg out.jpg
# 提取
outguess -k "my secret key" -r out.jpg hidden.txt
从二进制数据还原图片
下面的例子将每行第 5 个字符之后的数值映射为 2 bit,再按字节转换为十六进制数据:
from pathlib import Path
import binascii
values = [int(line[4:]) for line in Path("ttl.txt").read_text().splitlines()]
mapping = {63: "00", 127: "01", 191: "10", 255: "11"}
bits = "".join(mapping[value] for value in values)
hex_text = "".join(chr(int(bits[i:i + 8], 2)) for i in range(0, len(bits), 8))
Path("flag.jpg").write_bytes(binascii.unhexlify(hex_text))
尾部负载数据
- 图片结束后,尾部负载数据
- 尾部负载数据形式多样,可能缺失文件头、可能是倒序、单字节 XOR、位反转等。
PNG
文件结构
PNG 的通用数据块结构为:
4 字节长度 + 4 字节块类型 + 块数据 + 4 字节 CRC
IHDR 是 PNG 的第一个数据块,块数据长度为 13 字节:
| 偏移 | 长度 | 字段 |
|---|---|---|
0~3 |
4 | 宽度(Width) |
4~7 |
4 | 高度(Height) |
8 |
1 | 位深度:1/2/4/8/16 |
9 |
1 | 颜色类型:0 灰度,2 RGB,3 索引色,4 灰度+Alpha,6 RGBA |
10 |
1 | 压缩方法,PNG 固定为 0(DEFLATE) |
11 |
1 | 过滤方法,PNG 固定为 0 |
12 |
1 | 隔行扫描:0 无隔行,1 Adam7 |

题目中若只修改了 PNG 高度,常见现象是图片显示不完整或下方隐藏内容不可见。宽度和高度都在 IHDR 中,修改后必须同步计算 CRC。
CRC 爆破宽高
import argparse
import itertools
import struct
import zlib
parser = argparse.ArgumentParser()
parser.add_argument("-f", required=True, help="PNG 文件路径")
args = parser.parse_args()
data = open(args.f, "rb").read()
stored_crc = int.from_bytes(data[29:33], "big")
if zlib.crc32(data[12:29]) == stored_crc:
print("宽高的 CRC 校验正常")
else:
for width, height in itertools.product(range(1, 4096), repeat=2):
ihdr = data[12:16] + struct.pack(">II", width, height) + data[24:29]
if zlib.crc32(ihdr) == stored_crc:
print(f"宽度: {width} (0x{width:x})")
print(f"高度: {height} (0x{height:x})")
break
也可以使用 010 Editor 或随波逐流工具直接查看、修改 PNG 的 IHDR。
LSB 隐写
常用工具:
- StegSolve
- CTF-All-In-One
- LovelymemLuxe
- zsteg工具:
zsteg lipstick.png --quiet用于检测,zsteg -E b1,bgr,msb,xy用于提取。

LSB 是把数据写入像素通道的最低有效位。可以先提取各通道最低位,再分别查看、组合或异或:
import matplotlib.pyplot as plt
# rgba 为形状为 (height, width, 4) 的 numpy 数组
channels = {name: rgba[:, :, index] & 1
for index, name in enumerate(("R", "G", "B", "A"))}
for left, right in (("R", "G"), ("R", "B"), ("G", "B")):
result = channels[left] ^ channels[right]
print(left, right, result.mean())
plt.imshow(result, cmap="gray")
plt.axis("off")
plt.show()
通道 LSB 两两异或结果的均值接近 0.5 只能作为粗略统计特征,不能单独证明存在隐写。
LovelymemLuxe

自定义工具-stegscan
- 第一层:单个位平面、常见通道排列、MSB/LSB、常见扫描方向。
- 第二层:任意位掩码,例如 R0/G2/B5,以及 bit/byte 反转。
- 第三层:抽样、跳步、偏移、XOR、压缩识别。
- 每个候选执行魔数搜索,并对 PNG/ZIP/GZIP/JPEG 等做完整格式校验和评分,避免随机 BM、PGP 等误报。
隐藏文件与倒写数据

from pathlib import Path
Path("png.png").write_bytes(Path("flag.jpg").read_bytes()[::-1])
元数据隐写
flag 直接藏在 JPEG 图片的 XMP 元数据中。
QR码
图片是QR码,可以通过扫码获取信息。
StegSolve通道检测
- 不同通道可能隐藏其它信息。
- 可以使用[随波逐流](E:\Tools\CTF-编码工具[随波逐流]CTF编码工具 V7.4_20260708.exe)工具
GIF
分帧
from pathlib import Path
from PIL import Image
source = Path(input("GIF 路径: ").strip().strip('"'))
output = Path("frames")
output.mkdir(exist_ok=True)
with Image.open(source) as image:
for index in range(image.n_frames):
image.seek(index)
image.convert("RGB").save(output / f"{index + 1}.png")
合并帧
使用 ImageMagick 从左到右拼接:
montage frames/*.png -tile x1 -geometry +0+0 merged.png
-tile x1 表示单列排列,-geometry +0+0 表示图片之间没有间距。
读取 GIF 属性
identify -format "%T %w %h %m\n" flag.gif
其中 %T 是帧延迟时间,%w 和 %h 分别是宽度和高度,%m 是图像格式/模式。GIF 还可以结合帧间差异、帧顺序和动画速度进行分析。
BMP 与 TIFF
BMP
BMP 隐写通常从文件头、像素数据和通道最低位入手,也可以使用 StegSolve、010 Editor 或 CTF 工具查看。
TIFF
先检查 TIFF 文件头和元数据。文件损坏时,可尝试 TIFF 在线修复工具,但在线上传文件存在隐私风险。
通用编码与文件操作
Base64 转图片
使用 CyberChef 的 From Base64 操作,输入时只保留 Base64 数据本身,不要包含前后的说明文字或 Markdown 标记。

stegpy
stegpy Do_you_like_misc.png -p
Steghide
支持 JPEG、BMP、WAV、AU 等格式:
# 查询
steghide info carrier.jpg
# 提取(省略 -p 时交互输入密码)
steghide extract -sf carrier.jpg -p 123456
# 写入
steghide embed -cf carrier.jpg -ef secret.txt -p 123456
密码为固定格式时,可以先生成掩码字典:
from string import ascii_lowercase, digits
for letter in ascii_lowercase:
for a in digits:
for b in digits:
for c in digits:
yield_password = f"U{letter}{a}{b}{c}Klsq"
print(yield_password)
其他工具
- Image Steganography - A.Tools
- SilentEye
stegdetect:检测部分 JPEG 隐写方法。stegbreak:针对 JSteg-Shell、JPHide、OutGuess 等进行字典攻击。wbStego4open:支持 BMP、TXT、HTM、PDF。
strings -n 5 filename 可只输出连续长度至少为 5 的可打印字符串。
二维码
扫描与预处理
CTF-All-In-One 支持多种二维码格式、受损二维码和批量扫描。二维码模糊时,可以先尝试:
- 放大、锐化、二值化、反色;
- 补齐静默区;
- 将切碎且大小不一致的图片先拼接,再进行归一化处理;
- 使用 Photoshop 的“文件 → 脚本 → 将文件载入堆栈”导入多张图片。


图片被切碎且尺寸不一致时,先将图片拼接,再做归一化处理:


结构速查
- 版本与边长:
N = 17 + 4V。 - 特征符:
7×7,外黑、中白、中心3×3黑块。 - 静默区:至少
4个模块宽。 - 版本 1 到 40 的边长为
21×21到177×177。 - 纠错等级:L 约 7%,M 约 15%,Q 约 25%,H 约 30%。
- 数据模式:数字
0001、字母数字0010、字节0100、ECI0111。
详细的二维码格式信息、ECI 和数据编码见 [[MISC/隐写/图片/QR二维码]]。
01 转换为二维码图像
from pathlib import Path
from PIL import Image
bits = Path("hint.txt").read_text(encoding="utf-8").replace("\r", "").replace("\n", "")
size = 25
if len(bits) != size * size or set(bits) - {"0", "1"}:
raise ValueError("hint.txt 必须包含 25×25 个 0/1")
image = Image.new("RGB", (size, size), "white")
for index, bit in enumerate(bits):
x, y = index % size, index // size
image.putpixel((x, y), (0, 0, 0) if bit == "1" else (255, 255, 255))
image.resize((size * 10, size * 10), Image.Resampling.NEAREST).save("flag.png")

浙公网安备 33010602011771号