CAN 抓包之后,下一步就是 UDS
如果你看过站内那篇《OBD-II 与 CAN 总线抓包分析完整指南》,大概率会有同一个困惑:抓到了一堆 0x7E0、0x7E8 的报文,看起来有规律,但完全读不懂它在说什么。
原因很简单:CAN 只负责「把字节送过去」,不规定这些字节是什么意思。真正让诊断仪能和 ECU 对话的那套「语言」,叫 UDS(Unified Diagnostic Services,ISO 14229)。
打个比方:CAN 是公路,ISO-TP 是货运规则(一车装不下怎么拆包),UDS 是包裹上的单据格式。读懂 UDS,你才算能听懂车在说什么。
本文从帧结构一路讲到能跑通的 Python 代码,全部是可以在自己车上复现的流程。
一、三层结构,先建立地图
| 层 | 标准 | 职责 | 你看到的东西 |
|---|---|---|---|
| 应用层 | ISO 14229 (UDS) | 定义服务、参数、正负响应 | 22 F1 90、62 F1 90 ... |
| 传输层 | ISO 15765-2 (ISO-TP) | 长报文拆包/重组,流控 | PCI 字节 0x0N / 0x1X / 0x2X / 0x3X |
| 数据链路层 | ISO 11898 (CAN) | 仲裁、CRC、ACK | 帧 ID、8 字节数据 |
| 物理层 | — | CAN_H / CAN_L 差分信号 | OBD 口 6/14 脚 |
关键认知:UDS 报文不是「一条 CAN 帧」。超过 7 字节的响应必须靠 ISO-TP 拆成多帧,这也是一堆人卡住的地方——看到 10 14 开头就懵了。
二、ISO-TP 的四种帧
CAN 一帧最多 8 字节,第一个字节被拆成「高 4 位 = 帧类型,低 4 位 = 参数」。
| 类型 | PCI 高 4 位 | 含义 | 示例 |
|---|---|---|---|
| 单帧 SF | 0 | 低 4 位是数据长度(≤7) | 02 10 03:长度 2,数据 10 03 |
| 首帧 FF | 1 | 低 4 位 + 下一字节 = 12 位总长度 | 10 14 62 F1 ...:总长 0x014 = 20 字节 |
| 连续帧 CF | 2 | 低 4 位是序号(1 开始,循环 1–F) | 21 ...、22 ... |
| 流控 FC | 3 | 告诉对方「可以继续发」 | 30 00 00:继续,块大小 0,间隔 0 |
一条 20 字节的 VIN 响应,实际长这样:
7E8 10 14 62 F1 90 57 50 30 # 首帧:总长 20
7E0 30 00 00 # 接收方发流控:继续发
7E8 21 41 4C 30 30 30 30 30 # 连续帧 1
7E8 22 30 30 30 30 30 30 30 # 连续帧 2
拼起来就是 62 F1 90 + 17 字节 VIN。手工拼当然可以,但代码里用 ISO-TP 库更省事。
三、诊断地址:谁在跟谁说话
OBD 口的诊断报文走固定的一组地址(11 位 ID):
| 用途 | 请求 | 响应 |
|---|---|---|
| 功能寻址(广播给所有 ECU) | 0x7DF |
各 ECU 自己回 |
| 发动机 / 主 ECU(物理寻址) | 0x7E0 |
0x7E8 |
| 变速箱 | 0x7E1 |
0x7E9 |
| ABS / ESP | 0x7E2 |
0x7EA |
功能寻址适合「让所有 ECU 一起做某件事」(比如 3E 00 保活),物理寻址适合精确读某个 ECU 的数据。注意:功能寻址的请求不会有流控,也不支持多帧请求。
四、UDS 报文结构
一条 UDS 请求 = 服务 ID(SID)+ 子功能/参数。
- 正响应:SID +
0x40,后面跟解析后的数据。例如10 03→50 03 ... - 负响应:固定
7F开头,格式7F <原SID> <NRC>。例如7F 22 31表示「读 DID 请求超出范围」。
常见 NRC(否定响应码)
| NRC | 含义 | 排查方向 |
|---|---|---|
| 0x10 | generalReject 通用拒绝 | 报文内容不被接受 |
| 0x11 | serviceNotSupported | 这个 ECU 不支持该服务 |
| 0x12 | subFunctionNotSupported | 子功能号不对 |
| 0x13 | incorrectMessageLength | 长度不对,常见于多帧没拼对 |
| 0x22 | conditionsNotCorrect | 会话/车速/点火状态不满足 |
| 0x31 | requestOutOfRange | 参数越界(DID 不存在最常见) |
| 0x33 | securityAccessDenied | 没做安全访问 |
| 0x78 | responsePending | 不是错误,ECU 还在算,继续等 |
0x78 值得单独说:ECU 处理耗时操作时会先回 7F xx 78,然后过几百毫秒再回正响应。不会处理 0x78 的诊断工具会把正常流程误判成失败。成熟的库(如 udsoncan)会自动等。
五、常用服务速查
| SID | 服务 | 典型用途 |
|---|---|---|
| 0x10 | DiagnosticSessionControl | 切会话:01 默认 / 02 编程 / 03 扩展 / 04 安全系统 |
| 0x11 | ECUReset | 复位 ECU |
| 0x14 | ClearDiagnosticInformation | 清故障码 |
| 0x19 | ReadDTCInformation | 读故障码及其状态 |
| 0x22 | ReadDataByIdentifier | 最常用:按 DID 读数据(VIN、版本号、传感器值) |
| 0x27 | SecurityAccess | 种子/密钥解锁 |
| 0x28 | CommunicationControl | 关闭/开启某类报文发送 |
| 0x2E | WriteDataByIdentifier | 写配置(高风险) |
| 0x31 | RoutineControl | 执行例程:自检、学习、标定 |
| 0x3E | TesterPresent | 保活,防止 ECU 退回默认会话 |
| 0x34/0x36/0x37 | RequestDownload / TransferData / Exit | 刷写固件(极高风险) |
记住一个规律:读服务(0x19 / 0x22)练手安全,写服务(0x2E / 0x31 / 0x34)可能把 ECU 写坏。初学阶段只碰读。
六、一次完整会话:读 VIN
在 Linux 上用 SocketCAN,先把 500 kbps 的 can0 起起来(绝大多数乘用车诊断 CAN 是 500k):
sudo ip link set can0 up type can bitrate 500000
candump can0
第 1 步:进入扩展会话
发送 7DF [02] 10 03
接收 7E8 [06] 50 03 00 32 01 F4
解析:50 = 0x10 + 0x40,表示正响应;03 是当前会话;后面 4 字节是 P2/P2* 定时参数(这里 0x0032 = 50 ms,0x01F4 = 500 ms×10)。
第 2 步:读 VIN(DID = 0xF190)
发送 7E0 [03] 22 F1 90
接收 7E8 [10 14] 62 F1 90 57 50 30 # 首帧,20 字节
发送 7E0 [30 00 00] # 流控
接收 7E8 [21] 41 4C 30 30 30 30 30
接收 7E8 [22] 30 30 30 30 30 30 30
把 62 F1 90 后面的 17 个字节按 ASCII 拼起来,就是 VIN 码。这一步能跑通,说明你的硬件、波特率、ISO-TP 全链路都是对的。
七、用 Python 跑起来
手拼帧只适合验证,实际分析用 python-can + isotp + udsoncan 三件套。
pip install python-can isotp udsoncan
import can
import isotp
from udsoncan.connections import PythonIsoTpConnection
from udsoncan.client import Client
import udsoncan.configs
# 1) 打开 CAN
bus = can.Bus(interface="socketcan", channel="can0", bitrate=500000)
# 2) 配置 ISO-TP:物理寻址 7E0 -> 7E8
addr = isotp.Address(isotp.AddressingMode.Normal_11bits, txid=0x7E0, rxid=0x7E8)
stack = isotp.CanStack(bus=bus, address=addr, params={"blocksize": 0, "stmin": 0})
conn = PythonIsoTpConnection(stack)
conf = dict(udsoncan.configs.default_client_config)
conf["request_timeout"] = 3
with Client(conn, config=conf) as client:
# 3) 进扩展会话
client.change_session(0x03)
print("会话已切换到扩展模式")
# 4) 读 VIN
r = client.read_data_by_identifier([0xF190])
print("原始响应:", r.service_data)
# 5) 读当前故障码
dtc = client.get_dtc_by_status_mask(0xFF)
print("DTC:", dtc.service_data)
几点实践说明:
udsoncan会自动处理0x78和 ISO-TP 流控,比自己写状态机省太多。- 不同版本返回对象的字段名略有差异,建议先
print出来再取。 - Windows 上没有 SocketCAN,要用
interface="slcan"、"pcan"或"vector",并在channel里填串口名/通道号。 - ICSim 这类纯 CAN 模拟器不支持 UDS——它只会伪造车速、门锁这些原始信号。要练 UDS,得用真实 ECU、台架,或者支持 UDS 的仿真工具。
八、安全访问 0x27 是怎么回事
很多服务(写配置、执行例程)要求先解锁。流程是标准的挑战-应答:
- 工具发
27 01(请求种子) - ECU 回
67 01 <seed> - 工具用某个算法把 seed 算成 key,发
27 02 <key> - ECU 回
67 02表示解锁成功,否则7F 27 35(密钥无效)
算法由厂商定义,没有统一标准,常见做法是查表、异或固定值、或者基于 UDS 之外的私有算法。这也是为什么「破解」不同车型时,工作量和难度差异巨大。
从防御角度看,这一点很重要:把安全算法硬编码在 ECU 固件里,本质上只是增加门槛,不是安全边界。真正的防护要靠网关隔离、诊断防火墙和访问控制。
九、几个高频坑
| 现象 | 原因 | 处理 |
|---|---|---|
| 发出去没任何响应 | 波特率不对 / 用错了寻址方式 | 先 candump 看总线上有没有任何流量 |
只回 7F xx 11 |
该 ECU 不支持此服务 | 换 ECU 地址试,或确认车型是否走 UDS(老车可能是 KWP2000) |
| 多帧响应读不全 | 没发流控,或块大小配错 | 用 ISO-TP 库自动处理,别手拼 |
| 会话总是掉回默认 | 超过 S3 定时(通常 5 s)没发保活 | 周期性发 3E 00 |
读 DID 回 7F 22 31 |
DID 不存在或该会话不可读 | 先 22 F1 86 读当前会话确认状态 |
| 操作后车辆报一堆故障 | 动了写服务 / 例程控制 | 断电重启,用 0x14 清码,必要时去 4S 恢复 |
十、风险与边界
这一节不是套话,是真会出事的部分:
- 只读服务相对安全,写服务风险极高。
0x2E写错一个字节,可能让 ECU 进入不可用状态;0x31执行错误例程,可能触发标定丢失。 - 不要在行驶中操作。涉及制动、转向、动力系统的 ECU,任何异常都可能直接变成事故。
- 只在自有车辆或书面授权的车辆上做。未经授权接入他人车辆诊断系统,可能触犯《网络安全法》与《刑法》第 285 条。
- 刷写(0x34/0x36/0x37)不要碰,除非你有原厂工具和恢复方案。
十一、小结
把学习路径压缩成四步:
- 看懂帧:CAN 抓包 → 认出 0x7E0/0x7E8 → 理解 PCI 字节
- 跑通只读:
10 03切会话 →22 F1 90读 VIN - 用库替代手拼:python-can + isotp + udsoncan
- 再谈写入:理解 0x27 与风险之后,才碰 0x2E/0x31
延伸阅读:
- 抓包与信号逆向:《OBD-II 与 CAN 总线抓包分析完整指南》
- CAN 报文逆向:《CAN 总线消息逆向实战:ICSim + cansniffer + SavvyCAN》
- 把手机变成车载测试工具:《用 NetHunter 把你的汽车变成渗透测试工具》
免责声明:本文仅用于自有车辆的诊断学习与授权范围内的安全研究。任何未授权的车辆接入、数据写入或功能篡改,均可能违法并造成人身财产损害,后果自负。
- 1、本网站名称: 星宇安全实验室
- 2、本站永久网址:https://bk.zhaozhiqiang.pw/
- 3、本网站的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系站长进行删除处理。
- 4、本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
- 5、本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向站长举报
- 6、本站资源大多存储在云盘,如发现链接失效,请联系我们我们会第一时间更新。
- 7、文章版权归作者所有,未经允许请勿转载。













暂无评论内容