3、功耗分析攻击实战:搭建功耗采集平台
各位同学,欢迎来到侧信道攻击的实战环节。说实话,理论讲再多,不如亲手跑一遍数据来得实在。这一章,我们就来搭建一套完整的功耗采集平台,然后对AES加密过程实施DPA攻击。
你可能会问:为什么要自己搭平台?买现成的侧信道分析设备不香吗?嗯,我当年也这么想过。但后来发现,自己搭建的过程,才是真正理解功耗分析精髓的捷径。而且,一套入门级的设备,成本其实比你想象的低得多。
3.1 硬件平台选型与搭建
先说说核心器件。我们需要三样东西:示波器、采样电阻、以及被攻击的目标板。
3.1.1 示波器的选择
我个人习惯用200MHz以上带宽、1GSa/s采样率的数字示波器。为什么是这个规格?因为AES的时钟通常在10-50MHz之间,要捕捉到每个时钟周期内的功耗变化,采样率至少得是时钟频率的10倍以上。
我在项目中遇到过用100MHz示波器采集AES轨迹的情况,结果波形糊成一团,根本分不清哪是哪。后来换了台好点的设备,问题迎刃而解。
3.1.2 采样电阻的接法
采样电阻要串联在目标板的电源路径上。具体来说:
- 位置: 串在VCC与目标芯片之间,或者串在GND回路中
- 阻值: 1Ω到10Ω之间。我一般用10Ω,信号幅度够大
- 功率: 1/4W或1/2W就够,别用大功率电阻,感抗太大
我曾经犯过一个低级错误:用了绕线电阻做采样。结果高频分量全被电感吃掉了,采集到的波形像被低通滤波过一样。后来换成贴片电阻,效果立竿见影。
3.1.3 目标板准备
我们用的目标板是一块STM32F103开发板,上面跑着AES-128加密程序。为什么选这个?因为资料多、便宜、而且功耗特征明显。
你需要做的是:
- 把采样电阻串在板子的3.3V供电回路中
- 用示波器探头夹在电阻两端(差分测量效果更好)
- 把示波器的触发信号接到目标板的某个GPIO上,在AES开始加密时拉高
这样,每次AES加密开始,示波器就会自动捕获一段功耗波形。
3.2 采集AES加密功耗轨迹
硬件搭好了,接下来就是采集数据。这一步看似简单,但坑不少。
3.2.1 触发设置
示波器的触发是关键。我习惯这样设置:
- 触发源: 选择连接GPIO的那个通道
- 触发模式: 上升沿触发
- 触发电平: 设为1.5V(3.3V逻辑的一半)
- 时基: 设为500ns/div,这样能完整看到一轮AES加密
为什么要用上升沿?因为我们在AES开始时拉高GPIO,所以上升沿就是加密开始的标志。
3.2.2 采样参数
示波器设置好了,还得调一下采样参数:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 采样率 | 1GSa/s | 每个时钟周期能采到20-50个点 |
| 记录长度 | 10K点 | 足够覆盖一轮AES加密 |
| 垂直分辨率 | 8位 | 够用,12位更好但数据量太大 |
| 输入耦合 | DC | 别用AC,会滤掉低频功耗信息 |
嗯,这里要注意:采样率不是越高越好。1GSa/s已经能捕捉到足够多的细节了。再高的话,数据量会爆炸,处理起来很慢。
3.2.3 开始采集
一切就绪后,让目标板反复执行AES加密,每次使用不同的明文和相同的密钥。示波器会捕获到一系列功耗轨迹。
我一般会采集1000到10000条轨迹。数量太少,统计噪声压不下去;数量太多,处理时间太长。1000条是个不错的起点。
3.3 使用Python进行DPA攻击
数据到手了,接下来就是Python上场的时候。我们一步步来。
3.3.1 数据预处理
示波器导出的数据通常是CSV或二进制格式。我们需要把它读进Python,然后对齐、去噪。
import numpy as np
import matplotlib.pyplot as plt
# 读取轨迹数据
traces = np.load('aes_traces.npy') # 形状: (1000, 10000)
plaintexts = np.load('plaintexts.npy') # 形状: (1000, 16)
# 对齐轨迹(如果示波器触发有抖动)
# 这里用互相关对齐,我习惯用scipy.signal.correlate
from scipy.signal import correlate
aligned_traces = []
for trace in traces:
corr = correlate(trace, traces[0])
shift = np.argmax(corr) - len(trace) + 1
aligned_traces.append(np.roll(trace, -shift))
traces = np.array(aligned_traces)
为什么要对齐?因为示波器的触发可能有几个采样点的抖动。不对齐的话,后续的统计结果会变差。
3.3.2 选择攻击点
DPA攻击通常瞄准AES第一轮或最后一轮的S盒输出。我们选第一轮的第一个S盒作为目标。
攻击思路是这样的:
- 猜测密钥的一个字节(0-255)
- 用这个猜测的密钥和已知明文,计算S盒输出
- 根据S盒输出的某一位(比如最低位),把轨迹分成两组
- 计算两组轨迹的平均值之差
- 如果猜测正确,差值波形上会出现明显的尖峰
3.3.3 实施攻击
def dpa_attack(traces, plaintexts, key_byte_pos=0):
"""
对AES第一轮的第一个S盒进行DPA攻击
"""
# S盒查找表(AES标准S盒)
sbox = [
0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5,
# ... 省略完整S盒,实际使用时请补全
]
num_traces = traces.shape[0]
num_samples = traces.shape[1]
# 存储每个密钥猜测的差分波形
diff_traces = np.zeros((256, num_samples))
for key_guess in range(256):
# 计算S盒输出
sbox_out = sbox[plaintexts[:, key_byte_pos] ^ key_guess]
# 根据S盒输出的最低位分组
group0 = traces[sbox_out & 1 == 0]
group1 = traces[sbox_out & 1 == 1]
# 计算差分
if len(group0) > 0 and len(group1) > 0:
diff_traces[key_guess] = np.mean(group1, axis=0) - np.mean(group0, axis=0)
return diff_traces
# 执行攻击
diff_results = dpa_attack(traces, plaintexts, key_byte_pos=0)
# 找出差分幅度最大的密钥猜测
max_peaks = np.max(np.abs(diff_results), axis=1)
correct_key = np.argmax(max_peaks)
print(f"猜测的密钥字节: {hex(correct_key)}")
运行这段代码,如果一切顺利,你会看到某个密钥猜测对应的差分波形上有一个明显的尖峰。那个尖峰出现的位置,就是S盒输出被使用的时刻。
3.3.4 结果验证
攻击完第一个字节,用同样的方法攻击剩下的15个字节。全部攻破后,你就得到了完整的AES密钥。
验证方法很简单:用你恢复出来的密钥加密一段明文,和目标板加密的结果对比。如果一致,恭喜你,DPA攻击成功了。
3.4 本章小结
这一章我们走完了DPA攻击的完整流程:从硬件搭建、数据采集,到Python攻击实现。说实话,第一次成功跑出差分尖峰的那一刻,还是挺有成就感的。
你可能会觉得,这攻击好像也没那么难?嗯,确实。但别忘了,我们是在理想条件下做的——目标板没有防护、采样环境干净、轨迹数量充足。真实场景中,你会遇到各种干扰和防护措施,那才是真正考验技术的地方。
好了,动手试试吧。有什么问题,欢迎交流。