差分功耗分析(DPA)原理与实现

说到功耗分析,很多人第一反应是看波形、找特征——那种叫简单功耗分析(SPA)。但实际项目中,你不可能每次都遇到那么“配合”的设备。密钥藏在波形里却看不出来,怎么办?

这时候就该 DPA 上场了。差分功耗分析,说白了就是统计学的魔法。它不依赖肉眼找特征,而是靠一堆采样数据的统计差异把密钥“撬”出来。我最早接触 DPA 时也觉得这东西玄乎,但真正动手写过一次算法后,才发现它其实很朴实。

DPA 的基本思路

你想想看,芯片运行时电流会随数据变化而波动。这个波动很小,单看一条轨迹根本看不出什么。但如果我重复做同一个操作(比如加密同一个字节),每次的数据不同,功耗轨迹就会在某几个时间点出现统计差异。差异的位置和大小,正好反映被处理的数据是谁。

DPA 的核心假设是:功耗与某个中间状态比特位存在相关性。通常是目标设备的输出比特或 S 盒输出。攻击者猜测密钥的一部分,计算这个中间比特,然后根据该比特的取值把功耗轨迹分成两组。

关键结论:如果猜测正确,两组功耗轨迹在某个时间点上的平均差会很明显(出现尖峰)。如果猜错,两组差异几乎为零。

记得我有一次在给一块智能卡做评估时,第一次写 DPA 脚本,分组条件写反了,愣是跑了一天没出结果。后来发现是比特选择方向反了……真的是,有些坑踩一次就记住了。

SPA vs DPA:它俩到底差在哪?

对比项SPADPA
轨迹数量单条或少几条成百上千条
分析方法视觉比对 / 模板统计差值 / 相关系数
抗噪能力弱(噪声淹没信号)强(大量平均抵消噪声)
密钥恢复需已知算法细节可逐位猜测 / 分片攻击
典型场景识别操作序列提取具体密钥字节

简单说,SPA 适合看“形”,DPA 适合看“数”。当我只需要判断芯片在做什么操作(比如你是乘法还是加法),SPA 够了。但如果我要把 128 位密钥一个一个比特抠出来——老老实实上 DPA 吧。

DPA 攻击的标准流程

下面这张图是我当年在项目里画给团队看的,后来它几乎成了我们实验室的 DPA 入门图。你们参考参考:

DPA攻击核心流程 1. 采集功耗迹 2. 选择中间值模型 3. 猜测密钥子集 4. 计算中间比特值 5. 划分两组(0/1) 6. 计算平均差分 7. 观察差分峰值 8. 循环迭代所有猜测 正确猜测 → 峰值出现 错误猜测 → 差分平坦

你看,这个流程是迭代的。从第3步到第8步会跑很多次(比如对每个 S 盒或每半个字节)。我曾经在一个评估项目里,对 AES 最后一轮做 DPA,每个字节需要跑 256 次猜测,8 个字节就是 2048 遍。脚本没写循环?等死吧。

小技巧:采集功耗迹时尽量保证输入数据是随机均匀的。如果输入全都一样,那中间比特分布会偏,差分信号会被淹没。推荐用 PRNG 生成随机的明文或密文。

实现中的关键点和坑

1. 对齐问题

别以为把探头夹上就能出结果。采样触发的抖动会让每条功耗迹的时间轴错开,一平均,尖峰就被抹平了。我遇到过最头疼的一次是某款 MCU 的时钟有 1% 的漂移,乖乖补了 200 行对齐代码才搞定。

  • 最简单的对齐:用同步信号做触发(比如加密开始引脚)。
  • 更鲁棒的方法:对频率做重采样(插值+归一化),或者用互相关找时延。

2. 中间值选择

不是所有中间比特都适合做 DPA。一般选输出阶段的某个 S 盒输出比特,或者密文的某个汉明重量。但注意:如果中间值是由多个比特的异或构成的(比如 DES 的某个函数),那 DPA 的成功率会下降很多。我个人的经验是——尽量选与密钥有直接非线性关系的比特,比如 AES SubBytes 之后的某一位。

3. 统计量选择

最早 Kocher 用的就是简单的差值均值(Difference of Means)。后来大家发现 皮尔逊相关系数 更稳定,尤其在信噪比低的时候。相关系数本质上是把差值做了归一化,峰值更明显。不过它计算量稍大,嵌入式评估时注意一下性能。

表格:常用统计量对比

统计量公式(简化)优点缺点
Difference of MeansAvg(Group1) – Avg(Group0)直观、计算快受信号幅度差异影响
Pearson Correlationcov(X,Y)/(σX·σY)抗噪声、归一化计算开销略大
t-testmean_diff / std_error带显著性检验需要样本量较大

4. 选点范围

不需要对整个时间轴做差分。如果我知道加密的某个子操作发生在第 1000~1500 个采样点,那我只在这 500 个点上做统计就行。一是省时间,二是不需要的点引入的噪声少。嗯,这个在项目里可以节省几个小时的计算。

注意!千万不要在采集阶段混入非加密操作的功耗数据(比如 IO 传输时的功耗)。那会污染你的分组。我早期有一个版本就是因为忘了过滤掉握手脉冲,导致差分一直不收敛。浪费了整整一天。

代码实践(Python 伪代码)

import numpy as np

# trajectories: shape (N, T) N条功耗迹,每条T个采样点
# input_data: shape (N,) 对应的明文或密文
# key_guess: 猜测的密钥字节 0..255

def dpa_attack(trajectories, input_data, key_guess):
    # 假设目标:AES SubByte 输出的一位 (bit 0)
    # 计算中间比特:Sbox[input_data XOR key_guess] 的 bit0
    sbox_output = sbox[input_data ^ key_guess]   # sbox 需要预定义
    bit = (sbox_output >> 0) & 1                # 选最低位

    group0 = trajectories[bit == 0]             # 分组
    group1 = trajectories[bit == 1]

    # 差分 = 平均之差
    diff = np.mean(group1, axis=0) - np.mean(group0, axis=0)
    return diff   # 长度为T的数组,峰值指示正确密钥

# 主循环
for kg in range(256):
    diff = dpa_attack(traces, ciphertext, kg)
    peak = np.max(np.abs(diff))
    print(f"kg={kg:3d},  peak={peak:.6f}")

这段代码套在一个循环里,你就能看到正确密钥的峰值比其他高出一大截。我第一次跑通时,盯着那个尖峰愣了好几秒——嗯,就感觉这方法真的挺神奇的。

一点避坑心得

  • 采样率不是越高越好。太高了数据量暴增,而且高频噪声也会进来。一般用 5~10 倍信号带宽就够了。
  • 注意触发抖动。前面说了,对齐是第一优先级。你可以先画一条轨迹看看重复性,抖得太厉害就先解决触发问题。
  • 多次尝试不同中间比特。有时候 bit0 没反应,换个 bit3 可能就出来了。别死磕。

我自己踩过最大的坑是什么?是采集器触发时忘了关看门狗。芯片跑到一半被复位了,采到的全是垃圾数据。查了两天才发现……从那以后,我每次做 DPA 前都会先发一条空命令看看波形正不正常。

好了,DPA 的原理和实现就这些。下面你可以去动手试试看,拿一条已知密钥的 AES 实现跑一遍上面的流程,感受一下“差分尖峰出现那一刻”的快乐。