
1. 一份没人讲得清的固件究竟不清在哪里上个月我接到一个活儿处理一份没人讲得清的固件。光看文件名是upgrade_v1.2.bin里面到底是什么分区、什么文件系统、能不能解包重打包前同事丢下一句能用就行然后就离职了。桌面上是一个2.5GB的bin文件没有源码、没有文档、没有分区表。这种固件你要直接刷大概率是把设备变砖但你要是能把它读透那就是一把完整的自定义设备、升级功能、备份恢复的钥匙。先说清楚固件到底是个什么东西。我们平时说的固件不是单一的程序而是电器设备里整套软件的总称。以常见的电视盒子、路由器、开发板为例固件至少包含引导加载程序Bootloader、内核Kernel、根文件系统RootFS和一堆配置/数据分区。这些内容被组织成分区布局然后塞进一块NAND/NOR Flash或者eMMC里。交付给产线的升级包则是一个或多个人为打包出来的镜像文件把上述所有内容按固定偏移装配起来。很多固件没人讲得清其实逃不开下面几个盲区分区布局未知不知道第一个Bootloader占多少字节不知道kernel到底在哪个偏移更不知道后面跟着多少个孩子分区。文件系统类型未知可能是SquashFS、Cramfs、JFFS2、UBIFS也可能是没压缩的ext2/4甚至厂商私有格式用普通的Linux挂载命令根本识别不了。头部结构和校验未知固件开头那些字节是不是魔数末尾的CRC怎么算的是MD5还是SHA256整个固件是否被加密过完全靠猜。修改后无法回写就算解包出了文件系统改完一个配置文件后没有合适工具重新压缩、填充、修正头部和校验改完就刷回去照样启动失败。我遇到的这份固件属于典型的三无产品。用binwalk扫了一下前面能看到一段LZMA压缩数据再往后就是大片的“No entropy”的空白区想拆又拆不干净拆出来的碎片也没法组合回去。更头疼的是这个固件带了厂商自定义的头部解包后无法重打包。所以那段时间我基本上是在对着十六进制流瞎猜这里可能是偏移那里可能是长度。猜来猜去也没猜出完整的表结构最后索性决定不再依赖现成工具自己动手写一个专门针对这类固件的处理工具把“解包、修改、打包、校验”这条链路完整打通。如果你也遇到类似的固件我建议你先不要急着刷机。第一步永远是备份原始镜像并且记录它的大小、SHA256值、hexdump开头若干字节。后面所有分析都以这个备份为基准否则改坏了连后悔药都没得吃。2. 自研解包工具的设计决策为什么不用现成的binwalk先说结论现成工具很好但它们解决不了“解包之后还能原样打包回去”的问题。网上最常用的固件分析工具是binwalk严格来说它是一个强大的“启发式扫描器”能帮你发现固件里有哪些已知的签名、压缩流、文件系统。可是遇到厂商自定义了头部格式、把分区表藏在尾部或者做了一层简单加密时binwalk就只会在日志里打出一堆“possible”开头的猜测然后给你留下海量的碎片文件。firmware-mod-kit算是一代经典很多嵌入式开发者都靠它解SquashFS、重打包固件。但它的核心脚本库已经多年没更新对新版SquashFS、新版内核和大量私有固件支持得很差。一旦遇到大于2GB的镜像内存占用直接失控我在公司老旧的16G内存本上跑过一次它吃了14G虚拟内存然后卡死最后还是强行杀掉进程才结束。我决定自研工具核心原因有三个可逆性我不只想“拆开看”我还要“改完装回去”。现成工具解包后会把各个分区导出来但是要用它重新聚合回原始布局、补齐填充字节、重算校验值基本等于重写一遍所以不如一开始就自己控制所有细节。私有格式适配这份固件有一套自己的硬件版本标识、起始偏移、分区长度表可能还带厂商签名结构。此类格式只有自己的工具能做得干净利落。交接可复现同项目里后面还会有同事接手我不希望他们再用一团乱麻的Python脚本加一堆人工步骤去处理。我想做一个命令统一、输出清晰的命令行工具往后来一句fwtool unpack xxx.bin就能把分区解出来再来一句fwtool repack就能打回去。工具选型上我最终选了Python。理由很实在Python的标准库里有struct、binascii能方便地解析二进制结构有argparse直接做CLI而且pycryptodome、zstandard、lzma等模块覆盖了绝大多数加密压缩需求。性能上处理几GB镜像确实吃紧但对分区边界识别和解包这种IO密集型操作只要用mmap读文件、按偏移切分速度完全可以接受。整个工具我起了个朴素的名字fwtool。它分三层CLI入口层负责接收unpack、pack、verify、info这些子命令统一读取配置文件。核心解析层包含分区表解析器、文件系统识别器、头部编解码器、压缩解压器这一层跟具体设备模型解耦。文件操作层实现安全的偏移读写、对齐填充、CRC计算、差分备份避免直接修改原始文件。有了这款工具我对这份固件的“盲区”开始慢慢收缩。它不会像binwalk那样给你一大堆不可控的碎片而是输出一个清晰的目录结构每个分区一个文件附上一份partitions.json记录所有元数据。后面同事再拿到固件只需要一条命令就能看清全局。3. 核心实现分区识别、文件提取、再打包的三步闭环如果你要自己写类似的工具最重要的不是怎么解压而是怎么稳定地识别分区边界。这一步搞定了后面就顺理成章。3.1 分区识别从魔数到偏移表一份固件常见魔数有这些Bootloader头部UBOOT、0x905016等内核镜像ARM常见的zxImage、uImage0x27051956文件系统SquashFS的hsqs非压缩超块、Cramfs的0x28cd3d45、JFFS2的0x1985、UBIFS的UBI#等最笨但可靠的方法就是扫描整个镜像把所有魔数位置全部找出来再结合文件大小推测哪个是分区起始。比如SquashFS的超块前四个字节是hsqs紧接着的字段里就写着压缩类型、块大小和总大小。通过这个总大小就能得出文件系统分区的结束偏移。fwtool里识别分区的核心逻辑大致像这样import struct, mmap def find_partitions(data: bytes): hits [] # 搜索常见魔数 magic_squash bhsqs magic_uboot b\x27\x05\x19\x56 # uImage magic_ubi bUBI# for m, name in [(magic_squash, squashfs), (magic_uboot, uimage), (magic_ubi, ubi)]: start 0 while True: pos data.find(m, start) if pos -1: break hits.append((pos, name)) start pos len(m) hits.sort() return hits实际用的时候还有大量细节要处理有些魔数会被填充字节覆盖的风险、有些固件会故意在头部后面塞一段随机数据来伪装所以要结合熵值分析和文件系统自带的长度字段来做二次确认。binwalk那套熵值图我看着不够直观就自己写了一个简单的熵检测——把文件按4KB一个块计算香农熵凡是熵值从低变高的位置多半就是压缩数据或加密数据的起点。3.2 文件提取从文件系统里抽出“真身”识别出分区后剩下的就是大量“搬运工”的工作。支持的文件系统类型我优先实现了SquashFS和JFFS2因为这两者在路由器、机顶盒固件里出现频率实在太高。SquashFS提取有个关键点它有很多版本。4.0之前是老式布局4.0之后引入了xattr和新的元数据压缩方式。老工具unsquashfs对新版镜像经常报错所以我自己实现时直接调用底层库来解压数据块但保留了原始压缩元数据。修改文件后再打包必须保持旧版SquashFS的版本字段否则Bootloader认不出来。JFFS2更麻烦。它没有传统的文件表而是通过一段扫描得到节点链表。我在工具里实现了按inode节点遍历并处理了删除节点标记如果你是第一次接触建议先把它当作一个只读解析器。3.3 再打包让修改过的固件还能通过原厂验证“打包”这一步是真正拉开差距的地方。很多开源工具只能拆不能装或者装上后校验失败。原因很简单原始固件在分区表头部存有长度、偏移、CRC你改了文件系统大小就必须同步修正这些字段并且还要保证每个分区之间的填充对齐。我采取的做法是解包时把原始固件头部和每一块“填充区域”的原始字节全部记录下来保存为template.bin。打包时新文件系统生成后按原样拼接头部 新的第一个分区 原始填充 新的第二个分区……所有长度字段重新计算最后对整个文件或指定区域重算CRC/哈希值。为了保证Bootloader不挑剔对齐我对每个分区的起始偏移强制对齐到0x1000字节不够就用0xff或0x00补足。下面是打包时修正头部长度字段的简化示例def repack_partitions(header, partitions: list): new_size 0 base 0 for part in partitions: part.offset base part.size len(part.data) base part.size # 对齐 align 0x1000 - (base % 0x1000) if align ! 0x1000: base align header.total_size base header.update_crc() return header.serialize() b.join(p.data for p in partitions)3.4 配置修改自动化拆包后能干的最实用的事拆包后大部分人最想做的一件事是改默认配置。比如把路由器固件里的/etc/config/dropbear改一下或者把机顶盒开机Logo替换成自己的。fwtool里我加了一个batch-edit子命令你只需要提供一份修改清单工具就会解包→替换文件→重新打包→输出新的固件镜像全程不需要手工干预。每个被修改的文件都会记录原始哈希方便回滚。这样给同事使用的时候他们不需要懂SquashFS的细节只要说一句“帮我把logo换成新图”工具就能自己干完。到这里解包工具的主链路算是跑通了。但真正让我头疼的是另一件事固件校验和加密。4. 签名校验与固件加密合法调试场景下的处理策略很多量产设备并不是简单地把固件写进Flash就能启动。Bootloader在做系统引导之前会先校验几个东西分区哈希、完整固件签名、甚至硬件唯一ID与固件绑定。前几年听过不少做第三方ROM的开发者费劲解包改好图片刷回去设备亮红灯不开机就是栽在签名校验上。我想先强调一个前提我这里说的“处理”都是在合法授权范围内进行的。比如你手上有一台自己购买的设备、一块自己开发板的固件或者你是公司内部负责研发/售后需要分析设备的工程师。任何绕过许可证、盗取商业镜像、篡改他人授权固件谋取利益的行为都不在本文支持范围内。在合法调试场景中处理加密和签名校验的思路有两条正面破解永远不是第一选择而是去找设备留给开发者的合法通道开开发者模式/安全启动关闭很多SoC瑞芯微、海思、全志都提供了熔丝位或调试引脚可以通过特定状态关闭校验或者在进入Uboot后打断自动启动进入命令行手动加载内存内核。这种情况下你不需要破解任何签名只要按厂商的引导流程刷入自编译固件即可。从设备运行态获取解密后数据如果固件本身被加密但设备能够正常启动进入系统那么系统运行时内存里一定有解密后的完整分区内容。你可以通过串口、ADB、SSH进入shell再用dd把Flash分区读出来。例如在部分海思平台上直接cat /dev/mtd0就能导出分区镜像。这个方法只涉及分析和备份自己设备的数据完全合规。如果确实需要分析固件是否带签名可以用strings、openssl做初步探查。比如在固件尾部看到一段PEM格式的证书公钥那说明多半有签名验证或者在Bootloader里看到rsa_verify之类的字符串也能确认。下面是一个快速检查固件中是否包含证书特征的命令strings upgrade.bin | grep -i BEGIN PUBLIC KEY strings upgrade.bin | grep -i rsa遇到加密固件最常见的加密方式是AES-CTR、AES-CBC、LZMA 私有头。有些厂商把真正的解密密钥藏在Bootloader的未加密区域甚至在NAND的OTP区想从固件包本身提取几乎做不到。所以我一般建议评估一下“是否有必要解”如果不能合法获取密钥就换一条路直接从运行系统里备份。这个方法我在瑞芯微RK3368设备上验证过先开ADB然后adb pull /dev/block/mmcblk0把整块eMMC镜像拉下来再进行离线分析和修改完全没有触碰任何加密环节既干净又安全。5. 实测复盘从刷机回读到校验一致的完整验证工具写完了不能只在开发机上自嗨。我找了一台老家翻出来的海思Hi3798MV100机顶盒来做实测。这个盒子当时系统卡成狗但还能进设置我正好拿它当小白鼠顺便让它重获新生。我先做的动作是备份通过ADB进入shell读取分区表。adb shell cat /proc/mtd输出大致是dev: size erasesize name mtd0: 00400000 00020000 fastboot mtd1: 00100000 00020000 bootargs mtd2: 00500000 00020000 recovery mtd3: 05000000 00020000 system ...知道了分区名称和大小之后我把每个分区都dd出来保存到电脑上adb shell dd if/dev/mtd0 bs1M count4 fastboot.bin adb shell dd if/dev/mtd1 bs1M count1 bootargs.bin adb shell dd if/dev/mtd2 bs1M count5 recovery.bin adb shell dd if/dev/mtd3 bs1M count80 system.bin这一套原始镜像备份就是整个验证过程的基准。接着用fwtool info查看system.bin它识别出里面是一个SquashFS JFFS2 over lay的混合文件系统。我解包了SquashFS分区把开机动画替换成一张自己做的纯黑图片又改了一个默认配置然后重新打包。整个操作在工具里就是三行命令fwtool unpack system.bin -o system_root # 修改 system_root/logo.png 和 /etc/motd fwtool pack system_root -O system_new.bin fwtool verify system.bin system_new.binverify子命令会对比原始镜像和解包重打包镜像的差异。正常逻辑下如果我没做任何修改重新打包后的镜像应该和原始镜像逐字节一致做了修改verify会给出一个差异报告列出所有新增、删除、修改过的文件。这一步非常重要它验证了我前面写的所有长度修正逻辑是否正确。刷机时我先刷的是recovery分区system分区而不是直接刷整个固件。用小范围修改降低风险万一失败还能从已有的备份刷回来。刷完重启等待的几十秒钟我其实手心冒汗因为如果头部大小或校验算错了屏幕会永远停在Logo。结果它顺利进入了新的系统界面我可以看到开机图片变了配置也生效了。随后我又做了一次反向验证把设备里运行的分区重新dd出来和本地system_new.bin比对内容完全一致。不过我也翻过车。第一次打包时由于没有按照原始文件系统的块大小对齐导致Uboot在挂载根文件系统时报magic mismatch设备开不了机。那次教训让我在工具里加了一个自动对齐检测只要发现新生成的文件系统大小没有落在合法块边界上直接拒绝输出。这种安全阀看着不起眼但关键时刻能避免把一堆废砖发给测试组。6. 这半年里踩过的坑和工具最终沉淀下来的能力工具从第一版到稳定差不多磨了半年。期间踩过的坑我总结成了一张“翻车清单”每条都是花钱买来的教训6.1 对齐陷阱尺寸不对一切白费嵌入式系统的Bootloader非常挑剔对齐。有些要求2KB对齐有些要求64KB对齐。你单独看一个文件系统镜像大小本身是对的但把它放到分区里的起始偏移不对内核mtd_read的时候就报错。所以工具里除了自动对齐我还做了一个“偏移抖动检测”把新镜像和原始镜像的每个分区起始偏移都打印出来肉眼就能看出是否有变化。6.2 大小端和位域固件头部的隐藏暗坑很多固件头部是C结构体里面存着版本号、分区个数、总大小其中有些字段是bit-field不是标准整数。用Python解析时如果直接按4字节整型解包很可能拿到一个被组合过的值。例如某个字段低4位表示压缩算法高12位表示版本如果不拆位数字就会完全对不上。我在工具里加了统一的位域解析函数并且用原始固件实测数次才把这一层搞定。6.3 文件系统压缩参数直接解压再压缩镜像会变大SquashFS默认块大小是128KB如果你在解包后重新打包时没有指定同样的块大小而是用默认值整个镜像体积可能膨胀三四倍。如果分区大小固定Bootloader要么拒绝写入要么覆盖后面分区。所以重新打包时压缩参数必须严格从原文件系统超块里读出来包括块大小、压缩算法、是否使用碎片块、是否使用尾部空间。这一点没有现成工具能帮你承担只有自己的解析器最清楚。6.4 分区表版本变更同一份固件不同批次也未必一致我遇到过一个真实案例同一型号设备不同批次的固件其中一个分区多出来4KB的签名尾巴。用同一套工具解析老批次校验通过新批次直接报错。后来我把分区识别改成了按魔数在“小区域窗口内搜索”而不是全文件扫描才解决了这个问题。同时也养成了一个习惯拿到固件首选看版本号字符串不要盲目套用经验。最终沉淀下来的fwtool目前支持这些能力分区表自动识别支持常见魔数和自定义魔数配置解包/打包SquashFS、JFFS2、Cramfs、ext2/4、UBIFS镜像自动对齐检查、CRC/哈希校验、差异报告配置文件批量替换、镜像体积膨胀预警与设备端ADB/串口协同支持直接分区回读和刷回这些功能里我最满意的倒不是解包算法而是“差异报告”机制。它让所有改动能被审计也能自动生成一个补丁说明告诉同事你这次改了哪些文件、动了哪个分区、原始哈希是什么。后来部门里再有人拿到没有文档的固件都会先丢进fwtool跑一圈至少能快速说出这个固件由几个分区构成而不是望着一堆二进制发呆。说到底接手一份没人讲得清的固件其实不是最可怕的。最可怕的是拿到固件就直接开刷、凭感觉改配置、改坏了再换一块砖。有了工具和完整验证流程之后再“脏”的固件也能变成一份清晰可读的图纸。如果你也在做类似的事我的建议很简单先备份、再解包、小步验证、记录哈希做到这四步你就能在固件这片黑森林里走出自己的路。