JPEG 隐写术¶
对于JPEG图像来说,隐写术主要集中在DCT(离散余弦变换)系数上。由于JPEG压缩的特殊性,在DCT系数中嵌入信息既能保证较大的容量,又能保持良好的隐蔽性。
JPEG压缩基础¶
JPEG压缩流程¶
JPEG图像压缩是有损压缩,其核心步骤如下:
原始图像
↓
颜色空间转换 (RGB → YCbCr)
↓
分块处理 (8×8像素块)
↓
DCT变换 (离散余弦变换)
↓
量化 (使用量化表)
↓
熵编码 (霍夫曼编码/算术编码)
↓
JPEG文件
DCT变换原理¶
离散余弦变换(Discrete Cosine Transform) 将空间域的图像数据转换到频率域。
对于8×8的图像块,DCT变换将像素值转换为频率系数:
空间域 (8×8像素块): 频率域 (DCT系数):
┌─────────────────┐ ┌─────────────────┐
│ 52 55 61 66 ... │ │1260 -1 -12 -5│ ← DC系数(左上角)
│ 56 57 64 65 ... │ DCT │ -23 -17 -6 -3│
│ 62 59 68 64 ... │ → │ -11 -9 -2 2│
│ 68 65 70 65 ... │ │ -7 -2 0 1│
│ ... ... ... ... │ │ ... ... ... ... │
└─────────────────┘ └─────────────────┘
低频 ↖ 高频 ↘
关键概念:
- DC系数:左上角第一个系数,表示整个8×8块的平均亮度
- AC系数:其余63个系数,表示频率分量
- 低频系数:左上角区域,对视觉影响大
- 高频系数:右下角区域,对视觉影响小,适合隐写
量化过程¶
量化是JPEG压缩的有损步骤,将DCT系数除以量化表中的值并四舍五入:
重要性:隐写通常在量化后的DCT系数中进行,因为这些是最终存储在JPEG文件中的数据。
Zig-Zag扫描顺序¶
JPEG使用Zig-Zag顺序扫描DCT系数(从低频到高频):
扫描顺序:
0→ 1 5 6 14 15 27 28
↓ ↗ ↓ ↗ ↓ ↗ ↓
2 4 7 13 16 26 29
↗ ↗ ↗ ↗ ↗
3 8 12 17 25 30 39
↓ ↗ ↓ ↗ ↓ ↗ ↓
9 11 18 24 31 38 40
...
DCT系数隐写原理¶
为什么选择DCT系数隐写?¶
- 不可见性好:修改高频系数对视觉影响极小
- 抗压缩:DCT系数是JPEG的本质数据,重新保存不会破坏
- 容量大:每个8×8块有63个AC系数可用
- 难检测:与JPEG压缩噪声混合
基本隐写策略¶
LSB替换策略¶
系数选择策略¶
JSteg隐写¶
JSteg 是最早的JPEG隐写算法之一(1997年),采用简单的LSB替换。
原理¶
在量化后的非零AC DCT系数的最低有效位(LSB)中嵌入数据。图片中的DCT系数经过量化后,会产生大量的整数值,JSteg利用这些整数值的最低位来隐藏信息。
例如,DCT系数为35(二进制:100011),如果要嵌入bit 0,则将其改为34(二进制:100010)。由于只修改了最低位,对图像质量影响极小。
嵌入流程¶
1. 读取JPEG文件,获取量化后的DCT系数
2. 按Zig-Zag顺序遍历所有8×8块的AC系数
3. 对于每个非零系数:
- 如果系数 = 0 或 ±1,跳过
- 否则,将系数的LSB替换为消息bit
4. 重新编码JPEG文件
出题脚本示例:
from jsteg import JSteg
# 嵌入
jsteg = JSteg()
jsteg.embed('cover.jpg', 'secret.txt', 'stego.jpg')
# 提取
jsteg.extract('stego.jpg', 'output.txt')
容量计算¶
理论容量 = (可用AC系数数量) × 1 bit
实际例子:
- 图像尺寸: 512×512像素
- 8×8块数量: (512/8) × (512/8) = 4096块
- 每块可用AC系数: 约30-40个(假设35个)
- 总容量: 4096 × 35 = 143,360 bits ≈ 17.5 KB
检测方法¶
卡方(Chi-Square)攻击¶
JSteg的致命弱点:LSB替换会改变DCT系数的分布。在正常的JPEG图像中,相邻的DCT系数对(如2和3,4和5)出现的频率是不同的。但经过LSB替换后,这些系数对的频率会趋于相等,这就是统计异常。
# 卡方检测原理
# 检测相邻系数对的频率分布异常
def chi_square_attack(dct_coeffs):
# 统计 (2k, 2k+1) 系数对的出现频率
# 如果有隐写,这些对的频率会趋于相等
pairs = {}
for coeff in dct_coeffs:
if coeff % 2 == 0:
key = (coeff, coeff+1)
pairs[key] = pairs.get(key, 0) + 1
# 计算卡方值
chi_square = 0
total = sum(pairs.values())
expected = total / len(pairs)
for observed in pairs.values():
chi_square += (observed - expected)**2 / expected
return chi_square > threshold # 高值表示可能有隐写
优缺点¶
优点:
- 实现简单
- 嵌入速度快
- 容量较大
缺点:
- 容易被统计检测(卡方攻击)
- 修改所有可用系数,留下明显痕迹
- 没有加密保护
解题¶
# 使用StegDetect检测
stegdetect image.jpg
# 输出: image.jpg : jsteg(***)
# 使用JSteg提取
jsteg extract stego.jpg output.txt
# 或使用现代工具
jsteg-modern -x -i stego.jpg -o output.txt
F5隐写¶
F5算法(2001年,Andreas Westfeld)是对JSteg的重大改进,解决了LSB替换的统计检测问题。
原理¶
F5算法通过三大创新技术实现了更安全的隐写:
- 矩阵编码(Matrix Encoding):不是每个bit都修改一个系数,而是通过数学编码减少修改次数
- 置换嵌入(Permutative Straddling):使用密钥对DCT系数进行伪随机置换
- 收缩修正(Shrinkage):当系数被修改为0时,该系数被跳过
矩阵编码¶
F5使用(1,n,k)矩阵编码,其中n=AC系数数量,k=嵌入bit数。
工作原理:
不是每个bit都需要修改一个系数,而是:
- 读取n个系数
- 计算它们的LSB的XOR值
- 如果XOR结果不等于要嵌入的bit,只修改1个系数
示例(1,3,1):
系数: [5, 7, 12] → LSB: [1, 1, 0] → XOR: 1⊕1⊕0 = 0
要嵌入: 1
因为 0 ≠ 1,修改一个系数:
[5, 7, 11] → LSB: [1, 1, 1] → XOR: 1⊕1⊕1 = 1
效率提升:
- 传统LSB:嵌入1 bit需要修改1个系数
- F5矩阵编码:平均修改率 = ½
置换嵌入¶
为了抵抗顺序分析,F5在嵌入前对DCT系数进行伪随机置换。
# 使用密钥生成伪随机序列
def permute_coefficients(coeffs, key):
random.seed(key)
indices = list(range(len(coeffs)))
random.shuffle(indices)
return [coeffs[i] for i in indices]
收缩修正¶
F5的独特之处:当系数通过减1变为0时,该系数被"收缩"掉,不再使用。
嵌入流程¶
1. 提取所有非零AC DCT系数
2. 使用密钥对系数进行伪随机置换
3. 将消息比特分组(根据矩阵编码参数)
4. 对每组:
a. 计算当前系数组的LSB哈希值
b. 如果哈希值 = 要嵌入的值,不修改
c. 否则,递减一个系数的绝对值
d. 如果系数变为0,收缩并跳过
5. 逆置换,重新编码JPEG
出题脚本示例:
# 使用F5嵌入
import subprocess
# 嵌入数据
subprocess.run([
'java', '-jar', 'f5.jar',
'e', # embed模式
'-e', 'secret.txt', # 要嵌入的文件
'-p', 'password123', # 密码
'cover.jpg', # 原始图片
'stego.jpg' # 输出图片
])
# 提取数据
subprocess.run([
'java', '-jar', 'f5.jar',
'x', # extract模式
'-p', 'password123', # 密码
'-x', 'output.txt', # 输出文件
'stego.jpg' # 隐写图片
])
容量与嵌入率¶
嵌入率 = 修改系数数 / 总可用系数数
F5的嵌入率:
- 理论: 1/2 (矩阵编码)
- 实际: 约0.3-0.4 (考虑收缩)
对比JSteg:
- JSteg: 1.0 (每个系数都可能被修改)
- F5: 0.3-0.4 (更隐蔽)
优缺点¶
优点:
- 抗统计检测(卡方攻击无效)
- 嵌入效率高
- 安全性好(密钥保护)
缺点:
- 算法复杂
- 提取需要知道密钥
- 容量相对较小(收缩导致)
检测方法¶
F5抗卡方攻击,但仍可通过以下方法检测:
直方图攻击¶
分析DCT系数直方图的对称性,F5的矩阵编码会破坏某些统计规律。
校准攻击(Calibration Attack)¶
解题¶
# 下载F5.jar
wget https://code.google.com/archive/p/f5-steganography/downloads/f5.jar
# 检测F5隐写
stegdetect -s 10.0 image.jpg
# 输出: image.jpg : f5(***)
# 提取数据(需要密码)
java -jar f5.jar x -p password123 stego.jpg -x output.txt
# 如果不知道密码,可以尝试爆破
# 创建密码字典 passwords.txt
java -jar f5.jar x -p password1 stego.jpg -x test.txt
java -jar f5.jar x -p password2 stego.jpg -x test.txt
# ... 循环尝试
OutGuess隐写¶
OutGuess(由Niels Provos开发)是一种统计保持型隐写算法,重点在于维持统计特性。
原理¶
OutGuess的核心思想是将DCT系数分为两组:一组用于嵌入数据,另一组用于统计补偿。这样即使嵌入了数据,整体的统计特性仍然接近原始图像。
传统的LSB替换会改变系数的分布,例如系数5的频率会增加,系数4的频率会减少。OutGuess通过调整未使用的系数来补偿这种变化,使得系数5和系数4的总体频率保持不变。
两阶段嵌入¶
OutGuess将DCT系数分为两组:
嵌入流程¶
1. 提取所有可用DCT系数
2. 使用伪随机数生成器(PRNG)选择系数子集A
3. 在子集A中嵌入数据(LSB替换)
4. 计算嵌入后的统计变化(直方图)
5. 使用剩余系数子集B来补偿统计变化
- 调整B中的系数,使整体统计特性接近原始
6. 输出隐写图像
详细过程:
def outguess_embed(dct_coeffs, message, key):
# 1. PRNG选择嵌入位置
random.seed(key)
total_coeffs = len(dct_coeffs)
embed_positions = random.sample(range(total_coeffs), len(message))
# 2. 计算原始直方图
original_hist = build_histogram(dct_coeffs)
# 3. 在选定位置嵌入
for i, bit in enumerate(message):
pos = embed_positions[i]
if bit == 1:
dct_coeffs[pos] |= 1
else:
dct_coeffs[pos] &= ~1
# 4. 计算嵌入后直方图
embedded_hist = build_histogram(dct_coeffs)
# 5. 统计校正
correction_positions = [i for i in range(total_coeffs)
if i not in embed_positions]
for coeff_value in range(-256, 256):
diff = original_hist[coeff_value] - embedded_hist[coeff_value]
# 调整未使用的系数来补偿差异
while diff != 0 and correction_positions:
pos = correction_positions.pop()
if diff > 0:
# 需要增加此值的频率
dct_coeffs[pos] = coeff_value
diff -= 1
return dct_coeffs
统计保持机制¶
示例:
假设某DCT系数值5的频率:
原始: 100次
嵌入后: 105次 (由于LSB替换)
OutGuess补偿:
- 在未使用的系数中,找5个系数
- 将它们从其他值改为5
- 结果: 系数5的总频率仍然是100
容量分析¶
OutGuess容量 = JSteg容量 × 嵌入率
嵌入率通常设为 50%:
- 50% 系数用于数据嵌入
- 50% 系数用于统计校正
实际容量:
- 512×512图像
- JSteg容量: ~17 KB
- OutGuess容量: ~8.5 KB (50%嵌入率)
优缺点¶
优点:
- 统计隐蔽性好
- 能抵抗卡方攻击
- 直方图接近原始
缺点:
- 容量减半(相比JSteg)
- 仍可能被先进的随机性测试检测
- 校正过程可能引入新的异常
检测方法¶
RS分析(Regular-Singular Analysis)¶
检测LSB嵌入引入的统计失真,即使经过统计补偿。
二次样本对分析(SPA)¶
分析相邻系数对的关系,OutGuess的校正可能破坏这种关系。
解题¶
# 安装OutGuess
sudo apt-get install outguess
# 嵌入数据
outguess -k "password" -d secret.txt cover.jpg stego.jpg
# 提取数据
outguess -r -k "password" stego.jpg output.txt
# 不带密钥提取(如果没有使用密钥)
outguess -r stego.jpg output.txt
# 检测OutGuess隐写
stegdetect image.jpg
# 输出: image.jpg : outguess(***)
Steghide隐写¶
Steghide 是一个通用的隐写工具,支持JPEG、BMP、WAV、AU等多种格式。对于JPEG,它使用了改进的图论匹配算法。
原理¶
Steghide不是简单的LSB替换,而是使用了一种基于图论的嵌入算法。它的核心思想是将嵌入问题转化为图匹配问题。
图论匹配¶
Steghide将DCT系数看作图的顶点,将可以互换的系数对连接成边。然后通过寻找完美匹配来嵌入数据,使得修改次数最少。
例如,系数52和53可以通过修改LSB互相转换,它们就构成一条边。要嵌入bit 1,如果当前是52(LSB=0),就改为53;如果已经是53(LSB=1),就不修改。
加密与压缩¶
Steghide在嵌入前会对数据进行:
- 压缩:使用zlib压缩数据,减少嵌入大小
- 加密:使用密码对数据进行加密(默认使用Rijndael,即AES的前身)
- 打乱:使用伪随机序列确定嵌入位置
嵌入流程¶
特征¶
- 需要密码:没有密码无法提取数据
- 自动压缩:数据会被自动压缩
- 自适应嵌入:根据图像特性选择最佳嵌入位置
- 校验和:嵌入的数据包含校验和,确保提取正确性
优缺点¶
优点:
- 安全性高(密码保护+加密)
- 嵌入质量好(图论优化)
- 自动压缩增加容量
- 使用广泛,工具成熟
缺点:
- 必须知道密码才能提取
- 算法较复杂
- 对于大文件,可能容量不足
解题¶
# 安装Steghide
sudo apt-get install steghide
# 查看隐写信息(不需要密码)
steghide info image.jpg
# 输出会显示是否包含嵌入数据
# 提取数据(需要密码)
steghide extract -sf image.jpg
# 会提示输入密码,然后提取数据
# 指定输出文件
steghide extract -sf image.jpg -xf output.txt
# 尝试空密码
steghide extract -sf image.jpg -p ""
# 爆破密码(使用stegcracker)
sudo apt-get install stegcracker
stegcracker image.jpg wordlist.txt
# 会自动尝试字典中的所有密码
# 常见密码尝试
steghide extract -sf image.jpg -p "123456"
steghide extract -sf image.jpg -p "password"
steghide extract -sf image.jpg -p "admin"
出题脚本示例:
# 嵌入文件
steghide embed -cf cover.jpg -ef secret.txt -p "mypassword" -sf stego.jpg
# 参数说明:
# -cf: cover file(载体图片)
# -ef: embed file(要嵌入的文件)
# -p: passphrase(密码)
# -sf: stego file(输出的隐写图片)
检测工具¶
StegDetect¶
StegDetect 是一个自动检测JPEG隐写的工具,可以识别多种隐写算法。
安装¶
使用¶
# 基本检测
stegdetect image.jpg
# 输出示例:
# image.jpg : jsteg(***) # 检测到JSteg
# image.jpg : f5(***) # 检测到F5
# image.jpg : outguess(***) # 检测到OutGuess
# image.jpg : negative # 没有检测到隐写
# 敏感度调节(-s参数,范围0-10,默认1)
stegdetect -s 5.0 image.jpg
# 数值越大,越敏感,但误报也越多
# 批量检测
stegdetect *.jpg
# 详细输出
stegdetect -n -s 2.0 image.jpg
检测原理¶
StegDetect使用统计分析方法:
- JSteg检测:卡方检验
- F5检测:字节对分析
- OutGuess检测:RS分析
- JPHide检测:特征匹配
StegBreak¶
StegBreak 是StegDetect的配套工具,用于暴力破解密码。
其他检测工具¶
JPHide检测器¶
专门检测JPHide/JPSeek算法。
# 使用StegDetect
stegdetect -tjopi image.jpg
# t: 检测类型
# j: jsteg
# o: outguess
# p: jphide
# i: invisible secrets
StegExpose¶
基于机器学习的隐写检测工具。
总结¶
JPEG DCT系数隐写是CTF中常见的题型,主要算法包括:
| 算法 | 原理 | 特点 | 检测方法 | 提取工具 |
|---|---|---|---|---|
| JSteg | LSB替换 | 简单,容量大 | 卡方攻击 | jsteg |
| F5 | 矩阵编码+收缩 | 抗统计检测 | 校准攻击 | f5.jar |
| OutGuess | 统计补偿 | 保持直方图 | RS分析 | outguess |
| Steghide | 图论匹配 | 密码保护 | 密码爆破 | steghide |
解题思路¶
1. 使用file和exiftool查看基本信息
2. 使用StegDetect检测隐写算法
stegdetect -s 5.0 image.jpg
3. 根据检测结果选择对应工具:
- jsteg: jsteg extract
- f5: java -jar f5.jar x
- outguess: outguess -r
- 未知: steghide extract(尝试空密码)
4. 如果需要密码:
- 查看题目提示
- 尝试常见密码
- 使用字典爆破
5. 提取后检查文件类型:
file output.bin
6. 如果是压缩包,尝试解压