4、安全访问服务(0x27):种子与密钥机制、安全等级划分、算法实现(AES/Custom)、防重放攻击设计、我在项目中的踩坑记录
安全访问服务,也就是 0x27 服务,是 UDS 协议里我最常打交道的一个服务。说白了,它就是一把锁。你想动 ECU 里那些敏感数据,比如刷写程序、标定参数,必须先通过这把锁的验证。
很多刚入行的朋友觉得这服务简单,不就是发个种子、算个密钥、回个应答嘛。嗯,表面看确实如此。但实际项目中,这里面的坑多到你想象不到。我当年就因为在安全算法上少考虑了一个字节序问题,导致整个产线停了两天。那滋味,真不好受。
4.1 种子与密钥机制:一次握手,两次对话
0x27 服务的核心流程,其实就三步:
- 请求种子:诊断仪发 0x27 + 子功能(比如 0x01),ECU 回一个种子(Seed)。
- 计算密钥:诊断仪用这个种子,通过约定好的算法算出密钥(Key)。
- 发送密钥:诊断仪发 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 都升级完成后,再移除旧算法。
最后一句:安全访问服务,看似简单,实则细节满满。多花点时间在设计上,总比在产线上救火强。