4、Python工具链:PySerial通信、NumPy数据处理、Matplotlib可视化、Scipy信号分析

做智能卡渗透测试,说白了就是跟一串串的字节流打交道。你发送一条APDU指令,卡片返回一串响应,中间可能夹杂着时序噪声、电压波动、甚至电磁辐射的痕迹。这时候,光靠肉眼盯着十六进制看,效率太低了。

我个人习惯把Python工具链分成四个层次:通信层、处理层、分析层、展示层。下面这张图能帮你快速建立整体认知:

智能卡Python工具链四层架构 PySerial 通信层 NumPy 数据处理层 Scipy 信号分析层 Matplotlib 可视化层 APDU收发 · 波特率 · 超时控制 数组运算 · 统计 · 波形切片 滤波 · FFT · 峰值检测 · 相关性 时序图 · 频谱图 · 眼图 · 热力图

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秒,别设太长
)
避坑指南: 我曾经在调试一块老旧读卡器时,发现总是收不到响应。折腾了半天,最后发现是 DTR/RTS 流控没关。很多读卡器在 DTR 为低电平时会复位,导致卡片一直处于复位状态。记得加上 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}")
核心技巧: 做功耗分析时,经常需要把波形和密钥猜测对齐。NumPy 的 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()
注意: 在 Jupyter Notebook 里画图,记得加 %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(相关性功耗分析),底层逻辑没变过。说白了,工具只是手段,关键是你要理解数据在每一层经历了什么变换。

我的建议: 刚开始别急着上复杂的分析算法。先把 PySerial 的通信调通,确保每次收发都稳定可靠。然后拿 NumPy 把数据存成 .npy 格式,方便反复回放。等数据积累够了,再上 Scipy 和 Matplotlib 做深入分析。一步一个脚印,比什么都重要。

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