AI摘要
概述
培根密码(Baconian Cipher / Bacon's cipher)由英国哲学家弗朗西斯·培根(Francis Bacon)于 1605 年在《学术的进展》中提出。与凯撒、仿射等替换密码不同,它的本质是一种隐写术(steganography):秘密不在于字母被替换成了什么,而在于信息被藏进了载体文本的"外在形式"——两种字体、两种大小写、两种符号……只要能区分出两种状态,就能承载信息。
每个明文字母被编码成长度为 5 的 A/B 序列(因为 $2^5 = 32 \ge 26$,5 位足以覆盖 26 个字母)。在 CTF 中,培根密码常以"密文只含两种符号、且有效长度为 5 的倍数"这一特征出现。
编码: 字母 → 5 位 A/B 序列 (2^5 = 32 ≥ 26)
隐写: A/B → 载体文本的两种"形态" (小写/大写、正体/斜体、两种符号…)
解码: 载体形态 → A/B 序列 → 每 5 位一组 → 查表还原字母
两套码表:
26 字母版 每个字母独立(现代 / CTF 常用,等价 5-bit 二进制)
24 字母版 I=J、U=V 合并(培根原版,1605)编码原理
隐写而非替换:为什么叫"双字母密码"
培根本人称之为 biliteral cipher(双字母密码)。"双字母"指整套系统只用两个基本符号(记作 a 和 b)就能表达全部字母表——这正是二进制编码思想的雏形,比莱布尼茨系统化二进制(1703)早了近一个世纪。
它与普通替换密码的根本差异,不在算法强度,而在"秘密藏在哪里":
| 维度 | 替换密码(凯撒 / 仿射) | 培根密码 |
|---|---|---|
| 秘密藏在 | 内容:字母被换成别的字母 | 形式:字母的字体 / 大小写 / 符号 |
| 密文外观 | 一串明显的乱码字母 | 一段"看似正常"的文本 |
| 安全依赖 | 敌人不知道密钥 | 敌人没意识到存在密文 |
| 学科归属 | 密码学 Cryptography | 隐写术 Steganography |
信息论视角:编码一个英文字母的理论下限是 $\log_2 26 \approx 4.70$ bit,而至少需要 $\lceil 4.70 \rceil = 5$ 位($2^4=16 < 26 \le 32 = 2^5$)才能一一区分,所以培根固定用 5 位。代价是每位只承载 1 bit——每个载体符号仅携带 1 bit,编码 1 个字母需要 5 个载体符号,载体文本因此膨胀 5 倍。这也是隐写容量的核心约束:要藏 $n$ 个字母,载体至少要有 $5n$ 个可区分单元。
培根密码的精妙不在替换本身(替换极其简单),而在于它把 0/1 悄悄"焊"进了正常文本的排版里。理解了这一点,就理解了从零宽字符到图片 LSB 的一整条现代隐写脉络。
编码对照表
26 字母版(现代 / CTF 常用)——本质就是"字母序号的 5 位二进制",a=0、b=1:
| 字母 | 序号 | 二进制 | Bacon 码 | 字母 | 序号 | 二进制 | Bacon 码 |
|---|---|---|---|---|---|---|---|
| A | 0 | 00000 | aaaaa | N | 13 | 01101 | abbab |
| B | 1 | 00001 | aaaab | O | 14 | 01110 | abbba |
| C | 2 | 00010 | aaaba | P | 15 | 01111 | abbbb |
| D | 3 | 00011 | aaabb | Q | 16 | 10000 | baaaa |
| E | 4 | 00100 | aabaa | R | 17 | 10001 | baaab |
| F | 5 | 00101 | aabab | S | 18 | 10010 | baaba |
| G | 6 | 00110 | aabba | T | 19 | 10011 | baabb |
| H | 7 | 00111 | aabbb | U | 20 | 10100 | babaa |
| I | 8 | 01000 | abaaa | V | 21 | 10101 | babab |
| J | 9 | 01001 | abaab | W | 22 | 10110 | babba |
| K | 10 | 01010 | ababa | X | 23 | 10111 | babbb |
| L | 11 | 01011 | ababb | Y | 24 | 11000 | bbaaa |
| M | 12 | 01100 | abbaa | Z | 25 | 11001 | bbaab |
24 字母版(培根 1605 原版,I/J、U/V 各占同一码):
| 字母 | 码 | 字母 | 码 | 字母 | 码 | 字母 | 码 |
|---|---|---|---|---|---|---|---|
| A | aaaaa | G | aabba | N | abbaa | T | baaba |
| B | aaaab | H | aabbb | O | abbab | U/V | baabb |
| C | aaaba | I/J | abaaa | P | abbba | W | babaa |
| D | aaabb | K | abaab | Q | abbbb | X | babab |
| E | aabaa | L | ababa | R | baaaa | Y | babba |
| F | aabab | M | ababb | S | baaab | Z | babbb |
关键差异:24 版从 I/J 合并处起,后续每个字母的码值都比 26 版"错位"。例如 26 版K=ababa,24 版K=abaab。CTF 解题时若一版解出乱码,务必换另一版再试。
编码 / 解码示意
① 明文编码(26 版,把 BACON 转成 A/B 序列):
明文 B A C O N
序号 1 0 2 14 13
↓ ↓ ↓ ↓ ↓
二进制 00001 00000 00010 01110 01101
↓ ↓ ↓ ↓ ↓
Bacon aaaab aaaaa aaaba abbba abbab
拼接: aaaab aaaaa aaaba abbba abbab
= aaaabaaaaaaaabaabbbaabbab (25 位 = 5×5)② 隐写解码(大小写载体,小写=a、大写=b)——这是培根密码的"杀手锏",密文伪装成一个普通单词:
载体 c r Y P T o G r a m ← "crYPToGram" 看着只是大小写怪异
大小写 ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
小 小 大 大 大 小 大 小 小 小
A/B a a b b b a b a a a
└──── aabbb ────┘└──── abaaa ────┘
分组: aabbb abaaa
查表: H I
明文: "HI"Python 完整实现
编码 / 解码类
同时支持 26 版与 24 版;解码时对无法识别的 5 位组返回 ?,便于定位噪声。
class BaconCipher:
"""培根密码编解码器,支持 24 / 26 两种码表"""
# 24 字母版:I=J、U=V 合并(培根 1605 原版)
TABLE_24 = {
'A': 'aaaaa', 'B': 'aaaab', 'C': 'aaaba', 'D': 'aaabb', 'E': 'aabaa',
'F': 'aabab', 'G': 'aabba', 'H': 'aabbb', 'I': 'abaaa', 'J': 'abaaa',
'K': 'abaab', 'L': 'ababa', 'M': 'ababb', 'N': 'abbaa', 'O': 'abbab',
'P': 'abbba', 'Q': 'abbbb', 'R': 'baaaa', 'S': 'baaab', 'T': 'baaba',
'U': 'baabb', 'V': 'baabb', 'W': 'babaa', 'X': 'babab', 'Y': 'babba',
'Z': 'babbb',
}
def __init__(self, version: int = 26):
if version not in (24, 26):
raise ValueError('version 只能是 24 或 26')
self.version = version
if version == 26:
# 26 版 = 字母序号的 5 位二进制,0→a、1→b
self.enc_table = {
chr(ord('A') + i): format(i, '05b').replace('0', 'a').replace('1', 'b')
for i in range(26)
}
else:
self.enc_table = dict(self.TABLE_24)
# 解码表:24 版中 abaaa/baabb 一码对应 I/J、U/V,取首个字母
self.dec_table = {}
for ch, code in self.enc_table.items():
self.dec_table.setdefault(code, ch)
def encode(self, text: str) -> str:
"""明文 → a/b 序列(忽略所有非字母字符)"""
return ''.join(self.enc_table[ch] for ch in text.upper() if ch.isalpha())
def decode(self, seq: str) -> str:
"""a/b 序列 → 明文(每 5 位一组,多余位丢弃)"""
seq = seq.lower()
groups = [seq[i:i + 5] for i in range(0, len(seq) - len(seq) % 5, 5)]
return ''.join(self.dec_table.get(g, '?') for g in groups)
# 使用示例
b = BaconCipher(version=26)
s = b.encode('BACON')
print(s) # aaaabaaaaaaaabaabbbaabbab
print(b.decode(s)) # BACON隐写:把密文藏进大小写
培根的原意是用两种字型(正体/斜体)承载 a/b;在纯文本环境里,最常见的等价载体是大小写。
def hide_in_case(secret: str, carrier: str, version: int = 26,
a_is_lower: bool = True) -> str:
"""
把 secret 编码为 a/b 序列后,藏进 carrier 的大小写中。
a_is_lower=True 表示 'a'→小写、'b'→大写。
carrier 的字母数必须 ≥ 5 × secret 的有效字母数。
"""
bits = BaconCipher(version).encode(secret)
letters = [c for c in carrier if c.isalpha()]
if len(letters) < len(bits):
raise ValueError(f'载体字母不足:需 {len(bits)} 个,仅有 {len(letters)} 个')
out, bi = [], 0
for ch in carrier:
if ch.isalpha() and bi < len(bits):
want_lower = (bits[bi] == 'a') == a_is_lower
out.append(ch.lower() if want_lower else ch.upper())
bi += 1
else:
out.append(ch)
return ''.join(out)
def reveal_from_case(stego: str, version: int = 26,
a_is_lower: bool = True) -> str:
"""从大小写隐写文本中提取 a/b 序列并解码"""
bits = ''.join(
'a' if (ch.islower() == a_is_lower) else 'b'
for ch in stego if ch.isalpha()
)
return BaconCipher(version).decode(bits)
# 使用示例
stego = hide_in_case('HI', 'cryptogram')
print(stego) # crYPToGram
print(reveal_from_case(stego)) # HI自动识别与变种爆破
CTF 里 a/b 未必是字母,可能是 01、大小写、两种标点、长短音……而且 a↔b 可能被反转、码表可能是 24 或 26。把"归一化"与"爆破"拆开处理:
def normalize(cipher: str) -> str:
"""把常见的两态载体统一成 a/b 序列"""
chars = [c for c in cipher if not c.isspace()]
kinds = set(chars)
# 情况 1:全文只由两种符号构成(AB、01、./- 等)
if len(kinds) == 2:
lo = sorted(kinds)[0]
return ''.join('a' if c == lo else 'b' for c in chars)
# 情况 2:大小写混合的字母 → 小写=a、大写=b(隐写载体)
letters = [c for c in cipher if c.isalpha()]
if letters and any(c.islower() for c in letters) and any(c.isupper() for c in letters):
return ''.join('a' if c.islower() else 'b' for c in letters)
raise ValueError('无法自动识别两种状态,请手动指定映射')
def auto_decode(cipher: str, hint: str = 'flag') -> list:
"""对归一化序列做 正反映射 × 24/26 版本 的全爆破,命中 hint 者排前"""
seq = normalize(cipher)
flip = seq.translate(str.maketrans('ab', 'ba'))
out = []
for label, s in (('原映射', seq), ('翻转 a↔b', flip)):
for ver in (26, 24):
plain = BaconCipher(ver).decode(s)
out.append((ver, label, plain))
out.sort(key=lambda r: hint.lower() in r[2].lower(), reverse=True)
return out
# 示例:一段大小写隐写,自动还原
for ver, label, plain in auto_decode('crYPToGram', hint='hi'):
print(f'[{ver}版 / {label}] {plain}')
# [26版 / 原映射] HI ← 命中,排在最前
# [24版 / 原映射] HI
# [26版 / 翻转 a↔b] YX
# [24版 / 翻转 a↔b] YWCTF 实战
常见载体形式(题型)
培根密码的"变装"能力极强,识别关键始终是找出那两种状态:
| 载体形式 | 密文特征 | 识别 / 解法 |
|---|---|---|
| 两种字母 | 只含 A、B(或 a、b),长度 5 的倍数 | 直接查表,试 A↔B 反转 |
| 二进制 01 串 | 只含 0、1,长度 5 的倍数 | 0=a 1=b(或反)按 5 位分组 |
| 大小写隐写 | 一段"正常"英文,大小写异常 | 小写=a、大写=b(或反)提取 |
| 字体 / 格式隐写 | 正体 vs 斜体、粗体、下划线 | 两种字型 → a/b(培根本意) |
| 长短 / 高低 | 两种长度的横线、两种音高… | 归一化为两种符号 |
| 套娃编码 | 培根 → 摩斯 / Base 等叠加 | 逐层按字符集判断 |
识别与解题流程
快速判定:密文是否同时满足——① 只有两种可区分状态;② 有效符号数是 5 的倍数(或去噪后接近)。两条都命中,优先怀疑培根密码。
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 识别 | 统计字符 / 大小写 / 字型种类 | 恰好两种状态是强信号 |
| 2 归一 | 两种状态 →a、b | 约定其一为 a,另一为 b |
| 3 对齐 | 按 5 位分组 | 长度非 5 倍数 → 排查噪声字符 |
| 4 查表 | 每组 a/b → 字母 | 26 版、24 版各试一次 |
| 5 试反 | A↔B 反转再解一遍 | 出题人常故意反转映射 |
| 6 验证 | 是否出现 flag 头 | 命中flag{、ctf{ 即停 |
穷举空间极小:2(映射反转)× 2(24/26 版本)= 4 种组合,毫秒级即可全部试完。
与其他编码 / 隐写对比
| 方案 | 基本符号 | 本质 | 典型识别特征 |
|---|---|---|---|
| 培根密码 | 2 种状态 | 隐写 + 二进制编码 | 两态、有效长度为 5 的倍数 |
| 摩斯电码 | · — | 变长编码 | 点划、有分隔、变长 |
| 二进制 / ASCII | 0 1 | 定长编码 | 长度为 8 的倍数 |
| Base64 | 64 字符 | 编码 | 出现+ / =,长度 4 的倍数 |
| 栅栏密码 | 26 字母 | 置换 | 字母表不变,仅顺序被打乱 |
| 凯撒 / 仿射 | 26 字母 | 替换 | 一串无意义字母 |
培根密码的"强度"几乎为零——一旦被识别,穷举只有 4 种组合。它的全部价值在于隐蔽而非强度:只要对手没发现文本里藏了东西,再弱的编码也是安全的。这正是隐写术与密码学的分野。


