AI摘要

文章针对CTF中Flag被多层Base64/Base32随机混合编码的难题,提出利用Base16、Base32、Base64字符集互斥的特性,通过依次尝试解码来还原Flag。核心策略是先试Base16排除,再试Base32,最后试Base64,并提供了相关Python脚本。

概述

CTF Crypto / MISC 中常见一类题目:将 Flag 用 Base64 / Base32 随机混合编码多层,要求选手解码还原。核心挑战在于——编码轮次未知、每轮用的是哪种 Base 也未知,需要"盲破"。

Flag 原文
    │
    ▼  随机选取 base64 或 base32
┌──────────┐
│ Round 1  │  → 编码后
└────┬─────┘
     │     ▼  再次随机选取
┌──────────┐
│ Round 2  │  → 编码后
└────┬─────┘
     │     ▼
     ...   重复 c 次 (c ∈ [20, 40])
     │
     ▼
 最终密文 (base2.txt)
编码总轮次 $c \in [20, 40]$,每轮 base64 或 base32 二选一。理论上总组合数为 $2^{20} \sim 2^{40}$,无法穷举——必须用"试探解码"策略。

Base 家族速查

三种编码对比

属性Base16 (Hex)Base32Base64
字符表大小163264
字符集0-9 A-FA-Z 2-7A-Z a-z 0-9 + /
每组输入1 字节5 字节3 字节
每组输出2 字符8 字符4 字符
膨胀率200%160%133%
= 填充有 (=)有 (=)
输出长度倍数2 的倍数8 的倍数4 的倍数

三种编码的互斥识别

最重要的特性:每种编码的合法输入字符集互不包含

Base16 合法字符: 0123456789ABCDEFabcdef
                 ↑ 不含 G-Z,不含 +/=

Base32 合法字符: ABCDEFGHIJKLMNOPQRSTUVWXYZ234567 =
                 ↑ 不含 0 1 8 9 + /

Base64 合法字符: A-Z a-z 0-9 + / =
                 ↑ 几乎包含所有可打印字符

利用这个互斥性:尝试 base16 解码 → 若报错 → 不是 base16 → 换 base32 → 若还报错 → 一定是 base64。

                    ┌─────────┐
                    │ 输入密文 │
                    └────┬────┘
                         ▼
              ┌─ try base16_decode ──┐
              │ 含 G/K/+ 等非法字符  │
              ▼ 报错                  ▼ 报错
         非 base16              ┌─ try base32_decode ──┐
                               │ 含 0/1/8/9/+ 等      │
                               ▼ 报错                  ▼ 报错
                          非 base32               try base64_decode
                                                         │
                                                         ▼
                                                   继续下一轮

编码脚本

import base64
import random

a = random.randint(0, 10)        # a ∈ [0, 10]
b = random.randint(20, 30)       # b ∈ [20, 30]
rounds = a + b                    # 总轮次 ∈ [20, 40]
print(f'编码轮次: {rounds}')

flag = b'flag{lalala}'
for _ in range(rounds):
    k = random.randint(0, 1)
    if k == 0:
        flag = base64.b64encode(flag)
    else:
        flag = base64.b32encode(flag)

with open('base2.txt', 'wb') as f:
    f.write(flag)
print('finish!')

关键参数:

变量范围说明
a0~10随机因子一
b20~30随机因子二
rounds = a+b20~40总编码轮次
k0 或 10 → base64; 1 → base32
编码路径总数$2^{rounds}$约$10^6 \sim 10^{12}$(不可穷举)

解密脚本

import base64

with open('base2.txt', 'rb') as f:
    data = f.read()

decoders = [base64.b16decode, base64.b32decode, base64.b64decode]

while True:
    for decoder in decoders:
        try:
            data = decoder(data)
            break
        except Exception:
            continue
    else:
        print('[错误] 全部解码尝试失败,密文可能已损坏')
        break

    try:
        text = data.decode('utf-8')
    except UnicodeDecodeError:
        continue

    if text.startswith('flag{') or text.startswith('flag'):
        print(text)
        break

试探顺序和原理:

顺序解码方式失败条件为什么会失败
1base16输入含G-Zg-z+/=编码从未使用 base16,密文中必然存在非 hex 字符
2base32输入含0189+/base32 仅含A-Z 2-7,base64 输出含上述非法字符
3base64几乎不会失败(若失败则密文损坏)base64 接受几乎所有字符
为什么不先试 base64?因为 base64 字符集包含 base32 和 base16 的全部字符——base64 总是"成功"解码,但输出是乱码。所以必须从最严格的(base16)到最宽松的(base64)依次尝试。

深层原理

为什么 Base16 放在第一位

编码脚本仅使用了 b64encodeb32encode,从未使用 b16encode。但解密脚本仍把 base16 放在第一位的原因:

base64 的输出一定包含不属于 base16 字符集的字符(如 g-z+/=),因此 base16 解码必然失败。它充当一个方向探测器——快速排除不可能的路径,自动 fallthrough 到 base32。
# Base16 非法字符触发的异常
base64.b16decode(b'abc')    # 成功 ('abc' 都是合法 hex)
base64.b16decode(b'abg')    # 报错: Non-base16 digit found

概率分析

项目
每轮选择 base64 的概率50%
每轮选择 base32 的概率50%
总轮次期望值$E[rounds]$$E[a]+E[b] = 5+25 = 30$
密文长度膨胀期望$1.33^{15} \times 1.6^{15} \approx 3.5 \times 10^4$ 倍
原始 flag 约 12 字节 → 最终密文约~420 KB

数据结构追踪

原始: "flag{lalala}"                    (12 字节)
    │
    ▼ b64encode
"ZmxhZ3tsYWxhbGF9"                    (16 字符)
    │
    ▼ b32encode
"KNUGK4TDMUQGK3RA..."                 (更长)
    │
    ▼ ... 重复 20~40 轮
    │
    ▼
最终密文: 数十万字符

CTF 应用

常见变体

编码组合解密顺序备注
b16 + b32 + b64b16 → b32 → b64最严格在前
b32 + b64b32 → b64跳过 b16 也可
b16 + b64b16 → b64b32 报错自动跳过
+ b85 (Ascii85)b16→b32→b85→b64在 decoders 列表中间插入base64.b85decode
+ URL-Safe Base64使用base64.urlsafe_b64decode字符集为A-Z a-z 0-9 - _ 不含 +/
+ 反转 + b32需额外判断密文反转后再解码

与 base64 隐写术的区别

混合编码(本文)Base64 隐写
目的混淆明文隐藏信息在= 填充位
操作多层 baseXX 编码修改填充位或等号前的 bit
解密逆序逐层尝试提取特定 bit 并重组
CTF 识别密文极长(多轮膨胀)密文长度正常,但可能含异常=

参考文献

  1. Python Software Foundation. base64 — Base16, Base32, Base64, Base85 Data Encodings [EB/OL]. https://docs.python.org/3/library/base64.html
  2. Josefsson S. RFC 4648 — The Base16, Base32, and Base64 Data Encodings [EB/OL]. https://datatracker.ietf.org/doc/html/rfc4648
  3. Wikipedia. Base64 [EB/OL]. https://en.wikipedia.org/wiki/Base64
投币支持一下吧
END