4. 安全访问服务(0x27):种子与密钥机制、安全等级划分、常见算法实现(AES/CRC)

安全访问服务,也就是咱们常说的 0x27 服务。说实话,这是我在实际项目中打交道最多的一个诊断服务。为什么?因为几乎所有需要防止误操作或者非法刷写的场景,都得靠它来把关。

说白了,0x27 服务就是一把锁。你想对 ECU 做点“敏感操作”——比如刷写软件、校准传感器、解锁高级功能——ECU 会先问你要密码。但这个密码不是直接发过去的,而是通过“种子与密钥”的机制来验证。

4.1 种子与密钥机制

这个机制其实挺巧妙的。我简单描述一下流程:

  1. 请求种子:诊断仪发送 0x27 01(假设安全等级为 1),ECU 返回一串随机数,这就是“种子”。
  2. 计算密钥:诊断仪用约定的算法,把种子算出一个“密钥”。
  3. 发送密钥:诊断仪发送 0x27 02,把密钥发给 ECU。
  4. 验证结果:ECU 内部也用同样的算法算一遍,比对是否一致。一致就解锁,不一致就报错。

嗯,这里有个细节要注意。ECU 返回的种子,每次都不一样。这就防止了有人录下密钥直接重放攻击。我在一个项目中就遇到过,客户说“为什么我每次发同样的密钥都失败?”——其实就是因为种子变了,密钥自然也得跟着变。

核心要点:种子是 ECU 给的,密钥是诊断仪算的。ECU 只负责比对,不负责告诉你算法。

4.2 安全等级划分

0x27 服务支持多个安全等级。你想想看,不同的操作,风险程度不一样,需要的权限自然也不同。

安全等级 子功能请求 典型用途
1 0x01 / 0x02 读取校准数据、执行例程
2 0x03 / 0x04 刷写非安全相关的软件模块
3 0x05 / 0x06 刷写 Bootloader 或关键安全模块
5 0x09 / 0x0A 厂商级调试、解锁特殊功能

我个人习惯把安全等级分成三类:

  • 低等级(1-2):日常诊断、标定用。算法相对简单,甚至可能是固定的 XOR。
  • 中等级(3-4):刷写用。算法会复杂一些,比如用 CRC 或者自定义的查表法。
  • 高等级(5+):厂商保留。我见过用 AES-128 的,密钥长度 16 字节,种子也是 16 字节。

避坑指南:我曾经在一个项目里,ECU 的安全等级 1 和等级 3 用了完全相同的算法。结果测试时发现,刷写工具用等级 1 的密钥去解锁等级 3,居然也能成功。这其实是个安全隐患。建议不同等级用不同的算法,或者至少用不同的密钥派生因子。

4.3 常见算法实现

算法这块,我挑两个最常用的来讲:AES 和 CRC。为什么是这两个?AES 是真正的加密算法,安全性高;CRC 虽然本质是校验,但胜在简单、速度快,很多老项目还在用。

4.3.1 AES 算法实现

AES 在 0x27 服务里,通常用 ECB 模式。为什么不用 CBC?因为种子和密钥都是一次性的,不需要链式反馈。ECB 简单直接,适合嵌入式环境。

下面是一个简化的 AES-128 密钥计算示例。注意,实际项目中密钥会做更多处理,比如加盐(Salt)。

// 伪代码:AES-128 密钥计算
// 输入:seed[16] 字节数组
// 输出:key[16]  字节数组

void CalculateKey_AES128(uint8_t* seed, uint8_t* key) {
    // 1. 定义固定密钥(实际项目中会存储在安全硬件中)
    uint8_t fixedKey[16] = {0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6,
                            0xAB, 0xF7, 0x15, 0x88, 0x09, 0xCF, 0x4F, 0x3C};

    // 2. 对种子进行 AES-128 加密
    AES128_ECB_Encrypt(seed, fixedKey, key);

    // 3. 可选:对结果再做一次 XOR 混淆
    for (int i = 0; i < 16; i++) {
        key[i] ^= 0xA5;
    }
}

你可能会问:“为什么加密后还要 XOR?” 嗯,这是我在一个项目中踩过的坑。当时直接用 AES 加密结果当密钥,结果发现 ECU 和诊断仪因为字节序问题,算出来的密钥不一致。加一层 XOR 其实是为了对齐和混淆,让问题更容易排查。

4.3.2 CRC 算法实现

CRC 在 0x27 里,通常用于低安全等级的场景。比如等级 1,种子只有 2 个字节,密钥也是 2 个字节。用 CRC-16 算一下,简单粗暴。

// 伪代码:CRC-16 密钥计算
// 输入:seed[2] 字节数组
// 输出:key[2]  字节数组

uint16_t CalculateKey_CRC16(uint8_t* seed) {
    uint16_t crc = 0xFFFF;
    uint16_t polynomial = 0x8005;  // CRC-16-IBM 多项式

    for (int i = 0; i < 2; i++) {
        crc ^= (seed[i] << 8);
        for (int j = 0; j < 8; j++) {
            if (crc & 0x8000) {
                crc = (crc << 1) ^ polynomial;
            } else {
                crc <<= 1;
            }
        }
    }

    // 取低 16 位作为密钥
    return crc & 0xFFFF;
}

注意:CRC 算法本质上是校验,不是加密。如果有人拿到了你的种子和密钥,反推多项式是分分钟的事。所以 CRC 只适合做“防误操作”,不适合做“防恶意攻击”。

4.4 自动化测试要点

讲完了原理和算法,咱们聊聊怎么测。自动化测试 0x27 服务,我总结了三个关键点:

  • 正向测试:用正确的算法计算密钥,验证能否成功解锁。这个是最基本的。
  • 负向测试:故意发错误的密钥,验证 ECU 是否返回 NRC 0x35(Invalid Key)。还要验证连续失败次数是否有限制(通常 3-5 次后锁定)。
  • 边界测试:种子全 0、全 F、随机大数,看看 ECU 会不会崩溃。我曾经遇到过种子为 0 时,ECU 内部算法除零异常,直接复位了。

另外,我个人习惯在测试脚本里加一个“重放攻击”测试。就是先记录一次成功的种子和密钥,然后下次连接时,用同样的种子和密钥再发一次。如果 ECU 接受了,说明安全机制有漏洞——因为种子应该是每次不同的。

好了,关于 0x27 服务,核心就是这些。种子与密钥的机制、安全等级的划分、AES 和 CRC 的实现,再加上自动化测试的要点。你想想看,这些东西搞明白了,以后遇到任何安全访问的问题,心里都有底。