4、UDS安全访问:安全访问服务(27h)原理、种子与密钥算法、解锁流程实现

各位同学,咱们今天聊一个非常核心的话题——安全访问。说白了,就是怎么给你的ECU上把锁。

你想想看,一辆车上那么多ECU,要是谁都能随便改数据,那还得了?比如刷写程序、标定参数、读取敏感信息,这些操作都必须有权限。UDS里专门有个服务干这事,就是27h服务——安全访问

4.1 安全访问服务(27h)原理

安全访问的原理其实不复杂。ECU先给你一个“种子”,你拿着这个种子,用约定的算法算出一个“密钥”,再发回去。ECU自己也算一遍,两边一比对,对上了就解锁。

我刚开始接触这个的时候,总觉得这跟小时候对暗号似的。ECU说“天王盖地虎”,你得回“宝塔镇河妖”。对不上?对不起,不让你进。

这个服务有两个子功能:

  • 请求种子(0x01/0x03/0x05...):客户端向ECU要一个随机数种子
  • 发送密钥(0x02/0x04/0x06...):客户端把算好的密钥发给ECU验证

注意看,子功能是成对出现的。奇数请求种子,偶数发送密钥。不同的安全级别用不同的子功能对,比如0x01/0x02是Level 1,0x03/0x04是Level 2,以此类推。

核心要点:安全访问的本质是“挑战-应答”机制。ECU挑战你,你应答正确,就放行。

4.2 种子与密钥算法

种子是什么?通常是一个4字节或8字节的随机数。ECU每次生成的种子都不一样,这就防止了重放攻击——你就算偷听到了上一次的密钥,下次也用不了。

密钥算法就五花八门了。有的用简单的异或运算,有的用CRC,有的用自定义的查表法,还有的直接上AES加密。我个人习惯是,项目初期先用简单的算法验证流程,后期再上强度。

这里我给大家展示一个典型的种子与密钥交互流程:

// 客户端请求种子 (Level 1)
// 发送: 02 27 01 00 00 00 00
// 种子回复: 06 67 01 AA BB CC DD (种子 = 0xAABBCCDD)

// 客户端计算密钥 (假设算法: 种子 + 0x12345678)
uint32_t seed = 0xAABBCCDD;
uint32_t key = seed + 0x12345678;  // 结果: 0xBCF02445

// 客户端发送密钥
// 发送: 06 27 02 BC F0 24 45 00
// 解锁成功回复: 02 67 02 00
// 解锁失败回复: 03 7F 27 35 (NRC 0x35 - 无效密钥)

我的经验:实际项目中,算法往往比这个复杂得多。我曾经见过一个项目,密钥算法里混了ECU的VIN码后几位,还加了时间戳。这种设计虽然安全,但调试起来也够呛。

4.3 解锁流程实现

好了,咱们来看看完整的解锁流程怎么实现。我建议你把这个流程刻在脑子里,因为几乎所有需要安全访问的操作,都走这个路子。

解锁流程分三步走:

  1. 请求种子:发送27 01,拿到种子
  2. 计算密钥:用算法算出密钥
  3. 发送密钥:发送27 02,等待结果

这里有个坑,我必须要提醒你。ECU对安全访问是有次数限制的。比如连续3次密钥错误,ECU会进入“锁定状态”,这时候你发什么都没用,必须等一段时间或者断电重启才能恢复。

避坑指南:我曾经在一个项目里,测试脚本没处理好重试逻辑,结果把ECU锁死了。后来查了半天才发现是连续失败次数超了。所以,一定要在代码里实现失败计数和等待逻辑

下面是一个更完整的实现示例,包含了错误处理:

// 安全访问解锁函数
uint8_t SecurityAccess_Unlock(uint8_t level) {
    uint8_t seed[4];
    uint8_t key[4];
    uint8_t status;
    
    // Step 1: 请求种子
    status = RequestSeed(level, seed);
    if (status != 0) {
        printf("请求种子失败,错误码: 0x%02X\n", status);
        return status;
    }
    
    // Step 2: 计算密钥 (使用自定义算法)
    CalculateKey(seed, key);
    
    // Step 3: 发送密钥
    status = SendKey(level, key);
    if (status == 0) {
        printf("安全访问解锁成功!\n");
        return 0;
    } else if (status == 0x35) {
        printf("密钥错误,剩余尝试次数: %d\n", GetRemainingAttempts());
        return status;
    } else if (status == 0x37) {
        printf("ECU已锁定,请等待...\n");
        return status;
    }
    
    return status;
}

4.4 关键NRC与注意事项

安全访问服务里,有几个NRC你必须要记住:

NRC 含义 说明
0x12 子功能不支持 你请求的安全级别不存在
0x22 条件不满足 比如已经解锁了,你又来请求种子
0x24 请求序列错误 你没请求种子就直接发密钥
0x35 无效密钥 密钥算错了
0x36 超过尝试次数 连续失败次数超了
0x37 超时 请求种子后太久没发密钥

嗯,这里要注意。0x37这个NRC很多人会忽略。ECU给你种子后,通常有个定时器,比如5秒内你必须把密钥发回来。超时了?重新请求种子吧。

实战建议:我建议你在实现安全访问时,把整个流程封装成一个原子操作。从请求种子到发送密钥,中间不要插入任何其他诊断服务。否则很容易触发0x24——请求序列错误。

4.5 总结与思考

安全访问服务,说白了就是ECU的看门狗。它不复杂,但细节很多。我见过太多人在这个服务上栽跟头,大多数问题都出在算法实现不一致上——ECU端和客户端用的算法对不上。

所以,我的建议是:先把算法文档写清楚,再动手写代码。算法文档里要明确种子长度、密钥长度、算法步骤、边界条件。两边开发人员拿着同一份文档做,基本不会出问题。

下一章,咱们聊聊诊断会话控制,看看ECU怎么在不同模式间切换。那个也挺有意思的。