第4章:安全访问(0x27):种子与密钥机制、算法实现(AES/自定义)、防重放攻击设计

各位同学,咱们今天聊聊安全访问服务,也就是UDS里的0x27服务。说实话,这是我在实际项目中踩坑最多的一个环节。你想想看,一个ECU要是谁都能随便刷写数据,那车还不得乱套?所以,安全访问就是给诊断通信加了一把锁。

4.1 种子与密钥机制:核心逻辑

0x27服务的本质,说白了就是“挑战-应答”模式。ECU先给你一个随机数(种子),你拿这个种子去算一个密钥,再发回来。ECU自己也算一遍,两边一比对,对上了就放行。

我个人习惯把整个过程拆成三步:

  1. 请求种子:诊断仪发0x27 + 子功能(比如0x01表示请求种子),ECU回一个种子(通常是4字节随机数)。
  2. 发送密钥:诊断仪用算法算出密钥,发0x27 + 子功能(比如0x02)+ 密钥数据。
  3. 验证结果:ECU比对密钥,成功就回肯定响应,失败就回NRC(通常是0x35或0x36)。

重要提醒:种子每次都得是随机的。我在项目中见过有人用固定种子,结果被破解得一塌糊涂。随机性就是安全的第一道防线。

4.2 算法实现:AES与自定义算法

算法这块,业内常用的有两种:AES和自定义算法。我分别说说我的经验。

4.2.1 AES算法

AES是标准算法,安全性高,但计算量稍大。对于现代32位MCU来说,完全不是问题。我个人建议优先用AES-128,密钥长度适中,性能也够用。

下面是一个简化的AES密钥生成示例(C语言风格):

// 假设种子是4字节,密钥也是4字节
uint8_t seed[4] = {0x12, 0x34, 0x56, 0x78};
uint8_t key[16] = {0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6, 
                   0xAB, 0xF7, 0x15, 0x88, 0x09, 0xCF, 0x4F, 0x3C};
uint8_t output[16];

AES128_ECB_encrypt(seed, key, output);
// output的前4字节就是密钥

我的小技巧:实际项目中,我习惯把种子和密钥都扩展到16字节,再去做AES。这样能充分利用AES的块加密特性,安全性更高。

4.2.2 自定义算法

自定义算法,说白了就是自己写一套加解密逻辑。好处是灵活、计算快,坏处是容易被破解。我在一个老项目里用过自定义算法,就是简单的异或加查表:

uint8_t custom_key_gen(uint8_t* seed, uint8_t len) {
    uint8_t key[4] = {0};
    for (int i = 0; i < len; i++) {
        key[i % 4] ^= seed[i];
        key[i % 4] += lookup_table[seed[i]];  // 查表混淆
    }
    return key;
}

嗯,这里要注意。自定义算法虽然简单,但一定要加一些非线性操作。纯异或太容易被逆向,我见过有人直接用种子取反当密钥,那跟没锁一样。

4.3 防重放攻击设计

重放攻击是什么?就是黑客抓到你一次成功的密钥交互,然后原封不动地再发一遍。你想想看,如果ECU每次都认同一个密钥,那安全访问就形同虚设了。

防重放攻击,我总结了三招:

方法 原理 我的评价
时间戳 种子里嵌入时间信息,过期作废 好用,但ECU需要时钟同步
计数器 每次成功访问后,ECU和诊断仪同步递增计数器 可靠,但实现稍复杂
一次性种子 种子使用一次后立即失效 最简单,我常用这个

我曾经踩过的坑:有一次,我设计的种子只检查了是否与上一次相同,没考虑时间窗口。结果测试时发现,只要在100ms内重放,ECU居然认了。后来我加了种子有效期(比如500ms过期),问题才解决。

具体实现时,我建议这样:

  1. 种子生成时,加入一个4字节的随机数 + 2字节的时间戳。
  2. ECU内部维护一个“已使用种子”的列表,每次验证前先查表。
  3. 种子有效期设为1秒,超时直接拒绝。

代码示例(伪代码):

bool verify_seed(uint8_t* seed, uint8_t* key) {
    // 1. 检查种子是否过期
    if (get_current_time() - seed_timestamp > 1000) {
        return false;  // 超时
    }
    // 2. 检查种子是否已被使用
    if (is_seed_used(seed)) {
        return false;  // 重放攻击
    }
    // 3. 计算并比对密钥
    uint8_t expected_key[4];
    generate_key(seed, expected_key);
    if (memcmp(key, expected_key, 4) != 0) {
        return false;
    }
    // 4. 标记种子已使用
    mark_seed_used(seed);
    return true;
}

4.4 实际项目中的避坑指南

最后,我分享几个实战经验:

  • 密钥长度别太短:4字节密钥在暴力破解面前撑不过几分钟。我建议至少8字节,有条件上16字节。
  • 失败次数要限制:连续3次密钥错误,ECU应该锁定安全访问30秒。这是防暴力破解的基本操作。
  • 算法不要硬编码:把算法参数(比如查表、密钥)放在可配置区域,方便后期升级。我见过有人把密钥写死在代码里,结果换密钥得重新刷写整个固件。
  • 日志记录要谨慎:别把种子和密钥明文打印到日志里。我曾经在调试时开了全量日志,结果被安全审计骂了一顿。

总结一下:安全访问的核心就三点——种子要随机、算法要可靠、防重放要到位。你把这三点吃透了,0x27服务基本就稳了。

好了,这一章就讲到这里。下一章咱们聊聊0x28服务(通信控制),到时候我会讲讲怎么在诊断会话里切换通信模式。有什么问题,咱们课后交流。