RSA功耗分析实战:从SPA到CPA的侧信道攻击与防御

发布时间:2026/8/26 9:41:13
RSA功耗分析实战:从SPA到CPA的侧信道攻击与防御 1. 为什么把RSA功耗分析又翻出来做了一遍RSA功耗分析Power Analysis这个话题说新不新Paul Kocher在1999年前后就把时序攻击和功耗攻击的完整思路整理出来了但直到今天我依然会在实验室里把它从头到尾重新做一遍。原因很简单RSA的数学安全性和物理实现安全性是两码事密钥在芯片里参与模幂运算时芯片的电流消耗会随着数据的变化而变化这种物理泄漏不会因为RSA算法被证明安全就自动消失。这篇文章面对的读者是那些已经在用C语言或硬件描述语言实现过RSA、但又觉得只要算法对就安全的开发者以及正在学习侧信道分析、想把SPA和DPA从理论落到示波器上的安全爱好者。我会把这次重新实验过程中的完整思路、硬件接线、数据采集、攻击脚本和踩坑记录都摊开讲能直接抄作业的那种。为什么是Again因为前几年做这个课题时我用的还是一块老旧的FPGA开发板和一台二手示波器当时只能勉强看到SPA的平方-乘交替特征。这次换了高分辨率采集设备又把DPA和CPA相关功耗分析完整跑通了并且发现早期实现里的一些想当然的防护措施在现代统计攻击面前其实千疮百孔。这篇文章就是这次重新探索的完整记录。2. RSA的功耗到底是从哪里漏出去的2.1 模幂运算的平方-乘实现RSA签名的核心运算是模幂s m^d mod n其中d是私钥指数n是模数。大多数教科书实现会用平方-乘算法Square-and-Multiply从左到右扫描指数比特伪代码如下res 1 for i bitlen-1 downto 0: res res * res mod n // 平方每次都执行 if (d_i 1): res res * base mod n // 乘法仅当当前比特为1时执行这个算法的计算量和密钥中1比特的数量直接相关。如果密钥的汉明重量二进制表示中1的个数较大模幂运算就需要执行更多次乘法。在硬件层面一次模乘是一个复杂的多周期运算多一次乘法就意味着功耗曲线上多出一组特征明显的运算块。这里的关键泄漏点就是那条if (d_i 1)。分支本身不产生功耗差异但分支两侧的指令序列长度差异巨大——平方永远执行乘法看比特。只要攻击者能从功耗曲线上区分平方和乘法这两种运算私钥比特就会像二进制书一样被一页页翻出来。2.2 芯片功耗与数据之间的物理关系要理解功耗为什么和密钥有关就得从CMOS电路的物理特性说起。CMOS门电路在输出状态发生翻转0变成1或1变成0时会对负载电容充电或放电这个过程中会形成一个短暂的直流通路产生动态功耗。动态功耗的大小和翻转的节点数量成正比。换句话说芯片在一个时钟周期内处理的数据如果变化越大瞬时功耗就越高。对于乘法器这类数据通路参与运算的中间值的汉明距离两个连续状态之间发生翻转的比特数直接决定了一条功耗曲线上的信号幅度。攻击里常用的功耗模型有两个汉明重量模型认为运算结果的功耗和结果中1的个数成正比。res * base mod n的结果有较多比特为1时功耗相对更高。汉明距离模型认为功耗和前一个状态与当前状态之间发生翻转的比特数成正比。这个模型更接近真实物理过程在寄存器级仿真中预测精度更高。用个生活化的类比你在厨房里开灯灯的数量越多电表转得越快。芯片里的灯就是那些发生翻转的寄存器和连线攻击者通过测量整块芯片的电流消耗相当于站在电表前面就能推断出屋里开了几盏灯。密钥就是那个决定开几盏灯的隐藏开关。2.3 SPA和DPA两条完全不同的攻击路线功耗分析并不只有一种打法。按照是否需要大量样本和统计处理通常分成两大类攻击类型样本数量核心原理适用场景SPA简单功耗分析单条或少量曲线直接观察功耗曲线形态识别运算序列差异无防护、无噪声、密钥比特直接决定分支DPA差分功耗分析数千到数万条曲线按密钥假设将曲线分组统计两组平均功耗差异寻找尖峰有噪声、有防护通过统计分析放大微弱泄漏CPA相关功耗分析数千到数万条曲线计算功耗模型与实际功耗的相关系数遍历密钥候选需要建立准确的功耗模型比DPA更精细SPA的特点是眼见为实如果平方和乘法的功耗特征差异足够明显一条曲线就能直接读出私钥。但SPA对信噪比要求很苛刻实际芯片上既有噪声又有操作系统的干扰单条曲线的可读性往往很差。而且一旦实现里加入了基本的掩码或盲化SPA就直接失效。DPA则是用统计换信噪比。它不需要你能肉眼区分平方和乘法只需要同一个中间值出现在不同密钥假设下会产生统计上可观测的功耗差异。通过采集大量签名运算的功耗曲线对每个密钥假设计算一个所谓的选择函数把曲线分成两组再求两组平均值的差分。如果某个密钥假设是对的差分曲线会在某个时间点出现明显的尖峰如果假设是错的两条平均曲线会像噪声一样混在一起。这次重新实验我特意把SPA和DPA都完整做了一遍后面会详细说两者的前置条件和各自的坑。3. 实操环境搭建与数据采集3.1 硬件选型与接线做功耗分析第一件事是决定测谁和用什么测。我这次的目标平台是一块STM32F103开发板里面用C固件实现了一个裸奔的RSA签名函数私钥以常量形式烧在Flash里每次通过串口接收消息并返回签名。选择STM32的原因很实际市面上源码资料多、调试方便、功耗特征典型而且很多真实的嵌入式设备就是类似的Cortex-M内核。采集设备我用了ChipWhisperer Lite。这是一块专门为侧信道教学设计的采集板集成了同步ADC、时钟控制和触发IO最大采样率能到100MS/s左右对于主频几十兆赫兹的嵌入式目标来说绰绰有余。如果你手头没有ChipWhisperer用一台带宽100MHz以上的数字示波器加一个电流探头也能搭起来但同步触发和对齐会麻烦很多。接线方面需要注意几个细节在目标板的VCC供电线上串入一个1Ω的采样电阻ChipWhisperer的VIN端直接接在采样电阻两端测量目标芯片的瞬态电流。目标板的复位引脚和串口分别接到ChipWhisperer的IO和串口通道用于同步控制。最关键的是触发信号在固件里进入RSA模幂运算前把一个GPIO引脚拉高运算结束后拉低。这样示波器就能在每一次运算开始时精准捕获曲线对齐会容易很多。3.2 触发信号与采样参数设置触发是功耗分析实验里最容易被低估的一环。没有稳定的触发每条功耗曲线的起点漂移几百个周期后续的数据处理和攻击脚本就可能完全失效。我在固件里加的触发窗口是这样的void rsa_sign(uint8_t *msg, uint8_t *sig) { trig_pin_write(1); // 触发拉高标志模幂开始 montgomery_power(msg, sig); // 执行蒙哥马利模幂 trig_pin_write(0); // 触发拉低标志模幂结束 }采集参数上我设置了50MS/s采样率也就是对每个目标时钟周期采大约3到4个点。采样率太低了平方和乘法的细微特征会被抹掉太高了单条曲线数据量过大采集和存储都吃力。一次完整的1024位RSA模幂在32MHz主频下大约要跑几毫秒以50MS/s采样就是几十万个采样点PC端处理起来会比较吃力所以我实际采集时只保留了模幂曲线的关键区段把不参与运算的通信开销全部裁掉。这里有一个经验不要一味追求高采样率。ADC的采样率提高后噪声带宽也会变大信噪比反而可能下降。我实测下来采样率取目标时钟频率的3到5倍波形已经足够用于SPA做CPA时更是主要靠统计平均过高的采样率不会带来额外收益只会拖慢脚本处理速度。3.3 一条功耗曲线里怎么看密钥采集到第一批曲线后我习惯先用Python直接画出几条原始波形肉眼观察。STM32的功耗曲线里模幂运算是由一个个连续的模乘运算块组成的每个乘法块内部又包含循环移位和加法操作呈现出类似山丘的周期性起伏。SPA的关键特征在于平方运算和乘法运算的执行时间不一样、操作数不一样功耗包络也会有差异。在无防护实现里曲线上的模式是平方、平方、乘法、平方、乘法……而乘法出现的位置恰好对应私钥指数中为1的比特位。如果把曲线按时间轴展开数一数每个模乘块的轮廓再对照模幂的扫描顺序就能把私钥一位一位挑出来。实测下来STM32上裸奔实现的平方-乘RSASPA曲线中乘法块和平方块的区别已经比较明显了但要想从一条曲线里直接分辨出全部1024位密钥还是得做预处理。我在脚本里对曲线做了低通滤波把高频噪声压掉再用差分方法突出每个运算块的边界。滤波后密钥中连续的1比特会表现为一段密集的乘法连发连续0则是平方连发肉眼数起来已经可行。需要说明的是SPA看起来简单实际对硬件环境和固件实现的要求很高。如果芯片外部有稳压芯片把功耗波动平滑掉或者固件里加入了随机延时单条曲线的特征就会被冲散。这也是为什么SPA在真实攻击里很少单独使用但它作为验证功耗泄漏是否存在的风向标价值很大。4. 手上的数据怎么变成密钥4.1 从时域对齐开始无论做DPA还是CPA曲线的对齐都是基础。即使是同一个固件、同一个密钥每次运算的功耗曲线在时间轴上也不是完美重合的因为中断、串口调度、时钟抖动都会让曲线产生微小的偏移。如果曲线没对齐统计攻击时尖峰会被扩散到多个时间点相关系数被稀释攻击就失败了。对齐方法我这次用了最稳妥的互相关对齐先选一条参考曲线然后对每条新曲线做互相关运算找到相关值最大的平移量再按这个平移量对齐。互相关法在曲线数量不多的时候效果很好但计算量较大。如果采集了几万条曲线建议先用粗粒度峰值搜索缩小范围再用互相关做精细对齐。另一种更工程化的做法是在采集时就解决对齐问题比如在固件里将模幂运算前的固定延时拉长并且在触发信号上升沿之后预留足够的空白区让ADC在模幂开始的同一相位开始记录。我在这次实验中采用的是触发对齐软件互相关的组合实测下来对齐后的曲线集在进行CPA时相关系数尖峰比未对齐前提高了近一倍。4.2 CPA相关功耗分析的实现CPA的原理比DPA更直接对每一个密钥候选值预测模幂运算中某个中间值的汉明重量然后计算这个汉明重量序列和实际功耗曲线之间的皮尔逊相关系数。相关系数最大的候选密钥就是攻击者认定的正确密钥。我实验时选择攻击的是模幂运算中第一次乘法操作res * base mod n的中间值。在平方-乘算法中第一轮循环的res初始为1如果私钥的最低位扫描顺序里最先处理的比特为1那么第一轮就会执行一次乘法乘法结果等于base本身这个结果的汉明重量和消息的特定字节直接相关。于是我对每个可能的密钥字节做假设建立功耗模型def hamming_weight(x): return bin(x).count(1) def cpa_attack(traces, plaintexts, model_points): # traces: 对齐后的功率曲线矩阵 # plaintexts: 每次签名输入的消息 key_candidates range(256) best_key, best_corr 0, 0 for k in key_candidates: hypothesis [] for p in plaintexts: intermediate (p * k) % MODULE # 第一轮乘法的输入 hypothesis.append(hamming_weight(intermediate)) corr compute_pearson(traces, hypothesis) if abs(corr).max() best_corr: best_corr abs(corr).max() best_key k return best_key, best_corr实际计算相关系数时主要瓶颈在矩阵运算。用纯Python循环处理几万条曲线、每个曲线几十万采样点会慢到怀疑人生。我改用NumPy的向量化运算先对整批曲线做转置再一次性计算假设序列与每个采样点的相关系数速度提升了上百倍。这是做CPA必须注意的工程细节很多教程不会讲。4.3 一个实验实例的复盘这次实验中我使用的是16位模拟私钥做验证把真实私钥的高位全部置0目标是把私钥从低位开始恢复。先用CPA恢复第一个字节实际攻击了一条128字节消息的曲线集每条曲线包含模幂前两轮运算的波形。第一轮攻击后相关系数尖峰出现在正确密钥候选上最高相关性达到了0.42左右而错误候选的相关性基本在0.05以下区分度非常明显。在拿到低字节后我把攻击窗口向后平移逐字节恢复整个16位私钥整个过程在普通笔记本上几分钟内就跑完了。这里有一个重要心得CPA攻击的关键窗口通常不在模幂运算的整体波形上而在某个特定操作发生的极短时间范围内。我一开始攻击失败就是因为把整个模幂曲线的所有采样点都拿去算相关性正确密钥的微弱信号被大量无关时间点的噪声淹没了。后来把窗口缩小到第一轮乘法的执行区间相关性立刻提升了一个数量级。这说明做功耗分析不能眉毛胡子一把抓必须理解算法流程知道该盯哪个中间值、哪个时间点。5. 防护手段与绕过的博弈5.1 指数盲化Blinding原理既然找到了泄漏源防护思路就很清晰让每次运算的中间值随机化使功耗曲线和固定密钥的关系被打断。最经典的防护就是RSA盲化在签名前先选一个随机数r把消息变成m m * r^e mod n然后用私钥对m做模幂最后再乘上r^{-1}恢复签名。选择随机数 r m m * r^e mod n s m^d mod n s s * r^{-1} mod n盲化的效果是即使攻击者知道消息m也无法预测m的中间值因为随机数r每次都变。我的SPA实验里加入盲化后单条曲线的平方-乘特征依然存在因为分支结构没变但CPA完全失效了——攻击者不知道该预测哪个中间值的汉明重量相关系数无论怎么扫都只是在噪声里打转。需要强调的是盲化不是万能的。它防住了DPA/CPA那一类基于已知消息可预测中间值的统计攻击但SPA依然可能从指令流的长度差异中读出密钥。所以现代密码库在实现RSA时通常是盲化恒定时间模幂一起上而不是只做一层。5.2 蒙哥马利阶梯与恒定时间对付SPA的经典方案是蒙哥马利阶梯Montgomery Ladder。它把平方-乘算法改造成无论密钥比特是什么每轮都执行一次平方和一次乘法只在寄存器分配和结果选择上做差异R0 1 R1 base for i bitlen-1 downto 0: if d_i 0: R1 R0 * R1 mod n R0 R0 * R0 mod n else: R0 R0 * R1 mod n R1 R1 * R1 mod n这样一来功耗曲线上每一轮的结构完全相同SPA就不可能再从有没有乘法上区分密钥比特。但蒙哥马利阶梯引入了新的泄漏寄存器R0和R1中存放的数据在不同比特下是互换的如果攻击者能够区分两个寄存器的物理位置比如通过模板攻击建立寄存器指纹依然可能恢复密钥。这就是侧信道攻防的猫鼠游戏加一层防护就逼攻击者换一种视角。另外真正安全的RSA实现还需要关注数据依赖的时间变化。如果模乘使用蒙哥马利约减但约减次数取决于操作数大小攻击者就能通过测量运算时间恢复部分密钥信息。现代实现里的恒定时间要求是连乘法器内部的进位传播链、内存访问模式都要尽量避免数据依赖。5.3 现代攻击思路为什么要一直Again既然盲化和阶梯已经存在了二十多年为什么我今天还要做RSA功耗分析原因在于实际设备里的RSA实现特别是中小型IoT设备里的商用密码库并不都像学术论文里写得那么完美。举个例子我在实验里发现很多所谓加了防护的固件其实只在固定的几个地方做了随机延时中间值仍然可以预测还有一些实现虽然用了蒙哥马利阶梯但读取私钥时的内存访问模式会暴露指数比特地址泄漏这种泄漏对波形形态的贡献远比运算分支更明显。对这些半吊子防护现代攻击思路早就不是直接做经典DPA了而是发展出了横向攻击Horizontal Attacks、碰撞攻击Collision Attacks和基于深度学习的功耗分析。横向攻击的思路很巧妙它不再横向对比多次签名的同一时间点而是纵向观察同一次签名中不同轮次之间的功耗差异。因为RSA模幂的每一轮都依赖前一轮的结果如果某两轮中间值相等它们产生的功耗特征就会碰撞攻击者可以从碰撞规律中反推密钥。这种攻击即使有盲化也未必被完全防住因为盲化只随机化输入不改变模乘内部的运算结构。我把五种常见防护和它们分别能挡住的攻击类型整理成了表格方便对照防护手段防SPA防DPA/CPA防横向攻击工程代价指数盲化部分有效一般低蒙哥马利阶梯有效部分一般中消息掩码部分有效部分中随机延时缓解缓解无效低恒定时间实现有效不一定有效部分高这个博弈没有终点正是Exploring RSA Power Analysis (Again)的价值所在每次你认为经典攻击已经被防护挡死了就总能在真实代码里找到新的泄漏维度逼着攻击者和防御者同时再往前迈一步。6. 常见问题排查与踩坑记录这个环节我本来不想写的因为写出来等于承认自己走了太多弯路。但回头翻实验日志发现有用的经验几乎都集中在差点放弃的时刻所以还是整理成速查表给后面做这块的同行省点时间问题现象可能原因排查方法曲线完全无特征模幂区段像纯噪声触发点没对齐或采集窗口截错了位置先打印一段原始数据看波形确认触发沿后调整窗口偏移SPA里平方和乘法区分不出来采样率过低或外部稳压芯片削弱了动态电流提高采样率到主频的3倍以上改用更高带宽的电流探头CPA所有候选密钥相关性都很低对齐质量差或攻击窗口选错先做互相关对齐再缩小到第一轮乘法的确定时间窗口相关系数尖峰出现在错误候选上功耗模型不准确或目标有掩码换用汉明距离模型检查使用的中间值是否真的在运算中出现攻击成功后换一批数据又失败固件更新改了RSA实现或系统频率变化重新确认算法实现细节不要假设固件始终不变采集速度极慢几小时都攒不够曲线串口通信和模幂运算串行化每轮等待过长在固件里使用DMA中断流水线先攒数据后处理通信还有一个很容易踩的坑做CPA时如果消息的分布不均匀比如所有消息最高字节都是0那么密钥的某些比特在统计上就永远无法被区分。攻击前要对明文做随机化处理或者采集时让消息尽量均匀分布。我这次一开始只用了固定消息重复采集结果CPA的区分度差到让人怀疑人生换成随机消息后效果立竿见影。再补充一个工程上的细节ChipWhisperer的ADC输入范围需要和目标板的信号电平匹配如果采样电阻上压降太小信号会被量化噪声吃掉如果太大目标板会出现供电不足导致运行异常。我最后用的采样电阻是1Ω在STM32满负荷运行时的压降大约几十毫伏配合ChipWhisperer的放大功能刚好处于最佳量程。最后一个建议如果你是想快速验证一个RSA实现是否有功耗泄漏不要一上来就搭完整的CPA攻击脚本。先花半天时间把SPA做通用肉眼确认平方和乘法的波形差异再用一条脚本自动化数出密钥比特。这一步能帮你建立对整条采集链路的信任后面做DPA/CPA时大部分时间其实是花在调试和对齐上而不是攻击算法本身。这次把RSA功耗分析重新做了一遍最大的体会是密码学里最危险的从来不是数学上的漏洞而是算法从纸面落到电路上时那一层物理实现的翻译损耗。你写的每一行C代码最终都会变成芯片上成千上万个晶体管的翻转而这些翻转的痕迹就是攻击者唯一的线索。拿到一条清晰的功耗曲线时那种密钥原来真的可以被看见的冲击感比任何教科书上的定理都来得真实。