如果你在一间硬件安全评估实验室里待过,大概率见过这样的场景:一台示波器、一根探头、一块正在跑加密算法的开发板。操作员把探头往芯片电源引脚附近一放,屏幕上的电流波形像心跳一样起伏,几分钟后旁边电脑上跳出一串十六进制字符串——私钥,就这样被读出来了。没有拆芯片,没有暴力穷举,甚至没有接触内部总线。这就是功率分析(Power Analysis)攻击,一种针对密码硬件实现的经典侧信道攻击。这篇文章要聊的,就是它在ECC(椭圆曲线密码学)硬件实现上怎么打、为什么能打穿,以及防御方该怎么堵。
1. 先分清两个ECC:本文说的是椭圆曲线密码学,不是纠错码
搜索资料时最容易踩的第一个坑,是ECC这个词本身。硬件领域里,ECC最常见的意思其实是 Error Correction Code——内存纠错码,大家常说的“ecc校验”“服务器内存带ECC”指的就是它。SAP系统里的“ECC”又是另一个东西,指 ERP Central Component,跟密码学半点关系没有。而本文要讨论的ECC,是 Elliptic Curve Cryptography,也就是椭圆曲线密码学。这三个完全不同的概念共用同一个缩写,检索的时候建议直接用全称,否则很容易翻到一堆内存条评测。
椭圆曲线密码学之所以在硬件安全场景里这么常见,是因为它用更短的密钥长度提供足够的安全性。P-256曲线的256位密钥,安全强度大致相当于3072位RSA。这就让ECC在资源受限的设备上特别吃香——智能卡、安全芯片、车规MCU、物联网安全模块、硬件钱包,到处都能看到它的身影。也正因为这些设备往往是物理可接触的,功率分析攻击的路子就走得通。
功率分析利用的,是CMOS电路一个非常朴素的特征:动态功耗与信号翻转挂钩。寄存器里0变1、1变0,会有充电放电电流;翻转的比特数越多,功耗越高。于是外部电源引脚上的电流波形,就是芯片内部状态变化的“心电图”。攻击者的思路很简单:密钥一旦参与运算,就会影响中间值,中间值的变化会体现在功耗波形上。只要用统计方法建立“假设的中间值”和“实测功耗”之间的相关性,就能逐位猜出密钥。
关键的问题在于:ECC在硬件上做的是什么样的运算,为什么恰好能被功耗波形出卖?这就要从标量乘法说起。
2. 标量乘法是侧信道攻击的“震中”:先从运算层面理解密钥是怎么漏出去的
2.1 一个私钥,一层层累出来的公钥
ECC的数学基础就不展开公式了,只讲攻击必须理解的模型。椭圆曲线上有一个公开的基点G,私钥k是一个随机整数,公钥Q = kG,也就是私钥乘以基点,得到曲线上的另一个点。所有ECC协议——ECDH密钥交换、ECDSA签名——底层都离不开这个标量乘法:给定k和G,计算kG。
攻击目标一目了然:从功耗波形里把k还原出来。k是256位整数,如果每一位都能从功耗上区分出来,那么整条密钥就相当于写在波形上了。
2.2 从不带防的Double-and-Add开始看
最简单的标量乘法实现是Double-and-Add,思路跟二进制幂运算一样:从最高位开始逐位扫描k,每一位都做一次点倍(Double,计算2R),如果当前位是1,就再做一次点加(Add,计算R + G)。
写成伪代码是这样:
R = 0 for i from n-1 down to 0: R = 2R # Double,每一位都做 if k_i == 1: R = R + G # Add,仅当当前比特为1 return R问题就出在这个 if 上。点倍和点加在硬件上需要不同数量的有限域运算。以Jacobian投影坐标为例,点倍大约需要10次模乘/模平方操作,点加大约需要16次左右。如果硬件里的模乘器是串行复用的,那么点加的功耗包络就明显比点倍更宽、峰值模式也不一样。示波器上看到的波形,会是一串近乎等间隔的“双倍”脉冲,中间偶尔插入一簇更复杂的“加法”脉冲。你只要在波形上数一数哪些位置多了加法脉冲,私钥的二进制表示就出来了——这就是教科书级的简单功率分析(SPA)。
我最早在FPGA上跑这个实验时,第一次看到那条波形上清晰多出来的“鼓包”,说实话是有点震撼的:安全芯片守了这么多年的私钥,在功耗面前根本没有秘密可言,一条trace就全招了。
2.3 Montgomery阶梯:SPA的对手,但不是终点
Double-and-Add的if语句太招摇了,所以很多实现会改用Montgomery阶梯。它的特点是不管密钥位是0还是1,每一轮都固定执行一次点倍加一次点加,运算序列完全固定:
R0 = 0 R1 = G for i from n-1 down to 0: if k_i == 0: R1 = R0 + R1 R0 = 2 * R0 else: R0 = R0 + R1 R1 = 2 * R1 return R0(这里的具体赋值顺序不同实现有差异,但总模式是每轮都做一个点加一个点倍。)
这样SPA读波形的方法基本失效了——波形上看不到“多出来的add”。但注意,点加和点倍的功耗特征差异并没有消失,只是不再直接对应密钥位。每个循环里,哪个时刻执行点加、哪个时刻执行点倍,仍然取决于当前比特是0还是1。换句话说,密钥位被编码成了“操作顺序”,而不是“操作是否存在”。这种顺序信息在单条trace上可能不好读,但配合统计方法,依然能被恢复出来。
更深一层的问题在于,有些Montgomery阶梯实现在做R0+R1时,需要先判断哪个点是无穷远点,或者需要对R0、R1做条件交换,这些条件操作如果依赖密钥位,功耗上照样会留缝。
所以结论是:标量乘法是整个ECC硬件里最容易泄漏密钥的路径。接下来的问题就是,在实际攻击实验里,波形和数据到底怎么采、怎么处理。
3. 一次完整的功率分析攻击:从实验布设到Trace采集
很多刚接触功率分析的人,把注意力全放在算法和统计上,结果上了实验台发现波形一团糟,什么信号都看不出来。问题往往出在采集环节。采集质量决定了攻击成败,这步值得花大篇幅说清楚。
3.1 实验环境清单
一套能跑的ECC功率分析攻击环境,通常包含这些:
- 目标设备:最可控的是FPGA开发板 + ECC点乘IP核,或者带硬件加速器的MCU(比如STM32系列跑软件ECC也行,但信噪比不如硬件实现高)。我推荐从FPGA开始练手,因为时钟可控、功耗信号干净、代码可观测性强。ChipWhisperer这一系列开源硬件安全工具也值得关注,它把目标板和同步采集做了很好的集成,适合快速验证。
- 功耗测量手段:侵入式方案是在核心供电轨上串联一个小电阻(1Ω到10Ω),用差分探头测电阻两端电压;非侵入式方案是用近场电磁探头靠近芯片表面。侵入式信号最强,但需要改板子;电磁探头用起来方便,但对探头摆放位置极其敏感,每一次微调都会改变波形形状。
- 示波器:带宽建议500MHz以上,采样率至少5GS/s,深内存模式一定要有。以P-256为例,一次标量乘法约256轮迭代,每轮1~2微秒(取决于时钟频率和乘法器设计),总时长约512微秒。10GS/s采样率下,全程采集需要512万个采样点,也就是5.12M点。示波器如果内存不够,会自动降采样,高频细节全没了,后面做统计就白搭。
- 触发信号:最好能在目标板上拉一根IO引脚,在标量乘法开始时拉高、结束时拉低,作为示波器触发源。没有触发信号的话,波形起点会有随机抖动,需要后期对齐,麻烦很多。
3.2 采集流程:“一条trace一条命”
采集的目标是拿到N条功耗波形,每条trace对应一次标量乘法运算。这N条有两种来源:同一把密钥下重复执行相同输入,或者同一把密钥下输入不同的公开数据(比如不同的基点/消息)。后者在CPA里更重要,因为要用输入明文来建模假设功耗。
具体操作一般是这样的:先用Python通过pyvisa或其他仪器控制库,让示波器按触发信号捕获一条波形,然后向目标板发送新的输入,再触发下一条。循环几百到几千次。这个过程看着机械,但有几个细节能直接影响数据可用性:
对齐问题。CRT触发信号如果和实际运算开始之间存在抖动,或者板子上有中断、DMA等干扰,每条trace里标量乘法的起点就可能错位。DPA/CPA对时间错位非常敏感——就像测心率时手表没戴稳,数据点错了一位,相关性就散了。解决方法是采集后做对齐:先选一条参考trace,用滑动窗口中互相关系数找到每条trace相对于参考trace的偏移,再做线性插值重采样。这一步是预处理里最重要的操作。
噪声问题。板上的DC-DC电源噪声、数字逻辑开关噪声都会叠加到功耗信号上。改善手段包括:用低噪声LDO单独给核心供电、探头尽量短、示波器开带宽限制(比如20MHz以上信号不需要就直接滤掉)、对同一条trace做多次采集平均(前提是输入不变,能触发多次)。如果只做SPA,平均几次就能看到明显改善;做CPA的话,只要每次采集的是不同输入,就不能靠平均解决,得靠统计量本身。
采样率与内存取舍。如果示波器深内存不够,另一个思路是分段采集:只采每一轮迭代的那一小段时间窗口,丢掉中间空隙。代价是触发逻辑复杂,但能保住采样率。对硬件实现来说,点加和点倍的区别可能就体现在几十纳秒到几百纳秒的细节里,采样率掉到1GS/s以下,特征基本就糊了。
3.3 一个判断数据质量的土办法
采集完第一批trace,别急着跑算法。先把几条trace画出来,看看有没有明显的周期性结构、有没有和标量乘法迭代次数匹配的重复节拍。如果波形看起来就是一团毛刺,说明要么触发没对齐,要么采集环节有问题。我会先做一次FFT,看主频谱分量在哪里,判断系统主时钟;再放大看单轮迭代的波形,确认能看到运算的边界。这个习惯能省下大量排错时间。
4. 读出密钥的两种姿势:SPA直接判读与CPA统计爆破
4.1 SPA:一条trace,肉眼读KEY
SPA是最直观的攻击方式。前提是算法实现没有做针对性的侧信道防护,比如还是那个朴素的Double-and-Add。
拿到一条干净的trace后,先画出波形,大概长这样:一串相似的脉冲,每个脉冲代表一轮Double;在若干位置上,脉冲后面多了一段更长的波形,那就是Add。你只需要人工或脚本标记出每一个Add出现的位置,对应到密钥的比特索引上,私钥就出来了。
实操里有个小技巧:如果单条trace噪声太大看不清Add,可以在相同输入下重复采集几十条,做算术平均,随机噪声会被压掉,信号成分保留。对SPA来讲,几十条平均已经非常够用了。
SPA最大的优点是“快”,几乎不需要统计计算;最大的局限是只对特定实现有效。Montgomery阶梯、随机延迟、伪操作插入,都能让它失效。
4.2 CPA:用相关系数说话
当单条trace读不出来了,就轮到统计方法上场。这里只讲最实用的相关功率分析(CPA),因为它在实操中比传统的差分功率分析(DPA)更容易用、判读也更直接。
CPA的基本思想是:硬件在执行某个与密钥相关的中间计算时,功耗和中间值的汉明权重(HW,即二进制表示里1的个数)近似成正比。原因前面说过:CMOS翻转功耗跟翻转比特数挂钩。于是攻击者可以:
- 猜一个密钥候选值;
- 用这个猜测值去计算某个已知输入的中间值;
- 算出这个中间值的汉明权重;
- 求这个汉明权重向量和实测功耗迹线每个时间点之间的皮尔逊相关系数;
- 如果相关系数出现明显尖峰,说明这个猜测值就是对的;如果曲线平平的,继续猜下一个。
伪代码大概长这样:
import numpy as np # traces: 二维数组,形状 (N, T),N条trace,每T个采样点 # inputs: 形状 (N,) 的公开输入数据 # key_guesses: 待猜测的密钥候选列表 def hamming_weight(x): return bin(x).count("1") best_guess = None best_corr = 0 for guess in key_guesses: # 1. 用猜测值计算每个trace对应的中间值 mid = [intermediate_func(inp, guess) for inp in inputs] # 2. 转成汉明权重假设功耗模型 hw = np.array([hamming_weight(m) for m in mid], dtype=float) # 3. 和每一条trace的每个时间点求相关系数 corr = np.array([ np.corrcoef(hw, traces[:, t])[0, 1] for t in range(traces.shape[1]) ]) # 4. 找这个猜测下的最大相关峰 peak = np.max(np.abs(corr)) if peak > best_corr: best_corr = peak best_guess = guess print("恢复的密钥字节:", best_guess, "相关峰:", best_corr)这里的intermediate_func要根据具体攻击目标来设计。针对ECDSA签名,可以攻击临时私钥nonce的某个字节;针对ECDH,可以攻击点乘迭代中某个域乘法输出值。选择中间值的原则是:它必须是输入数据的函数,又必须和密钥有关,而且必须在硬件里真实参与过计算。选错中间值的话,相关系数永远起不来。
判读标准:正确密钥候选下,相关系数峰值通常明显高于其他候选。具体数值取决于信噪比,硬件实现信噪比高,正确候选峰值0.3以上很正常;错误候选往往在0.1以下。如果正确和错误候选的差距不明显,一般要先怀疑对齐问题或中间值选得不对。
需要的trace数量:对一个大裸的硬件ECC实现,几百条trace经常就够了;加了一点随机化之后,可能就要上万条。这个数量用公式可以粗略估算,但实践中大家更多是“采一批试一下,不行再加”。注意采集trace是整套流程里最耗时的环节,动辄几万条的话,最好用自动化脚本跑一段时间,期间人别闲着——去检查下一次攻击用什么中间值。
5. 攻击者的绊脚石,也是评估者的考点:对策、绕过和常见坑
5.1 防御方怎么堵漏
做安全评估的时候,我见过不少自称“抗侧信道”的ECC实现,实侧之后发现只是挡了一层。真正的防御得从几个层面同时下手:
算法层:使用固定操作序列的Montgomery阶梯,避免因密钥位产生不同的运算路径。这能挡掉最基本的SPA。
数据层:加随机化,让功耗和真实中间值脱钩。常见做法是标量盲化,比如计算 k’ = k + r·ord(G),其中r是随机数,ord(G)是基点阶;计算用k’,完了再对结果处理。还有点盲化,随机取一个点R,先算(kP + R),再把R减掉。这两种方法都会让功耗迹线每次都不一样,攻击者需要的trace数量急剧上升。
物理层:门级掩码、双轨逻辑(Dual-Rail)通过让电路无论处理什么数据都保持恒定翻转数量来抹掉相关性;时钟随机化让trace对不齐;噪声注入用电源毛刺去掩盖信号。
关键是:不要以为上了某个单一对策就安全了。蒙哥马利阶梯防了SPA,但没防住数据相关性;随机化提高了CPA难度,但搞不定高阶分析。真正的评估一定是在实际采集的trace上,实际跑一遍多阶攻击才知道防护到位没有。
5.2 评估视角的绕过思路
作为评估者,你不能只测一阶CPA就下结论。常见需要测试的绕过手段包括:
- 高阶统计分析:对掩码实现,虽然一阶功耗和中间值不相关了,但中间值可能和两个时间点的功耗组合相关。一阶掩码的典型弱点就是“组合泄漏”,用二阶方差或联合分布就能恢复相关性。
- 预处理增强:先对trace做中心化、带通滤波、PCA降噪,把目标信号从噪声里先捞出来再跑CPA,经常能在trace数量不足时救回一命。
- 模板攻击:在可控的“相似设备”上先建立功耗模型,再对目标设备用极大似然分类。这个对随机化实现的威胁很大,因为特征不一定在“均值”里,而是在“分布”里。
这里补充一句,做这类攻击测试请务必在自己的实验设备、自己的实验室里进行,用于安全评估和学术研究。行业里相关标准和指南可以参考ISO 17825以及Common Criteria的侧信道评估框架,它们定义了从采集到分析再到结果判定的一套规范流程。
5.3 实操中踩过的一些坑
最后集中说说实操里最容易翻车的地方,这些坑我基本都踩过:
坑一:密钥硬编码在固件里。有些同行搭建验证实验时,图省事把测试密钥直接写死在代码里,最后攻击脚本恢复出来的密钥和源码里一字不差,还以为是bug,调了半天。密钥请务必从外部随机生成、通过串口下发,这样攻击结果才可信。
坑二:示波器触发到信号之间有随机延迟。用功耗信号本身做触发时尤其明显,每次触发的点不在同一相位。处理办法是采集后做互相关对齐,或者干脆用板上专用IO做硬件触发。
坑三:采样率被悄悄降了。深内存模式下很多示波器会自动切到4GS/s甚至更低,屏幕上看着波形挺漂亮,细节已经丢了。检查一下示波器的实际采样率和内存深度设置,确保和你的采集时长匹配。
坑四:算法仿真和硬件行为不匹配。你在软件仿真里验证了攻击模型,以为硬件也是这个行为模式,结果CPA相关性上不去。先别急着怀疑攻击算法,回到示波器上看真实波形里点加/点倍的位置,用实际数据修正你的中间值模型。
坑五:术语搜错方向。ECC、ecc校验、SAP ECC三个词一定要分开,不然检索出来的资料大概率是内存条和ERP系统,越查越迷糊。建议直接用“Elliptic Curve Power Analysis”或者“SPA/CPA on ECC”作为关键词搜索。
我在实际做这个项目过程中的体会是,功率分析对ECC硬件实现的威胁,本质上不是算法数学层面的问题,而是“物理实现是否忠实于数学抽象”的问题。密码学教科书写得很干净,功耗不参与运算;但真实芯片在每一纳秒都在翻转电容、消耗电流,任何与密钥有关的操作都会在物理世界留下痕迹。攻击者做的事情,不过是把这个痕迹放大、统计、还原而已。
最后再分享一个小技巧:当你准备在一颗新的目标芯片上跑CPA时,先做一个“已知密钥测试”——把密钥设置成已知值,采一批trace,用你的脚本恢复,看能否恢复出来。如果这步都失败,说明采集链路或中间值模型有问题,别急着挑战更复杂的防护实现。这个习惯能帮你节省大量调试时间。