嵌入式黑盒协议逆向实战:UART波性分析、光耦反相与单片机插桩解密

嵌入式黑盒协议逆向实战:UART波性分析、光耦反相与单片机插桩解密 1. 什么样的项目会让你走到“黑盒逆向”这一步1.1 三种常见的现实场景我最早接触嵌入式黑盒协议逆向不是出于什么研究兴趣而是被一个很实际的问题逼的手里有一块老设备的控制主板设备还在正常运转但厂家停产了配套的上位机软件也早就没人维护。想把这台设备接入到新的监控系统里唯一拿到的信息就是“主板上有一个四针端子三根线分别标着GND、TX、RX”——没有原理图没有协议文档连波特率是多少都不知道。这种情况在工控、医疗、车载和智能家居的存量设备维护里太常见了。归纳下来大家走上这条路基本是三类需求维修替代原厂配件买不到要自己做一个兼容面板、兼容传感器替换故障部件。系统集成老设备要接入新平台原厂不给开放接口只能自己分析总线协议。功能扩展与安全评估想在不改原硬件的条件下增加新功能或者评估设备是否存在通信层面的设计缺陷。只要需求落到这三类里黑盒逆向就是一个绕不开的环节。所谓“黑盒”就是你只能看到设备的外壳和外部接口内部芯片型号、固件、原理图一概不透明。你要从物理连接和信号波形开始一点点把对方的通信规矩“套”出来。1.2 黑盒逆向的三个前提条件很多人一听到“逆向”就觉得高深其实开始之前只需要确认三件事你能拿到完整的实体设备并且有权对它进行测试。这里的“有权”必须是合法合规的比如设备是你自己采购的、是你负责维护的或者你获得了原厂和使用方的明确授权。逆向的目的是维修、兼容开发或者安全评估而不是绕过授权去做不该做的事。设备能供电、能触发。也就是说你能让它正常工作起来让它产生真实的通信报文。你能接触到总线的物理触点。可能是排针、测试点、芯片引脚甚至是接插件后面的焊盘。如果连物理触点都碰不到后面所有方法都无从谈起。这三个条件缺一个都不行。尤其最后一条我见过不少人在外观上到处找不到测试点后来拆开发现主板上预留了0欧电阻位焊上一个排针就能引出信号所以拆机观察PCB走线这一步一定要做仔细。1.3 分层逆向的思路黑盒逆向也要分层不能上来就拿着逻辑分析仪瞎抓。我习惯按照通信协议的分层结构来安排工作顺序物理层确定电平标准、信号极性、空闲状态、波特率或位定时。这一层不解开后面全白搭。链路层找到字节、包的边界确认帧头帧尾、长度字段、校验方式。应用层理解每个字段的业务含义比如温度、状态、地址、指令码。每一步都有对应的工具和手段物理层主要靠万用表、示波器链路层靠逻辑分析仪和解码软件应用层就要靠单片机插桩做主动交互。整个流程走完一个原本陌生的协议就能变成你手里可读可写的“自己人”。下面从最容易被卡住的物理层开始讲。2. 物理层盲猜先把陌生总线“问”出来2.1 静态测量先行拿到总线触点之后我的习惯永远是先上万用表再上示波器。原因是万用表能快速告诉你这条线大概是什么类型避免一上来就烧设备。先测各触点对地的直流电压。以最常见的UART为例如果一根线在静态时测到3.3V或者5V左右大概率是TTL电平的TX或RX空闲状态为高。如果测到接近12V的正压或负压而且是负逻辑多半是RS232。如果两根线之间的电压在0.2V左右抖动那可能是RS485或者CAN这类差分信号光靠一根表笔测对地电压是看不出所以然的要改用差分方式测。这个环节里有个容易被忽略的细节一定要在设备“上电但不通信”和“正常通信中”两个状态下各测一遍。有些总线的空闲电平是确定的比如UART空闲高但有些总线上电后会进入高阻态电压会被外围电阻拉来拉去。两种情况看到的静态值完全不同只有把两种状态都记录下来你才不会对空闲电平产生误判。2.2 电平标准快速对照实测电压出来后可以和下面这张表做个快速对照确定研究方向。以下值都是典型值实际会有浮动以测量为准。电平标准静态电压特征线数常见场景TTL UART空闲约3.3V或5V低电平约0V2根信号线单片机间通信、传感器、面板RS232负逻辑范围约±3V~±15V2根信号线老式工控机、PLC调试口RS485A-B压差约200mV差分2根绞合线工控总线、门禁、楼宇自控CANCANH/CANL约为2.5V±1V差分2根线汽车电子、工业设备如果你测到的是差分信号后面接逻辑分析仪和解码器时不能直接把两路分别当单端信号采要先经过一个RS485转TTL或者CAN收发器模块。这一步不能省否则看到的波形会非常奇怪而且解十次错十次。2.3 波特率的盲猜方法确定电平标准后真正动手抓波形。示波器接上探头建议先不设置任何触发把时基放到10ms级别看整体你会发现通信时有一串串的脉冲。此时要抓单个bit的宽度最直接的做法是把时基缩小到us级别找一段干净的波形用示波器的光标测量“最短的低电平脉冲宽度”。原理很简单UART数据位是低位在前如果数据字节里出现0x00那会有连续8个bit的低电平但如果数据位是1电平只在起始位保持一个bit的低电平。所以同一个帧里反复出现的最短低电平宽度通常就对应一个bit的时间。反过来算波特率波特率 1 / 最短位时间。比如你量到最短低电平宽度约104us那波特率大约是9600量到8.7us左右大约是115200。这个估算不要求特别精确只要能定位到标准波特率附近就行。注意示波器光标测量是有误差的所以我通常的做法是拿量出来的位时间除以常见的标准位时间看哪个更接近。比如104us除以8.68us约等于12不对除以104us正好接近1那就锁定9600。如果手上只有逻辑分析仪操作更简单先按估算值设置一个候选波特率比如9600或者19200然后开启异步串口解码看解出来的字节是否呈现规律性。有些逻辑分析仪软件自带波特率自动扫描功能但我用过几次后发现在噪声环境下自动扫描容易误判不如自己先量位时间再人工选准确率更高。2.4 从波形特征反推协议类型光把波特率猜出来还不够你还要判断总线上的数据是单纯的UART还是I2C、SPI、CAN等其他协议。这里有一些非常直观的波形特征可以参考UART空闲为高发送时先拉低一个起始位然后按低位到高位吐数据结束后回到高。波形整体是标准方波。I2C有两根线SCL和SDASCL是连续的时钟脉冲SDA数据线在SCL高电平期间变化就对应数据但有STARTSCL高时SDA拉低和STOPSCL高时SDA拉高这样的特殊条件波形上能看到明显的“台阶”。SPI至少三根线SCLK连续输出时钟MOSI/MISO数据在时钟边沿变化还有一根CS片选线在整个帧传输期间保持拉低。时钟线和数据线同时出现是非常明显的标志。CAN总线上显性电平会占优空闲时CANH/CANL都偏2.5V左右发送时两根差分线的压差变化幅度很小波形看起来不像普通方波那么“刚猛”更像一条细碎变化的曲线。实际抓波形时不要只看一根线有条件就把所有候选信号线都接到逻辑分析仪上一起采。如果你看到一根规律的时钟线对着几根数据线那协议类型基本马上就锁定了。3. 光耦反相差点让我把全套解码推到重来的暗坑3.1 现象逻辑分析仪解出来全是乱码这是我第一次做光耦隔离设备逆向时的真实经历。当时的对象是一块变频器通信板前方有一个接插件看起来是一个UART口空闲电平也确实是高电平静态电压3.3V波形抓出来也挺正常标准方波位时间约104us我信心满满地把波特率设成9600N81挂上逻辑分析仪开始解。结果解出来的全是乱码而且是那种每一帧开头像固定特征、后面又乱七八糟的乱码。更诡异的是手动看波形里的数据位怎么数都跟解码结果对不上。我一度以为是自己的波特率算错了换了好几个候选波特率都没有改善。后来把示波器探头移到信号走线的源头才发现问题根本不是波特率而是波形极性整个反了。3.2 为什么光耦会让信号反相很多工业设备为了抗干扰会在外部接口和内部MCU之间加光耦做电气隔离。最常见的光耦型号是PC817、TLP521这类它们的输出侧本质是一个光敏三极管而且绝大多数电路用的是“集电极开路、发射极接地”的接法。我们来走一遍信号路径。假设外部接口的TX空闲时为高电平外部信号为高时光耦输入侧的LED导通发光输出侧三极管导通输出端被拉到地所以你在这个点量到的是低电平。外部信号为低时LED不亮输出侧三极管截止输出端靠上拉电阻被拉到高电平。也就是说外部进来的高、低电平经过光耦之后正好反过来了高变低低变高。如果你直接在光耦输出端接逻辑分析仪按普通UART去解码那自然会把起始位、停止位都认反结果只能是乱码。这里要特别说明不是所有光耦电路都反相它取决于输出侧的接法和输入侧LED的驱动方式。我遇到过少数电路在光耦后面还加了一级反相器反而把极性“负负得正”又翻了回去。所以光耦反相是一个高概率事件但不能当作绝对结论具体还是要实际测。3.3 反相问题的定位方法定位反相这件事最快的办法是用示波器双通道同时测光耦两侧。一路探头接外部接口的原始信号另一路接MCU接收引脚旁边的测试点两个波形放在同一屏幕对比如果两路波形一高一低完全互补那基本就是反相。如果两路波形形状一致那就说明这一级没有翻转。没有示波器双通道时也可以用逻辑分析仪同时采两个点然后对比通道0和通道1的空闲状态。原始信号侧空闲如果是高而光耦输出侧空闲是低那也说明反相了。还有一种更隐蔽的情况光耦输出侧的RC时间常数太大导致下降沿很快、上升沿很慢。这时波形看着不完全是标准方波上升沿拖着一条斜线。这种波形即使极性对解码也容易因为边沿过缓而多位错乱尤其是高波特率下更明显。遇到这种情况尽量把测试点改到光耦输入侧或者在后级找一个已经整形的信号点。3.4 反相修正与选型建议确认反相之后修正手段有好几种按操作难度排序逻辑分析仪软件里把对应通道设为“反相”Inverted。绝大多数PC端逻辑分析仪软件都支持这个选项点了之后波形就恢复正常视觉解码也按反极性执行。示波器测量时使用反相功能或者直接把探头移到光耦输入侧只测原始信号。自己画板子做插桩工具时在MCU引脚上启用GPIO反转或者通过程序对读到的电平取反。我建议首选第一种因为改软件设置不影响信号完整性。如果你用的是单片机插桩方案那就在代码里加一行取反逻辑后面会专门讲到。光耦反相这个坑之所以值得单独拿出来讲是因为它不会让设备损坏也不会让你一眼看出“硬件有问题”它只会在解码结果上制造一种“看起来有一点规律但永远解不对”的诡异状态极容易浪费整个排查周期。涉及光耦隔离的设备抓到波形后第一件事就是确认极性不要急着调波特率。4. 单片机插桩从“只看不发”到“边说边听”4.1 为什么逻辑分析仪不够用逻辑分析仪是把好工具但它本质上是一个“旁观者”只能被动记录总线上的电平变化。然实际逆向过程中我们需要的不只是看还要能主动参与想确认一个字段是不是温度值最好的办法是我们自己发给设备一个不同数值看设备有没有响应变化。想测试从机在异常帧下的反应逻辑分析仪做不到。现场需要长时间记录并解析总线数据时一台电脑加逻辑分析仪的体积和供电也往往不实用。单片机插桩就可以补上这里面的空缺。它可以作为协议分析仪、中间人代理、主动注入控制器而且能做成便携小工具。4.2 被动嗅探用MCU接总线抓数据插桩项目里最简单的形态是纯被动嗅探。把MCU的一个UART串口接到目标总线上目标设备正常通信MCU在旁边静默接收并记录数据。这里有个要点大多数MCU的UART外设要求波特率是预设好的。在协议盲猜初期我会先用示波器算出候选波特率然后在代码里让MCU按这个波特率接收同时在总线空闲时间打印输出。如果端口电平不是TTL比如是RS485或者CAN需要先挂一个转换模块。RS485模块的A、B端接总线TTL端接MCU。CAN的话要接CAN收发器。不要试图直接把MCU引脚强行挂在RS485总线上电平差异轻则采不到数据重则烧引脚。下面这段代码是用STM32加外部中断加定时器来测起始位和bit时间的思路适合在不确定波特率时直接把波特率“量”出来// 伪代码示意外部中断测低电平脉冲宽度 volatile uint32_t t_start 0; volatile uint32_t t_low_width 0; void EXTI_IRQHandler(void) { if (GPIO_PIN_IS_LOW) { t_start TIMER_GetTick(); // 记录下降沿时间 } else { t_low_width TIMER_GetTick() - t_start; // 上升沿到达得到低电平宽度 } } // 主循环中计算 bit_time // 多次采集后取最小值即可估算 1 bit 时间 uint32_t bit_time get_min_low_width(); uint32_t baudrate TIMER_FREQ / bit_time;实际工程里我更常用的是另一套组合UART接收DMA 空闲中断。先把UART按预估波特率配好DMA把收到的字节连续写进环形缓冲区硬件空闲中断表示一帧结束。这样即使主机发送一长串数据也能完整录下来之后再集中分析。核心代码思路如下// 伪代码示意UART DMA接收 空闲中断收帧 HAL_UART_Receive_DMA(huart1, rx_buf, RX_BUF_SIZE); __HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE); void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { // 环形缓冲写入记录当前帧数据或者直接打上时间戳 } void UART_IDLE_Callback(UART_HandleTypeDef *huart) { // 检测到总线空闲认为一帧结束可以解析这条完整帧 uint16_t frame_len RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart-hdmarx); parse_frame(rx_buf, frame_len); }被动嗅探要特别注意一点MCU的UART空闲判定可能和真实总线帧间隔不一致。有些协议帧与帧之间只有很短的停顿如果MCU判断空闲的时间设置得太长会把多帧合成一条设置得太短又可能把一帧拆散。所以收到数据后不要急于下结论先记录原始时序解析时再根据帧头特征人工分段。4.3 中间人插桩透明转发与按需改写纯被动嗅探只能“听”如果我们想改设备和上位机之间交互的内容就需要中间人插桩。结构很简单把原通信链路断开MCU一端接主机另一端接从机。MCU内部转发数据包需要改写的地方按规则改掉。这样主机以为自己在和原从机通信从机也以为自己在和原主机通信但中间的报文已经经过了我们处理。在温控设备、电池管理板、充电桩这类系统中中间人插桩最常见的用途是截获“设备地址”、“运行参数”这类关键字段。比如我想让主机认为从机序列号是另一个值就可以在转发的报文里定位到序列号字段做替换后再发出。中间人代码的雏形就是两个串口互相透传// 伪代码示意双串口中间人转发 while (1) { if (HAL_UART_Receive(huart1, c1, 1, 10) HAL_OK) { // 可选对 c1 做改写 HAL_UART_Transmit(huart2, c1, 1, 100); } if (HAL_UART_Receive(huart2, c2, 1, 10) HAL_OK) { HAL_UART_Transmit(huart1, c2, 1, 100); } }这个简单的透传只能算是骨架。真正用于逆向的中间人还要在两个方向各自维护缓冲区和帧解析状态机因为只有提取出完整一帧你才能安全地修改长度字段和校验字段。否则只做字节级透传遇到多字节字段想要修改就会发现无从下手。4.4 主动注入通过发帧确认字段含义最后一种插桩模式是主动注入这也是一锤定音的关键步骤。到了协议推演的中后期我已经猜到一个大概的帧结构和校验方式但不知道某个字节代表什么。这时候就主动发一帧过去观察被控制设备的行为变化。假设猜出温度字段是偏移量、实际温度等于原始值乘以0.1度那我在注入帧里把温度字节从0x01改成0x02如果设备显示从0.1变成了0.2猜得就对如果没反应就要换一种解释。这种“输入-观察-修正”的循环是整个逆向过程中最耗时但也是最有成就感的部分。MCU发送自定义帧最直接的办法还是用UART发送功能直接把字节数组灌进寄存器// 伪代码示意MCU发送自定义协议帧 uint8_t frame[] {0xAA, 0x55, 0x04, 0x11, 0x01, 0x05}; for (int i 0; i sizeof(frame); i) { while (!(USART1-ISR USART_ISR_TXE_TXFNF)); USART1-TDR frame[i]; }主动注入前务必想清楚可能带来的后果。有些设备收到异常帧会触发保护动作比如变频器直接报故障、电源板进入锁机状态。保险做法是在设备供电回路里串一个限流电阻或者可恢复保险丝并在代码里预留一个长时间无响应后的“复位发送”逻辑便于从异常状态拉回来。5. 实战复盘还原一块温控主板的私有协议5.1 项目背景有一次我做了一个温控器的兼容面板面板和温控主板之间通过三根线连接一根电源、一根地、一根串口数据线。温度主板能采集温度和按键状态然后周期性地把数据送给面板显示。因为原厂面板停产我手里只剩主板所以必须自己复刻一个能显示温度的面板。第一目标只是能显示温度所以通信方向是主板主动发面板被动收。但后续还希望扩展成能修改温度设定值那就需要面板发指令给主板所以双向通信都要掌握。5.2 物理层确认与极性修正先用万用表测数据线对地电压静态时约3.3V符合TTL UART空闲高的特征。上示波器抓波形解码时打开9600波特率的UART解码出现大量乱码。我没有急着改参数。拆开主板确认了数据线源头接了一个PC817光耦光耦输出侧接到主控MCU的RX引脚。用示波器双通道同时测光耦输入侧和输出侧看到两者波形正好互补立刻确认是反相。在逻辑分析仪通道设置中勾选“反相”再一次解码数据立刻变得有结构了。5.3 帧格式推演修正极性后连续抓了大约100帧所有帧都由下面这个模式组成字节偏移值含义推测00xAA帧头110x55帧头220x08负载长度30x21命令字4~11变化数据负载120x??校验字节帧头固定为AA 55这个组合在很多国内工控设备里非常常见原因是0xA5、0x5A这类字节能检测字节序和波特率错位而AA 55则能快速判断极性连续出现高电平1低电平0交替任何一位错位都能被发现。长度字段是0x08我数了一下后面的数据负载和校验加起来正好是9个字节。实际帧里第2字节一旦变化后面数据区的长度也随之变化基本确定是长度字段。校验字节的算法是我猜测了三种常见方案累加和、异或、CRC8写了一个小脚本把前N个字节做异或后和最后一个字节比较全通过确认校验算法是逐字节异或。5.4 字段语义猜解帧格式定了接下来猜字段语义。我让温控主板在正常运行状态下持续发送然后做了两个动作把温度传感器探头放到温水里对比前后数据帧发现第4和第5字节随温度变化数值大约0.1摄氏度一个字。按压主板上物理按键发现第6字节变化并且持续一段时间后恢复0从按键动作来推断这是一个按键状态字。通过这种“改变一个输入变量观察一个输出字节”的方式可以把大部分负载字节的语义猜出来。这个方法笨但非常可靠而且不需要知道对方程序源码。注意每次只改一个变量这样观察到的差异才能跟变量对上号。否则又加热又按键又拔插线数据一变就分不清是谁引起的了。5.5 插桩验证与最终成果协议推演完成后我做了两个验证实验。第一个是用MCU作为被动嗅探端挂在原主板的发送脚上连续记录两小时数据确认没有出现解析不了的特殊帧。第二个是主动注入把MCU串口接到主板接收脚按推演出的帧格式发一条“设置目标温度为26度”的指令主板上的继电器立刻吸合加热指示灯点亮证明指令被正确执行。到这里这个温控主板的私有协议算是完整拿下了。整个过程大概用了两个晚上第一晚卡在光耦反相上第二晚推完帧格式和字段语义。工具就是一台示波器、一个8通道逻辑分析仪、一块STM32开发板以及一个靠谱的PC817光耦反相判断。之后我又用同样的方法处理过充电桩通信板和仪表盘LCD协议流程都差不多。6. 逆向项目里最容易翻车的几个坑与工具准备6.1 工具清单与选型理由整个黑盒逆向项目用到的东西不算多但每一样都别将就工具推荐要求作用示波器100MHz带宽以上双通道起步测电平、位时间、确认极性逻辑分析仪8通道以上采样率20MS/s以上多路并行抓帧、异步解码USB转TTL模块带3.3V/5V电平选择和MCU调试口通信RS485/CAN收发模块按总线类型选型差分总线转TTL可调电源带电压电流显示安全供电STM32或其他MCU板至少2个UART被动嗅探/中间人/主动注入飞线、排针、杜邦线质量要好连接测试点示波器带宽看着不用太高但抓边沿时带宽不够会导致上升沿看起来很圆影响对波特率的准确判断。逻辑分析仪采样率也要注意20MS/s对应约200kbps以下协议足够如果目标是CAN、高速UART就需要更高采样率。6.2 十个翻车点这些坑都是我在实际项目里碰到过的列出来给大家排雷共地问题。逻辑分析仪、示波器、被测设备之间没有可靠共地波形全是噪声严重时把地线夹错位置会直接短路。电平不匹配。5V TTL信号直接进3.3V MCU引脚或者RS232的负压串进TTL电路板子当场冒烟。把差分信号当单端采。RS485和CAN必须用对应收发器直接表笔测只能看到乱码。光耦反相。前面已经专门分析过这个坑隐蔽度最高。上拉电阻缺失。总线空闲时处于高阻态波形悬空乱跳解码结果自然不稳定。波特率误差。目标设备用的时钟晶振不是常用值比如接近9600但实际是10000解码初期就会出零星错码。触发条件设置不当。帧间隔很长逻辑分析仪按电平触发容易抓空应该设置合适的前触发深度。继电器和电机干扰。抓到的波形里叠加强烈毛刺影响位宽度测量测量时尽量远离干扰源或开启滤波。协议编码特殊。有些设备不用标准N81而是用7位数据位、偶校验或者曼彻斯特编码按标准UART去解码就会一塌糊涂。固件管脚复用。MCU上电后UART引脚可能先被配置成GPIO输出导致空闲电平不确定给极性判断造成假象。6.3 几个工作习惯除了工具和避坑我更想强调记录习惯。黑盒逆向本质上是一个“不断提出假设、验证假设、推翻假设”的过程中间产生的中间数据非常多。我个人的工作方式是每抓一段波形立即截图并命名文件名包含测试点、波特率、极性状态、时间比如光耦输出_9600_invert_2030.bin。不然三天后你自己都认不出这是哪个阶段的截图。维护一份假设清单把每个字节的推测写下来验证结果也写下来验证失败的假设不要删除因为同一个字节可能有多种解释后来可能会用回旧假设。每次只改一个变量。改波特率就只改波特率改接线就只改接线不要同时做两件事否则出现问题时根本定位不到原因。这套黑盒逆向方法并不需要多高深的理论它靠的是清晰的思路和严格的实验习惯。我第一次独立完成整个过程后最大的感受是只要物理层判断准确、极性不过关后面再夸张的协议推演套路都是空中楼阁。反过来把物理层吃透把插桩工具搭好剩下的大部分工作就是耐心地跟设备“对话”而已。希望这篇指南能让你在碰到自家设备黑盒通信时少走几步弯路。