3、CAN总线攻击:CAN总线帧结构、CAN ID欺骗攻击、DoS攻击(总线淹没)、重放攻击

好,咱们今天聊点硬核的。CAN总线,这玩意儿在车上跑了快四十年了。说实话,它设计之初压根没考虑过安全。那时候的工程师想的是:怎么让几个ECU稳定地传数据?结果呢,现在成了攻击者的后花园。

我个人习惯,讲攻击之前,先得把协议本身扒干净。你连帧结构都看不懂,谈什么攻击?

3.1 CAN总线帧结构——你得知道敌人长什么样

CAN总线上的数据,是以“帧”为单位传输的。最常见的是数据帧。我直接给你拆开看:

字段 长度 说明
SOF 1 bit 起始帧,标志总线从空闲变忙
仲裁场(ID + RTR) 11 bit 或 29 bit 这就是CAN ID,决定优先级。ID越小,优先级越高
控制场 6 bit 包含DLC(数据长度码),告诉你后面跟了几个字节
数据场 0~8 byte 真正的数据载荷。最多8个字节,这是硬伤
CRC场 15 bit 循环冗余校验,防传输错误,但不防恶意篡改
ACK场 2 bit 应答位。接收节点如果收到,会把显性位拉低
EOF 7 bit 帧结束

你想想看,这里最要命的是什么?是仲裁场。CAN总线用ID来决定谁说话。谁ID小,谁就能抢到总线。这就给攻击者留了后门。

核心要点:CAN帧没有源地址,也没有目的地址。它只有ID。这意味着任何节点都可以声称自己拥有某个ID。说白了,就是“谁先喊谁有理”。

3.2 CAN ID欺骗攻击——我冒充了你的ECU

CAN ID欺骗,是我在项目中最常见的攻击手法。攻击者不需要破解什么加密算法,只需要做一件事:伪造一个合法的CAN ID,然后发送恶意数据

举个例子。假设某个车,刹车信号是通过CAN ID 0x1A3发送的。正常ECU发送“0x1A3 + 0x01”表示刹车踩下。攻击者只需要往总线上也发“0x1A3 + 0x01”,其他ECU根本分不清谁是真的。

为什么会这样?因为CAN协议没有认证机制。它只检查CRC,不检查你是谁发的。

我的经验:我曾经帮一家Tier1做渗透测试。他们有个网关,过滤规则写得挺严。但攻击者绕过了——他直接用一个更小的ID(比如0x001)去发伪造的刹车信号。因为ID小,优先级高,网关还没来得及过滤,消息就已经被目标ECU消费了。嗯,这里要注意:优先级攻击是ID欺骗的变种。

攻击代码其实很简单。用SocketCAN的话:

// 伪造一个刹车信号帧
struct can_frame frame;
frame.can_id = 0x1A3;  // 刹车信号的ID
frame.can_dlc = 1;
frame.data[0] = 0x01;  // 刹车踩下

// 直接往总线上怼
write(socket_fd, &frame, sizeof(frame));

你看,就这么几行代码。没有认证,没有签名。你想想看,如果这是高速行驶中的车,后果是什么?

3.3 DoS攻击(总线淹没)——让整个网络瘫痪

DoS攻击,说白了就是让总线忙死,谁也发不了正常消息。CAN总线的仲裁机制决定了:ID最小的帧永远能抢到总线

攻击者只需要做一件事:用一个极小的ID(比如0x000),以最高频率往总线上发数据。因为0x000是最高优先级,其他所有ECU的帧都会被它“挤掉”。

我见过最狠的攻击,是用一个单片机,以1ms的间隔连续发送0x000的帧。结果呢?整个动力总线的通信全部中断。发动机ECU收不到油门信号,ABS收不到轮速信号。车直接进入跛行模式。

警告:这种攻击在真实场景中非常致命。我曾经在实验室复现过,一个树莓派加一个CAN hat,就能让一辆车的仪表盘全部报错。更可怕的是,攻击者可以远程控制一个被攻破的ECU(比如车机)来发起这种攻击,不需要物理接触。

攻击代码示例:

// 总线淹没攻击
struct can_frame flood_frame;
flood_frame.can_id = 0x000;  // 最高优先级
flood_frame.can_dlc = 8;
memset(flood_frame.data, 0xFF, 8);

while(1) {
    write(socket_fd, &flood_frame, sizeof(flood_frame));
    usleep(1000);  // 1ms间隔
}

嗯,这里要注意:有些网关会做速率限制。但攻击者可以同时用多个ID轮询发送,或者用多个ECU同时攻击。防御起来并不容易。

3.4 重放攻击——录下来,再放一遍

重放攻击,是我觉得最“朴实无华”但最有效的攻击。攻击者不需要理解数据含义,只需要录制一段正常的CAN通信,然后在关键时刻重放

举个例子。你按一下车窗升降按钮,攻击者录下了这个CAN帧。等你下车后,他重放这个帧,车窗就自己降下来了。

更危险的是什么呢?是解锁信号。很多车的遥控钥匙信号是滚动码,但CAN总线内部通信往往是明文。攻击者只要录下“解锁成功”后的CAN帧,下次直接重放,就能把车门打开。

避坑指南:我曾经帮一个客户做安全审计。他们发现,某个T-Box在收到远程解锁指令后,会在CAN总线上广播一条“解锁成功”的消息。这条消息没有任何时间戳或计数器。攻击者只要在信号范围内录一次,就能无限重放。嗯,后来我们加了一个单调递增的计数器,才堵住这个漏洞。

重放攻击的代码,其实就是把录下来的帧原样发出去:

// 假设之前录下了一个解锁帧
struct can_frame replay_frame;
replay_frame.can_id = 0x2B0;
replay_frame.can_dlc = 4;
replay_frame.data[0] = 0xAA;
replay_frame.data[1] = 0xBB;
replay_frame.data[2] = 0xCC;
replay_frame.data[3] = 0xDD;

// 重放
write(socket_fd, &replay_frame, sizeof(replay_frame));

你想想看,这种攻击为什么难防?因为从协议层面看,它就是一个合法的帧。CRC正确,ID正确,数据格式正确。ECU根本不知道这是“旧酒装新瓶”。

3.5 防御思路——怎么堵住这些漏洞?

讲了这么多攻击,咱们也得聊聊怎么防。我个人总结了几个方向:

  1. CAN ID白名单:网关只允许特定ID通过。但要注意,攻击者可以用更小的ID绕过。
  2. 速率限制:对每个ID做频率限制。比如某个ID正常1秒发10次,突然1秒发100次,直接丢弃。
  3. 消息认证码(MAC):在数据场里嵌入一个认证码。接收方验证通过才执行。这是目前最靠谱的方案。
  4. 单调计数器:每条消息带一个递增的计数器,防止重放。接收方只接受比上次大的计数器值。
  5. 总线监控:部署一个独立的监控节点,检测异常行为。比如突然出现大量0x000的帧,立刻报警或切断总线。

我的建议:别指望单一防御手段能搞定所有攻击。CAN总线安全是“洋葱模型”——多层防御。我曾经见过一个客户,只做了ID白名单,结果被重放攻击打得毫无还手之力。后来加了MAC和计数器,才算真正安全。

好了,这一章的内容就到这儿。CAN总线攻击,说白了就是利用协议本身的“信任缺陷”。你理解了帧结构,理解了仲裁机制,就能明白攻击者为什么能得手。下一章,咱们聊聊更高级的攻击——CAN总线模糊测试。那玩意儿,才是真正考验防御能力的。