ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

CAN总线逆向工程实战:从协议原理到抓包控制全流程

CAN总线逆向工程实战:从协议原理到抓包控制全流程 上周帮朋友处理一辆车的诡异故障雨刷在晴天偶尔自己动一下仪表盘却一切正常维修店换了两个传感器都没解决。最后我们把一个 USB-CAN 记录仪挂在 OBD 口上抓了一天日志才发现问题出在一条私有 ID 的错误帧上——某个控制单元把这帧数据误当成了挡风玻璃雨量信号处理。那次之后我意识到一件事在现代汽车里CAN 总线就是整车的“神经系统”你看到的转向灯、车窗、空调、电机的每一次动作本质上都是总线上的报文在驱动。而“读懂”这些报文就是理解一台车真正在想什么的关键。这篇文章就是想把我自己做 CAN 总线逆向工程的完整工作流整理出来从最基础的协议原理到硬件选型、软件环境搭设再到抓包、信号定位、重放验证以及一套可以抄作业的 Python-can 和 Caring Caribou 组合方案。适合刚接触汽车电子的嵌入式工程师、想做车联网安全的运维同学也适合对协议分析有兴趣但不知道从哪里下手的爱好者。我会尽量用大白话把协议讲清楚把每一步为什么这么做也讲清楚让你看完之后能自己动手跑一遍。1. 为什么先从 CAN 协议讲起1.1 CAN 总线到底是个什么东西CAN 全称是 Controller Area Network最早是博世在 1980 年代为了解决车内线束越来越复杂的问题提出来的。当时一辆车里要用几十根甚至上百根电线点对点连接各种传感器、开关和执行器重量大、故障率高、排查困难。CAN 的思路是“总线制”——所有节点都挂到一对双绞线上通过一套仲裁和数据帧机制来通信这样线束大幅减少扩展性也强。物理层面CAN 一般只用两根线CAN_H 和 CAN_L。它用差分电压来传数据隐性和显性两种状态。发隐性位时两根线都偏近 2.5V发显性位时 CAN_H 升到大约 3.5V、CAN_L 下降到大约 1.5V。这种差分设计抗干扰能力很强所以汽车这种电磁环境极其恶劣的地方才敢大规模用。逻辑层面CAN 是半双工、多主、广播式的。没有“主机”这个概念任何一个节点随时都可以往总线发消息谁优先级高谁先发。正因为所有报文都在一条共享广播通道上流动才给了后续“挂在总线上就能听”的逆向分析空间——这是很多传统网络协议做不到的。说人话版本CAN 总线是一条“所有设备都在上面喊话”的公共走廊。走廊足够宽、抗噪音强汽车工程师把车里所有电子设备都塞进这条走廊。我们做逆向就是搬个小板凳坐在走廊旁边把每个人的喊话记下来再研究谁跟谁是一伙的。1.2 帧结构与仲裁理解“先来后到”CAN 2.0 协议族里最常见的帧类型是数据帧分标准帧CAN 2.0A和扩展帧CAN 2.0B。标准帧的 ID 是 11 位扩展帧是 29 位。我们做车载总线分析时标准帧和扩展帧都会见到但大多数乘用车车身控制相关的报文以 11 位 ID 为主商用车和诊断功能则经常出现 29 位 ID。一个标准数据帧从开头到结束大概长这样字段位数作用SOF1帧起始表示“我要开说了”仲裁段 ID11/29报文身份标识ID 越小优先级越高RTR/IDE1/1是否为远程帧、是否为扩展 IDDLC4数据长度0-8 字节绝大部分报文是 8 字节数据段0-64核心内容你要逆向的信号就在这里CRC15循环冗余校验查物理层错误ACK2应答位接收方确认收到EOF7帧结束仲裁机制是 CAN 最聪明的设计。多个节点同时发帧时它们在 ID 段逐位做“线与”。不断发的节点在每一位上对比自己发的值和总线上读到的值如果不一致就说明有更高优先级更小 ID的节点在发立刻退出发送。这个过程对上层完全透明不浪费带宽。这也带来一个反向工程的实用结论**总线上看到某一个 ID 的报文频繁出现不代表它是优先级最高只是它“抢着发”的欲望比较强。**比如发动机转速报文一般 ID 比较小因为对实时性要求高空调按钮状态的 ID 通常比较大慢一点没人管。1.3 三条最常见的理解误区我在论坛上看到很多刚入门的朋友容易被一些概念绕晕先帮你排一排雷第一“带数据库逆向工程”到底数据库是什么很多做 IT 数据安全的朋友一听到“数据库逆向”就以为是 MySQL 或者 Oracle但 CAN 总线里的“数据库”指的是 DBC、A2L 这类描述 CAN 信号布局的文件。DBC 文件定义的是“ID 0x100 的第 0 到第 11 位是车速”不是 SQL 表。第二FPGA 实现 CAN 总线是怎么回事FPGA 可以做 CAN 控制器逻辑甚至高速多路 CAN 网关但那是芯片设计层面的玩法。如果你只是做逆向实验买一块带 MCP2515 的模块或 USB-CAN 适配器就够了没必要为了做个实验去啃 FPGA。第三“CAN 总线用中断接收还是 DMA 接收”是嵌入式开发里最常见的二选一但它影响的是控制器的实时性和 CPU 占用跟总线上的报文格式没有任何关系。搞清楚这个问题对底层写代码有帮助但对“听总线数据”这件事没有决定性影响。2. 搭建一套能跑的实验环境2.1 硬件选型从学生党到路测专家的方案做 CAN 逆向硬件是绕不过去的第一道门槛。我拆成三档来说方案代表设备优点缺点适合人群入门级STM32 MCP2515 模块便宜几十块钱能理解控制器原理需要自己写固件调试麻烦嵌入式学习、DIY中坚级USB-CAN 适配器国产兼容、PCAN、周立功即插即用配合 Python 非常顺手稳定性好通常要几百到上千元大部分逆向实战专业级车载总线记录仪、CANoe多通道同步、精确到微秒的时间戳、支持 LIN/FlexRay价格昂贵、学习成本高整车厂、专业测试团队我个人建议如果你的目标是用 Python-can 做分析就不要折腾单片机方案了直接买一个主流 USB-CAN 适配器。什么叫主流就是 Python-can 的后端列表里明确支持的那几款比如 PCAN、canalystii、Kvaser 等。买回来装好驱动插上电脑就能被系统识别成一个虚拟串口或网卡接口省去大量重复造轮子的时间。如果你只是单纯想看报文格式、不想发送报文那 USB-CAN 逻辑分析仪或 Saleae 这类逻辑分析仪配合 CAN 解码插件也能做二三十块钱起步的逻辑分析仪就能抓波形适合抠物理层字节细节。但这种方式被动监听为主没法做重放所以我不把它作为主力方案。注意给车供电前先确认目标设备的 CAN 总线电压和你的分析仪输入范围一致。绝大多数车载节点是 5V/12V 供电的系统但分析仪逻辑电平有 3.3V 和 5V 之分无脑直连有烧硬件风险。实在不确定时用隔离 CAN 收发器模块保护一下电脑和仪器。2.2 软件安装与基础验证硬件到手之后软件层面有三样东西必备Python、python-can、以及用于信号解析的 cantools外加一个可选的 Caring Caribou。Caring Caribou 是汽车安全研究圈子里的一个开源工具包它在底层用 python-can 和 scapy 来收发、解析 CAN 数据封装了不少自动化模块非常适合在逆向阶段做 ID 发现、模糊测试和重放。我后面会专门演示它的用法。先安装 Python 依赖建议直接建虚拟环境python3 -m venv can-re source can-re/bin/activate pip install python-can cantools caringcaribouWindows 用户需要注意USB-CAN 适配器驱动装完之后在设备管理器里确认识别出的设备名。Python-can 在 Windows 上对大部分国产适配器的支持是通过 canalystii 或类似后端实现的装完 python-can 之后可能需要额外装对应 SDK。Linux 下最简单很多适配器直接通过 SocketCAN 驱动加载模块后把接口配置一下就能用sudo modprobe can sudo modprobe can_raw sudo ip link set can0 type can bitrate 500000 sudo ip link set up can0这里 500000 是 500 kbps是乘用车最常见的一种比特率但不是唯一标准。如果不知道目标总线速率可以先用逻辑分析仪测量一位显性位的宽度来推算或者用适配器的自动波特率检测功能去扫。后面会单独讲波特率不匹配的问题。2.3 第一行代码收发 CAN 报文环境就绪后先写一个最基础的收发脚本验证链路。假设你的接口是can0SocketCAN或pcanPEAK不同后端在 Python-can 里只是传参不同。import can bus can.interface.Bus(channelcan0, bustypesocketcan) # 也可以这样指定bus can.interface.Bus(channelPCAN_USBBUS1, bustypepcan) msg can.Message( arbitration_id0x123, # 标准帧 ID也可以开 extended_id 用 29 位 data[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88], is_extended_idFalse ) bus.send(msg) print(发送成功:, msg) while True: rx bus.recv(timeout1) if rx: print(接收:, rx)这一小段代码其实就把 CAN 逆向的基本能力集齐了发送字节、接收字节、解析报文对象。后面所有高级操作都是从这 20 行之内扩展出来的。3. 实战一个外部设备从抓包到控制3.1 监听总线先把“会话”建起来实战部分我给你设计一个安全、可控、可复现的实验环境手头有一个带 CAN 接口的 LED 灯控板这是一块独立的 CAN 节点也许是一块学习板或者一个电动车窗模拟器。我们的目标是搞清楚它接受什么报文、最终通过总线把它点亮。把 USB-CAN 适配器串进总线上。这里有个关键设计思路USB-CAN 适配器在物理上连接了总线的 CAN_H、CAN_L它的作用类似于将你的电脑“并联”到这条通信线路上不发送数据时不会干扰正常通信非常适合先做被动监听。写一个抓包脚本把收到的每一帧记录到本地文件import can import csv bus can.interface.Bus(channelcan0, bustypesocketcan) with open(capture.log, w) as f: writer csv.writer(f) writer.writerow([timestamp, id, dlc, data]) while True: msg bus.recv(timeout0.1) if msg is None: continue writer.writerow([ f{msg.timestamp:.6f}, hex(msg.arbitration_id), msg.dlc, msg.data.hex(), ]) f.flush()注意我把f.flush()加上了。这一步很重要因为默认的缓冲区可能在程序异常退出时丢失最近几百条报文导致抓了一个小时发现数据不完整。现场实测下来长时间监听时在每帧写完都 flush 一次牺牲一点性能换来数据安全完全值得。3.2 用统计迅速识别关键 ID总线上的报文通常很密集500 kbps 速率下 1 秒能传几百上千帧。直接看日志看不出规律要用统计方法做第一轮筛选。最容易识别的规律是周期性发动机相关报文通常在 10ms 到 100ms 周期内刷屏按键、开关、车门状态这类事件型报文平时几乎不出现只在状态变化时发一帧诊断报文则按诊断会话或请求时才出现。写一个简单统计脚本按 ID 统计帧数、最小间隔、最大间隔import can from collections import defaultdict stats defaultdict(lambda: {count: 0, last: None, min_gap: 1e9, max_gap: 0}) bus can.interface.Bus(channelcan0, bustypesocketcan) for _ in range(2000): msg bus.recv(timeout1) if msg is None: continue s stats[msg.arbitration_id] s[count] 1 if s[last] is not None: gap msg.timestamp - s[last] s[min_gap] min(s[min_gap], gap) s[max_gap] max(s[max_gap], gap) s[last] msg.timestamp for can_id, s in sorted(stats.items(), keylambda x: x[1][count], reverseTrue): print(hex(can_id), 帧数:, s[count], 最小间隔(ms):, round(s[min_gap] * 1000, 1), 最大间隔(ms):, round(s[max_gap] * 1000, 1))实测中你会发现像 0x100 这类 ID 可能毫秒级刷屏而 0x2F1 可能几十秒才出现一次。字符串到这一步基本能圈出一批“高频周期报文”和一批“事件型报文”。我们要找的控制报文大概率在事件型报文里因为它是在你按下开关或外部发送指令时才出现的。3.3 通过状态差异定位信号位这是整个逆向过程真正的分水岭——“同一个设备不同状态下的报文到底差在哪”。回到我们的灯控板例子。我故意设计一组数据灯灭时ID 0x3A8 持续周期性地发送 8 字节数据00 00 00 00 00 00 00 00当我按一下灯控板上的物理按钮灯亮了这时候总线上立刻多出一帧01 00 00 00 00 00 00 00然后灯控板维持周期性发送01 00 00 00 00 00 00 00。对比这两组数据差异非常明显第 0 个字节从 00 变成了 01。所以可以初步推断0x3A8 ID 的 payload 第 0 个字节就是灯光控制信号数值 01 代表点亮00 代表熄灭。但实战里信号往往不是这么干净的。一个 8 字节的报文里同时塞着车速、转速、档位、故障码等一堆信息单看两组状态变化往往不够。这时候需要交叉验证连续改变某个物理量比如轻踩油门、缓转方向盘观察哪几个 bit 在跟着变把一个字节拆开看不同 bit 的组合是否有逻辑意义用枚举法在安全环境中一个个翻转字节观察设备反应。我之前参与过一个案例目标设备的风扇转速控制信号藏在一个扩展帧的 bit 11-13 上高 3 位是开关、低 5 位是占空比单看 byte 级别完全无规律。最后是通过把同样的按键操作重复几十次把每次的原始帧对比才从 bit 级差异中找出了规律。所以建议你把“状态对比”脚本化import can state dict() bus can.interface.Bus(channelcan0, bustypesocketcan) while True: msg bus.recv(timeout1) if msg is None: continue if msg.arbitration_id 0x3A8: data_hex msg.data.hex() if state.get(last) ! data_hex: print(f[变化] {msg.timestamp:.3f} {hex(msg.arbitration_id)} {data_hex}) state[last] data_hex当发现只有某一位在变化时再去看这一位在物理动作上对应什么基本就锁定了信号位。3.4 重放验证与 Caring Caribou 自动化定位到信号位之后重放是验证结果最直接的手段。我们以刚才的 0x3A8 报文为例import can bus can.interface.Bus(channelcan0, bustypesocketcan) # 模拟“点亮”状态 msg_on can.Message(arbitration_id0x3A8, data[0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) bus.send(msg_on)实测中如果设备响应正常灯会亮起这个报文 ID 和 payload 方案就验证通过了。这也说明了为什么做逆向之前必须先确认目标设备和总线环境是安全的、被允许测试的——一旦发送错误报文轻则设备异常重则影响正常通信。当总线报文规模很大、ID 范围很宽时手工拼接数据帧太累就该让 Caring Caribou 这类工具上场了。Caring Caribou 的定位就是用可编程的方式去自动化和扩展 CAN 操作。它把底层 python-can 的消息对象封装成了 scapy 风格的报文结构方便你快速构造、发送和解析。印象中它的模型里把一段 CAN 消息拆成三个部分头部包含 ID、DLC、是否为远程帧等控制信息数据payload 的原始字节交互逻辑重放、广播、逐字节枚举等操作策略。用它的子命令可以快速跑一轮基础测试。为了准确覆盖你的版本先直接看自带的帮助caringcaribou --help实际执行时你会发现它列出了一些内置模块常见的大致有ID 发现/枚举对指定 ID 范围发送探测帧用来确认哪些 ID 是有效目标消息重放将目标 ID 的固定 payload 不间断发送观察设备响应模糊测试把 payload 随机化或按模板变异验证设备容错能力交互式数据包编辑像 Wireshark 一样对你的报文逐字节修改后立刻发送。如果你自己写过 python-can 重放脚本再用 Caring Caribou 做一轮自动化探测两者结合会有种“手工侦察 自动火力覆盖”的配合感。有一点要提醒你这类自动化工具的作用是把大量重复劳动交给程序但它不可能替你做领域判断。它扫出来的“有效 ID”到底是哪个功能payload 里的某一个值变化后设备具体有什么反应仍然需要你去跟目标节点的物理现象做对照。3.5 从抓包结果生成 DBC 数据库逆向的最终成果通常要沉淀成一个 DBC 文件。DBC 是 Vector 公司定义的 CAN 数据库格式里面描述每个报文的 ID、周期、发送节点、信号名称、起始位、长度、偏移、缩放系数、取值范围等。为什么要做这一步因为后续如果你要继续用 python-can cantools 做二次开发比如写一个数据仪表盘、做一个自动化测试台架、或者把采集的日志灌进模拟器里DBC 就是连接“原始字节”和“物理量”之间的桥梁。否则每次拿到一个报文都要重新从 bit 层面去解析太痛苦。下面是我从抓包结果整理出的一个极简 DBC 示例VERSION NS_ : BS_: BU_: ECU LED_MODULE BO_ 936 LEDControl: 8 LED_MODULE SG_ LEDState : 0|81 (1,0) [0|255] LED_MODULE这里的 936 是十进制 ID对应 0x3A8。0|81 (1,0)表示信号起始于字节 0 的 bit 0长度 8 位Intel 字节序无符号缩放系数 1偏移 0。有了 DBC 之后用 cantools 把载荷解成物理量import cantools import can db cantools.database.load_file(led.dbc) bus can.interface.Bus(channelcan0, bustypesocketcan) while True: msg bus.recv(timeout1) if msg is None: continue try: decoded db.decode_message(msg.arbitration_id, msg.data) print(decoded) except KeyError: pass这一步做完你的“不懂行”工作流就完整闭环了从物理信号到 CAN 帧再到 DBC 结构化描述以后分析其他报文只需要不断往 DBC 里补新信号即可。4. 常见问题与排查技巧实录4.1 错误帧刷屏先查这三件事CAN 总线上有一种特殊的帧叫错误帧会在检测到错误时主动发送破坏正在传输的数据强制当前报文出错。抓包日志里看到一堆以E开头的记录先不要怀疑总线坏了按下面三个方向快速排查第一波特率不匹配。这是最常见的错误帧来源。当分析仪设置的波特率和总线上不一致CRC 几乎必错总线会持续输出错误帧。排查办法很简单先检查适配器设置、再试常见的 125k、250k、500k、1M或用逻辑分析仪测量一位显性位的时长来推算出实际波特率。第二终端电阻缺失或异常。CAN 总线标准要求在总线两端各接一个 120 欧姆终端电阻用于匹配阻抗。缺少终端电阻时总线信号反弹严重会出现偶发的位错误。用万用表在总线两端量一下正常情况下等效阻抗应该在 60 欧姆左右。第三物理层连接问题。CAN_H 和 CAN_L 接反、地线没接好、接头氧化松动都会造成间歇性错误帧。最常见的是 DB9 接头拧得不紧或者杜邦线接触不良。我在工作室里踩过最大的坑就是杜邦线接触不良导致抓两个小时全是错误帧排查到最后发现只是线松了。4.2 总线繁忙和仲裁失败怎么区分当你用 python-can 发送报文时偶尔会遇到发送失败或者超时这两类问题的处理方式完全不同。总线繁忙通常是总线流量太大某个节点正在高频占用通道导致你的低优先级报文一直抢不到发送机会。表现是发送超时但不多后续重试又能成功。处理思路是提高报文 ID 优先级ID 越小越优先、或者换一个总线空闲窗口再发。仲裁失败则更隐蔽它发生在两个节点同时发送但你的报文 ID 更大、在仲裁字段输给了对方。表现是发送失败、但总线上没有错误帧因为低优先级节点是主动退让而不是出错。从协议角度讲这是正常现象但从应用角度讲如果你希望这个报文必须发出就要考虑是不是真的需要一个更高优先级的 ID 来承载或者能否改造收发策略。我自己的习惯是在重放关键报文前先抓一小段时间看目标报文 ID 的间隔分布如果最小间隔已经非常接近发送周期说明这个 ID 被占得很满重放时一定要控制发送频率避免干扰正常链路。4.3 工具链踩坑Windows、Linux 和树莓派Python-can 非常强大但也因为跨平台不同平台下的“坑”各有特色。我把实际用下来最容易翻车的地方列出来Windows 上驱动装不上是头号问题。很多国产 USB-CAN 适配器需要安装官方驱动甚至在驱动管理器里伪装成 COM 口但 Python-can 真正用的是厂商 SDK 提供的 DLL。装完驱动请马上写个 5 行测试代码收发一帧确认后端连通别等到车载现场再发现连不上。Linux 上SocketCAN 配置顺序容易漏。如果ip link set can0 up之后直接收发失败大概率是波特率没配对或者忘了加载 can_raw 模块。另外有些虚拟机环境下没有真正的 can 接口需要加载 vcan 虚拟接口做离线测试sudo modprobe vcan sudo ip link add dev vcan0 type vcan sudo ip link set vcan0 up树莓派上做车载抓包性价比很高但树莓派的 SPI 或 USB 资源紧张时高负载下会丢帧。如果你发现抓包时间戳有明显跳变优先检查是不是存储介质SD 卡写满导致阻塞把日志写进内存盘或者用分段滚动写入能明显改善。Caring Caribou 这类开源工具还有一个共性问题——对 Python 版本有一定要求特别在老系统上装容易和系统 Python 冲突。我的建议永远是新建虚拟环境从头安装依赖不要直接往系统 Python 里塞东西否则哪天覆盖了系统包整套环境就崩了。最后分享几个习惯做了几年 CAN 逆向我最大的体会是工具永远不是瓶颈思路才是。Python-can 和 Caring Caribou 都只是替你把“收发报文”这件事做顺手的架子真正关键的是你能不能从一堆看似随机的字节里看出状态变化的规律。平时我都会刻意训练自己“拿到任何二进制数据先找差异再找周期最后才下结论”的顺序这个习惯在 CAN 上适用在串口、Modbus、传感器协议分析上同样适用。另外一个很实用的小细节做现场测试时抓包脚本和笔记要同步留档。我吃过亏之前分析一个设备花了一晚上定位到信号位结果忘了记录当时的总线波特率第二天换地方测试怎么都连不上最后只能重新测波特率。后来我养成了把每次实验的时间、比特率、设备型号、甚至线序拍照记入 Markdown 笔记的习惯踩过的坑越来越少。CAN 总线的世界其实远比这篇文章能覆盖的更大——诊断协议、UDS、OBD/排放标准、网络安全验证、以及混合动力和纯电的整车 E/E 架构每一层都还有很多有意思的东西可以深挖。从一个外部节点开始把一条报文的来龙去脉搞清楚你就会发现整车所有控制器之间的对话其实都是有迹可循的。
返回列表