OBD-II/UDS 渗透测试(一):协议基础 — CAN 总线与 ISO-TP
OBD-II/UDS 渗透测试(一):协议基础 — CAN 总线与 ISO-TP
- 第 1 章 车载诊断的"为什么"和"是什么"
- 1.1 一句话起源
- 1.2 OBD-II 是什么、UDS 是什么、它们的关系
- 1.2.1 SID(UDS)vs Mode(OBD-II)—— 容易混淆的概念
- 1.3 为什么诊断协议是"攻击金矿"
- 第 2 章 CAN 总线层基础
- 2.1 帧结构(11-bit 标准帧)
- 2.2 11-bit 还是 29-bit?
- 2.3 OBD-II 11-bit 寻址规则(关键!)
- 2.4 实战采样:一帧完整 CAN
- 第 3 章 ISO-TP 传输层
- 3.1 为什么需要 ISO-TP
- 3.2 四种 PCI(Protocol Control Information)帧
- 3.3 PCI 字节详解
- 3.4 多帧重组的完整交互(实战)
- 3.5 多帧重组的关键代码
[AI 整理声明]本文内容来源于作者在汽车网络安全实战项目中积累的测试笔记,由 AI(Claude)整理归纳输出;行文措辞、内容组织和格式均由 AI 完成,原始测试发现与知识积累来自作者本人。
本文是「OBD-II/UDS 汽车网络安全实战手记」系列第一篇,内容基于真实渗透测试项目。每个知识点配有实际采集到的总线报文作佐证,由浅入深,从"什么是 CAN 总线"一路到"如何识别 ISO-TP 多帧"。文中 F-XXX 为测试发现编号。
系列文章:(一)协议基础 — CAN 总线与 ISO-TP(本文) · (二)UDS 服务字典 · (三)安全机制 — NRC + Session + SecAccess · (四)实战攻防与方法论
第 1 章 车载诊断的"为什么"和"是什么"
1.1 一句话起源
20 世纪 80 年代,加州空气委员会(CARB)要求所有上路汽车都必须能被一台外部设备读出排放相关故障——这就是 OBD(On-Board Diagnostics)。1996 年起 OBD-II 在美国成为法规要求,欧洲叫 EOBD(2001 年起),中国是 GB18352 系列法规(2007 年起强制)。
OBD-II 的最初目的非常窄:让维修工和环保部门读"我的发动机控制器排放有没有故障"。当年没人想过攻击者会用同一个接口去刷固件、关闭电池保护、抹掉攻击痕迹。
1.2 OBD-II 是什么、UDS 是什么、它们的关系
OBD-II是法规层面的最小诊断协议。物理上规定了一个 16 针的方梯形插座(J1962 接口),逻辑上规定了 10 个 Mode(0x01-0x0A)的命令:
| Mode | 名称 | 含义 |
|---|---|---|
| 0x01 | Show current data | 读实时排放参数(车速、转速、O₂ 浓度、冷却液温度等) |
| 0x02 | Show freeze frame | 读冻结帧(DTC 触发瞬间的现场快照) |
| 0x03 | Show stored DTCs | 读已存储的排放相关 DTC |
| 0x04 | Clear DTCs | 清除 DTC 和相关排放数据 |
| 0x05 | Test results, O₂ sensor | 氧传感器测试结果(仅非 CAN 总线用) |
| 0x06 | Test results, other | 其他排放部件/系统测试结果 |
| 0x07 | Show pending DTCs | 读 pending DTC |
| 0x08 | Control on-board component | 控制排放相关部件(EVAP 系统泄漏测试等) |
| 0x09 | Request vehicle info | 读车辆信息(VIN、校准 ID、CVN 等) |
| 0x0A | Permanent DTCs | 读永久 DTC(不能被 Mode 0x04 清掉) |
OBD-II 只关心"排放相关",连"开/锁车门"都没有。Mode 0x0A PermanentDTC 是排放反取证的法规防线——见第 8 章。
UDS(Unified Diagnostic Services,ISO 14229)是OEM 自定义诊断的统一标准。它定义了一套服务标识 SID(0x10-0x3E + 0x83-0xBF),覆盖整车所有 ECU 的所有诊断需求——读写 ECU 参数、刷固件、控制执行器、读故障码、控制安全访问……
UDS 的服务可以复用OBD-II 已有的功能(比如 UDS 0x19 ReadDTC 几乎涵盖了 OBD-II Mode 0x03)。所以现代车上 ECU 通常同时支持 OBD-II 和 UDS:OBD-II 接口用最少功能合规,UDS 是真正干活的接口。
1.2.1 SID(UDS)vs Mode(OBD-II)—— 容易混淆的概念
两者都是 CAN data 第一字节后面的"命令码"。区别如下:
| 对比项 | OBD-II Mode | UDS SID |
|---|---|---|
| 取值范围 | 0x01-0x0A(固定 10 个) | 0x10-0x3E+0x83-0xBF(~30 个) |
| 关心的事 | 只关心排放相关 | ECU全部诊断功能 |
| 请求 ID | 通常用0x7DF广播或0x7E0-0x7E7 | OEM 自定义 |
| 正响应规则 | Mode + 0x40(0x01 → 0x41) | SID + 0x40(0x10 → 0x50) |
| 负响应格式 | 7F + Mode + NRC | 7F + SID + NRC(完全一样) |
关键:两者取值范围不重叠(0x01-0x0A vs 0x10+),所以一个 ECU 收到0x03就知道这是 OBD(读排放 DTC),收到0x19就知道这是 UDS(也是读 DTC,但功能丰富得多)。两套命令复用同一条 CAN 总线。
部分服务功能等价:
OBD-II Mode 0x03 读 DTC ≈ UDS SID 0x19 ReadDTC(UDS 多 20+ sub-function) OBD-II Mode 0x04 清 DTC ≈ UDS SID 0x14 ClearDTC OBD-II Mode 0x09 读 VIN ≈ UDS SID 0x22 DID 0xF190 OBD-II Mode 0x0A 永久 DTC ≈ UDS SID 0x19 sub 0x15协议栈关系:
┌──────────────────┐ │ CAN 总线物理层 │ (ISO 11898) └──────────────────┘ │ ┌──────────────────┐ │ ISO-TP 传输层 │ (ISO 15765-2, 解决 > 8 字节的多帧重组) └──────────────────┘ │ ┌──────────┴──────────┐ │ │ ┌───────────────┐ ┌───────────────┐ │ OBD-II │ │ UDS │ │ (ISO 15031) │ │ (ISO 14229) │ │ 10 个 Mode │ │ ~30 个 SID │ └───────────────┘ └───────────────┘1.3 为什么诊断协议是"攻击金矿"
从渗透角度看,诊断协议有三个让人馋的属性:
- 诊断协议直接命中 ECU 内部状态机——绕过应用层,直接和 ECU 固件对话
- 接口物理可达——OBD-II 插座就在驾驶位下方,任何人都能插
- 协议明文 + 无加密——除非 OEM 主动加密(极少数高端车有),所有报文裸跑在 CAN 上
OBD-II 设计时假设"接到诊断口的是维修工/合规检测设备",完全没有威胁模型。直到 2010 年 Karl Koscher、Stefan Savage 等人发表的《Experimental Security Analysis of a Modern Automobile》、2015 年 Charlie Miller 与 Chris Valasek 远程刷写 Jeep 才把这个洞撕开。
第 2 章 CAN 总线层基础
2.1 帧结构(11-bit 标准帧)
CAN 是个广播总线——一帧报文,全总线所有节点都能听到。一帧标准 CAN 数据帧(500 kbit/s 是车上 OBD 常见波特率)的关键字段:
SOF | ID(11) | RTR | IDE | r0 | DLC(4) | Data(0-8 bytes) | CRC(15) | ACK | EOF 1bit | 11 bits | 1 | 1 | 1 | 4 bits | 0-64 bits | 15 bits | 2 | 7我们最关心的就两件事:
- ID:11 bit,决定总线优先级(数值越小优先级越高)和目标(OBD-II 用 ID 来路由请求/响应)
- Data:最多 8 字节负载
2.2 11-bit 还是 29-bit?
CAN 帧的 IDE 位决定是 11-bit (Classic) 还是 29-bit (Extended)。OBD-II 法规允许两种:
- 11-bit:诊断请求广播
0x7DF,响应来自各 ECU0x7E8-0x7EF - 29-bit:诊断请求广播
0x18DB33F1,响应0x18DA F1 xx
商用车(重卡)多用 29-bit(J1939 协议族要求);乘用车多用 11-bit。具体到诊断侧,11-bit 在 OBD 中更常见,本文示例均以 11-bit 为基础。
2.3 OBD-II 11-bit 寻址规则(关键!)
OBD-II 规范的请求/响应 ID 有配对规则,标准 OBD-II 响应 ID = 请求 ID + 8:
请求 (tester → ECU) 响应 (ECU → tester) 广播 0x7DF 各 ECU 用自己的 phy ID phy 0x7E0 0x7E8 ← 标准 OBD ECU 用 0x7E0-0x7E7 / 0x7E8-0x7EF phy 0x7E1 0x7E9 ... phy 0x7E7 0x7EF2.4 实战采样:一帧完整 CAN
最常见的诊断请求是0x10 0x03(进 extended session)。完整 CAN 帧:
CAN ID = 0x777 DLC = 8 Data = 02 10 03 00 00 00 00 00 │ │ │ └──────────────────► padding (5 字节 0x00) │ │ └──► UDS sub-function = 0x03 (extendedDiagnostic) │ └──► UDS SID = 0x10 (DiagnosticSessionControl) └──► ISO-TP PCI byte = 0x02 (SingleFrame, length=2)这是后面所有协议层的基础——CAN 帧只是个容器,真正的诊断信息在 Data 区里,Data 的第 1 字节是 ISO-TP 的 PCI 头。
第 3 章 ISO-TP 传输层
3.1 为什么需要 ISO-TP
CAN 一帧最多 8 字节。但 UDS 读个 VIN 就 17 字节,读个 SupportedDTC 列表可能几百字节。怎么办?
答案是ISO-TP(ISO 15765-2)——在 CAN 之上加了一层分段重组协议,类似 IP 之上的 TCP(但简单得多)。
3.2 四种 PCI(Protocol Control Information)帧
ISO-TP 用 Data 的第 1 字节高 4 位区分帧类型:
| 高 4 位 | 名称 | 含义 |
|---|---|---|
| 0 | SF (SingleFrame) | 数据 ≤7 字节,一帧打完 |
| 1 | FF (FirstFrame) | 多帧的第一帧 |
| 2 | CF (ConsecutiveFrame) | 多帧的后续帧 |
| 3 | FC (FlowControl) | 接收方控制发送速率 |
3.3 PCI 字节详解
SingleFrame (SF):
PCI = 0x0L, L = 数据长度 (1-7) 后 7 字节 = 实际数据 (不足补 padding)FirstFrame (FF):
PCI = 0x1L LL, LL...L = 12-bit 总数据长度 (1-4095) 后 6 字节 = 数据的前 6 字节ConsecutiveFrame (CF):
PCI = 0x2N, N = 序号 (0-F, 循环) 后 7 字节 = 后续数据FlowControl (FC):接收方发回,告诉发送方"我准备好了,继续发"
PCI byte 0 = 0x3F, F = Flag (0=continue, 1=wait, 2=overflow) byte 1 = BS (Block Size, 0=一次性收完, N=每 N 个 CF 后再 FC 一次) byte 2 = STmin (相邻 CF 间隔最小毫秒)3.4 多帧重组的完整交互(实战)
在读 0x759 的 SupportedDTC 列表时收到了 173 字节响应。完整流程:
方向 CAN ID Data 说明 ────── ────── ──────────────────────────────── ──────────────────────── T → E 0x751 03 19 0A 00 00 00 00 00 请求: SID 19 sub 0A param 00 │ │ │ │ │ └──► sub-function 0x0A (SupportedDTC) │ └──► SID 0x19 (ReadDTC) └──► PCI: SF, length=3 E → T 0x759 10 AD 59 0A 09 D1 00 00 FF: 总长 0x0AD = 173 字节 │ │ │ │ └──────────────────► 数据前 5 字节 │ │ └──► SID + 0x40 = 0x59 (正响应) + sub byte │ └──► 总长低 8 位 = 0xAD └──► PCI 高 4 位 = 1 (FF), 低 4 位 = 总长高 4 位 = 0 T → E 0x751 30 00 00 00 00 00 00 00 FC: 0x30 = continue, │ │ │ BS=0 (一次发完), STmin=0 │ │ └──► STmin (相邻 CF 间隔) │ └──► BS (块大小) └──► PCI = 0x30 (FC, flag=continue) E → T 0x759 21 D1 01 00 91 01 00 91 CF #1 (序号=1) │ └──────────────────────────► 后续 7 字节数据 └──► PCI: hi=2 (CF), lo=1 (seq=1) E → T 0x759 22 02 00 91 03 00 91 04 CF #2 (序号=2) ...继续直到收到 173 字节收齐 173 字节后,重组的完整 UDS 响应是:
59 0A 09 D1 00 00 D1 01 00 91 01 00 91 02 ... (171 字节 DTC 列表)3.5 多帧重组的关键代码
// 简化版多帧接收while(true){TCANFD f=receive_can_frame();if(f.FIdentifier!=expected_resp_id)continue;u8 pci=f.FData[0];u8 hi=pci>>4;// PCI 高 4 位决定帧类型if(hi==0){// SingleFrames32 len=pci&0x0F;memcpy(out,&f.FData[1],len);returnDONE;}if(hi==1){// FirstFrametotal_len=((pci&0x0F)<<8)|f.FData[1];// 12-bit 长度memcpy(out,&f.FData[2],6);// 前 6 字节collected=6;send_flow_control(req_id);// 关键!必须发 FCexpected_seq=1;}if(hi==2){// ConsecutiveFrameu8 seq=pci&0x0F;if(seq!=expected_seq)continue;// 序号不对就丢expected_seq=(expected_seq+1)&0x0F;// 0-F 循环memcpy(out+collected,&f.FData[1],7);collected+=7;if(collected>=total_len)returnDONE;}}