先说个有点丢人的事。我头一回在自己搭的功耗采集平台上跑Correlation Power Analysis(CPA),用的是 AES-128 第一个 S 盒输出的汉明重量做泄漏模型,跑完相关性矩阵后,最大相关系数对应的密钥字节居然和真实密钥差了整整 0x11。当时我第一反应是示波器触发没调好、波形没对齐,折腾了两天采集参数,结果问题根本不在采集端——是我对“泄漏模型”这四个字的理解太浅了。
这个经历让我意识到一个关键事实:CPA 这个工具虽然叫“相关性能量分析”,但它真正的灵魂是后面那个 LeakageModel。你选的模型假设芯片在哪里泄漏、以什么方式泄漏,直接决定了相关性计算是在帮你找工作,还是在帮你找幻觉。这篇文章我就把自己从原理到实战、从踩坑到收尾的整套经验梳理出来,写给正准备在嵌入式安全、硬件安全方向入门侧信道分析的朋友,也写给那些已经能跑通脚本、但总觉得攻击结果不够稳定的同行。
1. 为什么说CPA的成败一半押在泄漏模型上
1.1 CPA不是“测功耗猜密钥”,而是“用一个假设去匹配现实”
很多人第一次接触 CPA 时,容易把它理解成“采集很多功耗曲线,然后用统计学方法把密钥挖出来”。这个说法不算错,但它少了一个最关键的中间环节——你拿什么去和功耗曲线做匹配?
CPA 的实际工作方式是这样的:你先猜一个密钥字节,然后根据密码算法流程算出某个中间值(比如 AES 第一轮 S 盒的输出),再通过一个泄漏模型把这个中间值映射成“芯片在这个时刻应该消耗多少功耗”的预测值。接下来,你把这条预测的功耗值和实测的功耗曲线做相关性计算。如果猜的密钥是对的,预测值和实测值在某个时间点上会表现出明显的线性相关;如果猜错了,相关性就会像噪声一样散开。
换句话说,CPA 不是“看功耗猜密钥”,而是“用一个假设功耗去匹配实测功耗”。这个“假设功耗”就是泄漏模型的输出。它决定了你和实测数据之间建立的是哪一座桥。
1.2 模型选错时,相关系数照样能出峰值——但是错的峰值
这里必须说一个最容易让人困惑的现象:即使泄漏模型完全不对,相关性计算也能跑出峰值来。我当时第一次跑失败,并不是因为相关性矩阵里没有尖峰,恰恰相反,尖峰非常明显,只是它指向的密钥是错的。
问题出在模型和真实泄漏模式之间的错位。假设芯片实际泄漏的是寄存器翻转的功耗(Hamming Distance),你却用了某个中间值的汉明重量(Hamming Weight)做模型,那么在某些密钥候选下,模型预测值和实测功耗之间也会产生一定的“伪相关”——这种伪相关有时候甚至比真实相关更强,因为它恰好和噪声模式叠在了一起。
所以请你务必记住:相关系数有峰,不等于模型对了。模型与真实泄漏机制之间的匹配程度,才是正确密钥能被挑出来的真正原因。
1.3 先理解最简单的泄漏模型:汉明重量与S盒输出
在讨论复杂模型之前,先把最简单的跑通。以 AES-128 第一轮为例,加密时会把明文分组和轮密钥做异或,然后查 S 盒:
中间值 = Sbox[plaintext[i] ^ guess_key]这个中间值是一个字节,范围从 0 到 255。一个非常经典的泄漏假设是:芯片在计算这个中间值时,总线或者寄存器上“1”的个数越多,功耗就越高。这就是Hamming Weight 模型,它把中间值映射为:
功耗预测 = popcount(中间值)这个模型很简单,但它确实在很多实际芯片上有效,因为 CMOS 电路在翻转时消耗的动态功耗和输出节点的电容充电次数密切相关,而节点上的电平状态往往会和数据的汉明重量挂钩。我第一次跑通 CPA,用的就是它。只是后来我才知道,这个模型不是万能的,具体的适用边界,放到第 4 节细讲。
2. 相关性的数学直觉:怎么从一堆噪声里认出正确密钥
2.1 皮尔逊相关系数公式与直觉理解
CPA 核心的数学工具是皮尔逊相关系数。对某个猜测密钥 g,我们在每个采样时间点 t 上计算预测功耗向量和实测功耗向量之间的相关系数:
r(g, t) = Σ[(h_i - h̄)(p_i,t - p̄_t)] / sqrt(Σ(h_i - h̄)² · Σ(p_i,t - p̄_t)²)其中 h_i 是第 i 条明文对应的模型预测功耗值,p_i,t 是第 i 条功耗曲线在时间点 t 的实际采样值,h̄ 和 p̄_t 分别是预测值和实测值在 i 维上的平均。
直觉理解就是:如果猜测密钥正确,那么实测功耗里应该有一部分变化趋势和模型预测值同步;如果猜测密钥错误,模型预测值和实测功耗之间的变化就没有稳定关系。相关系数衡量的正是这种“同向变化的强度”。它的取值在 -1 到 1 之间,绝对值越接近 1,说明线性关系越强。
2.2 为什么用相关系数而不是直接看平均功耗差
在 CPA 出现之前,更早的 DPA(差分功耗分析)用的是更简单的办法:把功耗曲线按某个中间值的比特位分成两组,然后算两组平均功耗的差。如果猜测密钥正确,这个差值曲线在泄漏时间点会有一个明显的尖峰。
CPA 选择相关系数而不是平均差,核心原因是它能归一化掉功耗幅度的绝对大小对结果的影响。不同芯片、不同采集通道的功耗幅度差异可能很大,直接看功耗差值,阈值很难定;但相关系数天然落在 -1 到 1 的范围内,不同实验之间、不同密钥候选之间更具可比性。另一个原因是相关系数对线性关系的刻画更精细,在噪声较大时能提供更稳健的排序结果。
2.3 多重假设检验:相关系数矩阵怎么看
假设我们要攻击 AES-128 的第一个密钥字节,那么需要猜测 256 个候选值。对每个候选值,都会得到一条“相关系数随时间变化”的曲线。把这 256 条曲线叠在一起看,会得到一个二维结构:横轴是时间点,纵轴是密钥候选值,颜色深浅代表相关系数大小。
正确密钥的特征非常独特:它会在某个时间点(对应泄漏发生的时刻)出现一个明显高于其他所有候选的孤立尖峰。而错误密钥的相关性曲线整体应该贴近零,偶尔有一些小波动,但不会形成稳定的大尖峰。
这里有个实用的判据:算一下“正确候选的峰值”和“所有候选峰值分布的标准差”之间的比值。我用下来比较舒服的经验是,当这个比值超过 5 时,基本可以确信密钥恢复成功;如果只有 2 到 3,那多半是数据集有问题,或者模型选得不对。
3. 波形采集与对齐:模型再好,喂进去的数据不对也白搭
3.1 采样率、触发信号与示波器设置
很多人第一次搭台子时,把精力全放在写 Python 脚本上,忽略了采集端那些“看起来不重要”的设置,结果模型再漂亮也跑不出结果。我建议按下面的基准来起步:
- 采样率:至少保证每个时钟周期能采到 5 到 10 个点。AES 硬件加速模块的时钟通常在几十到几百 MHz,如果示波器采样率只有 100 MS/s,那基本是看不出精细泄漏点的。起步时用 500 MS/s 到 1 GS/s 比较稳。
- 带宽:示波器带宽建议不低于采样率的四分之一,否则高频功耗毛刺会被滤掉。
- 触发:最好能触发到加密开始的那个时刻。比如目标板在加密前拉高某根 GPIO,示波器就用这根 GPIO 的上升沿做触发。触发不稳定的话,波形在时间轴上会错位,相关性直接摊平。
- 通道:测功耗用的是电流探头或近场探头,注意把探头放在芯片电源引脚附近,尽量避免探到板子上其他无关电路的活动。
3.2 trace对齐:触发漂移是最大敌人
如果你发现相关系数尖峰不明显、或者同一批数据跑几次结果飘忽不定,优先怀疑触发漂移。示波器触发本身有抖动,再加上加密算法在开始前可能有一段随机的等待时间,就会导致每条功耗曲线在时间轴上出现几纳秒到几十纳秒的偏移。
处理方式有几种:
- 硬件对齐:修改目标板固件,在加密开始前输出一个极窄同步脉冲,使用这个脉冲做触发。
- 软件重对齐:如果触发点不够准确,可以先跑一次互相关算法,把每条曲线都平移到与某一参考曲线最对齐的位置。
- 重采样插值:对整数倍时钟周期的偏移,可以用重采样修正,但注意不要过度插值引入假的相关性。
我最常用的还是第一种,硬件上给同步脉冲,这是根治问题的方法。软件对齐可以作为辅助,但不要指望它把硬件问题完全弥补。
3.3 预处理:降噪、滤波与降采样
预处理这块容易走两个极端:什么都不做,或者什么都做。
什么都不做的风险是噪声太大,相关系数的峰值被拉低,需要更多曲线才能恢复密钥。什么都做的风险是滤波器把泄漏信号本身也抹掉了,峰值变得平坦。
我建议的预处理顺序是:
- 先做对齐检查,只保留偏移量在可接受范围内的曲线。
- 再做均值滤波,窗口不超过几个采样点,用来去掉高频开关噪声。
- 降采样到每个时钟周期 1 到 2 个点,这样计算量大幅下降,而对 CPA 来说精度损失很小,因为相关性主要靠泄漏时刻的总体趋势,不是靠采样点个数。
- 如果曲线有可见的交流电源干扰,可以做一次带阻滤波,但要先确认这个干扰频率和泄漏信号频段没有重叠。
有一点要特别注意:不要在预处理阶段对曲线做任何非线性的操作,比如硬限幅、削顶、逐点归一化后再归一化。非线性变换可能人为制造相关性或破坏本来存在的相关性,结果很容易误导判断。
4. 汉明重量还是汉明距离:教你选对泄漏模型的判断准则
4.1 Hamming Weight模型的适用条件
Hamming Weight 模型假设功耗与“当前总线/寄存器上 1 的个数”相关。在什么条件下它最贴近现实?
一种典型场景是芯片使用预充电总线结构,或者内存单元在读取数据时,位线上的放电量和数据中“1”的个数成正比。另一个常见场景是某些 RISC 微控制器的 GPIO 翻转行为,端口输出驱动能力较强时,打开的输出引脚越多、动态功耗越大。
我自己的经验是:如果你不确定芯片内部结构,先用 Hamming Weight 模型做一个快速侦察。实现简单、计算量小,如果跑完能看到明显的孤立尖峰,那说明泄漏模式确实与数据值相关,只是具体机制还需要细化;如果完全看不到尖峰,那再往 Hamming Distance 方向想。
4.2 Hamming Distance模型:寄存器跳变才是功耗来源
Hamming Distance 模型的假设是:功耗与“某个寄存器或总线上的状态从一个时钟周期到下一个时钟周期的翻转位数”成正比。用公式表示就是:
HD(旧状态, 新状态) = popcount(旧状态 XOR 新状态)这个模型比 Hamming Weight 更贴近大多数 CMOS 数字电路的实际物理过程,因为动态功耗主要来自节点电容的充放电,而充放电是由电平翻转引起的。也就是说,一个始终保持 0xFF 的寄存器,用 Hamming Weight 模型看功耗可能很大,但它实际可能根本不消耗多少动态功耗,因为它一直没翻转。
所以如果你的目标芯片是典型的同步数字电路,Hamming Distance 模型通常比 Hamming Weight 模型更容易成功。代价是需要知道中间值的“旧状态”是什么。在 AES 第一轮场景里,旧状态可以是 S 盒输出发生前的寄存器值,新状态可以看作 S 盒输出写入寄存器后的值;如果中间值在“异或之后、查表之前”的位置,旧状态则取决于上一轮或上一条数据的状态。
4.3 多模型对比:同时跑多个模型看分布
既然模型选错的代价这么大,一个非常实用的做法是:一次攻击同时跑多个候选模型,然后比较它们的相关系数分布。
我在攻击一个未知固件的某国产 ARM 核 MCU 时,遇到过一种情况:Hamming Weight 模型跑出的最大相关系数只有 0.23,而且排第二的候选密钥和它只差 0.01,差一点点就误判;而 Hamming Distance 模型跑出的最大相关系数是 0.41,正确密钥的峰值明显孤立。那个项目之后,我养成了一个不太费事但很有效的习惯:第一轮先并行跑 4 到 5 个模型,其中包括:
- 中间值字节的 Hamming Weight
- 中间值寄存器跳变的 Hamming Distance
- 中间值字节的某一比特位(比如最低位)
- 中间值字节的倒码(反向后 Hamming Weight)
观察哪个模型的正确候选峰值最孤立、最稳定,就说明哪个模型的假设最贴近真实泄漏。这一步不费太多时间,但能帮你排除很多“攻击失败到底是芯片不泄漏还是模型不对”的疑惑。
4.4 不知道中间值在哪一层时,怎么用手动搜索
有时候你并不清楚芯片在哪个计算环节发生泄漏。比如一个安全的 AES 加速器,可能内部把 AddRoundKey 和 SubBytes 合并成一个周期完成,这时你单独用“异或后”的值做中间值就可能对不上。
解法是做分层扫描:对每个可能的时间点 d,扫描多个中间值函数和预测模型,计算它们与实测功耗的相关性。把每个候选在“最佳时间点”的相关系数画成一张热力图。这张图能直观看出哪个中间值层级的泄漏最高、发生在哪个时间位置。我在某款智能卡安全芯片上就用这个办法找到了与轮密钥第 9 轮加解密相关的强泄漏点,而这在正常加密流程里已经离输入数据很远了。
5. 四段翻车实录:我踩过的CPA实战大坑
5.1 采样率不够导致相关峰被抹平
第一次正式做 CPA 时,我用的示波器采样率只有 250 MS/s,目标板工作时钟约 48 MHz,理论上每个时钟周期能采 5 个点,看起来够用。但实际跑下来,相关系数峰值只有 0.18 左右,且位置在时间轴上很模糊,像被揉过一样。
排查后发现,问题在于目标芯片内部有较快的组合逻辑路径,泄漏信号在时钟边沿后的几百皮秒内就完成了大部分翻转,而 4 ns 的采样间隔把瞬态功耗毛刺平均掉了。后来我把采样率提高到 1 GS/s,同样的曲线数量下,相关系数峰值直接翻倍到了 0.4 以上。这个经验告诉我:“每个时钟周期采几个点”只是底线,对于快速逻辑电路,采样点越多,越能捕获到精细的泄漏窗口。
5.2 触发位置漂移导致“伪失败”
另一次翻车更有迷惑性:模型和数据都没问题,正确密钥候选的相关系数曲线确实在所有曲线中排第一,但峰值的绝对值只有 0.12,和错误密钥候选几乎拉不开差距。我一度怀疑是目标板的功耗输出太弱。后来我在软件里统计了每条曲线的触发点位置偏移,发现偏移量的分布宽度竟然接近一个完整时钟周期。
原因是硬件同步脉冲引脚旁边有一条数据线,在加密启动时数据线上的活动会干扰同步脉冲的边沿时刻判断。解决办法很简单:在固件里把同步脉冲宽度从原来的 100 ns 加宽到 500 ns,同时把同步引脚的驱动能力配置调强。这样触发抖动从几百皮秒级降到几十皮秒级,再次跑 CPA,正确峰值一下子冲到了 0.52。别小看这个“波形能不能对齐”的问题,它可能是你在实战中遇到的最隐蔽的失败原因。
5.3 模型用反:把最高相关给了一个错误密钥
回到文章开头那个翻车事故:当时我用 Hamming Weight 模型,中间值选的是 S 盒输出,跑出来的 256 条相关系数曲线里,有一个错误密钥候选的峰值非常突出。后来我把中间值换成 AddRoundKey 之后的异或值,再试了一次 Hamming Weight 模型,正确密钥才真正成为最高峰。
原因复盘起来很清晰:那个芯片的功耗泄漏主要发生在 AddRoundKey 到 S 盒输入的组合逻辑输出上,而我对“S 盒输出”建模型时,错误密钥在特定明文分布下也能与实测功耗形成虚假相关。错误模型加上有限样本量,就可能在错误候选上造出一个足够大的尖峰。从那之后,我再也不拿某一次攻击结果当结论,至少要用两组不同明文集合交叉验证,确认正确密钥在两个集合下的相关峰都出现,才算真正成功。
5.4 只盯着最大值,被偶然伪峰骗了
还有一种很难察觉的坑:正确密钥候选的相关系数确实排第一,但第二名的峰值也非常接近,且出现在不同的时间点。如果你只报告“最高相关系数对应哪个密钥”,就可能忽略这种不确定性。
我的处理方法是:报告“Top 5 候选密钥及其峰值位置”,然后观察正确密钥的峰值位置是否在所有候选里具有物理一致性。正确密钥的尖峰往往出现在一个稳定、可解释的时间点,比如 S 盒输出后的第一个采样窗口;错误密钥的伪峰则经常在时间轴上到处飘。这个判断维度比单纯比数值更可靠。
6. 攻击收尾:如何确认密钥没有白猜以及还能往哪走
6.1 用第二密钥字节交叉验证
当第一个密钥字节看起来已经被恢复后,很多人会立刻把结果写进报告。我劝你先停下来,做一次交叉验证。做法很简单:用同样一组功耗曲线,把目标从第一个 S 盒输出改成第二个 S 盒输出,重复一次 CPA。如果第二个字节也能恢复,而且它的峰值时间点和第一字节的峰值时间点在算法流程上逻辑自洽,那这次攻击结果才经得起复现。
如果第二个字节失败,一种常见原因是两条 S 盒查表路径的泄漏位置非常接近,相关性计算互相干扰。这时可以尝试对时间窗做裁剪,只保留第一条路径泄漏附近的时间段去做第二字节攻击。交叉验证的作用不只是确认密钥,也是帮你校准采集系统的可靠度。
6.2 从一条曲线到一个结论:trace数量与成功率曲线
另一个值得画出来的东西是成功率曲线:横轴是用于攻击的功耗曲线数量,纵轴是“正确密钥候选排第一”的比例。做法是把同一批数据分成若干份,比如 5 条一组、10 条一组、20 条一组,每组独立跑 100 次 CPA,统计正确密钥排第一的频率。
这条曲线非常有价值。它能告诉你实际攻击时最少需要采集多少条曲线,也成为评估泄露强度的量化指标。举个例子,如果 50 条曲线就能达到 95% 成功率,那说明该设备对 CPA 非常脆弱;如果到了 10000 条曲线成功率仍然只有 40%,那要么是你选错了泄漏模型,要么是采集质量达不到要求。很多论文里用相关系数随 trace 数量的收敛速度来衡量抗侧信道能力,原理就在这里。
6.3 从CPA出发还能往哪走:模板攻击与互信息分析
CPA 跑通之后,你有两条自然的进阶方向。
一条是模板攻击。CPA 用的是统计线性相关,但如果芯片的泄漏是非线性的,比如某些工艺节点下功耗与数据值呈抛物线关系,CPA 的线性假设就不够用了。模板攻击会对每个中间值类别的功耗分布做精细建模,可以用更少的曲线达到更高的成功率,代价是需要一个可以完全控制密钥的训练阶段。
另一条是互信息分析。互信息不假设线性关系,能捕捉到更宽泛的统计依赖。在 CPA 相关性很弱但真实泄漏存在的情况下,互信息分析往往还能看到一些希望。我个人的经验是:先把 CPA 用好、用透,再用互信息做交叉验证,能大幅降低误判率。
最后想分享一个小的实操习惯:每次攻击前,把采集参数、模型类型、trace 数量、相关系数峰值、候选密钥排序全部存成一个 JSON 元数据文件。别靠脑内记忆,人一旦连续采集几组数据,很容易弄混哪条是哪个模型跑的。发生过太多次“这个尖峰好像昨天见过,但记不清是哪个配置下跑出来的”这种窘境。把这些细节记录下来,复盘时会轻松得多——项目的实际推进速度,往往就快在这些不起眼的日常规范里。