[ misc 知识点 ] 二维码
2026-08-22 20:05:38

一、二维码概述

1.1 常见二维条码类型

类型 特点
QR Code 最主流,三定位图形,支持纠错
Data Matrix https://jie.2weima.com/datamatrix.html
Aztec Codeimages.webp https://products.aspose.app/barcode/zh-hans/recognize/aztec#/recognized
PDF417Pasted image 20260813154322.png https://products.aspose.app/barcode/zh-hans/recognize/pdf417
MaxiCode 圆形,物流用
汉信码 国产标准

1.3 QR 码的核心特性

  • 容量大:最多约 7089 个数字 / 4296 个字母 / 2953 个字节(版本 40)
  • 容错强:最高可恢复约 30% 的污损
  • 全方向扫描:任意角度(0°/90°/180°/270°)都能识别
  • 黑白即可:不依赖颜色,便于印刷和隐写

二、QR 码结构详解

2.1 版本(Version)与模块(Module)

  • QR 码有 1~40 个版本,版本号越大,矩阵越大。
  • 版本 1 是 21×21 模块,之后每增加一个版本,边长增加 4 个模块。
  • 公式:边长 = 17 + 4 × 版本号
    • 版本 1 → 21×21
    • 版本 2 → 25×25
    • 版本 40 → 177×177
  • 模块(module) 是最小单位,即一个黑白方块。

2.2 功能区域(一张图看懂)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌──────────────────────────────────────┐
│◈◈◈◈◈◈◈ ········· ◈◈◈◈◈◈◈ │ ◈ = 定位图形(7×7)
│◈·····◈ ········· ◈·····◈ │ · = 静区/分隔符(白)
│◈·◈◈◈·◈ ········· ◈·◈◈◈·◈ │
│◈·◈◈◈·◈ ········· ◈·◈◈◈·◈ │ 左上 / 右上 / 左下
│◈·◈◈◈·◈ ········· ◈·◈◈◈·◈ │ 各有一个定位图形
│◈·····◈ ········· ◈·····◈ │
│◈◈◈◈◈◈◈ ········· ◈◈◈◈◈◈◈ │
│··································│
│ ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆ │ ◆ = 时序图形
│··································│ (黑) ◇ (白) 交替
│◈◈◈◈◈◈◈ ························│
│◈·····◈ ························│
│◈·◈◈◈·◈ ······ ▣ ················│ ▣ = 校正图形(5×5)
│◈·◈◈◈·◈ ······ ▣ ················│ (版本2以上才有)
│◈·◈◈◈·◈ ······ ▣ ················│
│◈·····◈ ························│
│◈◈◈◈◈◈◈························│
│ 数据区 + 纠错区(填满剩余空间) │
└──────────────────────────────────────┘

2.3 各功能区的作用

功能图形 数量/位置 作用
静区(Quiet Zone) 四周空白,≥4 模块 分隔二维码与背景,帮助扫码器定位
定位图形(Finder Pattern) 3 个(左上/右上/左下),7×7 回字形 判定方向和位置的核心
分隔符(Separator) 定位图形外围一圈白色 隔离定位图形和数据区
时序图形(Timing Pattern) 横向+纵向各一条,黑白交替 确定模块坐标和尺寸
校正图形(Alignment Pattern) 版本2+,多个,5×5 大版本纠偏(透视/弯曲)
格式信息(Format Info) 2 份(冗余存放) 记录纠错等级 + 掩码编号
版本信息(Version Info) 版本7+,2 份 记录版本号
数据区 + 纠错区 剩余全部空间 实际内容 + 纠错码

记忆口诀:三定位、两时序、格式成双、版本成对、数据纠错填满。


三、编码原理

3.1 数据编码模式(Mode)

QR 码支持多种编码模式,前缀用 4 bit 模式指示符区分:

模式 指示符 字符集 位/字符
数字(Numeric) 0001 0-9 3.33
字母数字(Alphanumeric) 0010 0-9 A-Z 空格 $%*+-./: 5.5
字节(Byte) 0100 ISO-8859-1 / UTF-8 8
汉字(Kanji) 1000 Shift JIS 13
混合(ECI/其他) 0111 等 - -

CTF 里二维码内容大多是 Byte 模式(UTF-8)字母数字模式

3.2 纠错等级与 Reed-Solomon 纠错

QR 码用 Reed-Solomon(里德-所罗门)纠错码,在伽罗瓦域 GF(256) 上做多项式运算。

纠错等级 字母 可恢复比例
Low L ≈7%
Medium M ≈15%
Quartile Q ≈25%
High H ≈30%

RS 纠错的意义(CTF 关键)

  • 纠错码冗余存储数据,所以二维码污损一部分仍能扫出
  • 这也意味着:你可以故意破坏/遮挡二维码的一部分,只要不超过纠错能力,内容不变——这是很多题的出法。

3.3 掩码(Mask)

编码时,为了避免出现「大面积同色块」导致扫码器误判(比如一片纯白/纯黑),数据区要和一个掩码图案做 XOR

  • 8 种掩码(编号 000~111),每种是固定的黑白图案公式。
  • 编码器会试全部 8 种掩码,选「得分最高」(黑白分布最均匀、无大块、无易误读图形)的那个。
  • 掩码编号记在格式信息里,解码时先读格式信息、再做对应掩码还原。

3.4 完整编码流程

1
2
3
4
5
6
7
1. 数据分析      → 选模式、版本、纠错等级
2. 数据编码 → 文本转成二进制位流(模式指示符 + 字符计数 + 数据 + 终止符)
3. 纠错编码 → 对数据码字做 RS 运算,生成纠错码字
4. 分块组装 → 按版本规则把数据码字 + 纠错码字分成若干块交错排列
5. 布置模块 → 在矩阵里放置功能图形,再按蛇形顺序填数据
6. 掩码处理 → 试 8 种掩码,选最优,对数据区 XOR
7. 写入格式/版本信息

3.5 解码流程(逆向理解)

1
2
3
4
5
6
7
8
9
10
拍照/读图
→ 灰度化 + 二值化(阈值分割黑白)
→ 找 3 个定位图形
→ 判断方向(转正) + 透视校正
→ 读格式信息(纠错等级、掩码)
→ 读版本信息(版本7+)
→ 按掩码还原数据区
→ 按蛇形顺序读码字
→ RS 纠错(修复误读)
→ 还原位流 → 解析出内容

四、方向判定机制

4.1 为什么二维码旋转不影响扫描

三个定位图形(Finder Pattern)分布在左上、右上、左下右下角故意留空

1
2
3
4
5
6
7
┌──────┬──────┐
│ ◉ │ ◉ │ ◉ = 定位图形(7×7 回字形)
│ │ │
├──────┴──────┤
│ ◉ │ ← 右下角没有定位图形(留空)
│ │
└────────────┘

扫码器流程:

  1. 找到这 3 个「回」字形图案
  2. 判断哪个角是空的 → 唯一确定方向
  3. 旋转/镜像纠正
  4. 再解码

所以 0°/90°/180°/270° 旋转都不影响结果,这是 QR 码设计的核心特性。

4.2 定位图形「回」字形的巧妙设计

回字形( 形状:3×3 黑块 + 外围白圈 + 最外黑圈)有特殊性质:

  • 无论从哪个方向看,黑白比例都是 1:1:3:1:1(沿任意穿过中心的水平线扫描)
  • 这个比例在自然图像中几乎不出现 → 可以被快速、唯一地检测出来

4.3 利用方向判定的 CTF trick

标准二维码旋转不影响内容 → 如果一道题「换个角度扫出不同内容」,说明二维码被破坏了方向判定(镜像 / 定位图形被遮挡 / 多码叠放),导致扫码器「转正」出错,同一个图案被读成不同的数据。详见 [[#5.4 隐藏信息的各种手法]] 与 [[#八综合案例换表-base64–二维码]]。


五、CTF 中的二维码

5.1 考点分类总览

考点 说明 难度
二维码损坏/遮挡 涂掉一部分,需手动修复 ★★
反色 黑白互换,直接扫不出
旋转 / 镜像 方向处理后再扫
拼接复原 二维码被切成碎片 ★★~★★★
隐写(颜色/灰度/LSB) 信息藏在像素里 ★★★
多码叠放 多个二维码叠加,需分离 ★★★
二维码内容加密 扫出来是密文(换表 base64 等) ★★★
二维码藏密码 密码藏在二维码里,供后续解压 ★★

5.2 基础处理:让扫不出的码能扫

① 反色(黑白互换)

1
2
3
from PIL import Image, ImageOps
img = Image.open('qr.png').convert('L') # 灰度
ImageOps.invert(img).save('qr_invert.png') # 反色

② 旋转 / 镜像穷举

1
2
3
4
5
6
7
8
9
10
11
from PIL import Image
img = Image.open('qr.png')
for name, im in [
("原图", img),
("转90", img.rotate(-90, expand=True)),
("转180", img.rotate(180, expand=True)),
("转270", img.rotate(90, expand=True)),
("水平镜像", img.transpose(Image.FLIP_LEFT_RIGHT)),
("垂直镜像", img.transpose(Image.FLIP_TOP_BOTTOM)),
]:
im.save(f'out_{name}.png')

③ 补全定位图形(手动修复)

如果定位图形被涂掉,扫码器找不到方向就扫不出。做法:

  • 用 PS / GIMP / 画图,照着另外两个完整的定位图形,把缺失的画回去
  • 或者用脚本复制一个完好的定位图形贴过去

5.3 常见工具

工具 用途
pyzbar / zxing(Python) 脚本扫码
ZBar(C 库,pyzbar 底层) 扫码核心
zbarimg(命令行) 直接扫图片
CyberChef 在线各种编码/图像处理
Stegsolve 通道分离(CTF 必用)
Photoshop / GIMP 手动修复、通道操作
微信/支付宝扫码 快速验证(注意:会缓存、可能不认镜像/反色)
草料二维码 / 在线生成器 生成对照二维码

5.4 隐藏信息的各种手法

① RGB 通道分离

二维码的「黑」不是纯黑,而是某个通道(R/G/B)有信息。用 Stegsolve 或脚本分离:

1
2
3
4
from PIL import Image
img = Image.open('qr.png').convert('RGB')
for ch, name in [(0,'R'), (1,'G'), (2,'B')]:
img.getchannel(ch).save(f'{name}.png') # 每个通道单独存成灰度图再扫

② 灰度分层 / 多阈值

图片看起来是二维码,但用了多个灰度级,不同阈值下是不同的码:

1
2
3
4
from PIL import Image
img = Image.open('qr.png').convert('L')
for t in range(0, 256, 16):
img.point(lambda p: 255 if p > t else 0).save(f'th_{t}.png')

③ LSB 隐写

二维码的黑白方块里,LSB(最低有效位)藏了信息。用 Stegsolve 的 LSB 模式,或脚本提取最低位:

1
2
3
4
5
from PIL import Image
img = Image.open('qr.png').convert('RGB')
px = img.load()
w, h = img.size
# 提取每个像素 R 通道的最低位,拼成新的二值图

④ 多码叠放 / 错位

两个二维码(内容不同)以不同角度、透明度或颜色叠在一起。分离方法:

  • 通道分离(两个码用不同颜色)
  • 旋转分离(一个正、一个斜)
  • 透明度/图层分离

⑤ 数据区篡改

直接改二维码的数据区(在纠错范围内),让扫出的内容变化。解题需还原原数据或用纠错恢复。

5.5 二维码内容加密

扫出来的二维码内容不是明文 flag,而是密文,需要进一步解码。最常见的结合:

  • 换表 Base64(打乱 Base64 字符表)→ 见 [[#八综合案例换表-base64–二维码]]
  • 凯撒 / ROT13 / 摩斯 / 栅栏等古典密码
  • AES / RSA 等现代密码(配合密码学考点)

六、工具清单(速查)

6.1 扫码

1
2
3
4
5
# 命令行 zbarimg(需安装 zbar)
zbarimg qr.png

# Python pyzbar
pip install pyzbar pillow
1
2
3
4
import pyzbar.pyzbar as pyzbar
from PIL import Image
for d in pyzbar.decode(Image.open('qr.png')):
print(d.type, d.data.decode('utf-8', errors='replace'))

6.2 生成二维码(对照 / 出题用)

1
pip install qrcode[pil]
1
2
import qrcode
qrcode.make("flag{test}").save("test.png")

6.3 图像处理

1
pip install pillow opencv-python

七、Python 实战脚本

7.1 一键穷举「旋转 + 镜像 + 反色」并扫码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from PIL import Image, ImageOps
import pyzbar.pyzbar as pyzbar

def scan_all(path):
img = Image.open(path).convert('L')
variants = {
"原图": img,
"反色": ImageOps.invert(img),
"转90": img.rotate(-90, expand=True),
"转180": img.rotate(180, expand=True),
"转270": img.rotate(90, expand=True),
"水平镜像": img.transpose(Image.FLIP_LEFT_RIGHT),
"垂直镜像": img.transpose(Image.FLIP_TOP_BOTTOM),
}
for name, im in variants.items():
res = pyzbar.decode(im)
if res:
for r in res:
print(f"[{name}] {r.data.decode('utf-8', errors='replace')}")

scan_all("qr.png")

7.2 通道分离 + 多阈值 + 扫码(组合拳)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from PIL import Image, ImageOps
import pyzbar.pyzbar as pyzbar

img = Image.open('qr.png').convert('RGB')

# 1) RGB 三通道
for ch, name in [(0,'R'), (1,'G'), (2,'B')]:
gray = img.getchannel(ch)
for t in range(0, 256, 32):
bw = gray.point(lambda p: 255 if p > t else 0)
for d in pyzbar.decode(bw):
print(f"[{name}通道 阈值{t}]", d.data.decode('utf-8', errors='replace'))

# 2) 灰度 + 多阈值
gray = img.convert('L')
for t in range(0, 256, 32):
bw = gray.point(lambda p: 255 if p > t else 0)
for d in pyzbar.decode(bw):
print(f"[灰度 阈值{t}]", d.data.decode('utf-8', errors='replace'))

7.3 换表 Base64 解码(二维码内容解密常用)

1
2
3
4
5
import base64
STD = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

def decode(cipher, table):
return base64.b64decode(cipher.translate(str.maketrans(table, STD)))

八、综合案例:换表 Base64 + 二维码

典型真题(Base64Encryption,出题人 pjx1314)的完整流程,融合了「换表 Base64」和「二维码藏密码」两个考点。

8.1 题目结构

  • 附件 1:cipher.txt —— 一个 ZIP 文件的换表 Base64 密文
  • 附件 2:README.txt —— 明文已知,提供恢复换表的已知明文

8.2 解题全流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
① 识别换表 Base64(标准解不开 + 题目明示"字符表打乱")

② 收集已知明文↔密文对(ZIP 头 PK、PNG 头尾、README 全文)

③ 恢复换表(已知明文 → 标准 base64 → 逐位对齐反推表)

④ 补全缺失字符(爆破排列)

⑤ 用完整表解码 cipher.txt → 得到 ZIP

⑥ 解压 ZIP → 得到二维码 PNG

⑦ 扫二维码(旋转/镜像穷举)→ 得到 ZIP 密码

⑧ 用密码解压 → flag

8.3 关键步骤代码

恢复换表(已知明文攻击)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def recover(pts, enc='utf-8'):
tbl = [None] * 64
std = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
for p, c in pts:
if isinstance(p, str):
p = p.encode(enc)
idxs = []
for i in range(0, len(p), 3):
ch = p[i:i+3]
v = ch[0] << 16
if len(ch) > 1: v |= ch[1] << 8
if len(ch) > 2: v |= ch[2]
idxs += [(v>>18)&0x3F, (v>>12)&0x3F]
if len(ch) > 1: idxs.append((v>>6)&0x3F)
if len(ch) > 2: idxs.append(v & 0x3F)
cc = [x for x in c if x != '=']
for i, ch in zip(idxs, cc):
tbl[i] = ch
return tbl

已知明文数据(文件头/尾是天然明文)

1
2
3
4
5
6
7
8
d = [
(b"\x50\x4b\x03", "7RA8"), # ZIP 头 PK\x03
(b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44",
"cZydiQ1tkpgKKKKNf7SR"), # PNG 头
(b"\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82",
"KKKKKR0IWOfDYhrr"), # PNG 尾 IEND
("README 全文...", "对应密文..."),
]

二维码扫描(旋转/镜像穷举) 用 [[#71-一键穷举旋转–镜像–反色并扫码]] 即可。

8.4 本题考点小结

  1. 换表 Base64:已知明文(文件头尾)恢复换表,缺位爆破补全。
  2. 二维码藏密码:ZIP 有密码,密码藏在二维码里,需要「换个角度」扫。
  3. 二维码「换个角度」的原理:破坏/利用 QR 码的方向判定(3 定位图形 + 缺角),使同一个图案在不同方向被读成不同数据。

九、二维码解题思路速查

9.1 常见 trick 速记

# 思路 说明
1 BMP 转二维码 BMP 是未压缩位图,像素直接对应黑白,可逐像素重建成二维码图再扫
2 十六进制转 pyc 给一串 hex,还原成 .pyc,用 uncompyle6 / pycdc 反编译,找生成二维码的源码
3 字符串制作二维码 给一串 0/1(或字符),长度是平方数,reshape 成方阵画成二维码
4 四个 TTL 值拼一个字节 网络包 TTL 字段编码二进制,提取特征位组合成字节

9.2 字符串(0/1)制作二维码

核心:二维码是方形,0/1 串长度必须是平方数(n²),才能 reshape 成 n×n 方阵。制作前先手动把长度补成平方数(补 0 或截断)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import numpy as np
from PIL import Image
import math

def str_to_qr(s, out='qr.png'):
# 1. 长度修正为平方数(向上取整,末尾补 0)
n = math.ceil(math.sqrt(len(s)))
s = s.ljust(n * n, '0')
# 2. reshape 成 n×n 方阵
arr = np.array([int(c) for c in s]).reshape(n, n)
# 3. 转黑白图(1=黑 或 1=白,两种都试)
img = Image.fromarray((arr * 255).astype(np.uint8), mode='L')
img = img.resize((n*20, n*20), Image.NEAREST) # 放大便于扫描
img.save(out)

# 例:一个长度 81(=9²)的 0/1 串
str_to_qr("11010101...") # 长度自动补齐到平方数

如果扫不出,把黑白色调转(1 - arr)再扫一次。

9.3 BMP 转二维码

1
2
3
4
5
6
7
8
9
from PIL import Image
import numpy as np

def bmp_to_qr(path, out='qr.png'):
img = Image.open(path).convert('L') # 灰度
arr = np.array(img)
# 二值化:阈值可调
bw = (arr > 128).astype(np.uint8) * 255
Image.fromarray(bw, mode='L').save(out)

9.4 十六进制转 pyc

1
2
3
4
5
6
# 1. hex 还原成文件
xxd -r -p hex.txt > out.pyc # 或 Python: bytes.fromhex(hexstr)

# 2. 反编译
pip install uncompyle6
uncompyle6 out.pyc > source.py # 或 pycdc

十、其他类型二维码

平时见得最多的是 QR Code,但实际还有很多其他类型的二维码。CTF 中偶尔出现,特征是「定位图形」和 QR Code 完全不同,扫码前要认清类型、选对工具。

10.1 类型总览

类型 定位特征 扫描工具
Aztec Code 中心「牛眼」定位(同心方块),无静区 ZXing
DataMatrix 左下 + 右上两条「L」形实线边界 + 两条虚线边界 ZXing
GridMatrix 网格矩阵(国产 GB/T 21049) 专用工具
MaxiCode 圆形,中央 3 个同心圆,六边形模块 ZXing
DotCode 点阵组成,无实线边界 ZXing
汉信码 类似 QR,但定位块方向有特殊性 专用工具
PDF417 堆叠式一维码(多条一维码叠起来) ZXing / pyzbar

10.2 各类型注意点

  • 汉信码左下角那块定位块的方向和另外几块不一样,有时候题目会把这块定位块反过来,导致扫不出,需要手动把它转回来。
  • PDF417:出题人可能把图片反相(黑白互换)导致无法直接扫描。解决:把图拉进 StegSolve 或 PS 反相处理后再扫。
1
2
3
from PIL import Image, ImageOps
img = Image.open('pdf417.png').convert('L')
ImageOps.invert(img).save('pdf417_invert.png') # 反相后再扫

10.3 扫描工具选择

工具 支持类型
pyzbar(Python) 主要 QR,部分 PDF417
ZXing(Java / zxing-cpp) QR、DataMatrix、Aztec、PDF417、MaxiCode、DotCode 等,最全
zbar QR、Code128 等一维 + 部分二维码
微信/支付宝 仅主流 QR

遇到非 QR 的码,优先用 ZXing(在线版 zxing.org,或本地 zxing-cppZXingReader)。


十一、信号与图像处理中的变换方法(FFT / DFT / DCT / DWT)

CTF Misc 图像隐写高频考点。核心:把图像从空间域变换到频域,在频域里藏/看信息。

11.1 为什么需要变换:空间域 → 频域

  • 空间域 = 像素值随位置变化;频域 = 把图像看成不同频率成分的叠加
  • 低频 = 大块、平缓变化的区域(天空、背景、大面积纯色)→ 图像的整体轮廓
  • 高频 = 急剧变化的区域(边缘、纹理、细节、噪声)→ 图像的细节
  • FFT 把「整体」和「细节」拆开,CTF 就常在这两个层面藏信息

11.2 DFT 离散傅里叶变换(Discrete Fourier Transform)

一维公式(N 个采样点):

1
X[k] = Σ_{n=0}^{N-1} x[n] · e^(-j·2π·k·n / N)      (k = 0..N-1)
  • x[n]:时域信号(输入)
  • X[k]:频域结果(输出,复数)
  • |X[k]|幅度谱(各频率的强度),∠X[k]相位谱

图像是二维信号,用二维 DFT:

1
F(u,v) = Σ_x Σ_y f(x,y) · e^(-j·2π·(ux/M + vy/N))
  • f(x,y):像素值
  • F(u,v):频域复数
  • |F(u,v)|:幅度谱,通常取 log 显示(因为动态范围太大)

频谱图怎么看

  • 经过 fftshift(低频移到中心)后,中心 = 低频四周 = 高频
  • 中心很亮(低频能量大),四周暗(高频能量小)

11.3 FFT 快速傅里叶变换(Fast Fourier Transform)

FFT 只是 DFT 的快速算法,数学结果完全等价,不是另一种变换。

  • 直接算 DFT 是 O(N²);FFT 用「分治 + 蝶形运算」降到 O(N log N)
  • 工程上所有「傅里叶变换」实际都用 FFT 实现(NumPy 的 np.fft、OpenCV 的 cv2.dft 底层都是 FFT)

记忆:DFT 是「做什么」,FFT 是「怎么快速做」。CTF 里写代码直接用 np.fft.fft2

11.4 DCT 离散余弦变换(Discrete Cosine Transform)

与 DFT 的区别:DCT 只用余弦分量(丢掉正弦/相位),结果是实数,而且有能量集中特性——大部分信息集中在少数低频系数上。

为什么重要JPEG 压缩的核心就是 DCT

  1. 图像分 8×8 小块
  2. 每块做 DCT → 64 个系数(左上角 = 直流/低频,右下角 = 高频)
  3. 量化:高频系数除以较大步长 → 很多变成 0(丢掉细节,实现压缩)
  4. 熵编码存储

CTF 关联:JPEG 的 DCT 系数上可以做隐写(改动高频系数藏数据,肉眼几乎看不出),经典工具 JSteg、F5、OutGuess

11.5 DWT 离散小波变换(Discrete Wavelet Transform)

傅里叶的局限:只能告诉你「有哪些频率」,不能告诉你「某个频率出现在哪里」(丢失位置信息)。
小波的优势同时定位频率和位置,多分辨率分析。

做法:一层层分解,每层把图像分成 4 个子带:

1
2
3
4
5
6
7
┌────────┬────────┐
│ LL │ HL │ LL = 低频近似(整体轮廓)
│ (近似) │ (水平细节)│ HL = 水平方向细节
├────────┼────────┤ LH = 垂直方向细节
│ LH │ HH │ HH = 对角方向细节
│(垂直细节)│ (对角细节)│
└────────┴────────┘
  • LL 继续下一层分解(递归),得到多级分解
  • JPEG2000 的核心就是 DWT(比 JPEG 的 DCT 压缩率更高、无块效应)

11.6 四者对比

变换 输出 核心用途 CTF 典型应用
DFT 复数(幅度+相位) 频率分析 频谱隐写、滤波分离
FFT 同 DFT(更快) 工程实现 同上
DCT 实数(能量集中) JPEG 压缩 JPEG 隐写(JSteg/F5/OutGuess)
DWT 多级子带(时频定位) JPEG2000 压缩 小波域隐写

11.7 CTF 应用与代码

① FFT 频谱隐写(最常见)

题目给一张图,肉眼看着正常,但信息藏在频域里。做法:做 FFT → 看幅度谱 → 频谱图里可能藏有文字/二维码/flag。

1
2
3
4
5
6
7
8
9
10
import numpy as np
import cv2

img = cv2.imread('flag.png', 0) # 灰度读入
f = np.fft.fft2(img) # 2D FFT
fshift = np.fft.fftshift(f) # 低频移到中心
mag = 20 * np.log(np.abs(fshift) + 1) # 对数幅度谱(必须取 log,否则一片黑)
mag = (mag - mag.min()) / (mag.max() - mag.min()) * 255
cv2.imwrite('spectrum.png', mag.astype(np.uint8))
# 打开 spectrum.png 看,或对 spectrum.png 再做后续处理(如扫二维码)

如果频谱图里看到了文字/二维码,说明出题人在频域里埋了信息,逆变换回空间域前把它提取出来即可。

② 高通 / 低通滤波分离

  • 低通:保留中心低频 → 得到模糊的轮廓图(大块信息)
  • 高通:保留四周高频 → 得到边缘、细节、隐藏的噪点信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import numpy as np, cv2

img = cv2.imread('a.png', 0)
f = np.fft.fft2(img)
fshift = np.fft.fftshift(f)
rows, cols = img.shape
crow, ccol = rows // 2, cols // 2

r = 30
mask_low = np.zeros((rows, cols), np.uint8)
mask_low[crow-r:crow+r, ccol-r:ccol+r] = 1 # 低通掩码(保留中心)

# 低通结果
low = np.fft.ifft2(np.fft.ifftshift(fshift * mask_low))
cv2.imwrite('low.png', np.abs(low).astype(np.uint8))
# 高通结果(1 - 低通)
high = np.fft.ifft2(np.fft.ifftshift(fshift * (1 - mask_low)))
cv2.imwrite('high.png', np.abs(high).astype(np.uint8))

③ DCT 隐写(JPEG)

1
2
3
4
5
6
7
8
from scipy.fftpack import dct, idct
import numpy as np, cv2

img = cv2.imread('a.jpg', 0).astype(float)
# 8×8 块 DCT
block = img[:8, :8]
d = dct(dct(block.T, norm='ortho').T, norm='ortho')
# 看 d 的系数分布:左上角大(低频),右下角小(高频)

更实用的做法是直接用现成工具:steghide(DCT 隐写)、OutGuessJstegF5。题目如果是 JPEG 隐写,先试 steghide extract -sf a.jpg

④ DWT 隐写

1
2
3
4
5
6
import pywt, cv2, numpy as np

img = cv2.imread('a.png', 0).astype(float)
coeffs = pywt.dwt2(img, 'haar') # 一层小波分解
cA, (cH, cV, cD) = coeffs # LL, HL, LH, HH
cv2.imwrite('LL.png', cA.astype(np.uint8)) # 低频近似,常藏信息

小波隐写题常把信息藏在 HH(对角高频)子带,或在 LL 子带里继续分解。

⑤ 常用 Python 库

1
pip install numpy opencv-python scipy PyWavelets
作用
numpy.fft FFT/DFT
scipy.fftpack FFT、DCT
cv2(OpenCV) dft、图像读写
PyWaveletspywt DWT

十二、例题与参考链接

12.1 例题

题目 来源 考点
qrssssssss 2022 NCTF QR 码标准 / 修复
LmqHmAsk 2023 羊城杯决赛 二维码隐写
QRcode2 GEEKCTF 2024 QR 码标准
练假成真 SHCTF 2024 week2 二维码伪造/修复

12.2 参考链接

关于 QR Code 标准,能出的考点极多,有时间建议直接读官方标准(上面的 GitHub 仓库存了一份)。


附:速记口诀

  • 二维码结构:三定位、两时序、格式成双、版本成对、数据纠错填满
  • 方向判定:三个定位缺一角,缺哪角定方向
  • 扫不出先试:反色 → 旋转 → 镜像 → 补定位
  • 扫出来是密文:换表 Base64 → 古典密码 → 现代密码
  • 藏信息:通道分离 → 灰度分层 → LSB → 多码叠放
上一页
2026-08-22 20:05:38
下一页