[ misc 知识点 ] 例题:换表 Base64
2026-08-22 20:05:38

Base64Encryption(换表 Base64 + 二维码藏密码 + AES-ZIP)

[info] 题目信息

  • 考点:换表 Base64、二维码方向判定、WinZip AES 加密
  • 来源:SHCTF Misc
  • 最终 flagSHCTF{fbf655a2-0661-4665-ac56-2331ca65e887}

一、题目给了什么

四个文件,其中三个是换表 Base64 密文,一个是已知明文:

文件 内容 作用
Readme.txt 明文 + 一串乱序表 b4CYzZ3RWg7p... 已知明文
Readme.txt.enc Readme 换表后的密文 用来验证恢复出的表是否正确
flag.zip.enc 一个 ZIP 的换表密文 最终目标
png.png.enc 一个 PNG 的换表密文 中间跳板(藏密码的二维码)

[warning] 关键提示
Readme 里那句「只要别人解不开,那就是加密?」是出题人的暗示:这不是加密,是换表 Base64。而且 Readme 里给的那张表 b4CYzZ3...烟雾弹,真正能解的表要靠自己反推。


二、核心概念:什么是「换表 Base64」

标准 Base64:把 3 字节(24 bit)拆成 4 个 6-bit 索引,用索引去一张固定字母表里查字符:

1
索引 0~63  →  标准表 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

「换表」就是把这 64 个字符重新排列,用新表来编码。它不是加密,只是混淆

换表 Base64 真正的加密
安全性来源 表本身(藏不住) 密钥 key
是否可逆 确定、无密钥 需要密钥
能否被已知明文攻破 一次就破 不能

[abstract] 柯克霍夫原则
密码系统的安全性只能依赖密钥的保密,不能依赖算法(或表)的保密。换表 Base64 恰好违反这条原则,所以「打乱表 ≠ 加密」。


三、解题流程总览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
flowchart TD
A[三个换表Base64密文] --> B[① 文件头 = 天然已知明文]
B --> C[② recover 反推换表 62/64]
C --> D[③ 爆破补全剩余 2 字符]
D --> E[得到完整换表 TABLE]
E --> F[④ dec 解码三个文件]
F --> G[flag.zip 真]
F --> H[png.png 真二维码]
H --> I[⑤ 旋转+镜像扫 → 密码]
I --> J[base64_15_n0t_3ncrypt10n]
G --> K[⑥ pyzipper AES 解压]
J --> K
K --> L[flag.txt.enc]
L --> M[⑦ 再 dec → SHCTF...]

四、Step 1:反推换表

4.1 文件头是「天然明文」

很多文件格式的固定字节是公开规定的,等于免费送你已知明文:

来源 明文(已知) 密文(从 .enc 抠出)
ZIP 文件头 50 4B 03(即 PK 7RA8
PNG 文件头 89 50 4E 47 0D 0A 1A 0A cZydiQ1tkpgKKKKNf7SR
PNG 尾 IEND 00 00 00 00 49 45 4E 44 ... KKKKKR0IWOfDYhrr
Readme 全文 明文已知 HHnaHgci...

[tip] 记住这些魔数

  • ZIP → PK50 4B
  • PNG → 89 50 4E 47 0D 0A 1A 0A
  • PNG 尾 IEND → 49 45 4E 44 + AE 42 60 82
  • JPEG → FF D8 FF、PDF → %PDF、GIF → GIF89a

4.2 recover 原理

用「已知明文 → 手动算 6-bit 索引 → 去密文里对齐取字符 → 填表」:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def recover(pts, enc='utf-8'):
tbl = [None] * 64
for p, c in pts:
if isinstance(p, str):
p = p.encode(enc)
idxs = []
for i in range(0, len(p), 3): # 手动算 base64 的 6-bit 索引
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
  • 明文每 3 字节 → 算出 4 个 6-bit 索引
  • 密文去掉 = 后逐字符和索引对齐
  • tbl[索引] = 密文字符,多组样本反复覆盖填充

结果恢复出 62/64,缺位置 3159

4.3 补全缺失字符

缺的位置 = 「所有已知明文里从没出现过的索引」,对应标准表里没用到的 Xz

因为标准表 64 字符的集合是固定的,已确定 62 个,剩下 Xz 只能落在空缺的 3159。只有 2! = 2 种排列,逐一试解码即可。

正确换表:

1
Kyr8RIkJwCtaTNdBYifW7Zo6UbmqPG9zpSc/O0hxg52jALDlQenV1+EM4HvX3Fsu

五、Step 2:解码三个文件

1
2
3
4
5
6
STD   = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
TABLE = "Kyr8RIkJwCtaTNdBYifW7Zo6UbmqPG9zpSc/O0hxg52jALDlQenV1+EM4HvX3Fsu"

def dec(cipher):
cipher = ''.join(cipher.split()) # 去空白/换行
return base64.b64decode(cipher.translate(str.maketrans(TABLE, STD)))
文件 解码结果
Readme.txt.enc 还原出 Readme 明文 ✓(验证表正确)
flag.zip.enc flag.zipPK 开头,AES 加密,内含 flag.txt.enc
png.png.enc png.png(二维码 PNG)

六、Step 3:二维码拿密码(有烟雾弹)

png.png 是二维码,直接扫得 There is no password here. —— 假的

[warning] 烟雾弹
出题人故意把二维码转了角度。正常扫描器会误判或识别出错误内容。正确做法是旋转 90°/180°/270° + 镜像穷举扫。

换角度后扫出真正的 ZIP 密码:

1
base64_15_n0t_3ncrypt10n

(读出来就是「base64 is not encryption」,再次点题。)


七、Step 4:AES-ZIP 解压(又一个坑)

flag.zip 的压缩方法是 99(WinZip AES),Python 标准库 zipfile 解不了,要用 pyzipper

1
2
3
4
import pyzipper
with pyzipper.AESZipFile('flag.zip') as z:
enc = z.read('flag.txt.enc', pwd=b'base64_15_n0t_3ncrypt10n')
# enc = '71S8ZRbXbhChN/7+UWwLT8UETf11N/U+aoI/NWULT/TVToNSN/Z0d8pMzY=='

flag.txt.enc 还是换表密文,再 dec() 一次:

1
SHCTF{fbf655a2-0661-4665-ac56-2331ca65e887}

八、完整脚本

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
53
54
55
56
57
import base64

STD = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
TABLE = "Kyr8RIkJwCtaTNdBYifW7Zo6UbmqPG9zpSc/O0hxg52jALDlQenV1+EM4HvX3Fsu"

def recover(pts, enc='utf-8'):
tbl = [None] * 64
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

def dec(cipher):
cipher = ''.join(cipher.split())
return base64.b64decode(cipher.translate(str.maketrans(TABLE, STD)))

# ---------- 已知明文对(文件头 = 天然明文) ----------
# 注意:Readme 明文需与原始 Readme.txt 完全一致(含换行符)
README_PT = "看我把Base64的字符表全都打乱了!只要别人解不开,那就是加密?\nb4CYzZ3RWg7pBuTyVmGrxaHhjtQMUqEno5XJscD/1d892vO+Pfk6NewlFLSKiI0A"
README_CT = "HHnaHgciHg2tYhIVbWU1HH2RHmE6HvnhvtkgHUogvUdFHghWHaheHa2kw9oB2Dchp9ow2sfvDDcxgsf4/9o3prV5p2B0AaJhhtu0c280j4Uu8Y5cNRNb90gV70GxNMyrGZiHZh+JPxSSfkS2GIINZ6IIqh3+oR5VU1YlToY4dWCEWnLYbhAEWhZMqRbT71L5fWyy"

pts = [
(README_PT, README_CT), # Readme 全文(最完整)
(b'\x50\x4b\x03', "7RA8"), # ZIP 头 PK
(b'\x89PNG\r\n\x1a\n', "cZydiQ1tkpg"), # PNG 魔数
]

tbl = recover(pts)
missing = [i for i, x in enumerate(tbl) if x is None]
print("恢复出的表:", ''.join('?' if x is None else x for x in tbl))
print("缺失位置:", missing) # 期望 [31, 59],即 'X' 和 'z' 的两种排列

# ---------- 补全缺失字符:标准表没覆盖到的字符,排列数 = n! ----------
for X, Z in [('X','z'), ('z','X')]:
tmp = tbl[:]
for pos, ch in zip(missing, [X, Z]):
tmp[pos] = ch
if ''.join(tmp) == TABLE:
print("补全成功,TABLE 正确")

# ---------- 解码 ----------
# 用 dec() 解码 flag.zip.enc / png.png.enc / Readme.txt.enc
# 二维码拿密码:旋转+镜像穷举扫 → base64_15_n0t_3ncrypt10n
# pyzipper 解压 flag.zip(压缩方法 99)→ 得到 flag.txt.enc
# 再 dec(flag.txt.enc) → SHCTF{...}

九、关键知识点总结

  1. 换表恢复 = 已知明文逐位对齐反推,recover 里 zip 对齐一旦冲突,说明某对明文猜错了。
  2. 文件头尾是天然明文PK、PNG magic、IEND 都是格式规定的固定字节。
  3. 缺字符补全:空位数量 = 标准表里「从未出现的字符」数,排列数 = n!,通常很小可爆破。
  4. 二维码方向判定:扫出奇怪内容先别信,试试旋转 + 镜像穷举。
  5. AES-ZIP(compress_type 99):标准 zipfile 解不了,用 pyzipper
  6. 烟雾弹:题面给的「表」、二维码直接扫出的「密码」,都可能不是真的,别轻信。
上一页
2026-08-22 20:05:38
下一页