在这里插入图片描述

[AI 整理声明] 本文内容来源于作者在汽车网络安全实战项目中积累的测试笔记,由 AI(Claude)整理归纳输出;行文措辞、内容组织和格式均由 AI 完成,原始测试发现与知识积累来自作者本人。

本文是「OBD-II/UDS 汽车网络安全实战手记」系列第三篇,深度解析 UDS 安全机制的三个层次:NRC 负响应码情报价值、Session 会话机制的攻击面,以及 SecAccess(0x27)Seed-Key 协议的常见漏洞。文中 F-XXX 为测试发现编号。

系列文章:(一)协议基础 — CAN 总线与 ISO-TP · (二)UDS 服务字典 · (三)安全机制 — NRC + Session + SecAccess(本文) · (四)实战攻防与方法论


第 5 章 NRC 字典与攻击者解读

ECU 拒绝请求时返回负响应7F <SID> <NRC>

NRC(Negative Response Code)字典本身就是攻击者的情报来源——不同 NRC 暗示不同的下一步。

5.1 常见 NRC 完整字典

NRC 名称 含义 攻击者解读
0x10 generalReject 通用拒绝 信息少
0x11 serviceNotSupported 该 SID ECU 不实现 ✓ 该 ECU 没这服务
0x12 subFunctionNotSupported 该 sub-function 不支持 服务存在,仅子功能不对
0x13 incorrectMessageLengthOrInvalidFormat 参数格式/长度错 服务存在,等正确格式
0x22 conditionsNotCorrect 条件不满足 服务存在,等条件
0x24 requestSequenceError 时序错 服务存在,缺前置步骤
0x31 requestOutOfRange 参数超范围(地址/DID/routineID) 服务存在,仅该 ID/地址不对
0x33 securityAccessDenied 安全访问被拒 服务存在,需要 SecAccess
0x35 invalidKey key 错 说明你已经过了 RequestSeed 这一步
0x36 exceedNumberOfAttempts 尝试次数超限 ★ 触发 lockout,留下证据
0x37 requiredTimeDelayNotExpired 还在延时中 ★ 防御机制工作中
0x70 uploadDownloadNotAccepted 不接受 U/D 服务存在
0x71 transferDataSuspended 传输被挂起 中间态
0x72 generalProgrammingFailure 编程失败 攻击中触发了 ECU 异常
0x73 wrongBlockSequenceCounter 块序号错 传输中途
0x78 requestCorrectlyReceivedResponsePending “我收到了,再等等” 不是错误,是延时
0x7E subFunctionNotSupportedInActiveSession 当前 session 不支持该 sub ★ 换 session 试试
0x7F serviceNotSupportedInActiveSession 当前 session 不支持该 SID 换 session 几乎必能解锁
0x92/0x93 voltageTooHigh/Low 电压异常 物理层问题

5.2 攻击者最爱的 NRC 模式

"★ 服务存在"型 NRC(0x12/0x13/0x22/0x24/0x31/0x33/0x70/0x7E/0x7F)告诉攻击者:

这个服务实现了,仅是当前调用不对。继续探索可以打开它。

最坏的实现是返回 NRC 0x33 SecAccessDenied——既告诉攻击者"服务存在",又告诉"仅需破解 SecAccess"。

最好的实现是返回 NRC 0x11 SvcNotSupported(让攻击者误以为服务不存在)或者沉默不响应(让攻击者无法区分"服务不存在"和"被拒")。

5.3 实战 — NRC 是情报金矿

同样是 routine ID 枚举(31 03 XXXX 探测 routine 0xXXXX 是否存在),不同 ECU 的 NRC 选择策略差异巨大

隐蔽防御(blanket response 最佳实现):用 NRC 0x33 作万能挡板,不管是否存在都返回同一个 NRC:

攻击者探 routine 0x0301 (存在但需 SecAccess):
T → 0x7C1   04 31 03 03 01 00 00 00       请求: 31 03 0301
E → 0x7C9   03 7F 31 33 00 00 00 00       NRC 0x33 SecAccessDenied

攻击者探 routine 0xDEAD (根本不存在):
T → 0x7C1   04 31 03 DE AD 00 00 00       请求: 31 03 DEAD
E → 0x7C9   03 7F 31 33 00 00 00 00       NRC 0x33 SecAccessDenied   ← 一样

→ 攻击者拿到两个一样的 NRC, 无法分辨哪个 routine ID 真实存在
→ 必须破解 SecAccess 才能验证, 大幅抬高攻击成本

F-039 反例 — 透明响应(最坏的实现)

攻击者探 routine 0x0301 (存在但需 SecAccess):
T → 0x7C0   04 31 03 03 01 00 00 00
E → 0x7C8   03 7F 31 33 00 00 00 00       NRC 0x33 SecAccessDenied   ← "服务存在"

攻击者探 routine 0xDEAD (不存在):
T → 0x7C0   04 31 03 DE AD 00 00 00
E → 0x7C8   03 7F 31 31 00 00 00 00       NRC 0x31 OutOfRange        ← "ID 不对"

→ 攻击者一秒分辨: NRC 0x33 = 有效 routine, NRC 0x31 = 无效 routine
→ 然后集中精力破解这些 routine 的 SecAccess

最佳实践:把所有 NRC 收敛成同一个值(blanket response)是最简单有效的"情报隐蔽"防御——攻击者拿不到差异信号。

5.4 特殊 NRC: 0x78 — “我还在处理,别超时”

5.4.1 为什么需要 0x78

UDS 协议规定 tester 发完请求后必须在 P2_max 内(标准 50 ms)收到 ECU 的第一个响应,否则就判超时

但现实里有些操作 ECU 真的需要很长时间

SecAccess 验证 key  → ECU 跑加密算法, 可能 100-500 ms
ClearDTC FF FF FF   → 清几百条 NVRAM 记录, 可能几秒
Routine 启动 NVRAM 擦除 → 可能 10+ 秒
RequestDownload     → 检查地址空间、分配缓冲区
进 Programming session → 切换内存映射、关闭看门狗

0x78 是这个矛盾的解法:让 ECU 用"我还活着,请再等等"的信号主动延长本次超时。

5.4.2 时序图
T(tester)                      E(ECU)
────────────────────────────────────────
   │   request (e.g. 27 04 KK KK KK KK)
   ├─────────────────────────────►
   │                              │
   │   7F 27 78  "处理中"         │  ECU: 我在算 key
   │◄─────────────────────────────┤
   │                              │
   │   计时器重置 → 用 P2*_max     │  ECU: 继续算... (500ms 默认, 比 P2 大 10x)
   │                              │
   │   7F 27 78  "还在处理"        │  ECU: 还在算 (允许重复发)
   │◄─────────────────────────────┤
   │                              │
   │   67 04                      │  ECU: 算完了, 解锁成功!
   │◄─────────────────────────────┤
5.4.3 攻击者 / 防御者视角

给攻击者的情报:哪些请求触发 0x78 = 哪些操作"贵"。

  • 触发 0x78 的 SecAccess key 验证 → 说明 ECU 真的在算(不是"假装算"返回 NRC)
  • 触发 0x78 的 Routine 0x31 → 说明这个 routine 是真实业务(不是 stub)
  • 不触发 0x78 直接返回 NRC → 说明 ECU 在常量表查询,操作"便宜"

给防御者的考量:0x78 是把双刃剑。合理使用可解决长操作问题;恶意 ECU 可以无限发 0x78 挂住 tester 会话 → DoS。所以 tester 端必须设上限(MAX_PENDING_COUNT)。

5.4.4 处理代码
static s32 wait_response(...) {
    s64 start;
    app.get_timestamp(&start);
    s32 pending = 0;

    while (true) {
        if (now - start > P2_max) return TIMEOUT;

        if (frame.data[1] == 0x7F && frame.data[3] == 0x78) {
            pending++;
            if (pending > MAX_PENDING_COUNT) {
                return GIVE_UP;
            }
            app.get_timestamp(&start);   // 关键: 重新计时
            continue;
        }
        // ... 处理正/负响应
    }
}

MAX_PENDING_COUNT 经验值:设 5-8 适合覆盖常见 ECU 慢操作,设 >20 给恶意 ECU 留下挂住会话的空间。


第 6 章 会话 Session 机制

6.1 会话的本质

UDS 把 ECU 的诊断功能按"威胁等级"分组。不同 session 解锁不同 SID 集合:

default session (0x01)
  ├── 仅最基础诊断 (0x10/0x14/0x19/0x22 等)
  └── 永远可用,上电默认在这里

extended session (0x03)
  ├── default 全部功能
  ├── + 0x2E WriteDID
  ├── + 0x31 RoutineControl
  ├── + 0x85 ControlDTCSetting
  └── 需要明确切换

programming session (0x02)
  ├── 固件刷写专用
  ├── + 0x34/0x35/0x36/0x37
  └── 通常 default 下 不可达, 需要先 SecAccess 或先 extended

6.2 标准 sessions 速查

名称 典型用途
0x01 defaultSession 上电默认
0x02 programmingSession 刷固件
0x03 extendedDiagnosticSession 大部分诊断
0x04 safetySystemDiagnosticSession 安全相关(ABS/AirBag)

6.3 S3 timer

S3 是 ECU 自动从非 default session 退回 default 的超时。ISO 14229 标准 = 5000 ms。但实际 OEM 实现五花八门:

  • 部分 OEM 5 秒严格
  • 部分 OEM 拉长到 20+ 秒(F-012:本次 10/11 ECU 在 extended 下 >20s 不退回)
  • 部分 OEM 完全不实现 S3 timer(一旦进 extended 就一直在,除非显式退)

S3 timer 的存在意义是自动闭合攻击窗口——攻击者切换到危险 session 后,必须周期发 0x3E TesterPresent 才能维持。S3 timer 缺失 = 攻击者只需切一次就一直留在危险 session。

6.4 实战 — session 切换

进 extended

T → 0x771   02 10 03 00 00 00 00 00      请求: 10 03
E → 0x779   06 50 03 00 32 01 F4 00      切换成功: 50 03 + P2 (50ms) + P2* (500ms)

进 Programming

T → 0x771   02 10 02 00 00 00 00 00      请求: 10 02
E → 0x779   06 50 02 00 32 01 F4 00      切换成功! ★

# 验证已进入
T → 0x771   03 22 F1 86 00 00 00 00      请求: 读 activeSession DID
E → 0x779   05 62 F1 86 02 00 00 00      响应包含 "02" → 真在 Programming

6.5 OEM session(0x40-0x7F)

OEM 可以自定义会话,通常藏着更深的功能(开发后门、生产配置)。攻击者会专门探这一段。

6.6 session 状态的"非对称"

一个易被忽略的细节:session 切换可能不对称

示例:
0x78C session 0x60:default → 0x60 ✓
0x78C session 0x60:extended → 0x60 ✗

意味着这个 session 只能从 default 进,不能从 extended 进。对状态机模糊的 OEM 实现,攻击面隐藏在状态转移图里


第 7 章 SecAccess (0x27) 深入

7.0 概念区分 — SecAccess level vs Session

进入 SecAccess 细节前,先澄清最常见的误解:0x27 的 sub-function(叫 SecAccess level)和 0x10 的 sub-function(叫 Session)很像但完全不同

Session       = ECU 状态机的"档位"      (全局状态)
SecAccess lv  = 当前 session 内的"权限钥匙"  (颗粒权限)

类比:

进医院 ≈ 切 session         (10 03 进 extended)
拿病房钥匙 ≈ SecAccess unlock  (27 05 拿"病房钥匙")

你得先进对的医院 (extended session), 才能拿对应的病房钥匙

对比表

对比维度 Session (0x10 sub) SecAccess level (0x27 sub)
决定什么 哪些 SID 整体可用 session 内细分权限
怎么获取 直接发 10 XX(通常无认证) 必须完成 Seed-Key 握手
失败响应 NRC 0x12/0x7E NRC 0x35 InvalidKey
持续期 S3 timer 内(5 秒-不限) session 内有效,session 切换通常作废
取值 0x01-0x04 标准 + 0x40-0x7F OEM 0x01-0x7E(奇数 RequestSeed / 偶数 SendKey)

Session 是外层,SecAccess level 是内层。访问受保护服务的完整流程:

┌──────────────────────────────────────────────────────┐
│ Step 1: 切到对的 session                              │
│   10 03 → ECU 切到 extended                           │
│                                                       │
│ Step 2: 在该 session 里做 SecAccess                   │
│   27 01 → ECU 返回 seed                               │
│   27 02 KK → ECU 验证 key → POS 解锁                 │
│                                                       │
│ Step 3: 享用解锁后的服务                              │
│                                                       │
│ Step 4: 切回 default 或 S3 超时                       │
│   → session 变了, SecAccess 通常一起作废              │
└──────────────────────────────────────────────────────┘

NRC 区分

  • NRC 0x7F = session 不对(换 session)
  • NRC 0x12 = level 不对(换 level,session 可能已对)
  • NRC 0x35 = key 不对(level 对了,session 也对了,就差算法)
同一 ECU 不同 (session, level) 解锁不同服务

一个 ECU 可能有这样的权限矩阵:

                       │ default │ extended │ programming
─────────────────────  │ (0x01)  │ (0x03)   │ (0x02)
22 F186 读 session     │  POS    │  POS     │  POS
22 F190 读 VIN         │  NRC    │  POS     │  NRC
2E F190 写 VIN         │  NRC    │  需 lvl1 │  NRC
31 03 routine 查询     │  NRC    │  POS     │  需 lvl5
34 RequestDownload     │  NRC    │  NRC     │  需 lvl5

常见 OEM level 分配模式

level 典型用途
0x01/0x02 通用维修(读 DID、清 DTC)
0x03/0x04 工程师(写参数、跑 routine)
0x05/0x06 编程权限(配合 programming session 刷固件)
0x11+ OEM 内部高权限(不公开)

7.1 Seed-Key 协议

SecAccess 是 UDS 唯一的 ECU 鉴权机制。本质是挑战-响应(Challenge-Response)

Step 1: Tester 请求 seed
   T → E   27 LL                          LL = 奇数 level
   E → T   67 LL <SS SS SS SS>            SS = N 字节随机 seed

Step 2: Tester 计算 key 并发送
   key = OEM_secret_algorithm(seed)
   T → E   27 (LL+1) <KK KK KK KK>
   E → T   67 (LL+1)                      POS = 解锁
   或:
   E → T   7F 27 35                       NRC 0x35 InvalidKey

关键属性

  • algorithm 是 OEM 私有秘密
  • seed 应该是强随机——每次 RequestSeed 都不同
  • 错误 key 应触发 lockout(防暴破)

7.2 实战 — 非标准 level(F-070)

T → 0x771   02 27 01 00 00 00 00 00      请求: RequestSeed level 1
E → 0x779   03 7F 27 12 00 00 00 00      NRC 0x12 SubFuncNotSupported
                                          → 不用 level 0x01!

意味着 0x779 用非标准 level——攻击者下一步要枚举 0x03/0x05/0x11/0x21/0x41 等。这本身就是情报泄漏(告诉了攻击者"答案不在 0x01")。

7.3 三种 seed 弱点

7.3.1 恒定 seed(最严重,F-015)
第 1 次 RequestSeed:
E → 0x77F   06 67 01 12 34 56 78 00      seed = 12 34 56 78

第 2 次 (10 秒后):
E → 0x77F   06 67 01 12 34 56 78 00      seed = 12 34 56 78  ★ 一模一样

第 N 次:
E → 0x77F   06 67 01 12 34 56 78 00      seed = 12 34 56 78  ★ 永远一样

攻击意义:攻击者只需抓一次合法维修工握手(用维修工具刷 ECU 时的报文),就能得到 (seed, key) 对。下次自己请求 seed 一样,发同样的 key 就解锁。

7.3.2 可预测 counter seed(极度危险,F-016)
T-0:    seed = 00 00 00 01
T-1:    seed = 00 00 00 02
T-2:    seed = 00 00 00 03
...
T-N:    seed = 00 00 00 NN

这种漏洞为什么"极度危险"?

恒定 seed 至少 OEM 后期能改算法把之前抓到的 (seed, key) 对作废。但 counter seed 是更结构化的弱点——它把"算法逆向"从被动等待降到主动可控。攻击思路分 3 个层面:

层面 1:多次握手抓包 = 结构化数据集

如果攻击者能在不同时间抓到合法维修工的多次握手:

握手 #1 (维修第一天):
  seed = 00 00 01 23    key = AA BB CC DD

握手 #2 (维修第十天):
  seed = 00 00 01 24    key = ?  (待分析)

关键洞察:seed 之间是已知线性关系,拿到的 (seed, key) 对不是孤立样本——这就把"通用算法逆向"降级为**“算法在小邻域内的行为分析”**。

如果发现 key_124 - key_123 = 固定常数 C,几乎可以判定算法是 key = α·seed + β 这种线性结构。强随机 seed 完全没有这种结构信号。

层面 2:在线试算法,counter 助攻

candidate_algos = [
    lambda s: s ^ 0xDEADBEEF,                # XOR 固定 mask
    lambda s: s + 0x5A5A5A5A,                # ADD 固定常量
    lambda s: ((s << 4) ^ s) & 0xFFFFFFFF,   # 简单位混淆
    lambda s: byte_reverse(s),               # 字节反转
    # ... 几十种 OEM 常用模板
]

for algo in candidate_algos:
    seed = request_seed()
    key  = algo(seed)
    rsp  = send_key(key)
    if rsp == 'POS':
        print(f"算法找到: {algo.__name__}")
        break
    if rsp == 'NRC 0x36':                    # lockout 触发
        wait_lockout_recover()
        continue

counter seed 的助攻:强随机 seed 每次都新,错的 key 浪费在了不同 seed 上,无法累积信息;counter seed 每次新但有规律,攻击者可以复用算法的中间分析结果,跨次 RequestSeed 协同试错。

层面 3:杂交攻击(最强)

Step 1: 抓 1 次合法握手 → 得 (seed=123, key=AABBCCDD)
Step 2: 攻击者自己 RequestSeed → 得 seed=124
Step 3: 离线试算法假设 f, 看 f(123) 是否等于 AABBCCDD
Step 4: 找到 f → 计算 f(124) → 在线 SendKey → POS!

= 只需 1 次握手抓包 + 几小时离线算 + 1 次在线尝试

与恒定 seed 攻击对比

攻击维度 恒定 seed counter seed
抓 1 次握手 → 直接 replay ✓ 永远有效 ✗(counter 已变)
抓 1 次握手 + 算法逆向 困难(只有 1 个样本) 容易(结构化样本)
不抓握手,纯算法盲猜 受锁机限制慢 大幅加速(counter 可控)
OEM 换算法后 算法改了就失效 结构性弱点(可预测)仍在
7.3.3 弱熵 seed

seed 看起来随机,但分析发现只有 16-bit 有效熵(4 字节中 2 字节是固定 pattern):

seed = XX XX 5A A5      ← 后 2 字节恒定

攻击意义:暴力破解搜索空间从 2^32 降到 2^16 = 65536 次尝试。

7.4 Lockout 防御

UDS 标准要求 ECU 在错 key 几次后进入"锁机"状态:

好的 lockout 示例

错 key 第 1 次:
E → 0x749   03 7F 27 35 00 00 00 00      NRC 0x35 InvalidKey

错 key 第 2 次:
E → 0x749   03 7F 27 36 00 00 00 00      NRC 0x36 ExceedAttempts (锁机开始)

试图再 RequestSeed:
E → 0x749   03 7F 27 37 00 00 00 00      NRC 0x37 TimeDelayNotExpired

等 15 秒... (锁机恢复 ✓)
问题 1:阈值递增
正常 (固定阈值):
  cycle 1: 错 5 次 → 锁机, 等 15s
  cycle 2: 错 5 次 → 锁机, 等 15s

实际表现 (递增):
  cycle 1: 错 5 次 → 锁机
  cycle 2: 错 8 次 → 锁机   ← 阈值变高
  cycle 3: 错 12 次 → 锁机

固定阈值 5/cycle 情形下,100 万次破解需 20 万 cycles × 15s ≈ 35 天(不可行)。递增阈值情形下,阈值线性增长,攻击者吞吐量比固定阈值高数十倍,破解时间可压缩到数小时。

问题 2:持久 lockout
触发 lockout 后做 IGN cycle (拔钥匙重启)...

T → 0x777   02 27 01 00 00 00 00 00     RequestSeed
E → 0x77F   03 7F 27 37 00 00 00 00     NRC 0x37 还在锁  ← 应该被 IGN cycle 解除

3 小时后仍锁定。这是 OEM 把 lockout 状态写到了 NVRAM 永久区而非 RAM。这是 DoS 漏洞:合法维修工只要触发一次锁机,这台车这个 ECU 几小时无法维修

问题 3:不一致
0x749 EPS:   错 2 次锁机, 15s 恢复
0x77F TBOX:  错 5 次锁机, >3h 不恢复
0x7C9 BMS:   错的 NRC 是 0x33 (混淆), 阈值未深测
0x78C DCDC:  NRC 0x37 延时 > 30s

整车无统一 baseline 意味着 OEM 内部没有统一的 SecAccess 安全标准

7.5 SecAccess 的攻击者方法论

Phase 1: 枚举 level
  - 试 0x01/0x03/0x05/0x11/0x21/0x41/0x61... 找返回 67/seed 的
  - 注意非标准 level 情形 (F-070)

Phase 2: 评估 seed 强度
  - 多次 RequestSeed, 看 seed 是否变化
  - 不变 = 恒定 (F-015 类)
  - 线性变化 = 可预测 (F-016 类)
  - 看似随机但有 pattern = 弱熵

Phase 3: 算法逆向
  - 从合法维修工抓 (seed, key) 对
  - 或从 ECU 固件 dump 出来 (需要别的 vuln)
  - 或暴破弱熵 seed 空间

Phase 4: 实战利用
  - RequestSeed → 算 key → SendKey → 进入危险 session

附录 B — NRC 速查表

0x10  generalReject
0x11  serviceNotSupported               ── 服务不存在
0x12  subFunctionNotSupported            ── ★ 服务存在, 子功能错
0x13  incorrectMessageLengthOrInvalidFormat  ── ★ 服务存在, 格式错
0x14  responseTooLong
0x21  busyRepeatRequest
0x22  conditionsNotCorrect               ── ★ 服务存在, 条件不满足
0x24  requestSequenceError               ── ★ 服务存在, 时序错
0x25  noResponseFromSubnetComponent
0x26  failurePreventsExecutionOfRequestedAction
0x31  requestOutOfRange                  ── ★ 服务存在, ID/地址错
0x33  securityAccessDenied               ── ★ 服务存在, 需 SecAccess
0x35  invalidKey                          ── SecAccess key 错
0x36  exceedNumberOfAttempts              ── 尝试超限, lockout 触发
0x37  requiredTimeDelayNotExpired         ── 延时未到
0x38-0x4F  SecurityAccess 衍生
0x70  uploadDownloadNotAccepted           ── ★ 服务存在, U/D 不接受
0x71  transferDataSuspended
0x72  generalProgrammingFailure           ── 编程异常
0x73  wrongBlockSequenceCounter           ── 块序号错
0x78  requestCorrectlyReceivedResponsePending  ── ★ 等等再回
0x7E  subFunctionNotSupportedInActiveSession   ── ★ 换 session
0x7F  serviceNotSupportedInActiveSession       ── ★ 换 session
0x80-0xFE  其他
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐