拓冰建站拓冰建站
首页 / 资讯中心 / 正文

RSA功耗分析实战:从SPA到CPA及防护绕过

1. 为什么时隔多年又回头折腾 RSA 功耗分析1.1 上次做 RSA 功耗分析是什么时候老实说我第一次接触 RSA 功耗分析已经是很多年前的事了。那会儿刚入行做嵌入式安全手头是一块老掉牙的 8 位智能卡芯片示波器还是模拟的电流探头靠手工校准每次采数据都要小心翼翼地把探针夹在供电线上一个不小心接触电阻变了整条曲线就得推翻重来。当时做的事情也很原始就是用简单功耗分析SPA去看 RSA 的模幂运算里平方和乘法在功耗曲线上有没有肉眼可见的区别。结果嘛勉强能看到一点规律但噪声一大就完全没法看最后草草收场。那一轮探索留给我的印象是功耗分析这东西门槛高、复现难、玄学成分大。尤其是 RSA 这种密钥长度动辄 1024 位、2048 位的算法一次模幂运算在示波器上就是几百毫秒甚至秒级的波形中间任何一个时钟周期的毛刺都可能让你前功尽弃。当时身边的同事大多也觉得SPA 看个热闹还行真要恢复出完整的私钥指数基本是实验室环境下才能成立的故事。1.2 这次重做有哪些新的动力真正让我决定把这套东西重新捡起来的是这两年工具链的变化。首先是 ChipWhisperer 这一整套开源侧信道分析生态成熟了示波器、目标板、采集软件、分析脚本一条龙几百块钱的硬件就能做到以前几万块设备的采集成效。其次是开源世界里可以跑的 RSA 实现到处都是目标板可以自己烧密钥可以自己控制实验的可复现性比当年不知道高了多少。还有一层原因是我自己工作上遇到了实际需求。最近在评审一些安全模块的 RSA 实现厂商文档上都写着已做功耗分析防护但到底防护得怎么样总不能光看文档拍脑袋。与其去信任那些含糊其辞的描述不如自己搭一套环境实测一遍用攻击者的视角去检验实现的安全性。这个动机和当年纯粹的好奇心完全不同它更功利但也更扎实。另外学术界这几年在侧信道方向也出了不少新东西。机器学习直接被拿来分类功耗曲线深度学习模型可以从一条或几条曲线里把密钥比特咬出来还有人用大数据集做跨设备、跨算法的迁移攻击。重做 RSA 功耗分析不光是复习老手艺也是为跟进这些新玩法打基础。1.3 这篇文章适合谁如果你满足下面任一条这篇应该对你有用刚接触侧信道分析想找一篇讲清楚 RSA 功耗分析为什么能成的文章而不仅仅是零散代码片段。以前试过 SPA 但失败了想知道问题出在采集、预处理还是攻击模型上。你是密码实现的工程师想知道蒙哥马利阶梯、盲化这些防护到底防的是什么攻击者实际打到哪一步会停下来。我尽量把物理原理、代码逻辑和实操链路放在一起讲不搞纯数学推导也不搞黑魔法玄学。每一条结论背后都是我这轮实测里真实跑出来的数据以及踩过的坑。2. RSA 功耗泄漏的根源CMOS 开关活动与模幂的物理指纹2.1 为什么功耗会泄漏密钥很多人第一反应是加解密不是数学运算吗数学运算跑在 CPU 里跟功耗有什么关系这个问题的答案藏在数字电路的物理实现里。现代芯片绝大多数是 CMOS 工艺。CMOS 电路在稳态下几乎不耗电真正显著的功耗来自晶体管开关瞬间对负载电容的充放电也就是所谓的动态功耗。动态功耗可以近似表示成 P α·C·V²·f其中 α 是翻转活动因子C 是负载电容V 是电源电压f 是时钟频率。在同一颗芯片、同一时刻、同等电压频率下唯一能影响功耗的就是 α也就是内部节点翻转的频率。问题来了一个节点翻转不翻转取决于它处理的数据是什么。一个 8 位的加法器处理 0x00 和处理 0xFF内部的状态翻转次数完全不同一个寄存器从 0x55 更新到 0xAA和从 0x00 更新到 0x01消耗的能量也完全不同。这就是数据依赖性功耗的含义。密码学里最经典的两个功耗模型都是基于这个现象建立起来的汉明重量模型认为功耗正比于数据中1的个数适合描述组合逻辑电路处理某个值时的瞬时功耗。汉明距离模型认为功耗正比于前后两个状态之间发生翻转的位数适合描述寄存器、总线这类存储单元更新时的功耗。这俩模型都不精确但胜在简单、好用是侧信道攻击最常用的近似。后面我会详细说它们怎么被用到 RSA 上。2.2 模幂运算的结构性特征RSA 的核心运算是模幂给定底数 m、私钥指数 d、模数 n计算 m^d mod n。直接硬乘是不现实的所以实际实现几乎都用平方-乘算法Square-and-Multiply从指数的最低位或者最高位开始逐位迭代。以最经典的从左到右二进制模幂为例result 1 for i bits-1 downto 0: result result * result mod n # 每一位都做平方 if d_i 1: result result * m mod n # 只有当前位为1才做乘法看清楚这个结构无论指数位是 0 还是 1平方运算都会执行但乘法运算只有指数位为 1 时才执行。这意味着如果我能从功耗曲线上区分出哪一段是平方、哪一段是乘法我就能直接把私钥指数的每一个比特读出来。一个 2048 位的私钥指数无非就是要识别 2048 次平方和大约一半数量的乘法。这还不是全部。平方运算的输入和输出都是同一个值乘法运算的两个操作数不同它们在数据路径上的翻转模式天然有差异。再加上很多实现里平方和乘法调用的底层例程比如蒙哥马利乘法在迭代次数、中间值上也有微小差别这些全部会映射到功耗波形上成为攻击者可以利用的指纹。2.3 从单条功耗曲线直接读到密钥SPA 的极限简单功耗分析SPA的思路就是拿着一条功耗曲线直接靠波形形状来推断运算类型和执行路径。看起来很简单但它能不能成功取决于三个条件时间分辨率够不够。每个平方或乘法运算在波形上要能看出明显的分块特征。要是主频太高、采样率跟不上或者运算时间短到几十个周期以下波形糊成一团SPA 就抓瞎了。噪声够不够低。芯片自身的工艺噪声、电源纹波、测量设备的量化噪声都会叠加到功耗信号上。信噪比太低的时候平方和乘法的差异会被淹没。实现方式够不够诚实。如果实现里用了蒙哥马利阶梯这种固定运算序列的算法或者加了随机延时、伪运算等干扰项SPA 直接读指数这条路就基本被堵死了。我在这次实测中发现针对没有防护的裸 RSA 实现SPA 的成功率其实比我想象中高。尤其是当我把采样率提到每个时钟周期 8 到 16 个点再做一点简单的滤波之后平方和乘法在波形幅度上的差异已经肉眼可见。这个结果让我挺震撼的——一个号称安全的非对称加密算法在物理世界里就这么裸奔了。3. 搭建可复现的功耗采集环境与预处理管线3.1 采集端的选型心得这次重做我没有走以前那种示波器加电流探头拼凑的老路而是直接上了一套 ChipWhisperer Lite配合它自家的目标板。原因很简单这套东西把采样触发、时钟同步、功率测量全部集成好了能极大降低环境带来的不确定性。选型上给几个具体建议一体化的开发板优先。ChipWhisperer Lite 的采样率虽然只有 20MS/s 左右但对付常见的 8 位和 32 位 MCU 已经够用。它的板载目标芯片可以直接跑你烧进去的固件触发信号也是硬件自动产生的省掉了很多示波器触发设置的功夫。如果非要自己搭注意探头位置。测量功耗的标准做法是在目标芯片的供电引脚上串联一个小电阻比如 1Ω 到 10Ω用差分探头测电阻两端的电压降然后换算成电流。串联电阻太小信号微弱串联电阻太大又会给芯片造成明显的压降影响工作稳定性。我实际用下来3.3V 供电的 MCU 挂 10Ω 电阻是兼顾信号幅度和供电稳定性的一个折中点。采样率和带宽要匹配。别以为采样率拉满就一定好。采样率过高会把高频噪声一起采进来增加了数据量但未必增加有效信息。我一般习惯让采样率是芯片主频的 4 到 10 倍采完再做软件滤波。芯片主频是 7.37MHz 时选 20MS/s 左右就很舒服。3.2 采集参数的设置功耗采集不是按下示波器的 START 就完事。有几个参数直接决定了后面分析能不能做下去。**触发点是第一关键。**RSA 运算通常不是上电就立刻跑的前面可能有一堆初始化代码。为了确保每次采集到的曲线都从模幂运算的同一个位置开始我们必须给目标板写一个带触发信号的固件在调用模幂函数的那一刻把一个 GPIO 引脚拉高运算结束再拉低。采集设备看到这个上升沿就开始记录这样每次采到的起点是一致的。ChipWhisperer 的 target 上专门有触发 pin写固件时记得加上。**采多少条曲线是个权衡问题。**SPA 场景下理想情况是一条曲线就能读密钥但噪声存在时往往需要同一条密文下重复采集多次然后做平均来压低随机噪声。对于后续要做的 CPA 攻击需要的曲线数量取决于噪声水平和功耗模型的准确度这个我在第 5 章展开讲。这次实测里采集了 200 条左右曲线每条长度大概是几十万个采样点数据量在百 MB 级别处理起来并没有压力。带宽限制和抗混叠滤波往往被人忽略。示波器前端带宽太高会引入超出分析范围的高频分量。如果设备提供了硬件低通滤波选项把带宽设到信号有效范围的 1.5 倍左右比较合适。软件端我还会再做一次移动平均进一步压低高频噪声。3.3 预处理对齐、降噪与归一化预处理这一步看起来不起眼却是决定攻击成败的分水岭。我见过太多人跑到 CPI 分析那一步才发现相关性一团糟回头查日志才发现问题出在曲线没对齐。**对齐Alignment**是最重要的一环。即使有硬件触发曲线之间仍然可能存在亚采样周期的时间偏移因为芯片内部的时钟抖动、供电电压的瞬时跌落都会影响运算速度。对齐的常规做法是选一个明显的波形特征点比如模幂函数开头的某个固定模式把每条曲线平移使特征点对齐更精细的做法是找到曲线的最大互相关位置做逐点的时移对齐。这次实测里我用的是基于峰值特征点的粗对齐加互相关精对齐两步走效果足够。**降噪Denoising**方面我推荐先做移动平均再做一次简单的低通滤波。移动平均的窗口大小需要根据采样率和信号带宽来调窗口太大把平方和乘法的波形差异也抹平了窗口太小噪声滤不掉。以 20MS/s 采样率为例窗口取 16 到 32 个点比较合适。**归一化Normalization**的作用是消除不同曲线之间的幅度差异。每条曲线的绝对幅度会受供电电压波动、接触电阻微小变化的影响如果不归一化统计攻击时这些幅度差异会被误当成信号变化。我习惯在预处理阶段把每条曲线都减去均值、除以标准差把幅度统一到零均值单位方差。这一步做完观察波形就能明显看到平方和乘法的周期性差异了。几条曲线一叠加波形主体稳定噪声被抑制SPA 的攻击条件基本就具备了。4. 区分平方与乘法SPA 恢复指数比特的实操链路4.1 从一次执行中定位运算序列采样和预处理做完第一件事不是急着去看波形而是先确认你采集的这段曲线确实覆盖了完整的模幂运算。我的做法是这样把整个采样数据按照触发后的时间戳画出来先看全局。正常情况下应该能看到一个大致的包络运算开始阶段功耗偏高中间平稳最后阶段出现收尾。找到第一个平方运算的位置。平方运算的输入是初始值 1输出也是 1属于一种特殊的边界条件波形往往和后面的运算有差异。从曲线中段截取一段典型的波形放大到能把每一个时钟周期都看清楚然后尝试标出周期性的运算单元。这里有个很重要的观察技巧不要直接用肉眼去抠每个波形细节先让计算机帮你算每个时钟周期上的平均功耗。把曲线按时钟周期切块每个周期取平均得到一条更平滑的运算级功耗曲线。在这条曲线上每个平方或乘法会呈现出一个类似梯形或者山峰状的凸起相邻凸起之间的凹陷就是运算切换的边界。RSA 的模幂运算在从左到右的平方-乘算法下每个指数位对应一个平方如果该位是 1 就多一个乘法。所以如果能数清楚凸起的数量和位置再结合 RSA 密钥位长的知识就能推断出哪些位是 1。4.2 如何用代码验证你的判断光靠肉眼看波形判断密钥位容易被个人主观影响带偏。更可靠的做法是用你推断出的密钥去加解密一组数据验证结果的正确性。具体操作分几步。先把模幂函数里指数的每一个比特都记录下来程序里用一个数组存着然后读取功耗曲线上识别出的平方/乘法序列自动恢复出候选指数。最后把这个候选指数和实际私钥指数逐位比对。如果两者一致说明你的识别流程是正确的如果不一致就需要查看到底是哪一位识别错了。我在实验里写了这样一段小工具作用就是把手动标记的运算次数和实际指数对应起来def recover_exponent_from_filtered_trace(trace_op_segments): bit_list [] # 从左到右二进制模幂每个指数位必然有一个平方 # 如果该位后再出现一个乘法则当前位为 1 for segment_type in trace_op_segments: if segment_type MULT: bit_list[-1] 1 else: # segment_type SQUARE bit_list.append(0) # 去掉最高位的初始平方得到指数比特 return int(.join(str(b) for b in bit_list), 2)这段逻辑虽然简陋但足够说明问题指数恢复的本质就是把波形上的平方和乘法序列翻译成比特串。真正的工程实现里更大的难点在于把波形上的凸起自动分割成独立的运算段以及处理序列开头和结尾的边界效应。我这次用了简单的阈值分割加人工检查的方式先自动标出候选边界再抽样人工确认效率和准确率都在可接受的范围。4.3 常见问题噪声太大、时序毛刺、触发偏移SPA 听起来简单实际上失败的方式五花八门。这次实测中我踩到的坑主要有三个**第一个坑是噪声太大导致平方和乘法波形区分不出来。**解决思路不是盲目加大采样率而是回到采集环节去改善信噪比。我试过两个有效手段一是把目标板的供电从那根又细又长的杜邦线换成了粗短导线接触电阻明显下降波形稳定了一大截二是对同一条密文重复采集多次做平均。理论上平均 N 条曲线能把不相关的随机噪声压低到原来的 1/√N我平均了 16 条波形就已经清晰很多了。**第二个坑是功耗曲线上出现莫名其妙的毛刺。**排查下来发现是触发信号和采样时钟之间不同步导致的。解决办法是在固件里让触发引脚的拉高动作和模幂函数的起点保持严格同步不要在函数调用之前插入无用代码。**第三个坑是触发偏移。**有时候示波器或采集器的触发有几十个采样点的随机延迟导致每条曲线的起始位置错开。这种情况靠互相关对齐能解决绝大部分。我会先用第一条曲线作为参考模板然后对每条后续曲线在时间轴上搜索使两者互相关最大的平移量统一搬到对齐位置。这三个坑都不深但任何一个不处理干净后面的 CPA 基本上都会失败。**预处理阶段多花一小时攻击阶段省一天。**这句话是我这次重做最深切的体会。5. 统计化攻击用 CPA 攻破带防护的 RSA 实现5.1 为什么 SPA 不行了就需要 CPASPA 的前提是能在单条曲线上看出平方和乘法的区别。但现实中的 RSA 实现不会这么配合常见的防护手段包括蒙哥马利阶梯Montgomery Ladder无论指数位是 0 还是 1每一轮都执行一次平方和一次乘法运算序列完全固定。SPA 直接失去区分依据。指数盲化Exponent Blinding每次计算前把私钥指数 d 随机化变成 d d r·φ(n)这样每次执行时使用的实际指数不同即使攻击者恢复出了 d 也无法直接导出 d。消息盲化Message Blinding把底数 m 先乘一个随机数 r计算 (m·r^e)^d mod n最后再把 r 除掉。这样每次进入模幂的数据都不相同。傅里叶变换域上的掩码在硬件实现里对中间值加上随机掩码让数据依赖的功耗关系被随机化。在这些防护面前SPA 这条一眼看穿的路径基本废掉。但这不代表 RSA 就安全了。攻击者还有更强大的武器——差分功耗分析DPA以及它的现代升级版相关功耗分析CPA。DPA/CPA 的核心思想是**即使单条曲线看不出任何规律但如果你猜对了一部分密钥那么基于这个猜测去计算的某个中间值其功耗模型会与真实测量值在统计学上显著相关。**猜错了相关性就只是噪声。把密钥分成小块逐个猜测相关性最高的那个候选值就是正确的密钥片段。5.2 建立功耗模型与相关性计算CPA 的攻击目标不是直接猜 RSA 的完整私钥指数那太大了。更实际的思路是攻击底层蒙哥马利乘法的中间值或者攻击 CRT-RSA中国剩余定理优化里的某个模乘子过程把密钥拆成一个字节一个字节地啃下来。为了把话说清楚我用一个简化模型来演示。假设模乘运算中某个中间值 T 是部分密钥 K 和已知输入数据 D 的函数即 T f(K, D)。对每个可能的 K 值比如一个字节就是 256 个候选我都能算出对应的 T然后用汉明重量 HW(T) 作为功耗模型的预测值。接着把这条预测功耗和所有采集到的真实功耗曲线做皮尔逊相关r(K) cov(HW(f(K, D)), P) / (std(HW(f(K, D))) · std(P))其中 P 是某时刻所有曲线在同一个采样点上的功耗值。对 256 个候选 K 各算一遍 r正确密钥对应的那条曲线相关性应该显著高于其他候选。实际操作中还要注意一条功耗曲线有很多个采样点不是每个点都对中间值敏感。常见的做法是只选兴趣点——即功耗模型中方差最大的那几个时间位置。算法逻辑大致如下def cpa_attack(traces, plaintexts, point_range): best_key None best_corr 0 for guess in range(256): hw_model [hamming_weight(f(guess, pt)) for pt in plaintexts] for point in point_range: corr pearson(hw_model, [t[point] for t in traces]) if abs(corr) best_corr: best_corr abs(corr) best_key guess return best_key, best_corr注意这里我取了相关性的绝对值。因为功耗模型的极性取决于芯片是1 越多功耗越高还是1 越多功耗越低两者都可能出现负相关取绝对值可以避免这种极性不确定导致的判断失误。5.3 实际攻击中的参数选择CPA 想跑通有几个参数需要认真选。这里结合我实测的经验给出一组参考值**曲线数量。**噪声越大、功耗模型越粗糙需要的曲线就越多。对一个裸奔的 AVR 实现我用汉明重量模型攻击一个字节的中间值大概几十条曲线就能收敛如果加了简单的掩码或者噪声高一些一百到几百条也是正常的。再多的场景通常是模型本身建错了而不是曲线不够。**采样点范围。**不要对整个几十万点的曲线都算相关性那样计算量大还容易引入虚假相关。先用方差分析法识别出与运算相关的窗口再在窗口内逐点计算 CPA。我这次把兴趣点范围压到了几千个点计算时间从几分钟降到了几秒钟。**功耗模型的选择。**汉明重量模型简单但有时不够准。如果中间值是寄存器更新汉明距离模型往往表现更好因为寄存器更新的功耗取决于从旧值翻转到新值的位数即 HW(old XOR new)。选择哪个模型取决于你对目标芯片内部结构的了解程度。不确定时可以两个模型各跑一遍看哪个的相关性曲线更尖锐。**分窗与并行的技巧。**RSA 模乘的中间值往往分布在多个时钟周期内可以把一个模乘过程分成多个时间窗每个窗口单独跑一次 CPA然后把结果合并投票。这样既能提高容错性也方便诊断哪一段运算的模型建错了。这次实测中我用 CPA 成功恢复出了一个带消息盲化的 RSA 实现中的部分中间值。本来预期需要大量曲线才能见到相关性凸起实际在 200 条曲线时就出现了非常清晰的相关峰。这个结果说明一个残酷的事实**只做盲化、不做运算序列隐藏的实现根本扛不住统计化的功耗分析。**盲化让 SPA 失效但 CPA 通过在统计域里消除随机化影响照样把信息捞了回来。6. 防护与绕过从这次重做中我重新认识到的几件事6.1 常见防护手段的有效性与局限这次把 RSA 功耗攻击从头到尾重做了一遍让我对防护措施的理解也深了一层。蒙哥马利阶梯确实能解决 SPA 的运算序列区分问题但它不能解决数据值相关的功耗泄漏。阶梯算法里每一轮仍然有以秘密值为输入的中间计算这些中间值的汉明重量或汉明距离仍然会泄漏到功耗上。所以蒙哥马利阶梯只防 SPA不防 DPA/CPA。消息盲化能有效破坏 DPA/CPA 的统计基础因为每次跑模幂的底数都变了攻击者没法把多条曲线上的中间值关联到同一个密钥假设上。但它也有被绕过的方法如果攻击者能多次触发同样的随机数比如通过重置设备的随机数发生器或者结合高阶统计分析攻击随机化后的联合分布盲化就可能被击穿。指数盲化在防御对私钥指数的直接恢复上很有效但它会显著增加计算开销。更关键的是盲化只能保护私钥指数如果底层蒙哥马利乘法中还有其他秘密相关的中间值比如 RSA 的 CRT 参数盲化并不能覆盖全部泄漏路径。掩码是目前比较彻底的方案通过在每个中间值上叠加随机掩码来切断数据与功耗之间的直接映射。但掩码不是免费的硬件面积变大、功耗增加、开发和验证复杂度飙升而且已经有多种针对高阶掩码的攻击方法在学术文献中被提出。做工程的人经常说没有绝对安全的密码实现这话放在功耗分析面前尤为贴切。6.2 重做之后对安全性的理解变化这轮实操让我对密码设备的安全性这个抽象概念有了更具体的认识。安全不是一个布尔量不是说用了 RSA 就安全或者做了某个防护就安全。它是由实现层面的无数选择共同决定的编译器有没有把源码里的if 指数位为 1优化成一个带有分支的跳转指令而这个分支在功耗上可区分内存对齐、堆栈布局、寄存器分配有没有让秘密值出现在更容易被探测的位置随机数发生器是否够随机有没有可能在设备重启后复用同一个随机数这些细节没有写在任何一本密码学教科书里但恰恰决定了设备在真实世界中的安全性。这一次重做 RSA 功耗分析最大的收获不是打通了 SPA 和 CPA 的完整链路而是养成了一种用攻击者视角审视实现的习惯。拿到一段密码算法代码先问三个问题秘密值在哪里流动这个流动过程在物理上会不会留下痕迹有没有办法在统计上把痕迹放大6.3 后续可以继续做的方向这轮探索做完我给自己列了几个可以继续深入的方向也分享给有兴趣的读者**方向一把 RSA 功耗分析和机器学习结合起来。**传统的 CPA 需要人工设计功耗模型而深度学习可以直接从原始波形中学习有效特征。已经有论文证明了 CNN 在少量曲线下就能完成密钥分类。这块的工程挑战在于训练数据的数量和标签质量但思路确实比传统方法更通用。**方向二做跨设备、跨平台的通用攻击流程。**现在的分析工具大多绑定特定芯片型号。如果能把采集、预处理、攻击模型抽象成一套标准接口在 AVR 上做的实验可以快速迁移到 ARM 或者 FPGA 平台上那这套流程的工程价值会大很多。**方向三结合形式化验证做防护设计的自动化评估。**不是每个工程师都有精力手工做功耗分析但如果有工具能自动扫描代码里的潜在泄漏路径并模拟出不同防护方案的抵抗能力那对产品设计阶段的帮助会非常大。我现在已经在着手整理这套采集和分析的脚本打算把它做成一个可复用的开源工具包。以后再遇到这个 RSA 实现到底防不防得住功耗分析这种问题直接跑一遍完整流程就有答案了。最后说一点个人体会。功耗分析这个领域表面上看起来是密码学和信号处理的交叉实际上更考验一个人把物理现象转化成工程判断的能力。工具在进步硬件在升级但攻击的核心逻辑没有变**秘密总要在某个物理量上留下痕迹攻击者的工作就是找到那个痕迹并放大它。**这轮重做 RSA 功耗分析对我来说既是一次技术上的回炉也是对实现安全这四个字的一次重新理解。如果你也想动手试试别一上来就想搞多高级的攻击先把 SPA 在裸实现上跑通再逐步加入防护、再逐一击破这条路走下来你对密码实现安全的理解会扎实得多。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门