4、Python工具链:PySerial通信、NumPy数据处理、Matplotlib可视化、Scipy信号分析
做智能卡渗透测试,说白了就是跟一串串的字节流打交道。你发送一条APDU指令,卡片返回一串响应,中间可能夹杂着时序噪声、电压波动、甚至电磁辐射的痕迹。这时候,光靠肉眼盯着十六进制看,效率太低了。
我个人习惯把Python工具链分成四个层次:通信层、处理层、分析层、展示层。下面这张图能帮你快速建立整体认知:
4.1 PySerial:跟智能卡对话的“嘴”
PySerial 是 Python 里最成熟的串口通信库。智能卡读卡器(比如 PC/SC 兼容的接触式读卡器、或者通过串口转 TTL 的调试板)通常都暴露为一个虚拟串口。
安装很简单:
pip install pyserial
我常用的初始化模板长这样:
import serial
import serial.tools.list_ports
# 先枚举所有串口,找到读卡器
ports = serial.tools.list_ports.comports()
for p in ports:
print(f"{p.device}: {p.description}")
# 打开串口,波特率通常用 115200 或 9600
ser = serial.Serial(
port='COM3', # Windows 下是 COMx,Linux 下是 /dev/ttyUSB0
baudrate=115200,
bytesize=8,
parity='N', # 无校验
stopbits=1,
timeout=0.5 # 超时0.5秒,别设太长
)
ser.setDTR(False) 和 ser.setRTS(False)。
发送 APDU 和接收响应的核心代码:
def send_apdu(ser, apdu_hex: str) -> str:
"""
发送十六进制APDU,返回响应
例如: send_apdu(ser, "00A4040008A000000003000000")
"""
data = bytes.fromhex(apdu_hex)
ser.write(data)
# 读响应:先读头部(通常4字节),再根据长度读后续数据
response = ser.read(256) # 保守读取
return response.hex().upper()
# 示例:选择MF
resp = send_apdu(ser, "00A4040008A000000003000000")
print(f"响应: {resp}")
嗯,这里要注意:超时时间很关键。智能卡有些操作(比如内部认证、计算签名)可能需要几百毫秒甚至几秒。我一般会在发送耗时命令前临时把 timeout 调大,完事再改回来。
4.2 NumPy:把字节流变成“数据”
PySerial 拿到的是一串字节。但做侧信道分析时,我们需要的是数值数组——比如电压采样点、电磁辐射强度、或者时序间隔。NumPy 就是干这个的。
import numpy as np
# 假设我们从示波器或逻辑分析仪拿到了采样数据
raw_bytes = b'\x12\x34\x56\x78\x9a\xbc\xde\xf0'
# 转成无符号8位整数数组
arr = np.frombuffer(raw_bytes, dtype=np.uint8)
print(arr) # [18 52 86 120 154 188 222 240]
# 如果是16位有符号采样值
samples = np.frombuffer(raw_bytes, dtype=np.int16)
print(samples) # 注意字节序!
我个人习惯把采集到的多条波形存成一个二维数组:行是采样点,列是不同次采集。这样后续做差分分析(DPA)时,矩阵运算一步到位。
# 假设采集了1000条波形,每条1024个采样点
traces = np.zeros((1000, 1024), dtype=np.float64)
# 填充数据...
# 计算所有波形的平均
mean_trace = np.mean(traces, axis=0)
# 找出能量最高的采样点位置
peak_idx = np.argmax(np.var(traces, axis=0))
print(f"方差最大的采样点索引: {peak_idx}")
np.roll() 和 np.correlate() 是我最常用的两个函数。前者做时间偏移对齐,后者做相关性计算。
4.3 Scipy:信号里的“显微镜”
Scipy 的信号处理模块(scipy.signal)是侧信道分析的利器。原始波形里往往混着 50Hz 工频噪声、高频开关噪声,你需要先滤波。
from scipy import signal
# 设计一个低通滤波器,截止频率 10MHz,采样率 100MHz
fs = 100e6 # 采样率
cutoff = 10e6
b, a = signal.butter(4, cutoff / (fs/2), btype='low')
# 对某条波形滤波
filtered_trace = signal.filtfilt(b, a, trace)
# 注意:filtfilt 是零相位滤波,不会引入延迟
为什么会用 filtfilt 而不是 lfilter?因为 lfilter 会引入相位偏移,导致波形在时间轴上“跑偏”。做时序分析时,哪怕偏移一个采样点,都可能让你找错加密操作的位置。我曾经因为这个原因,浪费了整整两天去排查一个根本不存在的“漏洞”。
另一个常用功能是峰值检测:
# 找波形中的峰值(比如加密操作的起始点)
peaks, properties = signal.find_peaks(
filtered_trace,
height=0.5, # 峰值高度阈值
distance=100, # 两个峰值之间最少间隔100个采样点
prominence=0.3 # 峰值的显著度
)
print(f"找到 {len(peaks)} 个峰值,位置在: {peaks}")
你想想看,如果我们要做模板攻击(Template Attack),需要从大量波形中提取特征点。Scipy 的 find_peaks 配合 peak_widths,可以自动定位每个加密轮次的起始和结束位置,比手动标定快十倍不止。
4.4 Matplotlib:把数据“画”出来
数据好不好,画出来才知道。Matplotlib 是 Python 可视化的标配。做智能卡测试时,我主要画三种图:
| 图表类型 | 用途 | 关键参数 |
|---|---|---|
| 时序图 (plot) | 查看波形整体形态,定位加密操作区域 | linewidth, alpha, color |
| 频谱图 (specgram) | 分析噪声频段,确定滤波器参数 | NFFT, noverlap, cmap |
| 眼图 (自定义) | 分析通信时序抖动,判断波特率是否准确 | 需要自己叠加绘制 |
一个实用的波形叠加对比模板:
import matplotlib.pyplot as plt
# 设置中文字体(防止乱码)
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
fig, ax = plt.subplots(figsize=(12, 6))
# 画三条波形对比
time = np.arange(1024) / fs # 时间轴
ax.plot(time, trace_1, label='正确密钥', alpha=0.8)
ax.plot(time, trace_2, label='错误密钥', alpha=0.8, linestyle='--')
ax.plot(time, trace_1 - trace_2, label='差分信号', linewidth=2, color='red')
ax.set_xlabel('时间 (s)')
ax.set_ylabel('电压 (V)')
ax.set_title('正确 vs 错误密钥 功耗波形对比')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
%matplotlib inline。但如果你在远程服务器上跑,建议用 plt.savefig('trace.png', dpi=150) 保存图片,再下载到本地看。我曾经在树莓派上远程调试,忘了这茬,结果图形界面弹不出来,卡了半天。
4.5 实战组合:一条命令链
最后,我把这四个工具串起来,展示一个完整的分析流程:
import serial
import numpy as np
from scipy import signal
import matplotlib.pyplot as plt
# 1. 通过PySerial采集100次加密操作的波形
ser = serial.Serial('COM3', 115200, timeout=1)
traces = []
for i in range(100):
ser.write(b'\x00\xA4\x04\x00') # 发送SELECT命令
raw = ser.read(2048) # 采集2048个采样点
trace = np.frombuffer(raw, dtype=np.uint8).astype(np.float64)
traces.append(trace)
traces = np.array(traces)
# 2. NumPy做预处理:去直流分量
traces -= np.mean(traces, axis=1, keepdims=True)
# 3. Scipy做低通滤波
b, a = signal.butter(4, 0.1, btype='low')
filtered = signal.filtfilt(b, a, traces, axis=1)
# 4. Matplotlib可视化
plt.figure(figsize=(10, 5))
plt.plot(np.mean(filtered, axis=0), label='平均波形')
plt.fill_between(range(2048),
np.mean(filtered, axis=0) - np.std(filtered, axis=0),
np.mean(filtered, axis=0) + np.std(filtered, axis=0),
alpha=0.3, label='±1σ')
plt.legend()
plt.savefig('analysis_result.png', dpi=150)
print("分析完成,结果已保存")
这套流程我用了好几年,从最初的简单 APDU 收发,到后来做完整的 CPA(相关性功耗分析),底层逻辑没变过。说白了,工具只是手段,关键是你要理解数据在每一层经历了什么变换。
公众号:蓝海资料掘金营,微信deep3321