python

图片隐写术检测与提取

2026-07-06 #渗透测试#CTF#红队攻防#隐写术

图片隐写术检测与提取

本文仅供合法授权的安全研究与学习使用,请勿用于非法用途。读者应确保行为符合当地法律法规。

前言

在网络安全攻防对抗中,信息隐藏(Steganography)是一种将秘密数据嵌入到普通载体中的技术。与加密不同,隐写术的目标是隐藏”通信存在”这一事实本身。图片隐写术是最常见的隐写方式之一——攻击者可以将C2指令、flag、后门代码等敏感信息嵌入到一张看似普通的图片中,通过社交平台、邮件附件等渠道传递,绕过流量监控和安全审计。

在红队演练和CTF竞赛中,图片隐写术更是高频考点。一张”黑客海报”可能隐藏着下一阶段的攻击指令,一个GitHub头像背后可能暗藏密钥。本文基于真实的Python项目源码,深入讲解图片隐写术的检测与提取技术,涵盖EXIF数据分析、LSB隐写检测、二进制尾部追加检测以及多层级编码解密链的完整剖析。

技术背景

1. 图片隐写术的常见手法

图片隐写术主要有以下几种实现方式:

  • LSB(Least Significant Bit)隐写:将秘密信息嵌入到像素值的最低有效位中,人眼几乎无法察觉变化。
  • 文件尾部追加:在图片文件的结束标记之后追加额外数据。JPEG格式的结束标记为FF D9,PNG的结束标记为IEND。大多数图片查看器会忽略结束标记后的数据。
  • EXIF数据隐藏:利用EXIF元数据字段(如相机型号、拍摄时间等)存储隐藏信息。
  • 文件拼接:将一个完整文件(如ZIP压缩包)附加在图片文件末尾,实现”图片+压缩包”的双重载体。

2. 关键技术点

  • PIL/Pillow库:用于读取图片EXIF数据和进行图像处理。
  • stegano库:一个Python隐写术库,支持LSB隐写的嵌入与提取。
  • 十六进制分析:通过分析文件的二进制内容,定位文件结束标记并提取追加数据。
  • 魔数(Magic Number)识别:通过文件开头的特定字节判断文件真实类型。
  • 多层编码链:实际攻击中,隐写数据往往经过多层编码(十六进制 → UTF-8 → ROT13 → 索引提取),需要逐层解密。

实现思路

本项目采用多维度检测策略,从三个层面分析图片中可能存在的隐写信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌─────────────────────────────────────┐
│ 图片隐写检测架构 │
├─────────────────────────────────────┤
│ 1. EXIF元数据分析 │
│ └─ 提取相机信息、GPS、注释等 │
├─────────────────────────────────────┤
│ 2. LSB隐写检测 │
│ └─ 使用stegano库提取LSB隐藏消息 │
├─────────────────────────────────────┤
│ 3. 二进制尾部数据分析 │
│ ├─ 查找FF D9结束标记 │
│ ├─ 提取追加的十六进制数据 │
│ └─ 通用脚本:魔数识别+文件提取 │
├─────────────────────────────────────┤
│ 4. 多层解密链 │
│ └─ Hex → UTF-8 → ROT13 → 索引 │
└─────────────────────────────────────┘

整体流程是:先用基础脚本对图片进行三维度扫描(EXIF、LSB、二进制尾部),发现隐藏数据后再用解密脚本进行多层级解码,最终还原出原始信息。

核心代码解析

1. 基础检测脚本——三维度扫描

以下是项目的核心检测脚本,集成了EXIF分析、LSB检测和二进制尾部分析三大功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from PIL import Image
from PIL.ExifTags import TAGS
from stegano import lsb


def get_exif_data(image_path):
"""
获取图片的EXIF数据
:param image_path: 图片路径
:return: EXIF数据字典或None
"""
with Image.open(image_path) as img:
exif_data = img._getexif()
if exif_data:
# 将EXIF标签ID映射为可读的标签名称
exif = {TAGS.get(tag): value for tag, value in exif_data.items()}
return exif
else:
return None


def check_hidden_message(image_path):
"""
检查图片中隐藏的消息(LSB隐写检测)
:param image_path: 图片路径
"""
try:
# stegano的lsb.reveal会尝试从像素最低位提取隐藏文本
secret_message = lsb.reveal(image_path)
print("发现隐藏消息:", secret_message)
except IndexError as e:
# 如果图片没有经过LSB隐写处理,会抛出IndexError
print(f"Error: {e}. No hidden message detected.")


def analyze_binary_data(image_path):
"""
分析图片的二进制数据,查找文件结束标记后的追加内容
:param image_path: 图片路径
"""
with open(image_path, 'rb') as file:
binary_data = file.read()

# 将二进制数据转换为十六进制字符串
data_hex = binary_data.hex()

# 查找JPEG文件结束标记 FF D9
for i in range(len(data_hex)):
if data_hex[i:i+4] == 'ffd9':
print(f"找到 FF D9 在第 {i} 个字节。")
# 打印结束标记之后的所有数据(即追加的隐藏内容)
print(f"后面的内容: {data_hex[i+4:]}")

代码讲解

  • get_exif_data:通过Image._getexif()获取EXIF元数据,TAGS字典将数字标签映射为可读名称(如0x010FMake)。攻击者常在ImageDescriptionUserComment字段中藏匿信息。
  • check_hidden_message:调用stegano.lsb.reveal()自动提取LSB隐写数据。如果图片未经过LSB处理,会抛出IndexError
  • analyze_binary_data:这是检测文件尾部追加的关键。JPEG文件以FF D9结尾,任何出现在该标记之后的数据都是可疑的追加内容。

2. 通用文件类型检测脚本

上面的脚本仅针对JPEG格式。通用脚本通过魔数字典支持多种文件格式的检测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import os

def get_file_type_by_hex(file_path):
"""
根据文件的十六进制开头判断文件类型,并检查文件结尾后面是否有多余的文件十六进制开头或文本。
如果有多余的信息,尝试判断这些信息是否是一个文件,并根据多余信息的十六进制开头另存为另一个文件。
"""
# 常见文件类型的魔数和结尾标记
magic_numbers = {
b'\x89PNG\r\n\x1a\n': ('PNG', b'IEND\xaeB`\x82'),
b'\xff\xd8\xff': ('JPEG', b'\xff\xd9'),
b'GIF87a': ('GIF', b'\x3b'),
b'GIF89a': ('GIF', b'\x3b'),
b'\x42\x4d': ('BMP', None),
b'\x25\x50\x44\x46': ('PDF', b'%%EOF'),
b'\x1f\x8b': ('GZIP', None),
b'\x50\x4b\x03\x04': ('ZIP', b'\x50\x4b\x05\x06'), # End of central directory record
b'\x7fELF': ('ELF', None),
b'\xca\xfe\xba\xbe': ('JAVA CLASS', None),
}

# 读取整个文件的二进制内容
with open(file_path, 'rb') as file:
file_content = file.read()

# 判断文件类型
for magic, (fileType, footer_magic) in magic_numbers.items():
if file_content.startswith(magic):
if footer_magic:
# 查找文件结尾标记
footer_index = file_content.find(footer_magic)
if footer_index != -1:
# 提取结束标记之后的所有内容
extra_content = file_content[footer_index + len(footer_magic):]
if extra_content:
# 检查多余的内容是否是另一个已知文件
for extra_magic, (extra_file_type, _) in magic_numbers.items():
if extra_content.startswith(extra_magic):
# 如果是已知文件,自动保存为独立文件
extra_file_path = f"{os.path.splitext(file_path)[0]}_extra.{extra_file_type.lower()}"
with open(extra_file_path, 'wb') as extra_file:
extra_file.write(extra_content)
print(f"多余内容是文件:{extra_file_path}")
return fileType
# 如果不是已知文件类型,打印十六进制内容
print(f"多余内容不是已知文件:\n{extra_content.hex()}")
return fileType

return "未知"

代码讲解

这个通用脚本的精妙之处在于:它不仅检测文件尾部是否有追加数据,还能自动判断追加数据本身是否是一个独立文件(通过检查追加内容的魔数)。这在实际对抗中非常有用——攻击者经常将恶意脚本、压缩包等文件附加在图片后面,利用copy /b image.jpg + payload.zip combined.jpg等命令制作”复合文件”。

3. 多层解密链——真实CTF场景还原

在实际的红队对抗和CTF竞赛中,隐写数据的提取往往不是一步到位的。以下是一个真实场景的多层解密脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import codecs
from stegano import lsb

# Rot13 解密函数
def rot13_decrypt(text):
"""
使用Rot13算法解密字符串。
ROT13是将字母表旋转13位的简单替换密码。
"""
return codecs.decode(text, 'rot_13')

# 从图片二进制尾部提取的十六进制隐藏信息
隐藏的信息 = "0a0a677572206e61666a72652079767266207661206775722061687a6f7265662e204f6867206c626820756e6972206762207068672071626a6120677572206a62627166206775726c206f722072657162662e0a3232203731203634203438203636203730203736203438203638203730203836203332203934203837203130302039322032322037392037382037302038382038362038322033362036362035362037302037352034362036340a"

# 第一步:十六进制 → 字节流
隐藏的信息十进制 = bytes.fromhex(隐藏的信息)

# 第二步:字节流 → UTF-8字符串
隐藏的信息十进制字符串 = 隐藏的信息十进制.decode('utf-8')
隐藏的信息十进制字符串 = 隐藏的信息十进制字符串.replace('\n', '')

# 第三步:以"."分割字符串
隐藏的信息十进制列表 = 隐藏的信息十进制字符串.split('.')

# 第四步:对前两段进行ROT13解密
print(rot13_decrypt(隐藏的信息十进制列表[0])) # 解密提示文本
print(rot13_decrypt(隐藏的信息十进制列表[1])) # 解密提示文本
print(f"隐藏的信息十进制列表[2]:{隐藏的信息十进制列表[2]}") # 第三段是数字索引

解密链分析

这段代码展示了一个完整的四层解密链:

  1. 十六进制解码:图片FF D9后的追加数据是一段十六进制字符串,先用bytes.fromhex()转换为字节流。
  2. UTF-8解码:字节流实际是UTF-8编码的文本,解码后得到三段以.分隔的内容。
  3. ROT13解密:前两段经过ROT13加密,解密后得到一段英文提示语,暗示”答案就在眼前”。
  4. 索引提取:第三段是一串数字索引,用于从提示文本中按位置提取字符,拼出最终flag。

此外,项目还包含一个使用steghide工具提取隐写内容的辅助脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"""
使用kali的steghide工具提取
提取命令:steghide extract -sf hack.jpg
"""
ext_Infor = "Perhaps you see this and wonder what the fuck is going on..."
index = ["0", "4", "6", "16", "29", "72", "78", "99", "161", "155", "157", "181", "/", "163", "144", "104", "217", "3", "227", "182", "104"]

for msg in index:
if msg == '/':
print("/", end='')
else:
# 按索引从提示文本中提取字符,拼出最终答案
print(ext_Infor[int(msg)], end='')
print()

这个脚本的逻辑是:steghide提取出的隐写内容是一串数字索引,这些索引指向另一段文本中的特定字符位置,按顺序提取即可拼出隐藏的flag。

使用方法与运行效果

环境准备

1
2
3
pip install Pillow stegano
# 如果需要使用steghide(Kali Linux自带):
apt install steghide

运行基础检测脚本

1
python 图片隐写术信息检测提取.py

运行后会依次输出:

  1. EXIF数据(如果存在)
  2. LSB隐藏消息(如果图片经过LSB隐写)
  3. 十六进制表示和FF D9后的追加内容

运行通用检测脚本

1
python 图片隐写术信息检测提取通用脚本.py

该脚本会自动识别文件类型,检测文件尾部追加内容,并尝试将追加的文件自动保存。

典型运行输出

1
2
3
4
5
6
7
8
9
10
11
12
正在检查EXIF数据...
EXIF数据:
Make: Apple
Model: iPhone 13 Pro
Software: 17.1.1

正在使用stegano检查隐藏消息...
发现隐藏消息: hidden_flag_here

正在分析二进制数据...
找到 FF D9 在第 12345 个字节。
后面的内容: 0a0a677572206e61666a7265207976...

防御对策

从蓝队角度,针对图片隐写术的防御可以从以下几方面入手:

  1. 流量层面的隐写检测:部署DPI(深度包检测)设备,对传输的图片文件进行隐写分析。重点关注异常大的图片文件和文件尾部追加数据。

  2. 文件完整性校验:对关键系统中的图片资源进行哈希校验,检测是否被篡改或追加了额外数据。检查JPEG文件FF D9标记后是否有残余数据。

  3. EXIF数据清洗:在图片上传和分享环节,自动清除EXIF元数据,防止敏感信息(GPS位置、设备信息等)泄露。许多平台已实现这一功能。

  4. LSB隐写检测:使用统计分析方法(如RS分析、卡方攻击)检测图片是否经过LSB隐写处理。正常图片的LSB分布具有随机性,隐写后会呈现异常的统计特征。

  5. 终端管控:限制终端设备上steghide、stegano等隐写工具的使用,监控异常的图片处理行为。

  6. 邮件与社交平台审计:对邮件附件和社交平台分享的图片进行隐写扫描,阻断通过图片载体传递C2指令或数据外泄的通道。

总结

本文基于真实项目源码,系统地讲解了图片隐写术的检测与提取技术。从EXIF元数据分析到LSB隐写检测,再到二进制尾部追加数据提取和多层解密链还原,覆盖了图片隐写术的主要技术方向。

关键要点:

  • 图片隐写检测需要多维度并行扫描,单一方法容易遗漏。
  • 文件尾部追加是最简单但也最常用的隐写手法,通过检查FF D9/IEND等结束标记即可快速发现。
  • 实际对抗中,隐写数据往往经过多层编码加密,需要耐心逐层解码。
  • 通用检测脚本通过魔数字典可以支持多种文件格式的自动识别和隐藏文件提取。

在红队渗透中,图片隐写术不仅可以用于隐藏攻击指令和数据外泄,也是CTF竞赛中的常见题型。掌握这些技术,有助于攻防双方在信息隐藏这一维度上展开更深入的对抗。

评论
分享