UDS 31服务与27服务如何协同守护车载系统安全?
在现代汽车电子架构中,ECU(电子控制单元)的数量和复杂度呈指数级增长。从动力总成到车身控制,再到智能座舱与自动驾驶模块,每一个ECU都承载着关键功能。随之而来的,是对诊断访问权限的精细化管理需求——我们不能再允许“谁都能刷写固件”或“任意设备读取敏感参数”。
这就引出了两个至关重要的UDS(Unified Diagnostic Services)服务:Security Access(27服务)和Routine Control(31服务)。它们一个负责“验明正身”,另一个负责“执行任务”。只有当身份验证通过后,才被允许启动某些高风险操作。这种“先认证、后操作”的机制,正是车载系统安全设计的核心逻辑之一。
今天,我们就来深入拆解这两个服务是如何协同工作的,以及在实际开发中该如何正确实现这一安全链条。
为什么需要“安全解锁”才能调用31服务?
设想这样一个场景:某黑客利用一台廉价诊断仪连接车辆OBD接口,直接发送指令擦除Flash内存,导致发动机控制程序丢失——整车瞬间瘫痪。这听起来像电影情节,但在缺乏访问控制的系统中完全可能发生。
为防止此类攻击,ISO 14229-1标准定义了分层防护策略。其中:
- UDS 27服务(Security Access)提供动态身份验证;
- UDS 31服务(Routine Control)则用于触发ECU内部预设的功能性例程,比如:
- 擦除EEPROM
- 准备Flash编程环境
- 执行传感器自校准
- 启动通信链路测试
这些动作往往涉及硬件资源变更或持久化数据修改,一旦误用或滥用,后果严重。因此,在绝大多数主机厂的实际应用中,调用31服务前必须先通过27服务完成安全解锁,否则将返回NRC 0x33(Security Access Denied)。
换句话说:没有钥匙,别想开门干活。
先看基础:27服务是如何工作的?
它不是一个密码登录,而是一次挑战-响应博弈
27服务不是简单的“输入密码”机制,而是采用挑战-响应(Challenge-Response)模式,确保每次认证过程唯一且不可重放。
整个流程如下:
Tester请求Seed
Tester → ECU: 27 01ECU生成随机数并返回
ECU → Tester: 67 01 AA BB CC DD
这里的AABBCCDD是当前会话唯一的Seed值。Tester计算Key
使用OEM私有的算法(如AES加密、查表混淆、XOR掩码等),结合预存密钥对Seed进行处理,得出预期的Key。Tester提交Key
Tester → ECU: 27 02 EE FF GG HHECU本地验证Key是否匹配
若一致,则设置对应的安全等级标志位,例如security_level_3 = granted。
🔐 关键优势:由于Seed是随机生成的,即使攻击者截获一次通信内容,也无法复用该Key再次通过验证——这就是所谓的“防重放攻击”。
多级安全机制支持细粒度控制
不同子功能代表不同的安全等级:
| Subfunction | 含义 |
|---|---|
| 0x01 / 0x02 | Level 1 |
| 0x03 / 0x04 | Level 2 |
| 0x05 / 0x06 | Level 3 |
每个Level可对应不同敏感度的操作。例如:
- Level 1:读取标定参数
- Level 2:执行通信测试
- Level 3:进入编程模式(OTA升级必备)
此外,连续失败尝试会触发递增等待时间(Back-off Timer),甚至永久锁定,需断电重启才能恢复,进一步提升抗暴力破解能力。
再看核心:31服务到底能做什么?
它是ECU内部功能的“遥控开关”
你可以把31服务理解为一个标准化的远程任务调度器。它不直接执行具体逻辑,而是根据Routine Identifier去调用ECU内部注册好的函数。
其命令格式非常清晰:
31 [SubFunction] [RID_H] [RID_L] [Optional Data]常用子功能包括:
| SubFunction | 动作 |
|---|---|
| 0x01 | Start Routine |
| 0x02 | Stop Routine |
| 0x03 | Request Results |
举个例子:要启动“Flash编程准备”例程(RID=0x0002):
Tester → ECU: 31 01 00 02 ECU → Tester: 71 01 00 02 // 成功响应随后ECU就会执行类似关闭看门狗、切换时钟源、释放Flash保护等一系列底层操作,为后续刷写做准备。
支持异步执行与状态查询
对于耗时较长的任务(如EEPROM擦除),31服务支持异步模式:
- 发送
31 01 xx xx启动例程; - ECU立即返回确认,但后台继续运行;
- Tester定期轮询
31 03 xx xx查询执行结果; - 直到返回完成状态码为止。
这种方式避免了诊断会话因超时中断而导致失败。
协同工作流程详解:从连接到执行
下面是一个完整的典型交互流程,展示27与31服务如何配合完成一次安全操作。
🧩 步骤一:进入扩展会话
默认会话下仅开放基本服务,必须先进入扩展诊断会话:
Tester → ECU: 10 03 // 请求扩展会话 ECU → Tester: 50 03 // 确认进入🧩 步骤二:发起安全访问(27服务)
Tester → ECU: 27 05 // 请求Level 3的Seed ECU → Tester: 67 05 1A 2B 3C 4DTester使用OEM专用算法计算出Key(假设为9F 8E 7D 6C):
Tester → ECU: 27 06 9F 8E 7D 6C ECU → Tester: 67 06 // 验证成功,解锁Level 3此时ECU内部标记:security_level[3] = GRANTED
🧩 步骤三:调用31服务执行高权限例程
现在可以安全地启动受保护的例程了:
Tester → ECU: 31 01 00 02 // 启动Flash准备 ECU → Tester: 71 01 00 02 // 执行成功如果未解锁就直接调用?ECU将无情拒绝:
Tester → ECU: 31 01 00 02 ECU → Tester: 7F 31 33 // NRC 0x33: Security Access Denied🧩 步骤四:超时与状态清理
安全解锁状态不会永久有效。通常设定有效期为30秒至5分钟。超时后自动降级,下次调用需重新认证。
此外,任何复位事件(如电源重启、软件复位)都应清除所有安全状态,防止残留授权带来安全隐患。
权限模型设计:如何合理分配安全等级?
一个好的安全策略不应“一刀切”。我们应该根据不同例程的风险等级,分配合适的访问权限。
| Routine ID | 功能描述 | 推荐安全等级 | 场景说明 |
|---|---|---|---|
| 0x0001 | EEPROM Erase | Level 3 | 固件更新前准备 |
| 0x0002 | Flash Programming Prep | Level 3 | OTA升级必需 |
| 0x0010 | Communication Line Test | Level 2 | 维修站检测通路 |
| 0x0100 | Lighting Check Routine | Level 1 | 产线自动化测试 |
| 0x0200 | Sensor Calibration | Level 2 | 售后维修校准 |
这样既保证了安全性,又兼顾了可用性——产线工人不需要高强度认证即可运行灯光检测,而刷写操作则必须经过多重验证。
实战代码解析:如何在嵌入式端实现?
27服务简化处理逻辑
static uint32_t current_seed = 0; static bool security_unlocked[8] = {false}; // 支持多个Level static uint8_t active_level = 0; void HandleSecurityAccess(uint8_t subFunc, uint8_t *data, uint16_t len) { // 奇数子功能:请求Seed if (subFunc & 0x01) { current_seed = GetTrueRandom(); // 真随机源更安全 SendResponse(0x67, subFunc, (uint8_t*)¤t_seed, 4); } // 偶数子功能:提交Key else { uint32_t received_key = *(uint32_t*)data; uint32_t expected_key = OemCrypto_CalculateKey(current_seed); if (received_key == expected_key) { active_level = subFunc >> 1; security_unlocked[active_level] = true; SetSecurityTimer(active_level); // 启动超时定时器 SendPositiveResponse(0x67, subFunc); } else { IncrementFailCounter(); SendNegativeResponse(NRC_INCORRECT_KEY); } } }📌 注意事项:
- Seed应来自硬件TRNG(真随机数发生器)
- 密钥算法不得暴露于公开文档
- 失败计数建议存储于NVRAM,并支持渐进式延迟
31服务调度器实现
// 例程函数指针表 typedef struct { uint16_t rid; bool (*start)(uint8_t*); bool (*stop)(void); uint8_t (*result)(uint8_t*); uint8_t required_level; // 所需安全等级 } RoutineEntry; // 注册所有支持的例程 const RoutineEntry routines[] = { {0x0001, EepromErase_Start, EepromErase_Stop, EepromErase_Result, 3}, {0x0002, FlashPrep_Start, FlashPrep_Stop, FlashPrep_Result, 3}, {0x0010, ComTest_Start, ComTest_Stop, ComTest_Result, 2}, }; #define ROUTINE_COUNT (sizeof(routines)/sizeof(RoutineEntry)) void HandleRoutineControl(uint8_t subFunc, uint8_t *data, uint16_t len) { if (len < 2) { SendNegativeResponse(NRC_INVALID_FORMAT); return; } uint16_t rid = (data[0] << 8) | data[1]; uint8_t sec_level_required = 0; const RoutineEntry *routine = NULL; // 查找匹配的例程 for (int i = 0; i < ROUTINE_COUNT; i++) { if (routines[i].rid == rid) { routine = &routines[i]; sec_level_required = routine->required_level; break; } } if (!routine) { SendNegativeResponse(NRC_SUBFUNCTION_NOT_SUPPORTED); return; } // 检查安全权限 if (!security_unlocked[sec_level_required]) { SendNegativeResponse(NRC_SECURITY_ACCESS_DENIED); // 0x33 return; } // 分发操作类型 switch (subFunc) { case 0x01: // Start if (routine->start(&data[2])) { SendResponse(0x71, 0x01, data, 2); } else { SendNegativeResponse(NRC_CONDITIONS_NOT_CORRECT); } break; case 0x02: // Stop if (routine->stop()) { SendResponse(0x71, 0x02, data, 2); } break; case 0x03: // Query Result uint8_t res_data[4] = {data[0], data[1]}; uint8_t result = routine->result(res_data + 2); SendResponse(0x71, 0x03, res_data, 3); break; default: SendNegativeResponse(NRC_SUBFUNCTION_NOT_SUPPORTED); } }🔧 关键点说明:
- 每个例程绑定所需安全等级
- 在调度前统一检查权限
- 支持带外数据传递(Start时传参)
- 异常情况返回标准NRC码
常见坑点与调试秘籍
❌ 问题1:明明已解锁,为何仍被拒绝?
可能原因:
- 解锁的是Level 1,但例程要求Level 3;
- 安全状态超时失效;
- ECU复位后未重新认证;
- 子功能奇偶配对错误(如用05发Key);
✅ 解法:抓取完整CAN日志,核对Seed-Key流程及安全等级匹配关系。
❌ 问题2:Seed一直不变?
这通常是伪随机数种子固定所致。例如每次上电都用srand(1)初始化。
✅ 解法:使用ADC噪声、RTC计数差、Flash唯一ID等作为熵源,或启用MCU内置TRNG模块。
❌ 问题3:例程执行中收到其他请求怎么办?
若不加保护,可能导致资源竞争或堆栈溢出。
✅ 解法:
- 使用互斥锁(Mutex)保护临界区;
- 在例程运行期间暂停非必要诊断服务;
- 设置看门狗监控执行时间,防止单个任务卡死。
实际应用场景举例
场景一:OTA升级全流程中的角色
在空中下载升级过程中,31+27组合扮演关键前置角色:
- 车辆进入编程会话(10 02)
- 请求Level 3解锁(27 05 → 27 06)
- 调用31服务启动“Flash准备”例程
- 激活Bootloader分区
- 开始块传输(34/36/37服务)
整个过程形成闭环验证,确保只有授权服务器才能触发刷写。
场景二:产线终检自动化
在整车下线检测中,检测仪需批量执行功能测试:
- 启动灯光检测例程(RID=0x0100)
- 控制车灯闪烁
- 查询结果判断线路通断
虽然不涉密,但仍需Level 1认证,防止售后私自调用干扰生产流程。
最佳实践建议
| 项目 | 推荐做法 |
|---|---|
| Seed生成 | 使用硬件TRNG或强PRNG |
| Key算法 | OEM自研,禁止明文泄露 |
| 超时时间 | 30秒 ~ 5分钟,视场景而定 |
| 日志记录 | 记录每次认证尝试与31调用 |
| 安全等级 | 分级明确,最小权限原则 |
| 并发控制 | 单任务运行,避免冲突 |
结语:这不是功能,是防线
当我们谈论“uds 31服务”时,不能只看到它是一个可以启动例程的工具;更要意识到,它是暴露在外部世界的一个潜在攻击入口。而27服务的存在,就是为这个入口加上一把动态变化的锁。
掌握这两项服务的协同机制,不仅是实现UDS合规的基础,更是构建符合ISO 21434、UNECE R155等网络安全法规要求的关键一步。
对于每一位嵌入式开发者、诊断工程师、TIER1系统设计师来说,理解并正确实施这套“认证+操作”双因子控制模型,已经成为不可或缺的核心能力。
如果你正在开发ECU诊断功能,不妨问自己一句:
“我的31服务,真的有足够强的27服务守门吗?”
欢迎在评论区分享你的实战经验或遇到过的奇葩Bug!