3、功耗分析攻击实战:搭建功耗采集平台

各位同学,欢迎来到侧信道攻击的实战环节。说实话,理论讲再多,不如亲手跑一遍数据来得实在。这一章,我们就来搭建一套完整的功耗采集平台,然后对AES加密过程实施DPA攻击。

你可能会问:为什么要自己搭平台?买现成的侧信道分析设备不香吗?嗯,我当年也这么想过。但后来发现,自己搭建的过程,才是真正理解功耗分析精髓的捷径。而且,一套入门级的设备,成本其实比你想象的低得多。

3.1 硬件平台选型与搭建

先说说核心器件。我们需要三样东西:示波器、采样电阻、以及被攻击的目标板。

3.1.1 示波器的选择

我个人习惯用200MHz以上带宽、1GSa/s采样率的数字示波器。为什么是这个规格?因为AES的时钟通常在10-50MHz之间,要捕捉到每个时钟周期内的功耗变化,采样率至少得是时钟频率的10倍以上。

我在项目中遇到过用100MHz示波器采集AES轨迹的情况,结果波形糊成一团,根本分不清哪是哪。后来换了台好点的设备,问题迎刃而解。

小提示: 如果预算有限,入门级可以用Rigol DS1054Z(破解后200MHz),或者二手Tektronix TDS2024C。别买太便宜的玩具示波器,那玩意儿只能看个正弦波。

3.1.2 采样电阻的接法

采样电阻要串联在目标板的电源路径上。具体来说:

  • 位置: 串在VCC与目标芯片之间,或者串在GND回路中
  • 阻值: 1Ω到10Ω之间。我一般用10Ω,信号幅度够大
  • 功率: 1/4W或1/2W就够,别用大功率电阻,感抗太大

我曾经犯过一个低级错误:用了绕线电阻做采样。结果高频分量全被电感吃掉了,采集到的波形像被低通滤波过一样。后来换成贴片电阻,效果立竿见影。

注意: 采样电阻的引线要尽量短,最好直接焊在板子上。用杜邦线飞线的话,会引入额外噪声,影响轨迹质量。

3.1.3 目标板准备

我们用的目标板是一块STM32F103开发板,上面跑着AES-128加密程序。为什么选这个?因为资料多、便宜、而且功耗特征明显。

你需要做的是:

  1. 把采样电阻串在板子的3.3V供电回路中
  2. 用示波器探头夹在电阻两端(差分测量效果更好)
  3. 把示波器的触发信号接到目标板的某个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条是个不错的起点。

关键点: 每次加密的明文要随机,但密钥必须固定。这是DPA攻击的前提——我们通过统计不同明文下的功耗差异,来推断密钥的每一位。

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盒输出被使用的时刻。

经验之谈: 我第一次跑DPA时,结果全是噪声,一个尖峰都没有。后来发现是触发没设好,轨迹根本没对齐。所以,如果攻击失败,先检查数据质量,别急着怀疑算法。

3.3.4 结果验证

攻击完第一个字节,用同样的方法攻击剩下的15个字节。全部攻破后,你就得到了完整的AES密钥。

验证方法很简单:用你恢复出来的密钥加密一段明文,和目标板加密的结果对比。如果一致,恭喜你,DPA攻击成功了。

3.4 本章小结

这一章我们走完了DPA攻击的完整流程:从硬件搭建、数据采集,到Python攻击实现。说实话,第一次成功跑出差分尖峰的那一刻,还是挺有成就感的。

你可能会觉得,这攻击好像也没那么难?嗯,确实。但别忘了,我们是在理想条件下做的——目标板没有防护、采样环境干净、轨迹数量充足。真实场景中,你会遇到各种干扰和防护措施,那才是真正考验技术的地方。

好了,动手试试吧。有什么问题,欢迎交流。


专注资料整理