很多做嵌入式开发的朋友都有过这种经历:板子跑飞了,拉出JTAG线,在Keil里打个断点,打开Watch窗口看变量,一切顺畅得像吃饭喝水一样自然。调试功能之所以被当作“开发者的福音”,是因为它能让你实时看到芯片内部的状态——寄存器的值、内存里的数据、全局变量的变化。但如果你换一个角度想:别人拿到同样的JTAG接口,配合功耗分析(Power Analysis)技术,就完全可以把“调试接口”变成一把撬锁工具,把你固件里的密钥、算法甚至运行流程全都扒出来。这个就是本文要聊的核心话题:在Debug接口上做功率分析攻击,到底是怎么实现的,风险有多隐蔽,以及我们做产品时应该怎么防。
这篇文章适合三类人读:第一类是嵌入式工程师,尤其做产品固件、安全启动、加密算法集成的人;第二类是硬件安全测试人员,想了解如何用JTAG口配合功耗曲线做侧信道评估;第三类是项目技术管理者,需要判断“板子上留个调试口”到底会不会成为安全隐患。我会从调试口的原理讲起,把功率分析攻击的完整路径走一遍,再给出一套我自己实测过的低配攻击平台搭建方案,最后把量产固件里常用的关闭和锁定JTAG的方法连同常见的坑一起列出来。
1. JTAG端口:为什么调试后门最容易变成攻击面
1.1 JTAG到底在调试中扮演什么角色
JTAG的全称是Joint Test Action Group,最早是1990年被IEEE定为1149.1标准,初衷是为了PCB板级测试——通过一条串行链访问芯片内部引脚的状态,做边界扫描。但后来芯片厂商发现这条总线能干的事远不止测板子,于是基于它发展出了完整的调试架构。在ARM芯片上,JTAG-DP加上AP(Access Port)就能让外部调试器直接访问CPU核心、内存控制器和所有外设寄存器。
实际调试时它的价值非常直观。拿STM32F103来说,SWJ接口里最常用的4根线就是TCK(时钟)、TMS(状态机切换)、TDI(数据进)、TDO(数据出),另外还有TRST(复位调试逻辑)和GND/VCC参考。Keil、IAR、OpenOCD这些工具就是通过这4根线,把JTAG状态机推到指定的状态,然后读回CPU寄存器和内存数据。你在Keil的Watch窗口里输入一个变量名就能看到它的值,本质上是调试器通过JTAG口发出一条内存访问请求,芯片内的DAP(Debug Access Port)把目标地址的内容读出来原路返回。
这里要注意一个关键点:JTAG的职责是“调试”,它默认信任接线端的用户。也就是说,只要物理上能接上TCK/TMS/TDI/TDO四根线,调试器就有权限读写片上Flash、RAM、CPU寄存器,甚至可以在Flash里修改代码后让CPU重新执行。对安全设计来讲,这意味着一件事:JTAG口一旦暴露,芯片内部的代码和数据对于持有调试器的人来说就是透明公开的。
1.2 为什么说JTAG是“物理后门”
我这么说你可能觉得夸张,但很多做安全评估的实验室里,拿到一块产品板的第一件事就是翻开PCB找调试座、测试点或者排针焊盘,找到就接上J-Link或ST-Link,看看能不能连上芯片,能不能读Flash。
之所以把它叫“物理后门”,是因为它绕过了所有软件层面的安全机制。你的代码里有权限校验、有防火墙、有加密通信,但通过JTAG口读取Flash时,这些软件保护统统不生效,因为调试接口本身就是芯片硬件提供的、拥有最高权限的访问通道。如果芯片没有做读保护(比如STM32的RDP级别1/2),调试器可以直接把整个固件bin文件导出来,逆向分析算法和密钥。
即使芯片开了读保护,也还有一条更隐蔽的路:在目标设备正常运行加密运算时,通过JTAG线保持调试连接,同时采集整颗芯片的功耗变化。这就是标题里说的“Power Analysis Over JTAG Ports”——调试接口本身不直接泄露密钥,但它给了攻击者两条便利:一是能在不干扰目标运行的前提下做精确的时钟/触发同步,二是能通过调试接口读取中间计算结果来验证攻击是否成功。这两点配合功耗曲线,就能在密钥不落盘、不打印、不存储的情况下,把算法里的密钥猜出来。接下来我就把功率分析这套原理拆开讲。
2. 功率分析攻击:当功耗曲线变成密钥字典
2.1 芯片为什么会在运算时“漏电”
功率分析攻击能成立,根本原因在于CMOS数字电路的功耗和数据有关。CMOS电路里,动态功耗近似可以写成P = α×C_L×V_DD²×f,其中α是翻转因子(数据总线里有多少位跳变),C_L是负载电容,V_DD是供电电压,f是时钟频率。对同一个芯片来说,C_L、V_DD、f基本是固定的,所以瞬间功耗就主要由α决定——也就是“当前这一拍里,总线上有多少个bit从0变到1、从1变到0”。
打个形象的比方:一栋办公楼里,如果整层楼的灯全部同时开关,瞬间电流一定很大;如果只有几个房间的灯在缓慢切换,电流就小得多。芯片里的数据总线就像这栋楼的电路,加密算法执行到某个中间步骤时,中间值里“1”的数量不同,功耗就有微小的差异。这个差异很小,通常只有十几毫伏到几十毫伏,但几百条、几千条曲线叠加起来,统计特征就非常明显。
普通工程师平时根本不会注意到这种波动,因为芯片供电引脚旁边都加了去耦电容。但你只要在供电线路上串一个低阻值的采样电阻(比如10欧姆),用示波器测电阻两端的电压降,就能把这个“功耗的呼吸”看得非常清楚。攻击者捕获的就是这段电压波形,专业叫法叫功耗轨迹(power trace / leakage trace)。
2.2 CPA和DPA:统计学怎么把密钥“筛”出来
知道功耗和数据处理有关之后,下一步就是怎么从功耗曲线里反推出密钥。最经典的分析方法是DPA(差分功耗分析)和它的升级版CPA(相关性功耗分析)。
以AES-128算法为例,第一轮加密时,每个字节的明文P会和对应字节的密钥K做异或,得到的结果X=P⊕K,然后进入S盒做非线性替换,得到输出Y=Sbox[X]。S盒输出的汉明重量(Y的二进制表示中1的个数)会直接反映在芯片功耗上。这里关键点在于:只要K猜对了,Y就能算出来,Y的汉明重量就和实际功耗曲线高度相关;K猜错,算出来的Y就是随机数,和功耗曲线基本不相关。
于是攻击流程就变成:我采集N条不同明文对应的功耗曲线,然后对每个密钥候选字节(0~255共256种)分别计算Y和汉明重量,再拿这个假设的汉明重量序列和采集到的功耗曲线上每个时间点的电压值算皮尔逊相关系数。相关系数的绝对值越高,说明“这个密钥候选值对应的功耗模型越贴合实际功耗”,当前这256个候选值里相关系数最大的那个,就是真正的密钥字节。
这个思路放到整个AES-128上就是:每次都只攻击一个字节,16个字节全部猜完,就拼出完整16字节密钥。实际情况中,只要采集2000~5000条功耗曲线,一个字节的密钥大概在几分钟内就能解出来,即使是破解完整AES-128,配合合理门槛也就是几个小时的事。而且好消息是,这种攻击不要求你拆芯片、不要求你去除芯片封装,只需要能稳定地采集运行功耗。
需要强调的是,功率分析攻击并不需要JTAG口一直在线。JTAG口在攻击里更多的角色是“验证工具”:攻击者可以通过调试口观察中间状态,确认自己猜的中间值是否正确,或者通过调试口触发加密函数的位置来对齐功耗曲线。所以,真正安全的设备应该在出厂后把这个口彻底锁死,让攻击者无从下手。
3. 实战搭建:从零构造一个JTAG功率分析平台
3.1 硬件准备清单
如果你想把功率分析攻击从理论落到实际操作,不需要多昂贵的设备。我最初在实验室里搭的一套方案,核心就是一台200MHz带宽的示波器加上几个电阻电容,总成本控制得很低。
先说目标板。建议选一个引脚排布友好的MCU开发板,比如常见的STM32F103最小系统板、NXP的S32K144EVB,甚至一块Arduino Uno(板载ATmega328P)都可以。目标板上需要跑一个固定的加密程序,最省事的做法是移植一个AES-128实现,在主循环里对固定的明文做加密,加密开始前把一个GPIO引脚拉高,加密结束后拉低。这个GPIO就是给示波器用的“触发信号”。
然后是采集端。如果你手里有示波器,只需在MCU的VCC供电线上串联一个10欧姆采样电阻,示波器探头跨在电阻两端,保存电阻压降波形即可。触发用那个GPIO上升沿。如果没有示波器,还有一个低成本方案:用ChipWhisperer Lite Pro这类专用侧信道采集板,它自带24位ADC和同步采样逻辑,通过USB接电脑就能直接采集电压波形。ChipWhisperer这套工具链也是国内外硬件安全比赛和评估机构用得最多的平台之一,上手门槛比示波器低很多。
最后是调试器。J-Link、ST-Link或OpenOCD都行,作用有两个:一是烧录固件到目标板;二是在验证攻击结果时,通过JTAG/SWD读取芯片里的数据来对比你猜出来的密钥到底对不对。注意,如果目标芯片已经做了读保护,这一步可能会失效,但不影响前期的功耗采集。
3.2 采集功耗曲线的完整流程
整个采集流程我分成四步讲,按顺序操作会顺畅很多。
第一步,硬件接线。把采样电阻串到供电回路里。以3.3V供电的STM32F103为例,我会把3.3V先从稳压芯片或面包板电源引出来,经过一只10欧姆、1%精度、1/4W的电阻,再进到MCU的3.3V引脚。示波器CH1探头接电阻两端(探头地接靠MCU一侧,测的是MCU端相对电源端的压差,也就是电流在电阻上的压降),CH2接GPIO触发引脚。示波器设成上升沿触发,触发电平设在2V左右,时基建议先设成20~50us一屏,采样率至少设到25MS/s以上。存储深度最好能覆盖加密运算的全部窗口,如果加密很快,就把时基设短一点,只采集第一轮AES的功耗也行。
第二步,调试器烧录目标程序。我建议测量程序里加入一个空的延时循环,把AES加密的功耗脉冲拉长一点,方便观察。注意:只有在做安全评估时才这么干,正式产品固件里不要留这种“放慢运算”的调试后门。
第三步,采集曲线。让目标板循环运行加密程序,每次加密使用不同的明文(随机数),示波器每触发一次保存一条波形。用示波器自带功能或者写个Python脚本通过PyVISA接口读取波形数据,将每条曲线保存为numpy数组。我一般采3000条,明文随机生成,保存成一个.npy文件,再保存一份明文列表。这一步是整个实验里最耗时间的,尤其是用示波器串口传输时,一条曲线可能要几十毫秒,3000条需要几分钟。用ChipWhisperer的Capture API能快很多,它内部做了高速USB传输,几千条曲线几秒钟就能收完。
第四步,处理数据。将波形裁剪到加密运算有效区间,去掉触发前的不稳定部分,然后运行CPA算法,得到密钥。我自己用ChipWhisperer的Jupyter环境做这套流程非常顺,里面的chipwhisperer.capture.trace和chipwhisperer.analyzer模块直接封装了曲线对齐、重采样、密钥打分等步骤。如果用示波器采出来的原始数据,也可以自己写脚本处理,核心就是套用上一节讲的汉明重量模型和相关度计算。
3.3 用Python复现一次CPA攻击
我用Python写了一个简化版CPA脚本,方便你理解整个攻击流程。这里只做AES-128第一轮的单字节密钥猜测,实际扩展成16字节只需要在外面套一层循环。
import numpy as np # 假设你已经通过示波器或ChipWhisperer采集了如下数据 # plaintexts: 形状 (N, 16) 的明文数组,每行一次加密的16字节明文 # traces: 形状 (N, samples) 的功耗曲线数组,每条曲线是加密过程的电压采样 # AES-256 S盒,这里只截取部分示意,实际工程中直接替换为完整S盒即可 SBOX = np.array([ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, # ... 完整256字节 ]) def hamming_weight(x): return bin(x).count("1") def guess_key_byte(plaintexts, traces, byte_idx): # 对0~255共256种密钥猜测分别打分 best_corr = 0 best_key = 0 for key_guess in range(256): # 中间值:第一轮AES中 Sbox[明文 ^ 密钥] intermediate = SBOX[plaintexts[:, byte_idx] ^ key_guess] # 用汉明重量作为功耗模型 hypothesis = np.array([hamming_weight(v) for v in intermediate]) # 计算假设功耗序列与每条曲线各采样点的相关系数 # 这里只取曲线窗口中相关性最强的时刻作为该密钥候选的得分 max_corr_per_key = 0 for sample_idx in range(traces.shape[1]): corr = np.corrcoef(hypothesis, traces[:, sample_idx])[0, 1] if abs(corr) > abs(max_corr_per_key): max_corr_per_key = corr if abs(max_corr_per_key) > abs(best_corr): best_corr = max_corr_per_key best_key = key_guess return best_key, best_corr # 对16字节逐一猜测 full_key = [] for i in range(16): k, corr = guess_key_byte(plaintexts, traces, i) full_key.append(k) print(f"byte {i}: key=0x{k:02x}, corr={corr:.3f}") print("recovered key:", bytes(full_key).hex())这段代码的主要目的是让你看清楚CPA的核心逻辑:遍历密钥、算中间值、建立功耗模型、求相关度、取最大值。实际代码里会做两点优化:一是不用对每个采样点都套Python循环,可以矩阵化运算,大幅提速;二是使用chipwhisperer的correlation模块一次性算出所有字节和所有采样点的相关度。如果你用ChipWhisperer环境,还可以直接用chipwhisperer.analyzer里的calculate_traces和correlation接口,省去自己写这些底层循环的功夫。
我第一次跑通这个流程时印象很深:只用了3000条曲线,一个字节的密钥就在几秒内被找出来了。对比一下暴力搜索AES-128完整密钥宇宙级别的复杂度,功率分析的“轻松”程度确实有点吓人。
4. 常见问题与排查技巧实录
4.1 JTAG连接不稳:从“STICKY ERROR”说起
平时做调试和实验时,最常遇到的一类问题就是JTAG连接失败或连接后不稳定。典型报错有这几个:OpenOCD提示JTAG-DP STICKY ERROR,J-Link提示Cannot connect to target,Keil下载时报No target connected。排查顺序我建议遵循“供电、复位、引脚、时钟、速度”五查法。
先看供电。JTAG调试器工作电压和目标板电压要一致,很多板子用3.3V,但调试器输出如果是5V,导致IO电平不匹配,也会连接不稳定。再看复位。有些芯片需要复位引脚处于空闲状态,如果复位引脚被外部元件拉低,调试器就连接不上;也遇到过复位电容过大导致上电时序太慢的情况,解决方法是把复位电容从100nF改小到10nF试试。然后查TMS/TCK/TDI/TDO四根线有没有接反。TJAG连线或者杜邦线超过10cm,高频时钟信号容易畸变,建议把OpenOCD的适配器速度降下来,比如adapter speed 100甚至adapter speed 50。
还有一个很坑的场景:目标板上如果有其他外设把SWD/JTAG引脚复用成了GPIO(比如STM32的PA15、PB3、PB4默认是JTAG引脚),上电后运行了初始化代码把引脚重新配置成普通GPIO,调试器就会丢失连接。这种情况的典型表现是“用ST-Link第一次能下载,程序跑起来之后第二次下载失败”。我在第5章会展开讲如何通过恢复方式解决。
4.2 功率曲线噪声大、采样不同步怎么办
如果采集到的功耗曲线噪声很大,导致密钥每次都猜不对,先不要怀疑攻击算法有问题,优先检查几个物理层面的细节。
第一,采样电阻阻值太小或过大。阻值太小,电压降太微弱,示波器分辨率不够;阻值太大,会拉低芯片供电电压,可能导致运行不稳定。我试过1欧到50欧,10欧是一个比较均衡的值。第二,示波器探头的地线不要用那根长鳄鱼夹线,尽量用探头自带的短地弹簧,可以显著减少空间耦合进来的高频噪声。第三,触发信号和功耗波形的时间对齐要做准。GPIO触发信号虽然有延时,但只要它是固定延时,影响不大;怕的是加密函数内部有随机的延时或中断干扰,导致每条曲线加密起点不一样,这时候需要做“重对齐”处理。ChipWhisperer里有专门的alignment模块,用相关对齐或模式匹配把曲线挪到同一参考点。
另一个容易忽略的细节是平均次数。如果示波器采样率不够高,可以对同一明文重复采集多次再平均,消除随机噪声,提高攻击成功率。但注意:平均会抹掉与中间值相关的信号吗?不会,因为与明文相关的功耗信号是确定性的,平均只会增强它。我用50MS/s采样率、每次加密采集约2000个采样点的配置,跑通AES-128第一轮完全够用。
4.3 OpenOCD相关错误:一个一直让人疑惑的报错
很多人在VS Code里用Cortex-Debug插件配合OpenOCD调试STM32时,会遇到一个报错:cannot perform JTAG flash, because OpenOCD server is not running!
这个报错字面意思是“OpenOCD服务没有运行”,所以无法执行JTAG烧录。但实际原因往往不是OpenOCD没装,而是OpenOCD进程虽然启动了,却没有能成功连上目标板,或者启动后立刻退出了。最常见的几个原因:一是OpenOCD的interface和target配置写错,比如用ST-Link却写了interface/stlink.cfg,但实际用的调试器是J-Link,或者target型号和芯片不匹配;二是USB驱动或权限问题,在Linux下经常需要sudo权限访问USB设备;三是VS Code的Cortex-Debug插件启动OpenOCD的配置路径错误,导致OpenOCD启动参数不完整。
排查思路很简单:先在终端里手动跑一遍OpenOCD命令,比如:
openocd -f interface/stlink.cfg -f target/stm32f1x.cfg如果能正常输出Info : Listening on port 3333 for gdb connections,说明OpenOCD本身没问题,问题出在VS Code传参上;如果直接报错,就看OpenOCD报的具体错误,跟着修。还有一类情况是之前一次OpenOCD会话没有正常退出,端口被占用,把残留的openocd进程杀掉再试。
4.4 高云JTAG识别不到和FPGA调试的特殊坑
做FPGA开发时也经常出现“JTAG识别不到”的问题,比如高云(Gowin)的FPGA用下载器连接时报找不到设备。原因通常有三个:一是USB驱动冲突,高云下载器用的FTDI芯片可能被其它软件装过的驱动抢占,需要在设备管理器里手动更新驱动指向Gowin自带的驱动;二是下载器的线序不对,高云某些开发板的JTAG引脚定义不是标准的TCK/TMS/TDI/TDO顺序,必须先看板卡原理图确认;三是目标板供电不稳,FPGA在上电瞬间如果电源纹波大,JTAG状态机会进入异常状态,按一次复位再重新连接通常能解决。
FPGA和MCU还有一个不同点:FPGA的配置SRAM是易失的,断电后逻辑会丢失,但JTAG接口仍然由芯片硬件提供,所以依然可以通过JTAG读取芯片IDCODE等信息来排查连接问题。如果读取IDCODE都失败,多半是物理链路问题,优先检查线序和电平匹配。
5. 防御方案:把调试后门关上的几种姿势
5.1 MCU上的JTAG禁用和芯片锁定
聊完攻击,来说防守。最直接的做法是在量产固件里关闭JTAG调试端口。以STM32为例,在代码初始化时通过AFIO->MAPR寄存器的SWJ_CFG字段配置调试端口状态。
SWJ_CFG = 0b010:关闭JTAG-DP,保留SW-DP。此时PA15、PB3、PB4释放为普通GPIO,但PA13和PA14仍然作为SWDIO和SWCLK使用,方便生产测试仍然可以通过SWD接口烧录固件。SWJ_CFG = 0b100:完全关闭SWJ,PA13和PA14也释放为普通GPIO。此时JTAG和SWD都无法连接,芯片对外几乎完全封闭调试口。
代码很简单:
GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); GPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable, ENABLE); // 完全禁用SWJ // 注意:如果还需要SWD,改用 GPIO_Remap_SWJ_JTAGDisable但这有一个致命的问题:如果在开发阶段执行了完全禁用,下一次你想通过调试器烧录程序就连接不上了,除非进入系统存储器Bootloader。恢复方式是把BOOT0引脚拉高、复位芯片,然后通过串口ISP连接,用STM32CubeProgrammer把Flash擦除或重新烧录带调试口的固件。所以在量产固件里做禁用之前,一定要预先把ISP操作流程验证好,否则一旦锁死就是开不了盖的砖头。这个坑我踩过一次之后,现在做这类操作都会先把BOOT0拉高的跳线帽放在手边。
对NXP的S32K系列,JTAG保护机制更细。S32K1xx的FTFC模块里有个FSEC寄存器,其中的SEC位域可以从默认的00(unsecured)改为10(secured)状态,这会让芯片进入安全状态:调试接口无法访问Flash和RAM,同时启动时执行安全策略检查。如果FSEC被写成全0(0x00),芯片会被永久锁定,只能通过mass erase复位恢复。这提醒我们:在配置这类保护寄存器时,一定要仔细看手册,一旦烧写错误可能直接报废芯片。
另外,STM32还有RDP(Read Protection)级别保护。把RDP等级设为1,可以阻止通过调试口读取Flash内容,但芯片仍然可以正常运行。RDP等级2是永久性保护,设置后调试口永久失效,而且不能回头。产品量产时可以考虑把RDP等级设为2,将所有调试后门直接焊死。
5.2 PCB级方案和安全评估思路
除了芯片寄存器级别的保护,硬件设计上也有几个很实用防攻击的措施。
第一,尽量不把JTAG接口引到方便接触的位置。很多开发板喜欢留一排10pin调试排针,量产产品上应该去掉这个排针,把调试焊盘做成圆点并埋在PCB内层,或者只保留生产测试用的测试点,并在产品组装后覆盖三防漆或灌封胶。这样能显著提高物理接触难度。第二,在JTAG信号线上串接0欧电阻或磁珠,量产时通过割线来断开调试通路。这个做法成本低、操作直观,样机阶段保留,量产阶段直接去掉不焊接,也不会妨碍功能。第三,电源处理上,MCU的每个电源引脚都要有足够容量的去耦电容,并在PCB布局上尽量缩短电源回路。这个措施不仅能提高芯片稳定性,对侧信道攻击也有一定的削弱作用——它稍微增加了功耗泄漏的难度,因为电容会“吸收”一部分瞬间功耗脉冲。
给自己产品做安全评估时,建议至少做这四步:第一步,按上一节的流程搭建一套功率分析平台,跑一遍CPA攻击,确认能否在合理时间内恢复密钥;第二步,用软件代码检查工具扫一遍固件,确认没有把调试口意外打开;第三步,量产前用RDP等级2或等效机制彻底关闭调试,然后再次尝试连接调试器,确认连接失败;第四步,做一次物理检查,确认板卡上没有暴露的TCK/TMS/TDI/TDO测试点。每一步都记录结果,把“已关闭调试口”作为产品发布前的必选交付物。
6. 实际攻击演示:用STM32F103走一遍完整过程
为了让你对整套技术有更直观的感受,我复盘一下我在实验室里做的一个完整攻击过程。目标板是STM32F103C8T6最小系统,用ChipWhisperer-Lite作为采集板,J-Link作为调试器,目标程序是自己编译的一个AES-128加密循环,每次加密前把PB1拉高,加密结束拉低,作为触发信号。
硬件接线时,我将目标板的3.3V供电剪断,串入一个10欧姆采样电阻,电阻两端接到ChipWhisperer的测量端(它内部有ADC,能直接采集采样电阻上的压降)。PB1接ChipWhisperer的触发输入。J-Link通过SWD接口连接PA13/PA14,先烧录固件,然后在攻击成功后通过J-Link读取目标板Flash里的原始密钥做比对。
程序烧录好之后,我打开ChipWhisperer的Jupyter Notebook环境,先用它的采集API抓了30条曲线快速预览波形,确认触发时刻和加密窗口的位置。看到波形后把时基和采样窗口调整到只包含加密的前5轮运算(第一轮的关键中间值就足够CPA用了),然后开始批量采集。我采了5000条曲线,明文用伪随机数生成器生成,存在一个列表里。采集过程大概用了不到一分钟。
接下来跑CPA。ChipWhisperer的analyzer模块里有一个correlation方法,可以一次性算出所有密钥字节的相关系数热力图。运行结束后,热力图上关键字节的相关系数出现了明显的尖峰,而其它密钥候选值基本平铺。16个字节的密钥全部解出来之后,我通过J-Link读取芯片里存的真实密钥做比对,完全一致。整个过程从接线到出结果,大概花了两个多小时,其中大半时间花在写触发程序和调波形对齐上。如果是一个熟练的攻击者,把硬件平台固定好之后,重复攻击一块同型号新板子的速度会非常快,可能只需要十几分钟。
这个例子不是想说明设备不安全,而是想告诉你:功率分析攻击的工具链和知识体系已经完全普及,一个懂得基础密码学和安全测量的工程师,用一套几千块钱的设备和半天时间,就能把没有防护的加密设备密钥提取出来。防御侧如果不能同步到位,产品在物理攻击面前就是裸奔状态。
7. 几个日常调试中容易踩的坑
聊完完整的攻击和防御,我再分享几个日常调试里和高频搜索词相关的坑,这些坑虽然不是攻击,但和JTAG口的安全性、正确使用有很大关系。
第一个坑是“Keil里怎么看变量”。很多新手在Debug视图下打开Watch窗口,添加变量后看到的是<cannot evaluate>或0x0,以为变量不存在。实际上,只有当前断点停在被测函数作用域内时,局部变量才能立即算出值;全局变量在任意位置都可以看,但需要程序暂停在某个断点或单步执行状态。还有,优化等级打开后,变量可能被编译器优化掉,Watch窗口里显示“not in scope”或“optimized out”,这就是为什么调试时必须把优化等级调到-O0或-Og。这个现象和安全相关的一点是:优化会让芯片实际执行的代码和你写的源码相差很大,功耗曲线也会不一样,所以做功率分析时固件最好用-O2编译,攻击的是真实产品的运行状态,而不是调试配置下的状态。
第二个坑是“STM32禁用了JTAG怎么恢复”。这个问题在论坛里被问过无数次,我也被自己的愚蠢操作坑过一次:写了个GPIO初始化代码,里面不小心把SWJ引脚全部复用作GPIO,下载完一次固件后,第二次连接直接失败。解决办法是:BOOT0拉高,上电复位,让芯片进入系统存储器模式,删除或重烧Flash里的程序即可。如果芯片开启了RDP2的永久保护,那任何方式都救不回来,只能换芯片。所以量产时要谨慎选择保护等级,开发阶段千万别开RDP2。
第三个坑是“高云JTAG识别不到”。除了前面说的驱动和线序原因,还有一个容易被忽略的地方:FPGA的JTAG口如果被配置成普通IO(部分FPGA的JTAG可以通过配置位关闭),也会识别不到芯片。遇到这种情况,要么用配置工具重新加载BIT流并恢复JTAG,要么把BOOT配置引脚设到非用户模式再试。高云在数据手册里有一张很清晰的JTAG配置说明表,排查时建议直接翻到那一页看,比起搜论坛里零零散散的回答要靠谱得多。
第四个坑是和OpenOCD相关的“cannot perform JTAG flash”。这个报错我前面说过,多半是OpenOCD server没有正常启动或连接目标板失败。还有一类场景是,你在VS Code里点了开始调试,但调试器已经崩溃了一次,残留的openocd进程占用着USB设备,新进程启动不了。解决方法是把进程列表里的openocd全部杀掉,或者拔掉调试器重新插一次,再启动调试会话。
这些坑说明一个共同点:JTAG调试口是芯片上的物理资源,它的状态不仅影响开发和调试效率,更是设备安全边界的一部分。开发阶段可以随意折腾,但产品发布前必须把这个口收拾干净。
8. 关于硬件安全评估的一点个人心得
做了几年的固件安全和硬件侧信道评估,我自己最大的体会是:很多产品的安全问题不是因为用了弱算法,而是因为把调试口大大方方地留在了板子上。攻击者拿到一个硬件设备时,第一件事永远是找调试口、串口、SPI Flash,这些都是成本最低的切入点。功率分析攻击听上去高深,实际上不过是用统计学方法把芯片本身的一个物理特性放大了而已,而JTAG口在其中扮演的角色,恰好是给攻击者提供了一把高效验证钥匙的工具。
如果你负责的产品涉及加密通信、支付逻辑、版权保护,或者任何不想让别人逆向出来的算法,建议在设计阶段就把调试口的安全性考虑进去。不要等到样机做出来、固件写好了、已经流入市场了,才去想“这个口会不会有问题”。而且从测试的角度说,哪怕你不做完整的安全评估,至少在量产前把调试口全部关掉,再做一次“能不能重新连上调试器”的验证,这一步能挡掉绝大多数业余攻击者和好奇的逆向爱好者。
最后再分享一个小技巧:如果你需要在开发阶段保留调试能力,但又想在关键时刻模拟“已锁死”的状态,可以在固件里留一个通过串口命令触发的开关,把GPIO_PinRemapConfig的操作放在一个由特定命令触发的分支里。这样平时可以正常调试,需要测试锁定流程时发一串指令就能验证恢复机制是否可靠。但这个方法只适合开发验证,量产固件里一定要删掉这个命令行后门,否则它本身就是更大的安全漏洞。
希望这篇从JTAG调试口讲到功率分析攻击再到防御方案的文章能给你一些真正用得上参考。这类攻击模型的底层逻辑其实不难,难的是在产品研发流程里把“默认安全”当作一项硬性指标去执行。祝大家做大做强、调试顺利,也祝所有准备量产的设备都记得把调试后门关好。