4. 安全访问服务(0x27):种子与密钥机制、安全等级划分、常见算法实现(AES/CRC)
安全访问服务,也就是咱们常说的 0x27 服务。说实话,这是我在实际项目中打交道最多的一个诊断服务。为什么?因为几乎所有需要防止误操作或者非法刷写的场景,都得靠它来把关。
说白了,0x27 服务就是一把锁。你想对 ECU 做点“敏感操作”——比如刷写软件、校准传感器、解锁高级功能——ECU 会先问你要密码。但这个密码不是直接发过去的,而是通过“种子与密钥”的机制来验证。
4.1 种子与密钥机制
这个机制其实挺巧妙的。我简单描述一下流程:
- 请求种子:诊断仪发送 0x27 01(假设安全等级为 1),ECU 返回一串随机数,这就是“种子”。
- 计算密钥:诊断仪用约定的算法,把种子算出一个“密钥”。
- 发送密钥:诊断仪发送 0x27 02,把密钥发给 ECU。
- 验证结果: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 的实现,再加上自动化测试的要点。你想想看,这些东西搞明白了,以后遇到任何安全访问的问题,心里都有底。