全部文章
技术笔记、教程与思考。
正在加载文章列表
筛选
分类
标签
技术笔记、教程与思考。
正在加载文章列表
分类
标签
正在加载文章内容
这是一篇完整的密码分析实战记录。目标是一台家用光猫(I-040GW),它在配置备份里不存 PPPoE 明文密码,而是存一串看起来像乱码的 Base64。我从一组自己构造的「明文 → 密文」对出发,用不到一页纸的算术就反推出了它用的 8 字节密钥,并成功还原了真实的 PPPoE 密码。本文会完整记录这条思路:怎么拿到已知明文、怎么用排除法缩小假设空间、怎么反推密钥、怎么验证,以及最后的安全评价。
朋友把一台闲置的光猫丢给我,让我帮忙看看能不能重新拨号上网。设备信息如下:
| 项目 | 值 |
|---|---|
| 型号 | Alcatel-Lucent I-040GW |
| 固件版本 | I040GWR200110 |
| SLID | 223X175XXX |
| 序列号 | GTHG00BXXXX |
型号和固件版本是分析的关键,必须保持原样。SLID 和序列号是这台设备的唯一标识,本文已打码——它们对理解方法论没有任何影响,但保留真实值就等于公开了一台具体设备。
这台机器的前任主人搬走了,PPPoE 的账号密码没人记得。运营商那边说过可以重置,但走流程要等好几个工作日。朋友的意思是:「配置不是还在设备里吗,能不能直接捞出来?」
我打开 Web 管理界面,在 PPPoE 设置页里看到密码栏是一串不可读的字符串,而不是被人设置过的样子:
Dafup6keHdw=第一反应当然是想点开浏览器的开发者工具,看这个值是从哪个接口来的、字段名叫什么。但更让我在意的是另一件事——这个值是 Base64。结尾那个 = 是典型的 Base64 补位符。也就是说,设备把 PPPoE 密码做了某种变换之后,存进配置的是一个 Base64 编码的二进制串。
这就变成了一道很标准的密码分析题:已知一段密文,能否还原出明文?
我的计划是:先想办法拿到一组我完全知道的「明文 → 密文」对照数据,然后像做数学习题一样把密钥算出来。
这是整个分析里最关键、也最容易被跳过的一步。
密码分析最怕的不是算法复杂,而是你手里只有密文。只有密文的时候,你面对的是 种可能的密钥,任何计算都是徒劳的。但如果我有一组自己设定的明文,以及它对应的密文,那事情就完全不同了——因为密钥的每一字节都可以直接被算出来。
而这正好是"选择明文攻击"(chosen-plaintext attack)的最朴素形式:通过主动修改设备上的值,让设备把它变成我想要的样子。
操作步骤:
wu41629f。拿到的对照数据是:
| 值 | |
|---|---|
| 明文(我刚设的) | wu41629f |
| 密文(设备存的) | SefopKoZEIg= |
这里有个小细节值得注意:我改完密码以后,必须让设备真的应用并重新生成配置,再去读配置备份。如果只是改了界面上的输入框没点保存,读出来的还是旧值。这一步如果搞错,后面所有计算都是错的。
顺便说一句,选择明文攻击之所以在密码学里被反复讨论,就是因为它的威力:很多"看起来很复杂"的加密,在攻击者能控制输入的前提下会瞬间崩塌。真实系统会用 nonce、随机盐、时间戳来阻止同一明文产生同一密文,从而堵死这条路。这台路由器的实现没有做任何这类处理。
有了明文/密文对之后,我先不急着算,而是花十分钟想清楚:这个算法可能是什么?
这一步很重要。很多人一看到 Base64 就条件反射地开始算 MD5 或者 AES,但那些都是错的方向。
SefopKoZEIg= 一共 12 个字符,结尾一个 =。Base64 每 3 个字节编成 4 个字符,末尾的 = 数量告诉你差多少字节:
12 个字符,1 个 '=' → 3*3 - 1 = 8 字节所以 Base64 解码之后是恰好 8 个字节。
而 PPPoE 密码的长度上限正好也是 8 个字符。这不是巧合,它说明:这个"加密"是一个定长 8 字节的块变换,输入输出等长。
AES 的分组长度是 16 字节。8 不是 16 的倍数,8 字节的输入根本无法喂给 AES 的分组加密函数。所以 AES-ECB、AES-CBC 可以直接出局。
DES / 3DES 的分组是 8 字节,长度上是对得上的,不能靠长度排除。但注意:现代实现几乎不会用 DES 来"保护"一个配置里的密码——不是因为不安全,而是因为它需要 8 字节密钥、需要 IV、需要模式,而一个 8 字节定长的输入输出函数,用 XOR 实现只要几行代码。这个"可能性 vs 简洁性"的权衡是后面判断的关键。
最简单的猜想:设备只是把密码 Base64 编码了一下存起来,什么都没加密。这个一秒就能验证:
python3 -c "import base64; print(base64.b64encode(b'wu41629f').decode())"d3U0MTYyOWY=不是 SefopKoZEIg=。所以设备确实做了真实的变换,出局。
另一个常见猜想:存密码的哈希值(配置备份里存哈希是正确的做法)。MD5 输出 16 字节,截断前 8 字节:
python3 -c "import hashlib; print(hashlib.md5(b'wu41629f').hexdigest()[:16])"e065645122a04e5b也不对。而且这里有个原则性的理由:哈希是不可逆的,如果设备要用这份配置去拨号,它就必须能拿回明文。 拨号时它需要把真实密码放进 PPPoE 的 PAP/CHAP 认证包里。存哈希的设备无法拨号。所以存储值一定是可逆的变换,不是哈希。
这一点其实值得单独拎出来强调。很多人一看"密码相关"就往哈希方向想,但先问功能需求能砍掉大量错误分支:需要还原的东西,就不能用单向算法。
把候选清单列一下:
| 假设 | 状态 | 排除依据 |
|---|---|---|
| 明文直接 Base64 | 排除 | 实际编码结果不匹配 |
| MD5 / SHA 截断 | 排除 | 输出不匹配;且拨号需要明文,不可逆算法做不到 |
| AES-ECB / AES-CBC | 排除 | 分组 16 字节,输入只有 8 字节 |
| DES / 3DES | 可能但存疑 | 长度对得上,但需要密钥+IV+模式,对定长 8 字节函数来说过于复杂 |
| XOR | 最可能 | 长度天然匹配,实现最简单,代码量最小 |
XOR 排到了第一位。理由不只是"简单",而是一条更根本的密码学原理:
XOR 是唯一的二元运算,满足"自己作用自己等于零"。
用数学写就是:
a ⊕ a = 0这一条性质让 XOR 具备自反性:
加密: C = P ⊕ K
再异或一次: P = C ⊕ K加密和解密是同一个操作。对称密码里做到这一点的极少(AES 加解密是两个不同的函数,需要查不同的轮密钥表)。对固件里一个几十行的配置加密函数来说,选一个加解密同体的算法几乎是必然的工程选择。
而且更关键的是——XOR 允许从已知明文直接反解密钥,这正是我现在需要的。
XOR 的定义是逐位相加、不进位、不借位:
0 ⊕ 0 = 0 0 ⊕ 1 = 1 1 ⊕ 0 = 1 1 ⊕ 1 = 0由自反性可以推出一个更有用的等式。已知:
C = P ⊕ K两边同时异或 P:
C ⊕ P = (P ⊕ K) ⊕ P = (P ⊕ P) ⊕ K = 0 ⊕ K = K也就是:
K = P ⊕ C已知明文就能直接读出密钥,不需要任何暴力搜索。 这是 XOR 类方案最致命的弱点,也是这次分析的全部技术含量所在。
现在把两个字符串都转成十六进制。
明文 wu41629f 是 8 个可打印 ASCII 字符,ASCII 码本身就是 7 位:
w = 0x77
u = 0x75
4 = 0x34
1 = 0x31
6 = 0x36
2 = 0x32
9 = 0x39
f = 0x66明文 P: 77 75 34 31 36 32 39 66密文要先 Base64 解码:
python3 -c "import base64; print(base64.b64decode('SefopKoZEIg=').hex(' '))"密文 C: 49 e7 e8 a4 aa 19 10 88逐字节异或:
| 位置 | 明文 P | 密文 C | XOR = 密钥 K |
|---|---|---|---|
| 0 | 0x77 w | 0x49 | 0x3e > |
| 1 | 0x75 u | 0xe7 | 0x92 |
| 2 | 0x34 4 | 0xe8 | 0xdc |
| 3 | 0x31 1 | 0xa4 | 0x95 |
| 4 | 0x36 6 | 0xaa | 0x9c |
| 5 | 0x32 2 | 0x19 | 0x2b + |
| 6 | 0x39 9 | 0x10 | 0x29 ) |
| 7 | 0x66 f | 0x88 | 0xee |
于是密钥就是:
K = 3e 92 dc 95 9c 2b 29 ee顺手可以看一眼这 8 个字节 printable 不 printable、有没有更短的周期:
python3 -c "
k = bytes.fromhex('3e92dc959c2b29ee')
print('可打印 ASCII?', all(32 <= b < 127 for b in k))
print('有更短周期?', [L for L in range(1, 8) if all(k[i] == k[i % L] for i in range(8))])
"可打印 ASCII? False
有更短周期? []也就是说不存在「周期 4」「周期 2」这种更简单的循环密钥,它就是实打实的 8 字节常量。
这里有一个细节上的严谨性我想说明白,因为它涉及结论的可信度:
网上(包括一些 AI 助手)会把这件事描述成"循环 XOR"。但对 8 字节明文配 8 字节密钥的场景来说,「循环 XOR」「一次性密码本」「8 字节分组异或」这三种说法产生的运算结果完全一样,无法区分,也不需要区分。我在文中统一称作"8 字节定长 XOR"。
推导出密钥不等于结论成立。密码分析里最常见的翻车方式,就是"猜对了一个模型,但那个模型只是恰好拟合了你手上的数据"。
所以下面这几重验证是一层比一层硬的:前两层在纸面上自洽,第三层是统计论证,最后一层是物理实测。任何一层不通过,前面的结论都要推翻重来。
用算出来的密钥重新加密 wu41629f,必须精确还原成 SefopKoZEIg=:
77 75 34 31 36 32 39 66
⊕ 3e 92 dc 95 9c 2b 29 ee
= 49 e7 e8 a4 aa 19 10 88
= Base64 → SefopKoZEIg= ✓现在回到最开始那个值 Dafup6keHdw=:
python3 -c "
import base64
k = bytes.fromhex('3e92dc959c2b29ee')
c = base64.b64decode('Dafup6keHdw=')
print('C =', c.hex(' '))
p = bytes(x ^ y for x, y in zip(c, k))
print('P =', p.hex(' '))
print(' =', p.decode())
"C = 0d a7 ee a7 a9 1e 1d dc
P = 33 35 32 32 35 35 34 32
= 352255428 个字节全部落在 0x30–0x39,也就是 8 个 ASCII 数字,连成一个合法的 PPPoE 密码。
这一点值得展开,因为它正是这个结论能站住脚的原因。
前面我列假设时说过一句:仅凭一组明文/密文对,在数学上是无法唯一确定算法的。 任何足够复杂的函数都能构造出这样的映射。这句话是对的,也是我一开始不敢直接下结论的原因。
但概率论可以帮我量化"这有多巧"。
一个错误的密钥(也就是随便 8 个字节)解出结果落在可打印范围内,每个字节的概率大约是 95/256。而 PPPoE 密码在这个具体场景下只能是数字,每个字节的概率只有 10/256。所以 8 个字节全部是数字的概率是:
(10/256)^8 ≈ 5.4 × 10^-12也就是大约 分之一,一千八百多亿分之一。
换句话说:如果我的密钥模型是错的,那么第二组数据"恰好"解出 8 位纯数字的概率约等于零。观察到这个结果之后,正确的假设几乎只剩下一个。
顺带一提,WiFi 那篇讲过"两次一密"(two-time pad)。这里其实还有一个不依赖密钥的旁证:两个密文异或等于两个明文异或。验证一下:
Plain TextC1 ⊕ C2 = 44 40 06 03 03 07 0d 54 P1 ⊕ P2 = 44 40 06 03 03 07 0d 54 ✓ 相同这是 XOR 方案的固有性质。拿到两份密文(不需要任何明文),你就已经能知道"这两组明文的逐位异或"是什么了。对全数字密码来说,这仍然泄露了大量信息。
密码分析做得再漂亮,最后还是要看它能不能用。把 35225542 填回 PPPoE 设置页,保存,重拨。
拨号成功,拿到 IP。
这一下才是真正的闭环:我自己设的明文、设备返回的密文、反推出的密钥、解出来的密码,四者环环相扣,而且最后一步由运营商的认证服务器盖章作证。前面所有推导都只是纸面自洽,只有拨号成功这一下是无法伪造的。
在收尾时我检查了一件可能会推翻结论的事:这个密钥会不会是每台设备不同的?
如果它是「用 SLID 或序列号派生的」,那我这台机器的结论就不能推广。我把 SLID 和序列号都拿来和密钥逐字节异或、检查有没有字节重合(下文两个值已按前文打码):
python3 -c "
k = bytes.fromhex('3e92dc959c2b29ee')
for name, s in [('SLID', '223X175XXX'), ('SN', 'GTHG00BXXXX')]:
b = s.encode()
print(name, 'xor K =', bytes(x ^ y for x, y in zip(b, k)).hex(' '))
print(' 与 K 相同的字节:', [chr(c) for c in b if c in k] or '无')
"SLID xor K = 0c a0 ef cd ad 1c 1c b6
与 K 相同的字节: 无
SN xor K = 79 c6 94 d2 ac 1b 6b b6
与 K 相同的字节: 无没有任何关联。所以密钥是固件里写死的常量,同一固件版本的设备通用。
这一点有两个含义:
如果你手上有第二台 I-040GW,可以直接拿我算出的密钥去验证那个结论。
分析是一次性的,但使用不是。拿到密钥之后,工具应该是拿来即用的。
#!/usr/bin/env python3
"""破解 Alcatel-Lucent I-040GW (I040GWR200110) 配置备份里的 PPPoE 密码混淆值。
用法:
python3 pppoe-decrypt.py 'Dafup6keHdw='
python3 pppoe-decrypt.py 'SefopKoZEIg=' 'Dafup6keHdw='
"""
import base64
import sys
# 从已知明文/密文对反推出来的 8 字节常量密钥
KEY = bytes.fromhex("3e92dc959c2b29ee")
def transform(data: bytes) -> bytes:
"""设备的"混淆"就是逐字节 XOR。
XOR 满足 a ^ a == 0,所以它自反:同一个函数既能加密也能解密。
"""
if len(data) != len(KEY):
raise ValueError(f"长度 {len(data)} 字节,与 {len(KEY)} 字节密钥不匹配")
return bytes(p ^ k for p, k in zip(data, KEY))
def dump(label: str, value: str) -> None:
try:
raw = base64.b64decode(value, validate=True)
except Exception as exc: # noqa: BLE001
print(f"{label}: 不是合法的 Base64 ({exc})", file=sys.stderr)
return
plain = transform(raw) if len(raw) == len(KEY) else None
print(f"{label}")
print(f" 输入 {value}")
print(f" Base64 解 {raw.hex(' ')}")
if plain is None:
print(f" XOR 跳过:长度 {len(raw)} 字节,与密钥不匹配")
return
print(f" XOR {plain.hex(' ')}")
print(f" ASCII {plain.decode('ascii', 'replace')}")
print(f" 回代校验 {base64.b64encode(transform(plain)).decode()} (应等于输入)")
def main() -> None:
args = sys.argv[1:]
if not args:
print(__doc__, file=sys.stderr)
raise SystemExit(2)
for value in args:
dump("密文", value)
if __name__ == "__main__":
main()跑一下:
python3 pppoe-decrypt.py 'Dafup6keHdw='密文
输入 Dafup6keHdw=
Base64 解 0d a7 ee a7 a9 1e 1d dc
XOR 33 35 32 32 35 35 34 32
ASCII 35225542
回代校验 Dafup6keHdw= (应等于输入)最后那行"回代校验"是我特意加的。它在每次运行时重新加密一次解密结果,如果和输入不一致就说明密钥错了。对一个"输入只有 8 字节、结果看起来像密码"的工具来说,这个自查比什么都实用——因为任何 8 字节乱码"看起来都挺像密码",人眼判断很容易出错。
如果只想要结果、不在乎过程,一行 Bash 也够:
KEY='3e92dc959c2b29ee'
echo 'Dafup6keHdw=' | base64 -d | python3 -c "
import sys
k = bytes.fromhex('$KEY')
print(bytes(a ^ b for a, b in zip(sys.stdin.buffer.read(), k)).decode())
"35225542不算。这是混淆(obfuscation),不是加密。它在密码学上不满足任何一条基本要求。
真正的分组密码(DES、AES)里,改动明文一个 bit,会让输出的所有字节都变。算法内部有多轮 Feistel 结构、列混合、S-Box,目的就是制造这种"雪崩效应",让攻击者无法从局部的输出差异推回输入。
而 XOR 改一个 bit,输出只变一个 bit:
明文 77 → 翻最低位 → 76
密文 49 → 变 1 bit → 48完全没有扩散。严格地说,密文变化的 bit 数和明文变化的 bit 数完全相等——这在密码学里叫"零扩散",是 XOR 方案被判为弱的最直接证据。
S-Box、轮函数、MixColumns 这些结构存在的目的,是让每一个输出字节都依赖全部输入字节。这样一来,攻击者就无法只看输出的某几位就推测输入的某几位。
而 XOR 是逐字节独立的:输出第 i 个字节只依赖输入第 i 个字节。字节之间不传递任何信息。
这一点可以用一个很干脆的性质说明——输出的汉明距离恒等于输入的汉明距离。随便改明文,然后数一数密文有多少个 bit 跟着变了:
| 明文 | 改成 | 输入翻转 bit 数 | 输出翻转 bit 数 |
|---|---|---|---|
35225542 | 24334453 | 8 | 8 |
35225542 | 34225543 | 2 | 2 |
wu41629f | vt50738g | 8 | 8 |
00000000 | ffffffff | 32 | 32 |
规律是 1 : 1,一个 bit 进、一个 bit 出,一分不多一分不少。
作为对比,AES 或 DES 在同样输入下,输出大约有一半的 bit 会翻转(8 个 bit 进去、32 个 bit 出来),而且具体是哪几位无法预测。这个"输入变化量无法预测输出变化量"的性质,正是混淆(diffusion)要达到的效果。
顺便说一个我自己一开始搞错、值得记下来的点。XOR 常见的批评是"保留了明文的统计特征"——但对位置化密钥来说这句话其实不准确。密钥每个位置都不同,所以同一个明文字符出现在不同位置时,得到的密文字节是不同的:
明文 33 35 32 32 35 35 34 32 ← '2' 出现了 3 次,'5' 出现了 3 次
密文 0d a7 ee a7 a9 1e 1d dc ← 但 8 个字节几乎全不相同真正准确的批评是上面那条:逐位置、逐 bit 的双射,零跨字节扩散。混淆得"不够狠"和"完全不混淆"是两回事,XOR 属于后者。
这是最致命的一条。密钥 3e 92 dc 95 9c 2b 29 ee 硬编码在固件里,全球所有这台机器的所有用户共用同一个密钥。
一个加密方案的安全性不取决于算法强度,而取决于密钥的保密性。密钥一旦公开,算法再强也归零。这里算法和密钥同时公开,因为它本来就不是用来防攻击的。
我认为厂商的意图是明确的、有合理性的——它防的不是密码分析,而是随手翻配置。
具体来说,它防的是这些场景:
grep 一下就看到 PPPOEPassword = "SefopKoZEIg=",至少拿不到可直接复制的字符串。用安全术语说,这是 defense in depth 最外层的一层"防窥屏",不是"防攻击"。 它把一个需要主动密码分析才能还原的秘密,变成一个需要读代码才能还原的秘密——门槛从「会写脚本」提升到「懂密码学」。这个门槛提升本身是有价值的,只是不该被称作"加密"。
从这个角度看,厂商其实做了一个合理的工程权衡:用一个极简的、几乎不可能写错的函数,挡住数量最大的一类非恶意泄漏。 一个 8 字节 XOR 可以用三行 C 写完、可以一眼看懂、不需要密钥管理、不需要 padding 处理、不需要考虑字节对齐——这些优点对固件代码来说都是实打实的。
与之相对的失败案例是用哈希存密码。有些厂商会觉得"存哈希更安全",于是配置文件里放 MD5(password)。但就像前面说的,拨号需要明文,所以这类设备最终会在别的地方存一份明文,哈希只是个摆设——安全上没得到任何好处,还额外制造了"以为自己是安全的"这种更糟的错觉。知道设备的功能需求(要能拨号),能省掉大量猜测。
我认真想过这个问题,结论是:对于消费级路由器,厂商的方案已经接近当前条件下的最优解了。 理由是这样的。
PPPoE 拨号本质上要求设备手里必须有可逆的密码。没有密钥托管、没有硬件安全模块(SE、TPM)、没有安全元件的家用设备,密码必然要以某种形式存在于设备可读内存里。这是物理层面的约束,不是编程水平问题。
所以真正可改进的方向不是"把 XOR 换成 AES",而是:
哪怕是家用设备,也至少可以做到每台设备一个随机密钥,而不是全球共用一个常量。哪怕它依然弱,但至少把"破解一台"和"破解全型号"这两件事解耦了。而这个方案偏偏做不到——XOR 方案里密钥就是算法的一部分,要改就得改固件。
密码只在拨号的那几秒需要存在明文。完全可以:配置里存密文 → 拨号时内存里临时还原 → 用完立刻擦除内存(用 volatile 缓冲区、避免 swap)。这样即便有内存 dump 攻击,窗口期也小得多。
最彻底的方案是不存储密码,只验证密码,也就是密码学里的 PAKE(Password-Authenticated Key Exchange)思路——参考 WPA3 的 SAE:双方都不用传输密码,而是通过挑战应答互相证明"我知道这个密码"。
放到 PPPoE 上做并不现实(协议不支持)。但换个思路,厂商可以走"初始随机口令 + 强制首次修改 + 绑定 MAC 校验"这条路。公开资料显示这台 I-040GW 的 I040GWR200110 固件确实存在根据 LAN MAC 生成管理密码的机制——那本质上就是一个"每台设备不同"的密钥,比全局常量前进了一步。
如果只能做一件事,我会选:让 Web 管理界面默认绑定 192.168.1.1 之外不可达,并且在导出的配置里对敏感字段做显式标注和提醒。
配置备份是这类设备最致命的泄漏面。用户在"我只是想留个备份"的场景下,不会意识到这个文件等同于一张写着宽带密码的纸条。厂商至少应该让这件事可见。
这是我最想补上的一节,因为它是这类分析的通用能力,而不只是这一个案例。
拿到别人的配置备份、或者一时半会儿没法造出已知明文时,还有哪些路可以走?
这是最有效的一招,而且不需要交互。
已知密文是 8 字节,PPPoE 密码只能是数字或字母。于是密钥的每个字节都被限制在一个很小的候选集合里:
数字: 0x30 – 0x39 (10 个候选)
大写: 0x41 – 0x5A (26 个候选)
小写: 0x61 – 0x7A (26 个候选)如果再知道"这台机器的密码是纯数字"(很多运营商确实只发数字密码),那么每个字节只剩 10 个候选,整个搜索空间是 。这个量级在现代机器上完全现实:纯 Python 跑完 次大约要两分钟,换成 C 就是毫秒级。
import base64, itertools
C = base64.b64decode("Dafup6keHdw=")
DIGITS = range(0x30, 0x3A)
# 枚举所有 8 位纯数字密码,每个密码都唯一确定一把密钥:K = C ⊕ P
for pt in itertools.product(DIGITS, repeat=8):
p = bytes(pt)
key = bytes(a ^ b for a, b in zip(C, p))
print(f"候选明文 {p.decode()} 候选密钥 {key.hex(' ')}")这段代码在完全不知道密钥的前提下,穷举出了全部 个可能的纯数字密码,以及每个密码对应的唯一密钥。其中必然包含真实答案:
候选明文 35225542 候选密钥 3e 92 dc 95 9c 2b 29 ee这里有个容易写错的地方,我自己也踩了一次:一开始我写的是枚举密钥的字节(
itertools.product(DIGITS, repeat=8)),结果什么都没找到。原因是约束在明文上,不在密钥上——真实密钥3e 92 dc 95 ...本身并不是 8 个数字。正确的做法是枚举明文,再由K = C ⊕ P反推出密钥。写暴力搜索时,一定要想清楚"我枚举的是密钥空间,还是明文空间",因为只有明文空间的字符集才是你真正知道的。
如果换成含字母的密码(62 个候选字符),空间变成 ,就超出暴力破解的舒适区了。
这就是字符集约束的价值:它把"不可能"变成"很慢"。
用同一把密钥加密的两段数据,异或之后会消掉密钥:
C1 ⊕ C2 = (P1 ⊕ K) ⊕ (P2 ⊕ K) = P1 ⊕ P2如果你有同一台设备的两份配置备份(比如改密码之前和之后的),你就同时拿到了这段异或关系。再配合路径一的字符集约束,搜索空间会被进一步压缩。
这也是为什么路径一和路径二要一起用:约束筛掉不可能的,异或提供额外的关系。
只要你对设备有管理权限,路径三永远是压倒性的最优解,别的都可以不看了。而且它有一步比"改成随机字符串"更好的技巧:
把密码设成 8 个
0(00000000),然后读密文。因为
0x30 ⊕ K ≠ K,严格来说你还是要用0x30异或回去才能拿到K。但如果你能找到一个能输入全 NUL 字节的接口(某些设备的隐藏调试接口、或直接改配置文件后重启),那读出来的 Base64 解码就是密钥本身。
用这个技巧在这台设备上做了预言(如果哪天要复现,可以直接对照):
| 把密码设成 | 设备会存 |
|---|---|
00000000 | DqLspawbGd4= |
11111111 | D6PtpK0aGN8= |
AAAAAAAA | f9Od1N1qaK8= |
12345678 | D6DvoakdHtY= |
任何一行对上了,就说明你的密钥是对的。
如果你的分析目标是"判断这是不是某款设备的通用漏洞",那么最有说服力的证据是跨设备的一致性:拿同型号不同机器的密文,看同一段明文是否产生相同密文。
机器 A: wu41629f → SefopKoZEIg=
机器 B: wu41629f → SefopKoZEIg= ← 相同,则确认是固件常量如果不同,说明密钥是每台派生的,那结论就要改成"该设备存在漏洞"而不是"该型号全系通用"——这个区分对漏洞报告的严重性评级影响很大,值得多花时间确认。
给想自己走一遍的朋友,整理成可执行的步骤:
I040GWR200110)。= 补位符,数一下字符数,推算原始字节数。K = P ⊕ C,逐字节算。第 12 步和第 13 步是不能省的。前 11 步都只是在纸面上自洽,只有这两步能告诉你答案是不是真的。
这类分析的正当用途,我梳理了几条:
反过来,明确不应该做的:
厂商的混淆不是安全边界。它的设计意图明确是"降低被动泄漏的概率",所以它本来就没打算对抗主动的攻击者。但"它没打算防"和"你可以随便用"是两回事:能做不等于该做。理解一个东西怎么坏,是为了更好地加固它、以及知道该守好自己的东西,而不是为了撬开不属于你的那扇门。
回顾一下这次分析的完整链路:
| 步骤 | 做了什么 | 产出 |
|---|---|---|
| 拿明文 | 修改 PPPoE 密码,导出配置读取 | wu41629f → SefopKoZEIg= |
| 看长度 | Base64 补位符推算原始长度 | 恰好 8 字节 = PPPoE 密码上限 |
| 排除法 | 排除 Base64 明文、哈希、AES | 剩下 DES 类与 XOR |
| 选模型 | 长度匹配 + 加解密同体 + 实现最简 | 8 字节定长 XOR |
| 反推密钥 | K = P ⊕ C | 3e 92 dc 95 9c 2b 29 ee |
| 概率验证 | (10/256)^8 ≈ 1/1.8×10¹¹ | 排除巧合 |
| 往返校验 | 重新加密还原密文 | 一致 |
| 实测闭环 | 填回密码拨号 | 成功 |
| 查密钥来源 | 与 SLID / 序列号比对 | 固件常量,非每台派生 |
真正的技术核心只有一条公式:
K = P ⊕ C其余全是工程:怎么拿到 P、怎么排除掉错的假设、怎么验证结果不是巧合。
而这正是它最有教育价值的地方——很多人以为"破解加密"意味着暴力搜索、字典攻击、或者动用 GPU 上跑 hashcat。对固定密钥、且攻击者能控制输入的系统来说,密码分析的第一步往往是"造一个我已知的输入",然后读出密钥。 工具再强也比不上方法选对。
用一句话总结全文:
当一台设备把它的密码做了一轮变换之后,你要做的第一件事不是研究它的算法有多强,而是想办法让它把你指定的字符串变成密文。因为对于自反的加密,密文减去明文就是密钥本身。
顺带一句给厂商的:如果这个型号的固件还在线上更新,值得考虑把全局常量密钥换成每设备随机派生的密钥。它不会让方案变"安全",但会把"破解一台"和"破解整个型号"这两件事解耦——而这恰恰是弱密码存储里最值钱的那一步改进。