1. 固件逆向的起点为什么Hex文件值得你花时间搞嵌入式开发的人迟早会碰到一个绕不开的场景手里拿到一块板子或者一个产品的固件升级包里面只有一个.hex文件没有源码没有文档甚至连芯片型号都得靠猜。这时候你想改个功能、修个bug、或者单纯想看看人家是怎么实现某个逻辑的怎么办答案就是固件反编译。而Hex文件就是这条路的起点。我先把这个事情说清楚Hex文件本质上是一种文本格式的二进制镜像载体它用ASCII字符记录了固件的机器码、地址信息和校验数据。它不是可执行文件也不是源码而是一个“搬运工”——把编译后的机器码从编译器手里搬到烧录器或者芯片里。你要做的就是把这个搬运工手里的东西拆开还原成人类能读的C语言或者汇编。这篇文章适合谁看如果你已经会写单片机C语言知道什么是寄存器、什么是中断向量表但从来没接触过逆向工程那这篇就是给你写的。如果你是有经验的嵌入式工程师想系统梳理一下从Hex到C的完整链路这里也有不少实操细节和踩坑经验可以对照。我做过不少固件逆向的项目从8位机到32位ARM Cortex-M都有涉及。说实话这个过程不像很多人想的那么神秘但也绝对不是点几下鼠标就能搞定的事。它需要你对编译原理、芯片架构、链接脚本都有基本的理解。下面我把整个流程拆开一步一步说。2. 动手之前的认知准备Hex文件到底是什么2.1 Hex文件的格式与结构解析很多人拿到Hex文件第一反应是用文本编辑器打开然后看到一堆冒号开头的行完全不知道什么意思。其实Intel Hex格式是有严格规范的每一行叫做一条记录Record格式如下:LLAAAATT[DD...]CC:是每行的起始标志LL是数据长度表示后面有多少字节的数据AAAA是起始地址16位大端序TT是记录类型DD是实际数据字节CC是校验和记录类型有几种最常见的是类型码含义说明00数据记录包含实际的固件数据01文件结束标记Hex文件结束02扩展段地址用于8086实模式段地址03起始段地址指定CS:IP04扩展线性地址用于32位地址空间05起始线性地址指定入口点对于ARM Cortex-M这类32位芯片你主要关注的是类型04和类型00。类型04用来设置高16位地址类型00用来传输实际数据。举个例子:020000040800F2 :1000000000200020C1000008D1000008D1000008A0第一行是类型04表示扩展线性地址为0x0800也就是说后面的数据地址高16位是0x0800。第二行是类型00数据长度0x10起始地址0x0000所以实际地址是0x08000000。数据是00200020C1000008...这就是STM32的向量表开头——第一个字是初始栈指针值0x20002000第二个字是复位向量0x080000C1。注意不同工具生成的Hex文件可能有细微差异比如有些工具会在每行末尾加换行符有些不会。解析的时候要兼容这些情况。2.2 从Hex到二进制第一步转换反编译的第一步是把Hex文件转成纯二进制文件.bin。为什么因为反汇编工具通常需要连续的二进制镜像而不是带地址记录的文本格式。常用的工具有objcopy如果你有ELF文件直接用arm-none-eabi-objcopy -O binary input.elf output.binsrec_cat专门处理Hex、S19、bin等格式的转换工具Python脚本自己写一个解析器灵活但费时间我一般用srec_cat命令很简单srec_cat input.hex -intel -o output.bin -binary这条命令把Intel Hex格式的input.hex转成纯二进制的output.bin。如果你需要指定填充值比如未使用的区域填0xFF可以加-fill 0xFF参数。转换完成后用xxd或者hexdump看一下二进制内容确认开头是不是符合预期。比如STM32的固件开头应该是栈指针和复位向量这两个值应该落在RAM和Flash的地址范围内。实操心得有些Hex文件包含多个不连续的地址段直接转bin会导致中间出现大段填充。这时候最好先用objcopy或者脚本把各段分开处理或者用--gap-fill指定填充值。2.3 芯片架构与指令集的判断在反汇编之前你必须知道目标芯片是什么架构。这决定了你用哪个反汇编器以及怎么解读反汇编结果。常见的嵌入式架构有ARM Cortex-M系列Thumb/Thumb-2指令集32位最常见ARM7/ARM9ARM指令集32位AVR8位Arduino常用MSP43016位TI的低功耗系列PIC8位/16位Microchip的产品线RISC-V新兴架构32位/64位怎么判断几个线索Hex文件的地址范围STM32通常从0x08000000开始AVR从0x0000开始MSP430从0x8000或0x4400开始向量表特征Cortex-M的向量表第一个字是栈指针第二个字是复位向量且复位向量的bit0通常为1Thumb模式字符串信息固件里可能包含编译器版本、芯片型号等字符串文件大小和密度8位机的固件通常比较小32位机的固件相对较大如果你实在判断不出来可以先用binwalk跑一下看看有没有明显的文件头或者字符串特征。3. 反汇编工具链的选型与配置3.1 主流反汇编工具对比选对工具效率翻倍。我列一下我用过的几款工具以及各自的适用场景工具适用架构优点缺点IDA Pro全架构功能最强反编译质量高商业软件价格贵Ghidra全架构免费NSA出品插件丰富界面卡顿学习曲线陡radare2全架构命令行脚本化强上手难度大objdump全架构免费binutils自带反汇编质量一般无图形界面Hopperx86/ARMMac平台友好架构支持有限对于嵌入式固件逆向我的建议是预算充足IDA Pro ARM插件反编译质量最好预算有限Ghidra免费且功能足够快速查看objdump命令行直接出结果我个人的工作流是先用objdump快速看一下反汇编确认架构和入口点然后用Ghidra做深度分析最后用IDA Pro做交叉验证如果有license的话。3.2 Ghidra的安装与配置Ghidra的安装很简单下载解压运行ghidraRun即可。但有几个配置项需要调整内存分配默认的1GB不够用改成4GB以上。编辑support/launch.properties把MAXMEM改成4G或更高。反编译超时大固件的反编译可能超时在Edit - Tool Options - Decompiler里把超时时间调大。处理器模块Ghidra会自动检测架构但有时候会猜错。手动指定ARM Cortex-M或者对应的架构。导入固件的时候选择Raw Binary格式然后手动指定基地址。比如STM32的固件基地址填0x08000000。Ghidra会问你是否要分析选Yes然后等它跑完。注意Ghidra对Thumb指令集的识别有时候会出错特别是函数边界。如果发现反汇编结果不对劲可以手动调整指令集模式或者在Language里强制指定ARM:LE:32:Cortex。3.3 IDA Pro的加载与基地址设置IDA Pro的流程类似但更精细。加载二进制文件后选择处理器类型为ARM然后设置ROM start address和Loading address。对于STM32这两个都填0x08000000。加载完成后IDA会自动分析。如果固件是Thumb指令集IDA通常能正确识别但有时候需要手动按AltG设置T寄存器值为1。IDA的强项是反编译F5但嵌入式固件的反编译质量参差不齐。很多时候你需要结合汇编和反编译结果一起看。实操心得IDA的Options - General - Analysis里把Kernel options 1和Processor specific options都打开能提高分析准确率。另外如果固件里有大量中断向量可以手动创建中断处理函数帮助IDA识别代码边界。4. 从反汇编到C语言还原逻辑的核心步骤4.1 识别入口点与向量表反汇编的第一步是找到入口点。对于Cortex-M芯片入口点就是复位向量指向的地址。在反汇编结果里向量表通常位于固件开头格式如下0x08000000: 20002000 .word 0x20002000 ; 初始栈指针 0x08000004: 080000C1 .word 0x080000C1 ; 复位向量 0x08000008: 08000123 .word 0x08000123 ; NMI处理函数 ...复位向量的bit0为1表示Thumb模式。去掉bit0得到实际地址0x080000C0。跳转到这个地址就是复位处理函数的开始。在Ghidra里你可以手动创建这些向量然后让Ghidra自动分析函数。在IDA里可以用脚本批量创建中断向量。4.2 函数识别与命名反汇编工具会自动识别一部分函数但很多函数需要手动确认。识别的线索包括函数序言PUSH {R7, LR}或者SUB SP, SP, #0x10函数尾声POP {R7, PC}或者BX LR调用指令BL或者BLX字符串引用函数里引用了字符串常量我一般会先跑一遍自动分析然后手动检查几个关键函数复位处理函数、主循环、中断处理函数。这些函数通常有明显的特征比如主循环里会有大量的条件跳转和外设寄存器访问。命名函数的时候尽量用有意义的名称。比如访问GPIO寄存器的函数叫GPIO_Init处理串口中断的叫USART_IRQHandler。这样后续分析会轻松很多。4.3 数据类型与控制流还原反汇编出来的代码是汇编你需要把它还原成C语言。这个过程分几步识别局部变量栈上的偏移对应局部变量根据使用方式推断类型int、char、指针等识别全局变量固定地址的访问对应全局变量通常在.data或.bss段识别结构体连续的内存访问可能对应结构体比如外设寄存器组还原控制流if-else对应条件跳转for/while对应循环跳转switch-case对应跳转表举个例子下面是一段反汇编LDR R0, 0x40020000 LDR R1, [R0, #0x00] ORR R1, R1, #0x01 STR R1, [R0, #0x00]还原成C语言就是*(volatile uint32_t *)0x40020000 | 0x01;如果这个地址是GPIOA的MODER寄存器那就可以写成GPIOA-MODER | 0x01;注意反编译工具如IDA的F5能自动完成一部分还原但结果不一定准确。特别是涉及指针运算和结构体的时候经常需要手动修正。4.4 外设寄存器的映射与识别嵌入式固件的一个特点是大量访问外设寄存器。这些寄存器通常有固定的地址比如STM32的GPIOA基地址是0x40020000USART1基地址是0x40011000。识别外设寄存器的技巧查芯片参考手册找到外设基地址在反汇编里搜索这些地址看看哪些函数访问了它们根据访问模式推断外设类型比如读-改-写通常是GPIO连续读写通常是DMA我一般会先建一个外设地址表然后在反汇编里标注这些地址。Ghidra和IDA都支持自定义数据类型和符号可以把这些地址定义成结构体方便后续分析。5. 实战案例一个STM32固件的逆向过程5.1 目标固件的基本信息我拿一个实际的STM32F103固件做例子。这个固件是一个简单的LED闪烁程序通过串口输出调试信息。固件大小约12KBHex文件约40KB。首先用srec_cat转成binsrec_cat firmware.hex -intel -o firmware.bin -binary然后用xxd看一下开头00000000: 0020 0020 c100 0008 d100 0008 d100 0008 . . ............确认栈指针是0x20002000复位向量是0x080000C1符合STM32F103的特征。5.2 反汇编与函数定位用Ghidra加载firmware.bin基地址设为0x08000000架构选ARM Cortex-M。分析完成后找到复位处理函数void Reset_Handler(void) { // 初始化.data段 // 初始化.bss段 // 调用SystemInit // 调用main }在Reset_Handler里找到main函数的调用然后跳转到main。main函数的结构很清晰int main(void) { SystemInit(); GPIO_Init(); USART_Init(); while (1) { GPIO_Toggle(); USART_SendString(LED Toggled\r\n); Delay(500); } }5.3 关键逻辑的C语言还原GPIO_Init函数的反汇编LDR R0, 0x40021018 ; RCC_APB2ENR LDR R1, [R0] ORR R1, R1, #0x04 ; 使能GPIOA时钟 STR R1, [R0] LDR R0, 0x40010800 ; GPIOA_CRL LDR R1, [R0] BIC R1, R1, #0xF0 ; 清除PA2的配置位 ORR R1, R1, #0x30 ; PA2推挽输出50MHz STR R1, [R0]还原成C语言void GPIO_Init(void) { RCC-APB2ENR | RCC_APB2ENR_IOPAEN; GPIOA-CRL ~GPIO_CRL_MODE2; GPIOA-CRL | GPIO_CRL_MODE2_0 | GPIO_CRL_MODE2_1; }USART_Init函数类似主要是配置波特率、数据位、停止位等参数。通过分析寄存器写入的值可以反推出波特率是1152008位数据1位停止位无校验。5.4 验证与调试还原出C代码后怎么验证是否正确我的做法是用STM32CubeIDE新建一个工程把还原的代码填进去编译生成Hex文件和原始Hex文件对比如果功能一致说明还原成功当然完全一致很难因为编译器优化、库函数版本等因素会导致差异。但只要核心逻辑一致功能就能复现。实操心得验证的时候可以先从简单的功能开始比如GPIO控制。确认GPIO能正常工作后再验证串口、定时器等复杂外设。这样逐步推进容易定位问题。6. 常见问题与排查技巧实录6.1 反汇编结果全是乱码怎么办这是最常见的问题。原因通常有几个架构选错了比如把ARM固件当成AVR反汇编基地址设错了比如STM32固件基地址设成了0x00000000指令集模式错了比如Thumb固件用了ARM模式排查步骤确认芯片型号和架构确认固件的加载地址确认指令集模式Thumb还是ARM用binwalk或者strings看一下固件里有没有明显的字符串特征6.2 函数识别不准确怎么处理反汇编工具自动识别的函数可能不准确特别是优化过的代码。处理方法手动创建函数在IDA里按P键在Ghidra里右键选择Create Function调整函数边界有时候函数末尾被截断了需要手动扩展修复跳转表switch-case的跳转表经常识别错误需要手动修正6.3 外设寄存器地址对不上怎么办不同芯片的外设基地址不同甚至同一芯片的不同型号也可能有差异。解决方法查芯片参考手册确认外设基地址在反汇编里搜索这些地址看看有没有访问如果地址对不上可能是固件针对的是另一个型号或者使用了重映射6.4 常见问题速查表问题可能原因解决方法反汇编全是乱码架构/基地址/指令集错误确认芯片型号调整加载地址和指令集函数识别不全优化导致函数边界模糊手动创建函数调整边界外设地址对不上芯片型号不匹配查参考手册确认外设基地址反编译结果不可读编译器优化过度结合汇编分析手动还原逻辑Hex转bin后大小不对地址不连续用--gap-fill填充或分段处理避坑技巧反汇编之前先用strings看一下固件里的字符串。很多时候字符串能告诉你很多信息比如编译器版本、使用的库、甚至函数名。7. 工具链与效率提升的进阶技巧7.1 脚本化批量处理如果你经常做固件逆向建议把常用操作脚本化。比如用Python写一个Hex解析器自动提取向量表、识别函数入口、生成符号表。import struct def parse_hex(hex_file): data {} base_addr 0 with open(hex_file, r) as f: for line in f: line line.strip() if not line.startswith(:): continue length int(line[1:3], 16) addr int(line[3:7], 16) rectype int(line[7:9], 16) if rectype 0x04: base_addr int(line[9:13], 16) 16 elif rectype 0x00: for i in range(length): data[base_addr addr i] int(line[9i*2:11i*2], 16) return data这个脚本能解析Hex文件返回地址到数据的映射。你可以在此基础上扩展比如自动识别向量表、生成IDA脚本等。7.2 符号恢复与库函数识别很多固件会链接标准库或者HAL库这些库函数的特征比较明显。识别出库函数后可以大大减少分析工作量。常用的库函数识别方法特征码匹配比如memcpy、memset、strlen等函数有固定的指令序列字符串引用库函数通常会引用特定的字符串调用关系库函数被大量调用且功能通用Ghidra和IDA都有库函数识别的插件比如FLIRTIDA和Function IDGhidra。导入对应的签名文件能自动识别大量库函数。7.3 动态调试与静态分析结合静态分析有时候会遇到瓶颈比如加密算法、混淆代码。这时候可以结合动态调试用J-Link或者ST-Link连接目标板用GDB或者OpenOCD加载固件设置断点观察寄存器值和内存变化结合静态分析结果验证逻辑动态调试的优势是能直接看到运行时的行为特别是涉及外设交互和通信协议的时候。实操心得动态调试的时候可以先从已知的入口点开始比如main函数。然后逐步跟踪观察每一步的寄存器变化。如果遇到加密函数可以在加密前后设置断点对比输入输出推断加密算法。8. 从逆向到二次开发还原代码的工程化8.1 还原代码的编译与验证还原出C代码后下一步是编译验证。我的做法是新建一个工程选择相同的芯片型号把还原的代码填进去补全缺失的头文件和库函数编译生成Hex文件对比原始Hex文件和生成的Hex文件看看差异在哪里差异可能来自编译器版本不同优化等级不同库函数实现不同链接脚本不同只要核心逻辑一致功能就能复现。如果差异太大需要逐步排查。8.2 功能修改与重新烧录还原代码的最终目的是修改功能。比如修改LED闪烁频率修改串口波特率添加新的功能模块修改完成后重新编译、烧录、测试。如果一切正常说明逆向成功。注意修改固件的时候要确保不破坏原有的校验机制。有些固件有CRC校验或者签名验证修改后需要重新计算校验值。8.3 法律与道德边界最后说一点重要的固件逆向涉及法律和道德问题。你只能对自己拥有的设备或者获得授权的设备进行逆向。未经授权逆向他人的固件可能违反法律。我的原则是只逆向自己购买的设备不传播受版权保护的固件不用于商业目的尊重原厂的知识产权如果你是为了学习或者研究那没问题。但如果是为了破解或者盗版那就越界了。9. 我个人在实际操作中的体会做了这么多固件逆向的项目我最大的体会是耐心比技术更重要。很多时候你面对的是几万行反汇编代码没有任何符号信息全靠自己一点点梳理。这个过程很枯燥但当你最终还原出完整的逻辑看到代码跑起来的那一刻成就感是巨大的。另外工具只是辅助核心还是对芯片架构和编译原理的理解。如果你不懂ARM的指令集不懂链接脚本的作用不懂外设寄存器的映射再好的工具也帮不了你。所以基础很重要。最后分享一个小技巧逆向的时候先从简单的功能入手比如GPIO控制、延时函数。这些功能逻辑简单容易验证。确认没问题后再逐步深入分析串口、定时器、中断等复杂功能。这样循序渐进不容易迷失方向。这个领域还有很多可以扩展的方向比如自动化逆向工具的开发、基于机器学习的函数识别、固件漏洞挖掘等。如果你对这方面感兴趣可以沿着这个方向继续深入。
企业数字化 ERP 产品动态
相关推荐
ESP32 上跑 WebAssembly:打造单片机应用商店 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 13:17:00
2026年AI算力与汽车电子功率电感选型及替代验证实操指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 13:17:00
如何用规则引擎打造一个中文“去AI味”自查器 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 13:17:00
基于PyTorch的36类果蔬图像分类:数据集解析与迁移学习实战 简介:一份面向图像分类入门与实践的常见果蔬多类别标注数据集,包含香蕉、苹果、梨、葡萄、橙子、黄瓜、胡萝卜、辣椒、洋葱、土豆等36个类别,约4200张已标注图像。数据已完成预处理,可直接作为分类网络输入,并划分了独… · 2026/9/24 23:49:47
IP、域名、DNS、CDN:一条链路搞懂网络访问与故障排查 IP、域名、DNS、CDN,这四个概念到底在解决什么问题?做网站开发、网络运维或者刚入门云计算的朋友,迟早要跟这四个词打交道:IP、域名、DNS、CDN。我面试过不少年轻人,问起单个概念都能说个大概,但一落到实际… · 2026/9/24 23:49:40
树莓派实时摄像头共享实战:从链路级调优到跨平台稳定传输 1. 为什么“树莓派→PC实时摄像头共享”不是个简单问题,而是一条链路级工程你手头有一块树莓派4B,接上了OV5647摄像头模块,想把画面实时传到隔壁的Windows或Ubuntu PC上——听起来就是几行Python代码的事?我去年在做一个远程安防巡… · 2026/9/24 23:49:40
Mbps与MB/s区别详解:百兆、千兆、万兆带宽实际下载速度换算 做网络这块时间久了,一定会反复遇到同一个问题:家里拉了千兆宽带,手机测速却只有三四百兆;办公室改了万兆核心,拷贝大文件还是感觉不够快;监控项目装了十几个摄像头,交换机端口明明是百兆的&… · 2026/9/24 23:49:40
FreeRTOS内核12大核心机制深度解析:从任务切换到低功耗调度 1. 别再被“会用FreeRTOS API”骗了:为什么90%的嵌入式开发者卡在“伪入门”阶段你有没有过这种经历:照着例程把xTaskCreate()、vTaskDelay()跑通了,LED能闪烁,串口能打印,甚至还能接个传感器读数据——然后信心满满地… · 2026/9/24 23:49:40
ESP32+W5500有线以太网实战:SPI协议、驱动移植与排错指南 搞过 ESP32 的人应该都有这种感觉:点灯、串口、Wi-Fi 都是一把过,但一到 SPI 就懵了。什么 MOSI、MISO、SCLK、CS,还有 CPOL、CPHA、Mode 0、Mode 3,看手册像看天书,抄例程也不知道每行在干嘛。偏偏很多项目又绕不开 S… · 2026/9/24 23:49:40
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程 简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13
1D-CNN时间序列建模实战:从Conv1d原理到工业落地 简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26
柔软的L:汉语语流中被忽视的舌肌张力控制 1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44