OBD-II/UDS 渗透测试(一):协议基础 — CAN 总线与 ISO-TP

发布时间:2026/7/10 23:22:53
OBD-II/UDS 渗透测试(一):协议基础 — CAN 总线与 ISO-TP OBD-II/UDS 渗透测试一协议基础 — CAN 总线与 ISO-TP第 1 章 车载诊断的为什么和是什么1.1 一句话起源1.2 OBD-II 是什么、UDS 是什么、它们的关系1.2.1 SIDUDSvs ModeOBD-II—— 容易混淆的概念1.3 为什么诊断协议是攻击金矿第 2 章 CAN 总线层基础2.1 帧结构11-bit 标准帧2.2 11-bit 还是 29-bit2.3 OBD-II 11-bit 寻址规则关键2.4 实战采样一帧完整 CAN第 3 章 ISO-TP 传输层3.1 为什么需要 ISO-TP3.2 四种 PCIProtocol Control Information帧3.3 PCI 字节详解3.4 多帧重组的完整交互实战3.5 多帧重组的关键代码[AI 整理声明]本文内容来源于作者在汽车网络安全实战项目中积累的测试笔记由 AIClaude整理归纳输出行文措辞、内容组织和格式均由 AI 完成原始测试发现与知识积累来自作者本人。本文是「OBD-II/UDS 汽车网络安全实战手记」系列第一篇内容基于真实渗透测试项目。每个知识点配有实际采集到的总线报文作佐证由浅入深从什么是 CAN 总线一路到如何识别 ISO-TP 多帧。文中 F-XXX 为测试发现编号。系列文章一协议基础 — CAN 总线与 ISO-TP本文 · 二UDS 服务字典 · 三安全机制 — NRC Session SecAccess · 四实战攻防与方法论第 1 章 车载诊断的为什么和是什么1.1 一句话起源20 世纪 80 年代加州空气委员会CARB要求所有上路汽车都必须能被一台外部设备读出排放相关故障——这就是 OBDOn-Board Diagnostics。1996 年起 OBD-II 在美国成为法规要求欧洲叫 EOBD2001 年起中国是 GB18352 系列法规2007 年起强制。OBD-II 的最初目的非常窄让维修工和环保部门读我的发动机控制器排放有没有故障。当年没人想过攻击者会用同一个接口去刷固件、关闭电池保护、抹掉攻击痕迹。1.2 OBD-II 是什么、UDS 是什么、它们的关系OBD-II是法规层面的最小诊断协议。物理上规定了一个 16 针的方梯形插座J1962 接口逻辑上规定了 10 个 Mode0x01-0x0A的命令Mode名称含义0x01Show current data读实时排放参数车速、转速、O₂ 浓度、冷却液温度等0x02Show freeze frame读冻结帧DTC 触发瞬间的现场快照0x03Show stored DTCs读已存储的排放相关 DTC0x04Clear DTCs清除 DTC 和相关排放数据0x05Test results, O₂ sensor氧传感器测试结果仅非 CAN 总线用0x06Test results, other其他排放部件/系统测试结果0x07Show pending DTCs读 pending DTC0x08Control on-board component控制排放相关部件EVAP 系统泄漏测试等0x09Request vehicle info读车辆信息VIN、校准 ID、CVN 等0x0APermanent DTCs读永久 DTC不能被 Mode 0x04 清掉OBD-II 只关心排放相关连开/锁车门都没有。Mode 0x0A PermanentDTC 是排放反取证的法规防线——见第 8 章。UDSUnified Diagnostic ServicesISO 14229是OEM 自定义诊断的统一标准。它定义了一套服务标识 SID0x10-0x3E 0x83-0xBF覆盖整车所有 ECU 的所有诊断需求——读写 ECU 参数、刷固件、控制执行器、读故障码、控制安全访问……UDS 的服务可以复用OBD-II 已有的功能比如 UDS 0x19 ReadDTC 几乎涵盖了 OBD-II Mode 0x03。所以现代车上 ECU 通常同时支持 OBD-II 和 UDSOBD-II 接口用最少功能合规UDS 是真正干活的接口。1.2.1 SIDUDSvs ModeOBD-II—— 容易混淆的概念两者都是 CAN data 第一字节后面的命令码。区别如下对比项OBD-II ModeUDS SID取值范围0x01-0x0A固定 10 个0x10-0x3E0x83-0xBF~30 个关心的事只关心排放相关ECU全部诊断功能请求 ID通常用0x7DF广播或0x7E0-0x7E7OEM 自定义正响应规则Mode 0x400x01 → 0x41SID 0x400x10 → 0x50负响应格式7F Mode NRC7F SID NRC完全一样关键两者取值范围不重叠0x01-0x0A vs 0x10所以一个 ECU 收到0x03就知道这是 OBD读排放 DTC收到0x19就知道这是 UDS也是读 DTC但功能丰富得多。两套命令复用同一条 CAN 总线。部分服务功能等价OBD-II Mode 0x03 读 DTC ≈ UDS SID 0x19 ReadDTCUDS 多 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我们最关心的就两件事ID11 bit决定总线优先级数值越小优先级越高和目标OBD-II 用 ID 来路由请求/响应Data最多 8 字节负载2.2 11-bit 还是 29-bitCAN 帧的 IDE 位决定是 11-bit (Classic) 还是 29-bit (Extended)。OBD-II 法规允许两种11-bit诊断请求广播0x7DF响应来自各 ECU0x7E8-0x7EF29-bit诊断请求广播0x18DB33F1响应0x18DA F1 xx商用车重卡多用 29-bitJ1939 协议族要求乘用车多用 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, length2)这是后面所有协议层的基础——CAN 帧只是个容器真正的诊断信息在 Data 区里Data 的第 1 字节是 ISO-TP 的 PCI 头。第 3 章 ISO-TP 传输层3.1 为什么需要 ISO-TPCAN 一帧最多 8 字节。但 UDS 读个 VIN 就 17 字节读个 SupportedDTC 列表可能几百字节。怎么办答案是ISO-TPISO 15765-2——在 CAN 之上加了一层分段重组协议类似 IP 之上的 TCP但简单得多。3.2 四种 PCIProtocol Control Information帧ISO-TP 用 Data 的第 1 字节高 4 位区分帧类型高 4 位名称含义0SF (SingleFrame)数据 ≤7 字节一帧打完1FF (FirstFrame)多帧的第一帧2CF (ConsecutiveFrame)多帧的后续帧3FC (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 (0continue, 1wait, 2overflow) 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, length3 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, │ │ │ BS0 (一次发完), STmin0 │ │ └──► STmin (相邻 CF 间隔) │ └──► BS (块大小) └──► PCI 0x30 (FC, flagcontinue) E → T 0x759 21 D1 01 00 91 01 00 91 CF #1 (序号1) │ └──────────────────────────► 后续 7 字节数据 └──► PCI: hi2 (CF), lo1 (seq1) 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 freceive_can_frame();if(f.FIdentifier!expected_resp_id)continue;u8 pcif.FData[0];u8 hipci4;// PCI 高 4 位决定帧类型if(hi0){// SingleFrames32 lenpci0x0F;memcpy(out,f.FData[1],len);returnDONE;}if(hi1){// FirstFrametotal_len((pci0x0F)8)|f.FData[1];// 12-bit 长度memcpy(out,f.FData[2],6);// 前 6 字节collected6;send_flow_control(req_id);// 关键必须发 FCexpected_seq1;}if(hi2){// ConsecutiveFrameu8 seqpci0x0F;if(seq!expected_seq)continue;// 序号不对就丢expected_seq(expected_seq1)0x0F;// 0-F 循环memcpy(outcollected,f.FData[1],7);collected7;if(collectedtotal_len)returnDONE;}}