如果你和我一样,最开始看到“基尔霍夫-洛-约翰逊噪声(KLJN)安全密钥交换协议”这个名字,第一反应多半是:这不是物理课上的热噪声吗,怎么和密钥交换扯上关系?真正把协议在 Matlab 里完整仿真一遍之后,你会发现,物理层安全协议的薄弱点往往不在信道上,而在实现它的随机源上。今天要说的统计随机数生成器攻击,就是瞄准这个实现弱点的一种典型攻击方式。
这篇文章不是把 KLJN 的安全证明重新抄一遍,而是站在攻击者角度,讲清楚三件事:KLJN 协议到底把“秘密”藏在哪里;统计随机数生成器攻击为什么能威胁到它;以及如何用 Matlab 把这条攻击链路完整实现出来。适合正在做物理层安全、密码协议仿真、或者准备把协议落地到硬件原型的朋友参考。代码基于 Matlab R2026b 跑通,但核心函数从 R2019a 开始都能直接用。
1. 先从攻击视角重新理解 KLJN 协议
1.1 一句话原理:热噪声就是密钥载体
KLJN 协议的名字来自三个人名和一个物理定律:基尔霍夫(Kirchhoff)、洛(Law,这里指基尔霍夫定律)、约翰逊(Johnson,发现了电阻热噪声)。物理基础其实不复杂:任何处于绝对零度以上的电阻,都会因为内部电子的随机热运动产生一个随机电压,这就是约翰逊噪声。它的功率谱密度在很宽的频带内近似平坦,大小由下面的公式决定:
[ S_V(f) = 4 k_B T R ]
其中 (k_B) 是玻尔兹曼常数,(T) 是绝对温度,(R) 是电阻值。也就是说,电阻越大,噪声功率越大;温度越高,噪声功率也越大。这个特性在密码学里有一个非常值钱的用途:电阻值可以作为密钥比特的物理载体。
在 KLJN 协议中,通信双方 Alice 和 Bob 各自准备两个已知电阻,一般记作 (R_L) 和 (R_H),分别对应二进制 0 和 1。每一轮密钥交换,双方各自按自己的随机比特选择一个电阻接到同一根信道上。由于约翰逊噪声的功率和电阻值直接相关,所以当信道两端电阻配置不同时,本地测到的电压、电流统计量会携带“对方选了哪个电阻”的信息。而窃听者 Eve 只能看到信道上的整体噪声,在特定的 0/1 对称配置下,她无法区分比特到底是哪一边产生的。
这就是 KLJN 的基本思路:不是把密钥比特放在数字信号里传输,而是把它藏在物理电阻的噪声统计特性中。
1.2 攻击面的位置:物理层没崩,崩的是随机源
我第一次跑通 KLJN 的理想信道模型时,最大的感受是:数学上确实漂亮。Eve 在理想条件下能看到的信道统计量是对称的,协议的安全性建立在“完美随机源”这个假设上。
但工程实现里,这个假设很容易被破坏。Alice 和 Bob 不可能真的去掷硬币,也不可能每次都用硬件热噪声源来生成比特。大多数原型系统为了便宜和方便,会用一个伪随机数生成器(PRNG),比如线性同余生成器(LCG)、线性反馈移位寄存器(LFSR),甚至直接用一些商用芯片内部的伪随机序列。
问题就出在这里。伪随机生成器如果参数选得不好、种子空间太小、或者输出比特有偏,那么 Alice 和 Bob 每一轮选择的电阻值就会带有可被统计检测出来的规律。Eve 不需要破解热噪声本身,她只需要识别出“电阻选择序列”的统计缺陷,就能以高于 50% 的概率猜测密钥比特。这种攻击方式,就是标题里说的统计随机数生成器攻击。
换句话说,攻击者打的是协议实现层,不是物理层。这个区别决定了整个 Matlab 代码的构造方式:信道模型可以保持理想化,随机源模型必须做得足够“差”,差到能暴露统计规律。
1.3 这条攻击线适合谁
如果你只是想知道 KLJN 能不能防住窃听,那看协议论文就够了,不需要写代码。但如果你要做下面这几件事,这条攻击仿真线就很有价值:
- 评估一个实际 KLJN 系统的安全性,而不是纸面上的安全性;
- 在 Matlab 里复现攻击实验,给协议设计者提供量化指标;
- 测试不同随机数生成器对密钥交换成功率的影响;
- 为硬件原型设计“随机源健康检测”模块。
我自己的经验是,先实现攻击,再回头看协议的安全假设,往往能发现很多论文里没有展开的工程细节。下面我把攻击思路和 Matlab 实现逐步拆开。
2. 统计随机数生成器攻击的完整思路拆解
2.1 为什么“统计攻击”能成立
很多第一次接触这个攻击的人会问:Eve 不是只看到热噪声波形吗,她怎么从噪声里知道随机数生成器弱不弱?
答案在于,热噪声波形本身不是一个孤立信号。每一轮密钥交换,波形里都包含了 Alice 和 Bob 所选电阻的统计特征。如果随机源弱,那么“哪一轮选了哪个电阻”这件事就不是均匀独立分布的,而是存在概率偏差、序列相关性、甚至周期性。Eve 把大量观测到的噪声片段收集起来,按时间顺序排列,再统计每一片的幅度、频谱、自相关等特征,就能反推出随机源的部分状态。
我把攻击者能利用的统计弱点归纳成三类:
| 随机源弱点 | 在信道观测中的表现 | 攻击者可利用方式 |
|---|---|---|
| 输出概率有偏 | 某些电阻配置出现频率明显偏高 | 用频率统计直接猜高频比特 |
| 短周期或状态重复 | 噪声片段的统计特征呈周期重复 | 按周期分组,建立模板库 |
| 序列相关性 | 前后比特之间存在可预测转移关系 | 用马尔可夫模型预测下一比特 |
这三类弱点不是互斥的,实际弱随机源往往同时具备多个问题。比如一个 16 位 LCG,周期只有 65536,而且连续输出之间线性相关;Eve 只需要抓足够多的帧,就能把整个状态轨迹重建出来。
2.2 攻击者的知识假设与能力边界
在实现攻击之前,必须先明确 Eve 能知道什么、不能知道什么。否则很容易把攻击仿真做成一个“开卷考试”,结果虚高。
我给出的仿真模型采用以下知识假设:
- Eve 知道协议细节,包括 (R_L)、(R_H) 的具体阻值、采样率、帧长;
- Eve 能被动窃听信道,获取每一轮密钥交换的完整电压波形;
- Eve 知道对方使用的随机数生成器类型,但不知道当前种子;
- Eve 不能主动注入信号,也不能物理断开任何一方的线路。
这个假设比较接近真实场景。因为协议参数、阻值和采样率通常是公开的,随机数生成器类型也可能通过设备型号、开源代码或侧信道信息推测出来。真正需要攻击者付出代价的,是恢复当前的随机源状态。
2.3 攻击流程总览
整个攻击在 Matlab 里可以拆成四个模块,正好对应后面的四段代码:
- 观测采集:模拟 Eve 从信道上截获 N 帧电压波形;
- 特征提取:对每帧波形计算统计特征,比如整流均值、标准差、差分平方均值、功率谱特征;
- 状态重放:用候选随机数生成器和候选种子,生成 Alice、Bob 的电阻选择序列;
- 统计判决:把观测特征和候选序列做匹配,输出对每帧密钥比特的预测。
四个模块串起来后,攻击成功率是最终输出指标。我在实验中最关心的不是“能不能破”,而是“在什么参数条件下能破、什么条件下不能破”。这样才能给防御方一个明确的边界。
3. Matlab 仿真环境与模块设计
3.1 仿真总体架构
我没有用 Simulink,也没有调用复杂的电路仿真工具箱,因为这里要验证的是攻击逻辑,不是电阻网络的瞬态响应。用纯 Matlab 脚本反而更容易控制随机源、信道噪声和统计检测这三个环节。
工程目录结构可以这样组织:
kljn_attack/ run_demo.m % 主脚本,跑完整攻击实验 weak_rng.m % 弱随机数生成器模型 kljn_channel.m % KLJN 信道观测模型 attack_features.m % 统计特征提取 attack_decision.m % 判决与成功率统计这样的好处是每一层都可以单独替换。比如你想测试不同的弱随机源,只需要改weak_rng.m;你想测试不同信道带宽,只需要改kljn_channel.m的入口参数。整个实验做完,不用推倒重来。
3.2 参数设置
仿真参数我建议固定成一组基线值,后面做对比时才不会乱。下面这组参数是我在实验里用的:
| 参数 | 取值 | 说明 |
|---|---|---|
| 采样率 fs | 1 MHz | 模拟采样率,远高于噪声带宽即可 |
| 单帧时长 Twin | 1 ms | 每轮密钥交换的观测窗口 |
| 单帧采样点 N | 1000 | fs 乘以 Twin 得到 |
| 低温电阻 RL | 1 kΩ | 对应比特 0 |
| 高温电阻 RH | 10 kΩ | 对应比特 1 |
| 温度 T | 300 K | 室温假设 |
| 仿真帧数 M | 200 到 1000 | 视攻击稳定性需求调整 |
电阻比例取 10,主要是为了让不同配置在特征空间里有明显区分度。如果比例太小,信道统计量混在一起,不仅攻击困难,合法双方解调也会很难做。
3.3 弱随机数生成器的建模
攻击仿真里最重要的一步是构造一个“真实可信的弱随机源”。我选了线性同余生成器,因为它结构简单、周期可控、统计缺陷一目了然。
function bits = weak_lcg_bits(N, a, c, m, seed) % 经典线性同余生成器,用于模拟弱随机源 % N: 输出比特数; a, c, m: LCG参数; seed: 初始种子 x = zeros(N, 1); x(1) = seed; for k = 2:N x(k) = mod(a * x(k-1) + c, m); end % 映射到 [0,1),再转成比特 u = x / (m - 1); bits = double(u > 0.5); end这里我故意把 m 设成 65536,也就是 16 位。它的周期只有 65536,连续输出之间还有很强的相关性。只要 Eve 知道 a、c、m,她完全可以通过暴力搜索 65536 个种子来重放整条序列。这个函数虽然简单,但它抓住了真实弱随机源最核心的问题:可预测性。
3.4 KLJN 物理信道模块
信道模块不需要做晶体管级建模。我的做法是把 Alice 和 Bob 的电阻分别看成一个独立热噪声源,然后按照电阻分压关系合成信道上的观测电压。
function Vseg = kljn_channel(RA, RB, N, fs, T) % RA, RB: 两端电阻值 % N: 采样点数; fs: 采样率; T: 温度 kB = 1.380649e-23; bw = fs / 2; % 仿真中的等效噪声带宽 sigmaA = sqrt(4 * kB * T * RA * bw); sigmaB = sqrt(4 * kB * T * RB * bw); VnA = sigmaA * randn(N, 1); VnB = sigmaB * randn(N, 1); % 信道节点电压:两个电阻热噪声的加权叠加 Vseg = (VnA * RB + VnB * RA) / (RA + RB); end这个模型忽略了很多高频寄生参数,但用于统计攻击验证已经足够。关键在于,不同电阻组合会让 Vseg 的幅度和频谱产生差异,而统计特征提取就是要捕捉这个差异。
3.5 统计检测与分类模块
特征提取不能只用标准差,因为不同电阻组合的标准差容易受随机波动影响。我通常取三个互补特征:
function feat = attack_features(Vseg) % 从一段电压波形中提取统计特征向量 feat = zeros(1, 3); feat(1) = mean(abs(Vseg)); % 整流均值,反映幅度水平 feat(2) = std(Vseg); % 标准差,反映噪声功率 feat(3) = mean(diff(Vseg).^2); % 差分平方均值,反映高频能量 end整流均值和高频能量这两个特征,对采样率变化比较敏感,但对电阻差别的响应更稳定。只用标准差的话,一旦信道白噪声出现小幅波动,分类器就容易产生误判。
4. 核心代码实现与关键细节
4.1 生成 Alice 和 Bob 的电阻选择序列
在攻击仿真里,我们需要先按真实协议生成 Alice 和 Bob 的比特,再生成对应的噪声波形。这里要注意,攻击者并不知道这些比特,代码里保留它们只是为了最后评估预测准确率。
% 生成 Alice 和 Bob 的随机选择序列 M = 400; [bitsA, bitsB] = deal(zeros(M, 1)); % 预分配 rng(2024); % 为了方便复现 seedA = 12345; seedB = 54321; % 这里假设 Alice 和 Bob 各自使用独立的弱 LCG bitsA = weak_lcg_bits(M, 1103515245, 12345, 65536, seedA); bitsB = weak_lcg_bits(M, 1103515245, 12345, 65536, seedB);实际协议中 Alice 和 Bob 的随机源应该是独立的,否则攻击面会更大。我把种子设为不同值,模拟双方使用各自独立的伪随机生成器,但参数相同、周期相同的场景。
4.2 模拟窃听者看到的信道观测
接下来进行主循环:每一轮根据双方比特计算电阻值,生成电压波形,提取特征,并保存真实标签。
RL = 1e3; RH = 10e3; fs = 1e6; Twin = 1e-3; N = round(fs * Twin); T = 300; feats = zeros(M, 3); trueBits = zeros(M, 1); for k = 1:M RA = RL + (RH - RL) * bitsA(k); RB = RL + (RH - RL) * bitsB(k); Vseg = kljn_channel(RA, RB, N, fs, T); feats(k, :) = attack_features(Vseg); trueBits(k) = bitsA(k); % 攻击目标是恢复 Alice 的比特 end这里我选择恢复 Alice 的比特,因为只要恢复 Alice 的比特,再结合协议规则,就能推断出实际使用的密钥比特。如果你想把攻击目标设为 Bob,逻辑完全一样。
4.3 特征分类与攻击判决
得到特征矩阵后,把问题转换成一个监督分类问题。这里需要特别说明:真实攻击中 Eve 没有标签,但在仿真阶段我们作为“上帝视角”的评估者,可以用标签训练和验证分类器,看看信道观测到底泄露了多少信息。
% 用 10 折交叉验证评估分类准确率 model = fitcdiscr(feats, trueBits); cvmodel = crossval(model, 'KFold', 10); loss = kfoldLoss(cvmodel); attackAccuracy = 1 - loss; fprintf('攻击准确率: %.2f%%\n', attackAccuracy * 100); fprintf('随机猜测基线: 50.00%%\n');如果攻击准确率显著高于 50%,说明 Eve 确实能从电压波形的统计特征里恢复出 Alice 的随机比特。再进一步,如果 Eve 能同时恢复 Alice 和 Bob 的比特,她就能直接重建整个密钥交换过程。
4.4 完整的攻击主脚本调用
把上面的片段串起来,就是run_demo.m的主体。我习惯在最后加一个“随机源替换”的对比实验:把 Alice 和 Bob 的弱 LCG 换成 Matlab 自带的强随机数生成器,再跑一次同样的流程,作为攻击有效性的对照。
% 对照组:使用强随机源 bitsA_strong = randi([0 1], M, 1); bitsB_strong = randi([0 1], M, 1); featsStrong = zeros(M, 3); trueBitsStrong = zeros(M, 1); for k = 1:M RA = RL + (RH - RL) * bitsA_strong(k); RB = RL + (RH - RL) * bitsB_strong(k); Vseg = kljn_channel(RA, RB, N, fs, T); featsStrong(k, :) = attack_features(Vseg); trueBitsStrong(k) = bitsA_strong(k); end modelStrong = fitcdiscr(featsStrong, trueBitsStrong); lossStrong = kfoldLoss(crossval(modelStrong, 'KFold', 10)); fprintf('强随机源对照准确率: %.2f%%\n', (1 - lossStrong) * 100);对照组的结果应该非常接近 50%。如果强随机源也得到很高的分类准确率,那说明你的特征或信道模型出了问题,而不是攻击有效。这个对照实验是我调试时最重要的一步。
5. 实验结果怎么看:指标与判据
5.1 攻击成功率的定义
攻击成功率不是“破解整个密钥”的概率,而是“单个比特被正确预测”的概率。在 200 到 1000 帧的规模下,每提高 5 到 10 个百分点,对整个密钥的安全性影响都会非常显著。
我的典型实验结果如下:
| 实验配置 | 分类准确率 | 结论 |
|---|---|---|
| 弱 LCG,未知种子,需暴力搜索 | 100% 或接近 100% | 随机源状态完全可恢复,协议失效 |
| 弱 LCG,只攻击单帧,无历史帧 | 约 60% 到 70% | 有统计泄露,但不至于直接重建 |
| 强随机源(randi/Mersenne Twister) | 约 50% | 攻击失效,协议回到理想安全状态 |
| 弱 LCG,但增加白噪声干扰 | 55% 到 80% 不等 | 攻击效果取决于信噪比 |
从这些结果可以得出一个工程结论:KLJN 协议的安全性非常依赖随机源质量。你甚至不需要让随机源“完全不可预测”,只需要让它的输出接近均匀独立分布,就能把攻击准确率压回 50% 附近。
5.2 要排除的两个假阳性
调试过程中最容易出问题的是两种假阳性。
第一种是特征过拟合。如果你的特征里包含太多跟电阻绝对值直接相关的信息,分类器可能不是学到随机源规律,而是纯粹学到“电阻大噪声大、电阻小噪声小”这种物理映射。粗看准确率很高,其实换个种子就不灵了。解决办法就是用交叉验证,并且同时跑强随机源对照组。
第二种是帧边界对齐错误。Eve 必须知道每一轮密钥交换从哪个采样点开始、到哪个采样点结束。如果对齐错误,特征会被相邻帧混淆,攻击准确率反而下降。这个不能算攻击失败,只能说明你的观测采集假设太乐观。实际场景里,帧同步信息往往是协议公开的一部分,Eve 可以拿到。
5.3 关键参数对结果的影响
在我反复调参的过程中,有三个参数对攻击成功率影响最大:
- 单帧采样点数 (N):(N) 越大,特征估计越稳定,弱随机源的信息泄露越容易被检测到。但 (N) 太大意味着密钥交换速率变慢,合法系统也不会愿意牺牲性能。
- 电阻比例 (R_H/R_L):比例越大,不同电阻配置在幅度特征上分得越开,攻击准确率越高。所以协议设计者不能为了追求物理层区分度,而把电阻比例无限拉大。
- 随机源周期:周期越短,Eve 积累少量帧之后就能看到重复模式。周期超过观测帧数时,单纯靠周期检测就不再有效,这时要看序列相关性和分布偏差。
这三个参数形成了一条三角约束:想提高合法解调性能,就可能削弱安全性;想提高安全性,就必须忍受更慢的密钥速率或者更复杂的随机源。
6. 常见问题与避坑指南
6.1 为什么我的攻击准确率上不去
我见过最多的原因是随机源还不够弱。如果你用的是 Matlab 内建的rand或randi,默认 Mersenne Twister 的统计质量很好,分类器当然上不去。请先换成周期较短、输出相关性明显的 LCG,或者截断低位的 LFSR,先把攻击链路跑通,再逐步加强随机源。
第二个常见原因是特征选得不对。单帧幅度特征在小样本下波动很大,我建议至少加入差分能量特征,或者对功率谱的低频段做积分。很多随机源缺陷会在高频能量或者自相关结构上暴露出来,而不是简单体现在标准差里。
第三个原因是帧数太少。如果只跑几十帧,分类器没有足够样本,准确率波动会很大。建议至少跑 200 帧以上,并用交叉验证稳定评估。
6.2 采样率、位数和窗口长度怎么配
一个经常被忽略的细节是热噪声仿真里的等效带宽。离散仿真里,如果采样率是 (f_s),那么能表示的最高频率是 (f_s/2),所以热噪声标准差里用的带宽可以取 (f_s/2)。这个处理在理论上是近似的,但能让不同采样率下的结果可比。
窗口长度和采样率要一起调。窗口长度决定特征估计的方差,采样率决定你能否捕获电阻噪声的频谱差异。高采样率配合短窗口,和低采样率配合长窗口,虽然采样点数相同,但包含的物理信息不一样。
我常用的一组经验值:(f_s = 1\text{ MHz}),窗口 1 ms,电阻 1 kΩ 和 10 kΩ,温度 300 K。这组参数计算量小,特征区分度足够,跑 1000 帧在普通笔记本上只要几秒钟。
6.3 防御视角:如何在工程上补这个洞
攻击仿真做下来,最该带走的是防御视角。统计随机数生成器攻击之所以成立,根本原因是随机源的输出被直接用于电阻选择。防御可以从三个方向入手。
第一,换掉弱随机源。工程上至少要用密码学安全随机数生成器,最好配合硬件真随机源。第二,增加后处理。即使随机源有轻微偏差,也可以通过哈希、抽取、冯·诺依曼矫正等方式把输出压到接近均匀。第三,在协议层加隐私放大。每一轮密钥交换后不直接使用原始比特,而是做一次哈希压缩,这样即使 Eve 能猜中少量原始比特,也拿不到最终密钥。
我个人在实际操作中的体会是,KLJN 这种物理层安全协议最怕的不是攻击者数学很强,而是实现者默认“物理安全等于全部安全”。你把统计随机数生成器攻击的 Matlab 代码跑一遍,再去看协议里的安全假设,就会明白:随机源质量不是纸上谈兵,它是整个安全链条里最容易被忽略、也最容易被攻击者利用的一环。
最后再分享一个小技巧:这套攻击代码不止能拿来攻击,也能拿来当“随机源体检工具”。接入一个真实随机源之前,先按同样的流程采集一段它驱动的 KLJN 电压波形,跑一遍分类准确率。如果准确率明显高于 50%,说明这个随机源不合格,趁早上后处理,别等到密钥交换出问题再回头查。