4、安全访问服务(0x27):种子与密钥机制、安全等级划分、算法实现(AES/Custom)、防重放攻击设计、我在项目中的踩坑记录

安全访问服务,也就是 0x27 服务,是 UDS 协议里我最常打交道的一个服务。说白了,它就是一把锁。你想动 ECU 里那些敏感数据,比如刷写程序、标定参数,必须先通过这把锁的验证。

很多刚入行的朋友觉得这服务简单,不就是发个种子、算个密钥、回个应答嘛。嗯,表面看确实如此。但实际项目中,这里面的坑多到你想象不到。我当年就因为在安全算法上少考虑了一个字节序问题,导致整个产线停了两天。那滋味,真不好受。

4.1 种子与密钥机制:一次握手,两次对话

0x27 服务的核心流程,其实就三步:

  1. 请求种子:诊断仪发 0x27 + 子功能(比如 0x01),ECU 回一个种子(Seed)。
  2. 计算密钥:诊断仪用这个种子,通过约定好的算法算出密钥(Key)。
  3. 发送密钥:诊断仪发 0x27 + 子功能(比如 0x02)+ 密钥,ECU 验证通过后回复肯定响应。

这里有个细节我提醒你注意:种子和密钥的长度。标准里没规定死,常见的是 4 字节,也有用 8 字节甚至 16 字节的。我个人习惯,能用 8 字节就别用 4 字节,安全余量大一些。

关键点:种子必须是随机数,或者至少是伪随机数。如果每次请求返回的种子都一样,那这个安全机制就形同虚设了。

4.2 安全等级划分:不是一把钥匙开所有门

ECU 里的数据,敏感程度不一样。你想想看,读取故障码和刷写 Bootloader,能是同一个安全等级吗?

所以,0x27 服务引入了安全等级的概念。每个等级对应一个子功能,比如:

子功能 含义 典型用途
0x01/0x02 安全等级 1 读取敏感数据(如 VIN、序列号)
0x03/0x04 安全等级 2 写入标定参数
0x05/0x06 安全等级 3 刷写程序、解锁 Bootloader

我在项目中遇到过一种情况:客户要求把刷写权限和安全等级 2 合并。我当时就建议别这么干。为什么呢?因为一旦标定工程师误操作,可能把整个 ECU 刷成砖。安全等级划分,本质上是一种权限最小化的设计思路。

我的建议:每个安全等级使用独立的密钥算法。别图省事,所有等级用同一个算法。否则攻破一个等级,就等于攻破了所有。

4.3 算法实现:AES 与 Custom 的抉择

算法这块,是 0x27 服务的灵魂。常见的方案有两种:

4.3.1 AES 算法

AES 是标准算法,安全性有保障。但有个问题:计算量。ECU 的 MCU 性能有限,跑一次 AES 加密可能要几十毫秒。如果是在产线上,每台车都要做安全验证,这个时间成本就上来了。

我一般这样用 AES:

// 伪代码:AES-128 加密种子生成密钥
uint8_t seed[16] = {0x12, 0x34, 0x56, ...};
uint8_t key[16];
uint8_t aes_key[16] = {0x2B, 0x7E, 0x15, ...}; // 预置的 AES 密钥

AES128_ECB_encrypt(seed, aes_key, key);
// 返回 key 作为应答

注意,这里用的是 ECB 模式。为什么不用 CBC?因为 CBC 需要 IV,而 IV 的传递又是个麻烦事。ECB 简单直接,对于种子加密来说够用了。

4.3.2 Custom 算法

很多 OEM 喜欢自己搞一套算法。说白了,就是一些移位、异或、查表的组合。比如:

// 伪代码:自定义算法
uint32_t seed = 0xA5B6C7D8;
uint32_t key = 0;

for (int i = 0; i < 8; i++) {
    key ^= (seed >> (i * 4)) & 0x0F;
    key = (key << 4) | (key >> 28);
}
key ^= 0x5A5A5A5A;

这种算法好处是快,几乎没有计算开销。坏处是……嗯,容易被逆向。我曾经见过一个项目,Custom 算法只有 16 行代码,结果被第三方用一周时间就破解了。

踩坑记录:我曾经在一个项目里,把 AES 密钥硬编码在代码里,结果被安全审计发现了。正确的做法是:密钥从 HSM(硬件安全模块)中读取,或者至少存储在加密的 Flash 区域。

4.4 防重放攻击设计:别让黑客钻空子

什么是重放攻击?简单说,就是黑客截获了你的一次合法通信,然后原封不动地再发一遍。如果 ECU 没有防重放机制,那黑客就能用截获的密钥再次解锁。

防重放攻击,我常用的方法有这几种:

  • 种子一次性有效:每次请求种子后,ECU 生成一个新种子。如果诊断仪没有在规定时间内(比如 5 秒)发送密钥,种子就失效。
  • 计数器机制:ECU 内部维护一个计数器,每次成功解锁后递增。密钥计算时把这个计数器也参与进去。这样,即使种子相同,密钥也不同。
  • 时间戳:在种子中加入时间戳信息。ECU 验证时检查时间戳是否在合理范围内。

我个人最推荐的是计数器 + 种子一次性有效的组合。实现简单,效果也好。

注意:防重放设计不是可选项,而是必选项。尤其是对于 OTA 刷写场景,没有防重放机制,你的车可能被远程攻击。

4.5 我在项目中的踩坑记录

最后,分享几个我亲身经历的坑。希望能帮你少走弯路。

坑一:种子和密钥的字节序

有一次,我负责的 ECU 和诊断仪联调,死活通不过安全验证。查了两天,最后发现是字节序的问题。ECU 用的是大端,诊断仪用的是小端。种子和密钥在传输过程中被翻转了。

从那以后,我养成了一个习惯:在文档里明确标注字节序,并且在代码里加断言检查。

坑二:安全等级锁定

有些 ECU 设计成:如果连续 3 次密钥验证失败,就锁定安全访问功能,必须断电重启才能恢复。这个设计本身没问题,但有一次测试时,我们忘了在测试用例里加入断电操作,结果所有测试用例都卡在了安全访问这一步。

我的建议是:在开发阶段,把锁定次数设大一点,比如 10 次。等量产前再改回 3 次。

坑三:算法升级兼容性

有一次,我们升级了安全算法,从 Custom 换成了 AES。结果发现,已经出厂的 ECU 无法通过新算法的验证。因为那些 ECU 的 Bootloader 里还是老算法。

解决方案是:在 Bootloader 里同时保留新旧两套算法,通过软件版本号来切换。等所有 ECU 都升级完成后,再移除旧算法。

最后一句:安全访问服务,看似简单,实则细节满满。多花点时间在设计上,总比在产线上救火强。