CAN总线逆向实战:从被动监听到定位控制信号
发布时间:2026/9/28 5:53:19来源:尧图网络
你有没有好奇过一个问题当你按下车窗升降开关的那一瞬间车里的CAN总线上到底发生了什么又是谁在执行你的指令我最初接触CAN总线逆向工程就是因为这个好奇。当时手边正好有一辆旧款试验车仪表、BCM、ECU都能正常工作而我手上的设备只有一台便宜的USB-CAN适配器加一堆能免费装好的开源软件。一路折腾下来我对汽车协议逆向有了一个明确结论它没有想象中神秘但也绝不是随便看看文档就能糊弄过去的事。这篇文章把我踩过的坑、跑通的流程、以及最终的信号定位方法原原本本记录一遍。需要先交代一句底线这里说的逆向指的是在拿到授权、并且只针对自有试验车的前提下通过被动监听和受控注入把原本没有文档的私有CAN协议重新描述出来。整个过程用Python-can做总线收发用Caring Caribou做ID发现和主动探测最后把结论沉淀成一份能被工具反复解析的协议描述。适合汽车安全测试、车联网研发、嵌入式开发以及想验证自己动手能力的技术爱好者。1. 从整车通信到CAN总线这次逆向分析到底要解决什么1.1 没有文档时的真实需求场景也许你会问一辆车好好的为什么要去逆向它的CAN协议我在项目里遇到的需求大致有这么几类。第一类是扩展功能老车想加装第三方中控屏、行车记录仪、胎压监测需要读取车速、转速、油耗这类信号车厂却不会给你协议文档。第二类是诊断维修某些控制器损坏厂商报价高得离谱维修方想用替代件直接替换新件的报文格式和原车对不上只能自己摸。第三类是安全评估这也是很多汽车安全研究者的日常主机厂和Tier 1的私有协议本身带着“安全通过隐蔽性”的假设你不把总线上的数据读出来就谈不上评估车辆通信是否有异常。这三类需求落到技术上其实是同一件事在没有任何文档的前提下把一条物理总线上谁在发、发什么、数据如何映射成物理动作给复现出来。这也就是标题里“破解”二字的真实含义。它跟破解软件完全不同CAN总线上没有加密的会话密钥也没有登录认证你要面对的是一套有明确帧格式、明确仲裁规则但具体ID含义未知的通信系统。1.2 目标拆解和逆向的安全边界拿到一个逆向项目我习惯先把目标做四级拆解避免一开始就陷进海量CAN报文里出不来。第一级把环境搭好能稳定采集不掺错误帧的原始日志。第二级识别出总线上活跃的CAN ID弄清每个ID的大致调度周期和数据长度。第三级针对某个具体功能比如车窗、灯光、中控锁通过主动探测找到控制指令所在的ID和具体数据位。第四级把逆向结果整理成协议描述文件让后续程序能直接解码。这里必须强调安全边界。我做的所有主动发送测试都是在自己的试验车或授权台架上进行并且在测试前断开了安全气囊、刹车、转向、档位这类安全关键执行器全程不上公开道路。主动向未知ID盲发大量数据帧本质上是一种模糊测试如果对待测系统没有隔离和保护轻则干扰功能重则引发安全问题。这不是吓唬人我在后面会专门讲主动探测该怎么做才安全。2. 拆开CAN报文仲裁、错误帧和波特率的底层逻辑2.1 从物理层到数据帧这不是一条普通的电线很多人装上适配器、连上OBD口就开始抓数据结果发现日志里全是乱七八糟的帧连最基本的ID都看不出来根子在于没先理解CAN的底层。CAN总线的物理层使用双线差分信号两条线分别叫CAN_H和CAN_L正常传输时两者互为镜像。逻辑0叫显性位逻辑1叫隐性位显性位会“压倒”隐性位——这个“压倒”的特性是整个CAN协议设计的基石。不管是用MCU内置的CAN外设、FPGA里的CAN控制器IP核还是独立的USB-CAN适配器它们对帧格式的处理都遵循同一套协议规范区别只在于上层怎么使用收到的数据。数据链路层最常见的帧格式有两种。标准帧用11位标识符也叫CAN 2.0A扩展帧用29位标识符叫CAN 2.0B。乘用车动力域、车身域绝大多数用标准帧商用车和较新的域控平台才会用到扩展帧后面做主动探测也要分清楚发错格式等于在总线上制造噪声。标识符加DLC数据长度代码数据域再加上CRC、ACK、EOF这些固定字段构成一个完整数据帧。我整理过一张速查表方便收包时对照字段含义字段长度说明SOF1 bit帧起始固定显性位仲裁域12 bit标准或32 bit扩展含ID、RTR位控制域6 bitIDE、DLCDLC决定数据字节数数据域0~8 字节应用层数据CRC域15 bit 1 bit 分隔符校验传输错误ACK域2 bit接收节点应答EOF7 bit帧结束对做逆向的人来说最需要关注的其实是仲裁域和数据域。仲裁域决定这条消息在总线上的优先级数据域才是我们真正要解析的信号所在。2.2 优先级仲裁为什么ID越小越快上车理解仲裁机制之前要先接受一个和普通通信完全不同的设定CAN是多主总线没有主机和从机的概念任何节点只要总线空闲就能发。那如果两个节点同时发送呢CAN的仲裁规则是在ID位逐位比较遇到一个节点发送显性位、另一个发送隐性位时显性位获胜发送隐性位的节点自动退出发送。因为显性位是逻辑0所以ID数值越小优先级越高。这解释了为什么我抓到的日志里有些ID总是能插队。车辆状态类的高频帧大概率用较小的ID舒适性功能往往用较大的ID。你在主动注入的时候选择不同的ID其实就是在选择不同优先级。想要不影响总线其他业务就选一个优先级低的大ID做测试想要测试某个特定控制器就必须挑它正在监听的ID段这需要先做被动监听来确认不能瞎猜。2.3 错误帧是怎么产生的从位错误到bus-offCAN协议里有一类特殊的帧叫错误帧它不属于任何一个节点而是所有节点在发现总线异常时共同发出的“抗议信号”。错误帧的出现规则很严苛简单理解就是发送节点在发送时会实时回读总线电平如果发现自己发的是显性位、总线上却是隐性位或者反过来它就会立刻判定位错误并在下一bit拉低总线发出6个显性位的主动错误标志。还有填充错误、CRC错误、ACK错误、形式错误它们对应不同的异常来源。错误计数器会记录这些错误TEC、REC两个计数器各自累计。错误少的节点还能继续工作如果TEC超过127节点进入被动错误状态这时候它发错误帧时只能发6个隐性位不再影响其他节点一旦TEC超过255节点直接进入bus-off状态彻底脱离总线。理解这个机制对后续排查“为什么总线上一片错误帧”非常重要很多人一上来就怀疑硬件坏了其实大概率是波特率配错或者终端电阻缺失。2.4 波特率、终端电阻和采样点三个最容易翻车的基础项CAN总线的波特率不是自动协商的不像以太网那样插上就能跑。你抓不到数据首先就该怀疑波特率。常见车载CAN波特率有125kbit/s、250kbit/s和500kbit/s。我习惯用示波器或逻辑分析仪直接测显性位宽度再换算波特率而不是逐个去试因为试波特率时如果车辆总线一直有真实报文错误的波特率配置会触发大量错误帧干扰你自己的控制器。终端电阻同样关键。一条正常的总线段两端各有一个120欧姆电阻在OBD口量CAN_H和CAN_L之间应该是60欧姆左右。如果量出来120欧姆说明有一端电阻缺失远端信号反射会明显CRC错误就会变多。采样点则是一个容易被忽略的参数大多数设备默认75%采样点兼容性最好但如果总线上有信号完整性问题可以把采样点往82%~87.5%调整具体要看适配器固件支持什么。3. 硬件与软件栈准备让Python-can和Caring Caribou真正落地3.1 USB-CAN适配器选型与OBD-II接线先把硬件说清楚。市面上能接手CAN的工具很多从USB-CAN适配器到PCAN-USB、CANtact、canable核心功能都是把电脑的USB转成CAN接口。我的建议是研究学习阶段完全不需要买几千块的工业级适配器一个兼容gs_usb协议的CANtact或者canable就够了Linux下插上就能被识别成网络接口默认驱动是内核自带的gs_usb模块。如果手头只有那种便宜的SLCAN串口模块也能用只是需要在系统里挂一个虚拟串口转CAN工具配置稍微绕一点。接线是最容易出错的地方。车上的OBD-II诊断座里CAN_H在6号针脚CAN_L在14号针脚地线一般从4号或5号针脚引。接好之后别急着插电脑先用万用表量一下6号针脚和14号针脚之间的电阻正常的车应该在60欧姆左右。这个动作花不了10秒钟但能帮你过滤掉一半以上的采集异常问题。还有一条经验连接器的金属外壳最好和设备地共地特别是发动机运行时火花塞点火会产生很强的共模干扰不共地的话错误帧会明显增加。3.2 把Python-can跑起来Linux下的socketcan配置Python-can是一个总线抽象库同一套代码可以切换socketcan、pcan、slcan等不同后端。我这边以Linux下最常见的socketcan为例先把适配器驱动加载好。如果是canable这类gs_usb方案插入USB后执行sudo apt install can-utils python3-pip python3-venv sudo modprobe gs_usb sudo ip link set can0 up type can bitrate 500000 ip -details link show can0最后一条命令会看到can0的状态已经UP链路层是CAN_RAW波特率500000。这里有个细节ip link set配置bitrate后Python-can里就不需要再传bitrate参数了否则有的后端会重复设置导致报错。然后建一个虚拟环境装python-canpython3 -m venv venv source venv/bin/activate pip install python-can一个最基础的收发脚本长这样import can bus can.interface.Bus(channelcan0, interfacesocketcan) print(bus) msg can.Message( arbitration_id0x123, data[0x01, 0x02, 0x03], is_extended_idFalse, ) bus.send(msg) for rx in bus: print(f{rx.timestamp:.6f} {rx.arbitration_id:03X} DLC{rx.dlc} DATA{ .join(f{b:02X} for b in rx.data)})在Windows上如果用的是PCANinterface换成pcan、channel换成PCAN_USBBUS1用SLCAN串口模块的话interface填slcanchannel填COM口号。接口抽象的好处就在这车端代码基本不用动。3.3 Caring Caribou的安装和模块地图Caring Caribou是Uber开源的一套CAN安全研究框架跟Python-can是天然搭档。它的特点是封装好了几个高频操作ID发现、模糊测试、数据包重放、UDS诊断扫描等省得每次都自己写循环。安装方式pip install caringcaribou caringcaribou --help如果装的是比较新的版本它依赖Python版本和python-can的兼容性建议在虚拟环境里装。安装完成后你能看到几个模块discover、fuzz、grep、replay、uds。我最常用的是discover和fuzz后面会展开。grep适合在大量日志里匹配特征replay适合把采集到的原始帧按时间重放uds则是直接跟诊断协议打交道。3.4 连不上总线时的第一套排查顺序每次在新环境里搭这套工具链我都会按固定顺序做四步检查。第一看系统接口有没有起来ip link show can0状态必须是UP第二用candump can0 -T 1000收一秒看有没有滚动数据第三确认接线正确、终端电阻正常第四确认波特率匹配。如果这四步都过了还是没有数据就换另一台车或另一根线试排除适配器损坏的可能。排查的顺序很重要先系统层、再物理层、再协议层能少走很多弯路。4. 被动监听先行把总线上每个ID的“生活习惯”摸清4.1 监听前的三个准备清场、滤错、备份被动监听是整个逆向过程中信息密度最高也最安全的阶段因为这时候你只收不发不会对车辆产生任何干扰。监听前我会做三件事。第一确认总线上没有其他上位机在乱发如果有示波器或逻辑分析仪接在尾线上暂时拔掉避免额外负载影响信号质量。第二检查系统日志里有没有持续的错误帧用candump -L can0观察几秒如果有大量error帧先回到第3章的排查流程不要带着“脏数据”往下做。第三建立一个干净的日志目录建议每次监听都独立存档文件名带上日期、车辆状态、采样时长方便后续回溯。PC上用Python-can收发CAN消息实际上不涉及什么中断接收还是DMA接收的问题USB设备会自己处理FIFO但在MCU端总线上每一条消息到来时通常靠中断或DMA把数据搬进缓冲区这个细节不影响应用层的统计。你只需要知道被动监听阶段一定要让应用层处理速度跟上总线速率否则缓冲区溢出丢包统计结果会失真。Python脚本如果来不及处理可以先用candump -l把原始帧落盘再离线分析。4.2 写一个ID统计脚本谁在发、发多快、发多长被动监听的第一个产出是搞清楚总线上活跃的ID集合。我写过一个非常小的统计脚本跑几分钟就能得到一张表每个ID出现的次数、数据长度、平均发送周期。from collections import defaultdict import can counter defaultdict(int) dlc_set defaultdict(set) last_ts {} periods defaultdict(list) bus can.interface.Bus(channelcan0, interfacesocketcan) for msg in bus: if msg.is_error_frame or msg.is_remote_frame: continue aid msg.arbitration_id counter[aid] 1 dlc_set[aid].add(msg.dlc) ts msg.timestamp if aid in last_ts: periods[aid].append(ts - last_ts[aid]) last_ts[aid] ts if counter[aid] 50000: break for aid in sorted(counter): plist periods[aid] avg_period (sum(plist) / len(plist)) * 1000 if plist else 0 print(fID{aid:03X} count{counter[aid]:6d} DLC{sorted(dlc_set[aid])} period{avg_period:.2f}ms)脚本里我刻意跳过了错误帧和远程帧不然统计表会被异常数据污染。跑10分钟之后你会看到很典型的规律一部分ID的发送周期非常稳定比如10ms、20ms、50ms这些通常是传感器状态或控制器心跳还有一部分ID的发送毫无规律只在某个操作发生时出现这往往就是我们要找的命令帧。4.3 从周期和数据变化率推断帧的功能拿到统计表之后下一步是把ID和物理动作关联起来。方法是操作一种功能同时录日志看哪个ID的发送频率或数据内容有明显变化。这里有个很容易踩的坑不要只关注“发送频率变了的ID”还要关注“数据内容变了但频率没变”的ID。比如我测试车窗升降时有一个ID始终以100ms周期稳定发送频率一点没变但里面的第3个字节在车窗动作时才变化。如果我只按频率筛选就会漏掉这个最重要的信号。以我自己做过的一次试验车为例按下喇叭时ID 0x310出现了偶发帧打开左右转向灯时ID 0x1B0数据里的bit发生了连续翻转升降车窗时ID 0x2A0的数据会在一个很小的值域里来回变化。把这些观察记录下来就得到了下一阶段主动探测的候选目标。监听做得越细后面盲发的次数就越少。5. 主动探测与信号定位从“看到ID”到“控制一个部件”5.1 用Caring Caribou的discover做一轮ID发现被动监听能确认总线上的现有通信但有些控制器平时不主动发消息只在收到请求后才响应这类节点需要主动探测才能发现。Caring Caribou的discover模块就是干这个的它会枚举常用ID区间并发送探测帧观察哪些ID存在响应者。在can0接口上直接运行caringcaribou -i can0 discover它会进入交互式界面按提示选范围跑完后返回一串活跃ID列表。需要注意discover的原理决定了它只能发现那些会“回应”的节点。车身控制模块很多是“只干活不吭声”收到指令就执行但不会回复任何内容所以你还需要配合观察物理动作这就是下一节要说的模糊测试。5.2 分区间模糊测试控制变量才能得出可靠结论对目标系统做主动探测时我最忌讳的就是全ID、全数据域乱发。无差别盲发不仅低效而且容易误触发不想要的功能。正确做法是在被动监听的基础上把目标聚焦到某个候选ID附近的小区间一次只改变一个变量。比如根据监听结果我怀疑车窗控制集中在ID 0x280到0x2C0这段区间。那我可以写一个循环按ID递增发送固定payload同时观察车窗是否有反应。这个脚本要注意两点一是发送间隔不能太短给执行机构和总线留出反应时间实测50ms到100ms比较安全二是在发送前把车的引擎熄火、断开执行器只保留待测部件。import time import can bus can.interface.Bus(channelcan0, interfacesocketcan) for aid in range(0x280, 0x2C1): msg can.Message( arbitration_idaid, data[0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], is_extended_idFalse, ) bus.send(msg) time.sleep(0.1)跑完一轮记录哪些ID让车窗有了动作。如果完全没有就把范围扩大或者把payload从全FF改成按位变化的扫描值。在这种测试里你会深刻理解为什么叫“控制变量”如果不按ID分段而是随机发即使车窗动了你也不知道是哪一条消息起的作用。5.3 位级信号定位用固定变量法找到数据里的控制位当候选ID确定之后更难的部分来了数据域8个字节哪几个bit才是控制信号我的方法是固定变量法。假设ID 0x2A0让后视镜有反应那么保持其他7个字节不变只改变第0个字节观察现象没反应就改第1个字节以此类推。锁定某个字节后再逐个bit翻转把数据从00改成01、02、04、08这样按bit试探。定位过程的记录一定要表格化。我在试验中常用这种记录方式操作发送数据观察结果原始帧00 00 00 00 00 00 00 00无动作字节3置100 00 00 01 00 00 00 00后视镜左折字节3置200 00 00 02 00 00 00 00后视镜右折字节3置400 00 00 04 00 00 00 00门锁开字节3置800 00 00 08 00 00 00 00无动作这种逐位试探看起来笨但其实是最有效率的因为一个字节里往往压缩了多个信号。比如字节3的低3位控制后视镜折叠状态bit3控制门锁高4位是其他功能或校验位。信号还可能不是单字节而是跨字节的大端或小端整数比如车速、转速这类模拟量信号会占12到16位。遇到连续变化的物理量要先把字节序搞清楚我习惯用“观察值单调性”来验证发送一个递增序列比如从0x00递增到0xFF看仪表指示是否单调上升如果是说明这段数据的字节序和缩放比例就摆在那里。6. 错误帧排查逆向路上最常见的一道坎6.1 错误帧的分类不要一看见报错就怀疑硬件错误帧在大部分章节里属于噪音但你一旦开始做主动测试总线上错误帧变多就值得认真对待。先说怎么识别用candump收包时错误帧会被标注为(error)Python脚本里则可以判断msg.is_error_frame。不同类型的错误帧对应的根因差别很大。位错误和填充错误大概率是波特率、采样点不匹配因为接收节点在错误的时刻采样就会读到一个和预期相反的电平。CRC错误则更多和信号完整性有关线缆过长、屏蔽不好、终端电阻缺失都会引起。ACK错误出现时往往说明总线上只有发送方没有接收方或者接收节点因为某种原因没有应答。形式错误比较少见一般和设备内部逻辑异常有关。我在排查时是按“先协议层、再物理层”的顺序来的协议层就是波特率和采样点物理层再看接线电阻。6.2 一次真实的总线报错处理记录举一个我实际遇到过的案例。第一次把适配器接上一辆试验车用500kbit/s收包发现每秒会冒出十几个错误帧统计数据完全不可用。我做了三件事。先量OBD口的CAN_H和CAN_L电阻接近60欧姆说明终端电阻正常。然后换了一个不同的波特率试试当我把bitrate从500k改成250k之后错误帧立刻清零报文也都能正常解析了。这说明之前车辆总线实际跑的是250k我按500k去采样等于把两个bit当成一个bit来读自然满眼错误帧。这里的关键教训是总线波特率一定要先用示波器测显性位宽度或者用设备支持的自动波特率检测功能来确认而不是靠猜。猜一次就够浪费半天了。之后在另一辆车上遇到类似现象我直接量位宽1个显性位约2us换算下来就是500kbit/s整个排查只花了五分钟。6.3 主动测试时如何避免“人为制造”错误帧做主动测试的时候错误帧的来源往往不是车辆本身而是你自己的设备。最常见的情况是电脑里的两个程序同时操作同一个CAN接口比如Caring Caribou的discover还没退出又用Python脚本接着发数据两个发送线程在同一接口上抢会造成发送时序错乱。解决办法是同一时刻只保留一个发送端。另一个原因是发送间隔过短。CAN总线带宽有限比如500kbit/s的理论带宽再扣除协议开销实际每秒最多也就几千帧。如果脚本里用极短的循环毫秒级连续发送总线上就会挤满数据帧正常节点被迫反复重发错误计数上涨。我一般把主动探测的发送间隔控制在50ms以上既能保证现象可观察也不会干扰总线上其他节点的正常通信。最后如果你必须在发动机运转时做测试记得先确认适配器的地线接得够好否则点火系统的共模噪声也会贡献一批错误帧这类问题在台架上非常常见。7. 把逆向结果沉淀下来从裸数据到可复用的协议描述7.1 DBC不是玄学手写一个最小协议描述逆向做了几天手里积累了一堆ID和bit映射关系如果只记在草稿箱或者Excel里换台机器就丢了。我习惯把这些结果整理成DBC文件。DBC是CAN协议描述的标准格式CANalyzer、cantools、很多工控软件都能直接读取本质上就是给CAN消息建一份“数据库schema”。很多人一看到DBC就觉得复杂其实最小可用的结构很简单。比如我在试验车上确认ID 0x250十进制592的第3字节低3位控制后视镜折叠第3字节bit3控制门锁那DBC可以写成这样BO_ 592 BCM_STATUS: 8 Vector__XXX SG_ rear_view_fold : 24|31 (1,0) [0|7] Receiver SG_ door_lock : 28|11 (1,0) [0|1] Receiver解释一下24|31表示起始位在第24位、长度3位、1表示小端序、表示无符号。因为第3字节从0开始数的第0位就是第24位。倍率1、偏移0表示读出来的原始整数就是物理值。这段描述放在任何一个能解析DBC的工具里都能把原始数据直接解码成“后视镜折叠2、门锁1”这样可读的结果。7.2 用cantools把DBC变成Python对象协议描述文件的价值下一步就在Python里体现。cantools是解析DBC最顺手的库安装后几行代码就能加载pip install cantoolsimport cantools db cantools.database.load_file(bcm.dbc) msg db.get_message_by_name(BCM_STATUS) decoded msg.decode(bytes.fromhex(0000000700)) print(decoded) # {rear_view_fold: 3, door_lock: 0}写到这里你会发现所谓数据库逆向工程放在CAN总线语境下就是把“哪些ID对应哪些字段、字段的bit偏移和缩放关系”这套schema给反推出来。DBC就是承载这套schema的载体。有了它后续的解析脚本、报表、告警逻辑都可以共用同一个协议描述不用每次改动都去动代码。7.3 给逆向项目建立日志和版本管理习惯我建议从项目第一天就按这个目录结构归档project/ logs/ raw/ # 原始日志按日期命令保存 marked/ # 人工标记过的现象记录 scripts/ # 监听、统计、主动探测脚本 dbc/ # 最终协议描述和中间版本原始日志用candump -l落盘这类文件很大但不要删。很多信号刚开始看不出来是隔了两周回看才发现规律。每个关键发现都建议在marked目录里留一份简短的markdown记录写清楚操作、ID、数据变化、观察结果。整个目录用git管理哪天把协议改乱了随时可以回退。我自己的习惯是每定位一个有效信号就更新一次DBC文件并打个tag。第一次我花了一整天才确认一个灯控信号的位置但第二次换到另一台车上因为有了清晰的流程和归档只用了半小时就完成了类似功能。这个差距不是智商是方法论。最后说说个人体会。CAN逆向这套流程真正难的地方不是工具安装也不是命令参数而是在没有文档的情况下怎么约束自己的猜想、设计可复现的实验来验证它。我建议刚入手的读者先把目标放得小一点别一上来就想着把整车协议全部解开。从车窗、灯光、中控锁这类车身功能开始最合适它们响应直观、反馈及时、风险也相对低。等你把一条“按键到执行”的完整链路从物理层到数据位都打通你就已经迈过了很多人觉得跨不过去的那道门槛。再往后无论是UDS诊断、关键数据解析还是更深层的总线安全评估你都具备了一套可复用的起点。
网站建设高端定制企业官网