示波器基础与信号采集

说实话,很多刚接触功耗分析的朋友,第一反应都是:「不就是拿示波器抓个波形吗?有什么难的?」

嗯,我当年也是这么想的。直到第一次用ChipWhisperer抓AES加密的功耗轨迹,发现波形全是噪声,触发怎么也调不对……折腾了整整一个下午。从那以后,我老老实实把示波器这块重新啃了一遍。

这一章,咱们就把示波器在功耗分析中的那些门道,掰开揉碎了讲清楚。

示波器工作原理——它到底在测什么?

示波器说白了,就是一个「电压-时间」记录仪。它把随时间变化的电压信号,画在屏幕上给你看。

但在功耗分析里,我们关心的不是电压本身,而是电流的变化。你想想看,芯片在执行不同指令时,消耗的电流是不一样的。我们在目标板和地之间串一个小电阻(通常10Ω或50Ω),然后用示波器去测电阻两端的电压。根据欧姆定律,电压的变化就反映了电流的变化,也就是功耗的变化。

核心要点:功耗分析测的不是功耗,是电压。通过采样电阻把电流信号转换成电压信号,再用示波器捕获。

我习惯把采样电阻选在10Ω左右。太小了信号幅度不够,太大了会影响目标板正常工作。这个平衡点,我在一个物联网模块的项目上试了好几种阻值才找到。

ChipWhisperer示波器模块配置

ChipWhisperer的示波器模块,其实是一个数字存储示波器(DSO)。它不像台式示波器那样有旋钮和按钮,所有配置都在Python代码里完成。

先看一个最基本的配置代码:

import chipwhisperer as cw

# 连接ChipWhisperer
scope = cw.scope()

# 配置示波器模块
scope.gain.gain = 45      # 增益,单位dB
scope.gain.mode = 'low'   # 低增益模式
scope.adc.samples = 10000 # 采样点数
scope.adc.offset = 0      # 直流偏置
scope.adc.basic_mode = 'rising_edge' # 上升沿触发

这段代码我几乎每个项目都会用,但有个坑——增益不是越大越好。我曾经在一个智能卡分析项目里,把增益调到60dB,结果波形顶部全部削平了,数据全废。后来才意识到,增益太大信号会饱和。

我的经验:增益从30dB开始试,观察波形幅度。如果峰值不到ADC量程的1/3,再逐步增加。一般45dB左右是个不错的起点。

触发信号设置——让每次捕获都在同一位置

功耗分析的核心,是多次捕获相同操作的功耗轨迹,然后做统计分析。这就要求每次捕获的起始点必须一致。触发信号就是干这个的。

ChipWhisperer支持多种触发方式:

  • 上升沿触发:最常用。目标板在执行加密操作前,拉高一个GPIO引脚,示波器检测到上升沿就开始采样。
  • 下降沿触发:偶尔用。比如某些芯片在操作结束时拉低引脚。
  • 软件触发:调试时用。手动控制开始采样。

实际项目中,我一般这样配置触发:

# 配置触发
scope.trigger.triggers = 'tio4'      # 使用TIO4引脚作为触发源
scope.trigger.module = 'basic'       # 基本触发模式
scope.trigger.basic_mode = 'rising_edge' # 上升沿触发
scope.io.tio4 = 'serial_rx'          # 配置TIO4为输入

这里有个容易忽略的地方——触发信号的电平要匹配。目标板可能是3.3V,也可能是1.8V。如果电平不匹配,触发可能不工作,甚至烧坏引脚。我吃过这个亏,一块开发板就这么废了。

警告:连接触发信号前,务必确认目标板和ChipWhisperer的电平是否兼容。ChipWhisperer的I/O引脚是3.3V耐压,如果目标板是5V,需要加电平转换。

采样率与分辨率选择——抓多快?存多细?

这两个参数直接决定了你捕获到的波形质量。

采样率:每秒采多少个点。ChipWhisperer的ADC最高支持100MS/s(每秒1亿个采样点)。

分辨率:每个采样点用多少位来表示。ChipWhisperer是10位ADC,理论上可以区分1024个不同的电压等级。

怎么选?我一般遵循两个原则:

  1. 采样率至少是信号最高频率的5倍。AES加密通常在几MHz到几十MHz,20MS/s到50MS/s就够用了。
  2. 分辨率够用就行。10位分辨率对于功耗分析已经足够,不需要额外处理。

来看一个实际配置:

# 采样率设置
scope.clock.clkgen_freq = 100000000  # 100MHz时钟
scope.clock.adc_src = 'clkgen_x1'    # ADC时钟源
scope.adc.samples = 5000             # 采样5000个点
scope.adc.decimate = 1               # 不降采样

# 实际采样率 = 100MHz / 1 = 100MS/s

为什么采样5000个点?因为AES加密一个块通常需要几百到几千个时钟周期,5000个点足够覆盖整个加密过程。采样点太多,文件会很大,处理也慢;太少,又可能漏掉关键信息。

小技巧:先用最大采样点数捕获一次,观察加密操作持续多长时间。然后根据这个时间,调整采样点数。比如加密持续50μs,采样率100MS/s,那么5000个点刚好覆盖。

首次波形捕获实战——动手抓一条功耗轨迹

理论说了这么多,咱们来点实际的。下面是一个完整的波形捕获流程:

import chipwhisperer as cw
import time

# 1. 连接设备
scope = cw.scope()
target = cw.target(scope)

# 2. 配置示波器
scope.gain.gain = 45
scope.adc.samples = 5000
scope.adc.offset = 0
scope.trigger.triggers = 'tio4'
scope.trigger.basic_mode = 'rising_edge'

# 3. 配置目标板
target.baud = 38400
target.protocol = 'simple'

# 4. 准备明文
key = bytes([0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6,
             0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c])
plaintext = bytes([0x32, 0x43, 0xf6, 0xa8, 0x88, 0x5a, 0x30, 0x8d,
                   0x31, 0x31, 0x98, 0xa2, 0xe0, 0x37, 0x07, 0x34])

# 5. 捕获波形
target.set_key(key)
target.simpleserial_write('p', plaintext)
trace = scope.capture_and_read()

# 6. 查看结果
print(f"捕获到 {len(trace)} 个采样点")
print(f"波形最大值: {max(trace):.3f}")
print(f"波形最小值: {min(trace):.3f}")

运行这段代码,如果一切正常,你会看到类似这样的输出:

捕获到 5000 个采样点
波形最大值: 0.452
波形最小值: -0.123

嗯,看到这个输出,说明你成功捕获到了第一条功耗轨迹。但别高兴太早——我第一次跑这个代码,等了半天没反应。后来发现是触发信号没接对,TIO4引脚悬空了。示波器一直在等触发,当然不会开始采样。

避坑指南:如果捕获不到波形,先检查触发信号。用万用表量一下TIO4引脚,看加密操作时有没有电平跳变。我曾经花了两小时排查代码,最后发现是杜邦线松了。

捕获到波形后,我习惯先画出来看一眼:

import matplotlib.pyplot as plt

plt.figure(figsize=(12, 4))
plt.plot(trace)
plt.xlabel('采样点')
plt.ylabel('电压 (V)')
plt.title('AES加密功耗轨迹')
plt.grid(True)
plt.show()

你会看到一条上下波动的曲线。那些尖峰对应的是AES加密中的各种操作——S盒查表、字节代换、列混合……每个操作都有独特的功耗特征。这就是我们后续做攻击分析的基础。

本章知识体系

下面这张图,把示波器配置的整个流程串起来了:

示波器配置与波形捕获流程 1. 连接设备 2. 配置示波器参数 增益设置 (30-50dB) 采样点数 (5000-10000) 触发模式 (上升沿) 3. 配置目标板 4. 捕获波形 5. 验证波形质量

这张图把整个流程分成了5步。我每次做新项目,都会按这个顺序走一遍。特别是第5步「验证波形质量」,很多人会跳过,结果后面分析时才发现波形有问题,又得回头重抓。

我的习惯:每次捕获后,先看波形幅度是否在ADC量程的30%-80%之间。太低说明增益不够,太高可能饱和。另外,波形应该能看到明显的周期性尖峰,那是加密操作的标志。

好了,示波器这块就讲到这里。你手边如果有ChipWhisperer,不妨把上面的代码跑一遍。亲手抓到第一条功耗轨迹的感觉,还是挺有成就感的。


公众号:蓝海资料掘金营,微信deep3321