Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

2.6 安全访问(0x27)——获取病历权限

你有一个OBD诊断仪,它能做什么

如果你在默认会话、没有安全访问——你能读DTC、读DID、发TesterPresent。一切只能读、不能写。如果你想写入VIN码修改安全气囊抑制逻辑重标定ADAS摄像头——你必须先证明你有权限。

UDS的权限证明机制叫SecurityAccess(0x27)——你需要和ECU完成一套Seed/Key交互:ECU发给你一个随机种子,你用密钥算法算出答案送回去,ECU自己也在内部算了一遍——两者吻合,证明你拥有和ECU相同的密钥→解锁。


为什么是Seed/Key而不是RSA签名或TLS握手

这是UDS被安全领域嘲笑得最多的一个设计——用对称加密做鉴权?这不是上世纪70年代的方案吗?

你猜为什么。

2006年,UDS标准第一版发布时——一辆ECU的核心处理器是PowerPC e200(运行在40~80MHz),或者在高端ECU上一个ARM7/ARM9。这个芯片在处理点火角和喷油量计算已经接近满载——它没有剩余的算力和内存去做一次完整的RSA签名验证

Seed/Key的好处在哪里:

  • 只需要对称运算——ECU用同一个算法(通常是指定的单向哈希或对称算法版本)生成密钥和校验诊断仪的返回值。不需要生成/管理公钥基础设施。
  • 算法可以简单到 XOR + CRC16 组合。真正的安全性不依赖算法本身有多强——而是种子是随机的、每次都不一样。

核心洞察:UDS SecurityAccess不是密码学的最佳实践——它是在那个硬件约束下可实现的最高安全门槛。它不是用来阻止国家级攻击者——而是阻止“普通诊断仪没有授权去写VIN码“这样的日常安全违规。在物理访问受限的OBD口场景下,Seed/Key的防护强度足以让大多数未授权工具无法进入敏感操作。在现代汽车里,更高级的Authentication已经被ISO 14229-1的更新版本补充——使用0x29 Authentication服务进行更强的身份验证(如基于证书的握手)。


Seed/Key两极分层——奇/偶编码

0x27的子功能码编码规则有一个极简洁的数学关系:

请求种子:  SID + 奇数 (0x01, 0x03, 0x05, 0x07...0x41)
发送密钥:  SID + 偶数 (0x02, 0x04, 0x06, 0x08...0x42)
发送密钥 = 请求种子 + 1

对应的安全等级(security level):

请求种子(奇数)发送密钥(偶数)安全等级
0x010x02Level 1
0x030x04Level 2
0x050x06Level 3
0x07~0x410x08~0x42更高安全等级(车厂定义)

0x01/0x02(Level 1)和 0x03/0x04(Level 2)在行业实践中被广泛使用。

为什么奇/偶编码而不用独立的SID? 因为这个位具有隐式的状态机检查。如果一个请求是奇数——ECU可以确定性判断“诊断仪在问种子“。如果下一个请求是该奇数值+1的偶数——ECU可以确定“诊断仪在用送密钥回复我上次给的种子“。其他任何数字→NRC 0x24(requestSequenceError)。编码本身防止了顺序错误。

核心洞察:最优雅的协议设计不依赖额外的状态变量——而把状态编码在消息里。seed/key的奇偶编码是这种设计的教科书级范例。


完整的Seed/Key交互流程

以请求安全等级1为例:

Step 1: 请求种子
─────────────────
诊断仪 → ECU:   0x27 0x01
                  SID   子功能0x01(requestSeed, level 1)

ECU 内部:
  - 检查安全等级1是否存在配置表里
  - 检查是否在延迟定时器失效(如果在之前失败过太多次)
  - 生成随机种子 seed = 0xA3D45F12 (不定长, 由配置决定)
  - 存储种子, 设置 securityAccessReqInProgress = TRUE

ECU → 诊断仪:   0x67 0x01 0xA3 0xD4 0x5F 0x12
                  SID+0x40  level  seed[4 bytes]


Step 2: 计算并发送密钥
──────────────────────
诊断仪:
  - 从响应中取出 seed = 0xA3D45F12
  - 用保存在诊断仪ROM/诊断工具中的KeyAlgorithm计算Key
    Key = f(seed)  →  假设算法是在固定查表和XOR结合的结果
       = 0x3E 0xD5 0x8A 0x02
  - 构建请求

诊断仪 → ECU:   0x27 0x02 0x3E 0xD5 0x8A 0x02
                  SID   子功能0x02(sendKey)

ECU 内部:
  - 检查请求的子功能值是 (上一次requestSeed+1) ← 序列检查
  - 检查 reqInProgress == TRUE ← 状态检查
  - 自己用 同一种子 + 同种算法 计算出 expectedKey
  - 比较 expectedKey == 收到的 Key?
     - 不匹配 → NRC 0x35(invalidKey)
       增加错误尝试计数器
       如果达到限定次数→ startDelayTimer
     - 匹配 → 解锁安全等级1
       securityLevel = DCM_SEC_LEV_L1
       清除错误尝试计数

ECU → 诊断仪:   0x67 0x02
                  SID+0x40  echo子功能0x02 (解锁确认)

关键点

  • 种子的长度、密钥的长度由ECU配置决定——诊断仪根据收到的种子长度来推断密钥应计算的长度;
  • Seed可以是静态的(同一次驾驶循环不变)或动态的(每次请求新种子);
  • 每切换一次会话,安全等级复位到LOCKED——你必须重新解锁。

暴力破解防护 —— InvalidKey + DelayTimer

安全访问必须抵抗暴力攻击——反复用不同的密钥尝试直到猜中。

UDS的应对机制:

每次输入错误密钥 → 激活"错误尝试计数器" (最多预设次数, 通常3~5次)
超过预设次数     → 启动"延迟定时器" (Delay Timer, 通常几秒至几分钟)
延迟定时器运行期间 → 拒绝一切 securityAccess 请求
                    ECU回复 NRC 0x37(requiredTimeDelayNotExpired)
延迟结束          → 允许重新请求种子, 计数器清零
                   (但某些实现是每次等时, 不回零——永久锁死)

这样做把暴力破解的周期从“微秒“推到了“小时甚至年“——在物理可访问的诊断口上,这时间太长以至于暴力攻击没有可操作性。


请求格式

requestSeed(奇数子功能):

Byte 0: 0x27 (SID)
Byte 1: securityAccessType (奇数子功能)
        bit7  = suppressPosRsp (1=禁止正响应)
        bit6-0 = 0x01 (level 1 requestSeed)
                0x03 (level 2 requestSeed)
                0x05 (level 3 requestSeed)
Byte 2..N: securityAccessDataRecord[] (可选: 用于传递附加的身份验证参数)

sendKey(偶数子功能):

Byte 0: 0x27 (SID)
Byte 1: securityAccessType (偶数子功能 = requestSeed + 1)
Byte 2..N: securityKey[] (计算得到的密钥, 长度由ECU的安全等级配置决定)

正响应(requestSeed):

Byte 0: 0x67 (SID+0x40)
Byte 1: securityAccessType (回声子功能)
Byte 2..N: securitySeed[] (ECU生成的随机种子)
           特殊: seed=0x0000 表示当前已经解锁该安全等级

正响应(sendKey):

Byte 0: 0x67 (SID+0x40)
Byte 1: securityAccessType (回声子功能)
(无额外数据——极简确认)

三个暗坑

暗坑1:种子值为0的特殊含义

如果ECU在requestSeed正响应中返回一个全零的种子——0x67 0x01 0x00 0x00——这不是种子真的是0——它表示“这个安全等级已经解锁了“。

如果诊断仪忽略这个特殊含义,它会给算法传入全零种子算出某个有效Key去sendKey——ECU依然会通过,但正确的做法是:碰到种子=0→跳过sendKey、直接认为已解锁。

暗坑2:序列错误NRC 0x24

如果你先发了0x27 0x02(sendKey)而没有先走requestSeed步骤——ECU无法把收到的“密钥“关联到任何一个种子——返回NRC 0x24(requestSequenceError)。你必须按照先requestSeed、收到响应后、再sendKey的顺序。

同样地——你尝试在一个安全级别(level1)已经解锁的情况下再请求同名种子——返回值是该级别已解锁(种子=0)。如果你直接请求level2的种子(0x27 0x03)——ECU抹去level1、进入level2的解锁流程。

暗坑3:会话切换重置安全

这是全UDS最频繁造成诊断脚本故障的原因——你进入编程会话、解锁了安全等级2、下载固件、下载完成、退回到默认会话……然后你突然又用了之前的未重新解锁状态去请求敏感DID——得到的却是NRC 0x7F(SNSIAS)——而你花了20分钟怀疑是不是DID写错了——实际上是因为编程会话→默认会话的切换已将安全等级归零。

解决:在任何会话切换后重新检查安全等级


安全等级和应用场景的映射

安全等级医院映射典型保护的操作
Locked(0x00)公共陌生人只能读取DTC、读公开DID
Level 1挂号医生读取受保护的DID(VIN码、配置值)、执行非关键IO控制变更
Level 2专科医生写DID、执行敏感例行控制、进入编程会话解锁
Level 3+主任医师重大重标定、闪存安全区访问、清除硬安全系统数据

本篇小结

  1. 0x27 SecurityAccess使用Seed/Key对称机制——ECU发随机种子,诊断仪用共享算法算密钥,ECU内部算同样算法对比——匹配则解锁安全等级。
  2. 奇偶编码(奇数=requestSeed,偶数=sendKey)本身编码了状态机——任何顺序错误直接NRC 0x24——无需额外状态变量跟踪。
  3. 暴力破解防护靠错误计数器+延迟定时器——超过尝试上限后锁死一段延迟时间,使实时爆破不可行。
  4. 种子全零的特殊含义(表示该等级已解锁)、序列错误NRC 0x24的触发条件、会话切换重置安全的暗坑——是三个诊断仪实现中最容易被忽视的坑。
  5. 安全等级在会话切换时自动复位——跨会话后必须重新解锁。

【下集预告】:诊断专家挂了号、证明了自己是授权医生。接下来该开化验单了——读取DID。DID(DataIdentifier)不是一个地址——它是一个语义标签。你说’给我发动机转速’——ECU自己去它的内存里找这个值该从哪里取。“按DID读数据“的力量在于——诊断仪不用知道任何一个ECU的内存布局。我们进入0x22。