图片隐写

笔者:ILOVCTRY

分析流程

拿到图片后,可以按下面顺序检查:

  1. 确认文件类型、文件头和文件尾,必要时修改扩展名。
  2. 查看文件大小、宽高、帧数、颜色模式和元数据。
  3. 用 strings、binwalk、foremost 等工具查找附加数据。
  4. 根据图片格式选择隐写分析方法:JPEG 重点看 EXIF、F5、JPHide;PNG 重点看宽高、LSB 和 CRC;GIF 重点看分帧。
  5. 最后再检查 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 系数,不能仅凭文件扩展名判断是否存在隐藏数据。

F5 工具

binwalk或foremost提取

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

CTF 编码工具

JPHide / JPHSwin

JPHSwin 是 JPHS(JPEG Hide and Seek)的 Windows 图形界面版本:

  1. 打开 JPEG 文件。
  2. 提取数据时选择 Seek,输入密码并指定输出路径。
  3. 写入数据时选择 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 文件结构

题目中若只修改了 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用于提取。

StegSolve

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

LovelymemLuxe

自定义工具-stegscan

  1. 第一层:单个位平面、常见通道排列、MSB/LSB、常见扫描方向。
  2. 第二层:任意位掩码,例如 R0/G2/B5,以及 bit/byte 反转。
  3. 第三层:抽样、跳步、偏移、XOR、压缩识别。
  4. 每个候选执行魔数搜索,并对 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 标记。

Base64 转图片

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、ECI 0111。

详细的二维码格式信息、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")
posted @ 2026-09-04 04:56  MillionMind  阅读(45)  评论(0)    收藏  举报