前言
车辆诊断目的是为了快速判断车辆或某个控制器的故障及故障原因,以此进行维修。

常见的诊断协议有 ISO 14230,ISO 15031,ISO 15765,我们熟悉的 ISO 14229 就是 UDS 协议,在协议里定义了诊断请求,诊断响应的报文格式,以及 ECU 怎样处理诊断请求报文,以及诊断服务的应用。
一、UDS 诊断方法
大多数 ECU 都装有自诊系统 (OBD),能够及时发现传感器、执行器、ECU 和通信网络产生的电力故障。
当 OBD 检测到故障时,会生成相应故障码信息,并点亮故障灯。此外,把诊断仪插入 OBD 接口即可以和 OBD 系统进行通信,从而获取故障码和相关信号流等数据,实现快速定位故障源和故障类型。
表 1 UDS 诊断服务类型

这里先把我们称为客户端,OBD 称为服务端。
也就是我们使用诊断仪向 ECU 发送请求信息,等待 ECU 回复同意或拒绝。
这两种反馈称为正响应和负响应。
UDS 服务请求消息和服务响应消息都以报文的形式呈现。
UDS 明确定义了服务请求报文的基本格式为“SID+其他参数”,正响应报文的基本格式为“[SID+0x40]+其他参数“,负响应报文的格式为"0x7F+其他参数"。

上图我们得知,我们使用诊断仪向 ECU 发送请求报文”0x22 0x01 0x0a“,该报文中的第一个数据”0x22“表示当前请求的服务为 SID 为 22 的”通过 ID 读数据“服务。该报文中的”0x01 0x0a“表示要读的数据的 ID 为”010a“,其含义为油门开度,由汽车生产商指定。
如果 OBD 给出了正响应,则发送报文”0x62 0x01 0x0a 0x00 0x23“,该报文的第一个数据值为”0x62“是 0x22 加上 0x40 的结果,后面第二、三个数据为请求报文中指定的 ID 值”0x01 0x0a“,第四、五个数据”0x00 0x23“,为当前油门开度值”0x23(十进制为35)“,表示当前发动机油门开度为 35%。
如果 OBD 给出了负响应,则发送报文”0x7f 0x22 0x11“,该报文第一个数据值为”0x7f“,表示拒绝服务,第二个数据为被拒绝的服务的 SID 值”0x22“,第三个数据为被拒绝原因,该值由 UDS 定义,”0x11“表示当前服务不支持。
UDS 协议定义了所有服务的报文格式和数据含义(见ISO 14229-1),诊断仪和 OBD 都必须遵循该协议,才能正确完成各种诊断服务。参照 ISO 网络通信体系,该部分协议称 UDS 应用层协议。
二、UDS 传输方法
诊断仪和 OBD 生成的 UDS 报文需通过通信网络进行传输。但汽车 ECU 普遍使用 CAN 总线进行数据通信,因此 UDS 报文必须加载到 CAN 帧中才能发送。
但是每个 CAN 帧的最大传输数据量只有 8 个字节,而 UDS 报文的数据量是根据服务内容进行变化的。
最小的 UDS 报文有 2 个字节,大的 UDS 报文往往超过8字节。
所以超过 8 字节的 UDS 报文需多个 CAN 帧才能完成传输。为了保证报文传输的有效性和可靠性,需定义 UDS 报文在 CAN 总线上的传输协议(ISO-15765-2),其一般称为 UDS 的网络层或传输层协议。
UDS 定义了四种类型的 CAN 帧来传输 UDS 报文,分别单帧(Single Frame,SF)、首帧(Firsst Frame,FF)、连续帧(Consecutive Frame,CF)和流控帧(Flow Control,FC)。

每种 CAN 帧都占用第一个字节的高 4 位来存放帧类型数据。
单帧的第一个字节低 4 位存放了该帧传输的报文数据量,剩余字节存放 UDS 报文数据,有空余则填充为 0 作为对齐。
以图 2 中传输的请求报文"0x22 0x01 0x0a"和正响应报文"0x62 0x01 0xa 0x0 0x23"为例,其单帧数据区的内容如图 3 所示:

请求响应报文单帧。

正响应报文单帧。
能看到,单帧最多可传 7 个字节 UDS 报文,当要发送的报文的数据量大于7个字节时,就需要使用首帧、连续帧和流控帧了,机制如图 4 所示。

这里就是发送端将 UDS 报文写入首帧,首帧第一个字节低 4 位和第二个字节带着传输 UDS 报文总量,其余放数据。
接收端收到首端后回复流控帧,流控帧为了控制后续发送报文的速率。
发送端收到流控帧后把 UDS 报文其余数据填入连续帧,并标记顺序发出,以此来判断 UDS报文是否传输完。
流控帧的第一个字节的低 4 位存放 FlowStatus(FS)值。该值为 0 表示可以继续发送后续连续帧;如为 1 表示暂停发送后续连续帧;如为 2 表示接收端已经溢出。
流控帧的第二个字节存放 BlockSize(BS值),该值如果为 0 表示接收端后续将不会再发送流控帧,发送端直接发送余下的所有连续帧;如果为 01-0xff 之间的某个值,表示发送端在连续发送了该值数量的连续帧个数后,需要等待接收方再次发送流控帧。
流控帧的第三个字节存放 Stmin 值,该值为两个连续帧传输的时间间隔,单位 ms。流控帧的剩余字节存放填充数据。
连续帧的第一个字节低 4 位存放连续帧的序号 SN 值。第一个连续帧的序号为 1,后续连续帧序号依次加 1,知道 0xf 后,后续连续帧的 SN 值从 0 开始依次加 1。
1、传输案例
以传输 17 位 VIN 码为例:
(1)、诊断仪向 ECU 发出”0x22 0xf1 0x90“ 的 UDS 报文,请求读取车辆 VIN 码;
(2)、ECU 收到请求后回复正响应报文”0x62 0xf1 0x90 0x57 0x30 0x4c 0x30 0x30 0x30 0x30 0x34 0x33 0x4d 0x42 0x35 0x34 0x31 0x33 0x32 0x36“,共 20 个字节的数据量。
因此 ECU 先发出首帧,其数据区内容为图 5 所示:

(3)、诊断仪收到首帧后,向 ECU 回复流控帧,其数据区内容为如图 6 所示:

(4)、ECU 收到流控帧后,会依次发送剩余 2 个连续帧,其数据区内容为如图 7 所示:
三、UDS 时间管理
UDS 在传输的过程中,可能会遇到因网络故障或节点故障导致的通信延迟或终端的情况,因此 UDS 定义了通信时间管理基址来保证其工作的时效性,并基于设定的时间参数实现。
(1)、UDS 应用层时间参数
P2 Client:诊断仪成功发送请求报文后,接收到 ECU 响应报文的时间间隔
P2 Server:ECU 接收到诊断报文请求后,发出回复想要报文的时间间隔

(2)、UDS 网络层时间参数
N_As:发送端发送一帧所需的时间
N_Ar:接收端发送一帧所需的时间
N_Bs:发送端等待成功接收流控帧的时间间隔,超时则发送端接收流控帧失败
N_Br:接收端等待发送流控帧的时间间隔,超时则接收端流控帧发送失败
N_Cs:发送端等待发送一连续帧的时间间隔,Stmin
N_Cr:接收端等待成功接收一连续帧的时间间隔,超时则接收失败

UDS 协议明确给出了各个时间参数的取值范围,具体取值可以根据网络环境、节点速率和诊断应用需求进行调整。
四、仿真实验
首先我们先模拟搭建一个 ECU。
import can
import time
# 1. 初始化虚拟总线,起名叫vcan0 'vcan0'
bus = can.interface.Bus('vcan0', bustype='socketcan')
print(" [ECU] 模拟器已上线,正在监控总线...")
while True:
# 2. 持续接收总线上的报文
msg = bus.recv()
if msg is None: continue
data = msg.data
# 这里的 data 是一个字节数组,比如 [0x02, 0x10, 0x01, ...]
# --- 逻辑 A:处理单帧 (Single Frame) ---
# UDS规定:第一个字节的高4位是 0,代表单帧
if (data[0] & 0xF0) == 0x00:
length = data[0] & 0x0F
sid = data[1]
print(f"📩 [ECU] 收到单帧请求! 长度:{length}, 服务ID:{hex(sid)}")
# 如果是 10 01 (进入默认会话)
if sid == 0x10 and data[2] == 0x01:
# 回复 Positive Response: [长度, SID+0x40, 子功能, ...]
# 0x10 + 0x40 = 0x50
resp = can.Message(arbitration_id=0x7E8, data=[0x02, 0x50, 0x01, 0,0,0,0,0], is_extended_id=False)
bus.send(resp)
print("✅ [ECU] 已回复: 50 01 (OK)")
# --- 逻辑 B:处理首帧 (First Frame) - 准备多帧传输 ---
# UDS规定:第一个字节高4位是 1,代表后面还有很多货
elif (data[0] & 0xF0) == 0x10:
total_len = ((data[0] & 0x0F) << 8) | data[1]
print(f"📦 [ECU] 警告!收到首帧。对方要传 {total_len} 字节。发送流控帧(FC)...")
# 按照 CSDN 文章说的,我们要回 30 (流控帧)
# 30 00 00: 3代表流控,后面00代表不限速
fc = can.Message(arbitration_id=0x7E8, data=[0x30, 0x00, 0x00, 0,0,0,0,0], is_extended_id=False)
bus.send(fc)
# --- 逻辑 C:接收连续帧 (Consecutive Frame) ---
# 第一个字节高4位是 2
elif (data[0] & 0xF0) == 0x20:
sn = data[0] & 0x0F # 序号 1, 2, 3...
print(f"🧩 [ECU] 收到连续帧 SN:{sn} | 内容:{list(data[1:])}")
我们先模拟一个 ECU,然后我们向其发送报文。
单帧
import can
# 连接到同一条总线 vcan0
# 忽略那个警告,咱们按新版本写法:interface='virtual'
bus = can.interface.Bus('vcan0', interface='socketcan')
print("🚀 [Tester] 已就绪,发送单帧指令 (10 01)...")
# 构造 UDS 单帧:[长度02, 服务10, 子功能01, 后面补0]
msg = can.Message(arbitration_id=0x7E0,
data=[0x02, 0x10, 0x01, 0, 0, 0, 0, 0],
is_extended_id=False)
bus.send(msg)
# 等待 ECU 回复
reply = bus.recv(timeout=1.0)
if reply:
print(f" [Tester] 抓到响应报文: {list(reply.data)}")
else:
print(" [Tester] 没收到回复,检查 ECU 脚本是否在运行。")

此处为单帧测试结果,Linux 的 CAN 通信用的是 socketcan。
多种帧
import can
bus = can.interface.Bus('vcan0', interface='socketcan')
print("[ECU] 多帧适配版已上线...")
while True:
msg = bus.recv()
if not msg: continue
# --- A. 单帧处理 (之前通的) ---
if (msg.data[0] & 0xF0) == 0x00:
print(f"📩 [ECU] 收到单帧: {msg.data.hex(' ')}")
bus.send(can.Message(arbitration_id=0x7E8, data=[0x02, 0x50, 0x01, 0,0,0,0,0], is_extended_id=False))
# --- B. 首帧处理 (First Frame) ---
elif (msg.data[0] & 0xF0) == 0x10:
total_len = ((msg.data[0] & 0x0F) << 8) | msg.data[1]
print(f"📦 [ECU] 收到首帧! 总长度: {total_len} 字节。正在回流控帧...")
# 发送流控帧 (Flow Control)
fc = can.Message(arbitration_id=0x7E8, data=[0x30, 0x00, 0x00, 0,0,0,0,0], is_extended_id=False)
bus.send(fc)
# --- C. 连续帧处理 (Consecutive Frame) ---
elif (msg.data[0] & 0xF0) == 0x20:
sn = msg.data[0] & 0x0F
print(f"🧩 [ECU] 收到连续帧 SN:{sn} | 内容: {msg.data[1:].hex(' ')}")新版的ECU。
import can
import time
bus = can.interface.Bus('vcan0', interface='socketcan')
# 模拟我们要发送的 20 字节数据
# 2E F1 90 是 UDS 的 'Write Data by Identifier'
data_to_send = [0x2E, 0xF1, 0x90, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11]
# 1. 发送首帧 (First Frame)
# 10 14: 1代表首帧, 014代表20字节长度
ff = [0x10, 0x14] + data_to_send[0:6]
bus.send(can.Message(arbitration_id=0x7E0, data=ff, is_extended_id=False))
print(f"🚀 [Tester] 已发首帧: {bytes(ff).hex(' ')}")
# 2. 接收流控帧
msg = bus.recv(timeout=1.0)
if msg and (msg.data[0] & 0xF0) == 0x30:
print("🚦 [Tester] 收到流控帧,开始传送后续数据...")
# 3. 发送第一路连续帧 (SN=1)
cf1 = [0x21] + data_to_send[6:13]
bus.send(can.Message(arbitration_id=0x7E0, data=cf1, is_extended_id=False))
print(f"🚚 [Tester] 发送 CF SN:1")
time.sleep(0.05) # 模拟帧间延迟
# 4. 发送第二路连续帧 (SN=2)
cf2 = [0x22] + data_to_send[13:20]
bus.send(can.Message(arbitration_id=0x7E0, data=cf2, is_extended_id=False))
print(f"🚚 [Tester] 发送 CF SN:2")我们将发送的数据。

发送完毕。
ECU门禁版
现代的汽车安全漏洞中,0x27是最核心的:
如果想修改里程要过0x27。
想刷写恶意固件,也要过0x27。
想开车锁依然要过0x27。
所以我们现在复现一下截获种子->本地逆向算法->构造伪造响应的攻击链路。
import can
import random
# 绑定 vcan0
bus = can.interface.Bus('vcan0', interface='socketcan')
print("🛡️ [ECU] 安全门禁版已上线。状态:锁定中 🔒")
current_seed = 0
while True:
msg = bus.recv()
if not msg: continue
data = msg.data
# 1. 识别 27 服务 (安全访问)
if len(data) >= 3 and data[1] == 0x27:
# --- 子服务 01: 请求种子 (Request Seed) ---
if data[2] == 0x01:
current_seed = random.randint(0x10, 0xFE)
print(f"🔑 [ECU] 收到种子请求。生成种子: {hex(current_seed)}")
# 回复: [长度03, 响应67, 子服务01, 种子]
resp = can.Message(arbitration_id=0x7E8, data=[0x03, 0x67, 0x01, current_seed, 0,0,0,0], is_extended_id=False)
bus.send(resp)
# --- 子服务 02: 发送密钥 (Send Key) ---
elif data[2] == 0x02:
received_key = data[3] # 关键:从第4个字节取Key
# 计算公式: (Seed ^ 0x55) + 0x22
expected_key = ((current_seed ^ 0x55) + 0x22) & 0xFF
if received_key == expected_key:
print(f"🔓 [ECU] Key 正确 ({hex(received_key)})!权限已开启!")
# 回复: [长度02, 响应67, 子服务02]
bus.send(can.Message(arbitration_id=0x7E8, data=[0x02, 0x67, 0x02, 0,0,0,0,0], is_extended_id=False))
else:
print(f"❌ [ECU] Key 错误!收到:{hex(received_key)}, 期待:{hex(expected_key)}")
# 回复 NRC 35: Key Invalid
bus.send(can.Message(arbitration_id=0x7E8, data=[0x03, 0x7F, 0x27, 0x35, 0,0,0,0], is_extended_id=False))我们这里给 ECU 上一个门禁。
import can
import time
bus = can.interface.Bus('vcan0', interface='socketcan')
# 第一步:请求种子
print("📡 [Tester] 正在请求种子 (27 01)...")
bus.send(can.Message(arbitration_id=0x7E0, data=[0x02, 0x27, 0x01, 0,0,0,0,0], is_extended_id=False))
# 第二步:接收并计算
msg = bus.recv(timeout=1.0)
if msg and msg.data[1] == 0x67 and msg.data[2] == 0x01:
seed = msg.data[3]
print(f"👀 [Tester] 截获种子: {hex(seed)}")
# 核心算法
key = ((seed ^ 0x55) + 0x22) & 0xFF
print(f"🧠 [Tester] 算出密钥: {hex(key)}")
time.sleep(0.1)
# 第三步:回传 Key
# 数据包:[长度03, 服务27, 子服务02, 密钥值]
print("⚡ [Tester] 发射密钥尝试解锁...")
bus.send(can.Message(arbitration_id=0x7E0, data=[0x03, 0x27, 0x02, key, 0,0,0,0], is_extended_id=False))
# 第四步:确认结果
res = bus.recv(timeout=1.0)
if res and res.data[1] == 0x67 and res.data[2] == 0x02:
print("🏆 [Tester] 恭喜!破解成功,ECU 已解锁!")
else:
print("💀 [Tester] 失败。请检查 ECU 输出的 NRC 错误码。")我们将测试的脚本

破解成功。
参考文献:
https://zhuanlan.zhihu.com/p/682345565
https://zhuanlan.zhihu.com/p/135422985