前阵子接手了一台老安卓盒子,主控是海思 hi3798mv100,需求是定制开机动画、预装两个应用、改默认桌面。这种活儿放到有完整文档的厂商环境里半小时就能跑完,可现实是我手里只有一份update.zip,压缩包里嵌套着好几个没名字的 bin 文件,没有分区说明、没有打包脚本、没有平台手册,同事故交代得很直白:“这个应该能解包,你自己想办法吧。”
说实话,拿到那份固件的前两天我几乎零进展。file识别成 data,binwalk扫出来一堆琐碎特征,strings里能看到一些路径但完全拼不出结构。网上确实能搜到不少同名机型固件包,也有付费代改的服务,但没几个人愿意把这类“黑盒固件”的内部结构讲透。后来我花了两周时间,把这些 bin 一层层拆开,又把反复踩坑沉淀的过程固化成一个小工具 fwparser。这篇博客就把完整分析流程、工具的设计思路和实操中遇到的坑一次写全,给正准备接手类似固件的同行做个参考。
1. 接手之后先别急着写工具:固件到底是怎么组成的
1.1 为什么一份固件会“没人讲得清”
这种情况在行业里太常见了。嵌入式产品的固件通常由方案商写好,整机厂只做外壳、排线和包装;方案商交付给产线的是最终量产镜像,不会把完整文档和源码一起移交。等产品过了两三年停产、方案商那边换了对接人,打包环境和源代码基本就彻底失联了。设备要返修、定制、升级,你手里只剩一份不完整镜像,这是最典型的“没人讲得清”。
更麻烦的是,很多固件在出产前会被反复拼接。设备里实际跑的不是一个 bin,而是 bootloader、kernel、recovery、system、userdata 等一串镜像按固定偏移拼成的大容器。有些厂商还会在容器外再套一层自解压或简单加密。没文档、没脚本时,唯一的出路是把结构特征一项项从二进制里挖出来。
1.2 固件的基本解剖结构
固件像一栋楼:bootloader 是承重墙,负责上电初始化、初始化 DDR、加载内核;kernel 是水电管线,提供驱动、网络栈和 flash 读写;rootfs 是室内装修,存放应用、链接库、配置脚本;另外还有几个“杂物间”分区,放 MAC 地址、型号参数、环境变量、开机 logo。
具体到常见平台,结构大致是 uboot(或 Rockchip 的 loader)→ boot 分区 → recovery 分区 → system 分区 → 私有配置分区。Rockchip 平台喜欢把所有分区镜像统一塞进一个update.img,头部写有每个分区的偏移和大小;海思平台常见update.zip里放多个 img 镜像,由升级脚本按块刷写;Amlogic 平台又往往用一个自定义头部把多个分区串在单个镜像里。这些平台之间没有统一标准,通用工具很难面面俱到,这也是为什么我坚持自建一套分析流程。
1.3 准备工作清单
工欲善其事,必先备好料。硬件方面,USB 转 TTL 串口线强烈建议准备,很多信息靠串口日志才看得出来。如果设备有烧录模式,瑞芯微 Maskrom 模式、海思 HiTool、Amlogic USB Burning Tool 都得先确认怎么进。软件方面,Linux 环境最顺手,主力命令基本是这些:
file unknown.bin hexdump -C unknown.bin | head -30 binwalk unknown.bin strings -n 8 unknown.bin | grep -iE "kernel|android|hisilicon|rockchip|amlogic|squash|ext4|jffs2" | head -40这几条命令能快速判断平台、文件系统类型和大致分区位置。大多数时候 30 分钟内就能确定方向。
2. 工具设计思路:把“猜”变成“算”
2.1 为什么不用现成 binwalk 硬扫
binwalk 是逆向固件的入口工具,但面对这种没人讲得清的固件,短板特别明显。第一,关键特征如果被自定义头或加密壳挡住,binwalk 只能扫出一堆碎片特征,给不出完整架构;第二,它不关心偏移对齐和分区大小约束,容易把正常文件尾部误判成分区边界;第三,对“无魔数”的私有容器基本无能为力。我要的不是“找出几个可疑碎片”,而是把固件完整还原成可修改、可重新回刷的形态。
所以工具目标在设计之初就很明确:输入一个 bin,输出分区清单、可解包的文件系统、可重新打包的镜像。自动识别常见平台结构,如果平台未知,允许手工标定,并把标定结果保存成可复用模板。
2.2 fwparser 的三层设计
第一层是指纹识别。维护一张特征表,记录常见 bootloader、SquashFS、JFFS2、ext 系列、gzip、cpio、Android boot header、Rockchip 分割头、Amlogic header 的魔数与偏移特征。扫描时不仅比对魔数,还要校验魔数两侧结构的合理性,减少误报。
第二层是结构标定。根据指纹结果列出候选分区边界,支持人工修正偏移。把“system 起始地址按 4KB 对齐”“镜像尾部补零到下一个块边界”这类约束记录成 profile。这样同平台新一代固件来了,直接套 profile 就能秒级复用。
第三层是重组打包。按 profile 重新生成整个镜像,自动对齐、填充、修正校验值。没有这一层,分析再透彻也只敢看不敢改。
2.3 分析模板的作用
模板文件就是一个 YAML 或 JSON,记录各分区偏移、大小、文件系统类型和校验位置。大致长这样:
name: "hisilicon_hi3798mv100" loader: { offset: 0x0, size: 0x100000 } boot: { offset: 0x100000, size: 0x2000000 } recovery: { offset: 0x2100000, size: 0x2000000 } system: { fs_type: "squashfs", offset: 0x4100000, size: 0x30000000 }遇到同平台新版本固件,先把模板套上去,校验各分区偏移是否吻合,全部对上就能直接进修改环节。比每次从 hexdump 重新猜偏移快太多。
3. 核心实现细节:识别、提取、加密与重组
3.1 指纹识别与偏移扫描
魔数只是起点。U-Boot 镜像头部有 “U-Boot” 字符串,Android bootimg 头部是 8 字节 “ANDROID!”,SquashFS 是 “hsqs”,ext 系列是0x53 0xEF,gzip 是0x1F 0x8B,cpio 是0x070701或0x070702。扫到魔数之后还必须做三件事:看偏移是否落在合理区间;看头部记录的长度与实际文件长度是否一致;看起始偏移能不能被对齐单位整除。约束越多,误报越少。
有个常用技巧:拿同一型号两个不同版本 bin 做对比。cmp -l看差异字节分布,差异区域的边界往往就是分区边界或校验字段。这是没有文档时最可靠的定位手段之一。
3.2 文件系统提取
电视盒子、路由器最常遇到的 rootfs 是 SquashFS 和 ext4。SquashFS 用unsquashfs -d rootfs system.squashfs解包,但要先确认是不是被塞进了自定义头。遇到binwalk定位不准时,直接dd按推算偏移把区段切出来,再file一次看类型。ext4 分区可以先用debugfs -R "ls -l /" system.img查看,确认没问题再 loop 挂载或用debugfs -R "dump"导出。
JFFS2 是 NAND 设备常见格式,jefferson工具能解,但经常遇到交叉编译版本差异导致崩溃。这时可以在 Linux 上模拟挂载mount -t jffs2,利用内核自带支持读取;遇到自研块格式就只能strings找路径后手动按 inode 结构分析了。
3.3 加密与安全启动
带加密的固件处理顺序值得分享一下。先判断是不是“伪加密”,很多设备只做了简单异或或整体 gzip,头部和尾部还留着明文结构,用hexdump和strings就能发现线索。再把 uboot 的 env 分区 dd 出来,看看有没有解密密钥或开关项,有些平台会把关键参数直接放在 env 的bootargs里。如果确认启用了芯片级 secure boot,修改后直接刷回去必然失败,这种场景一般不去碰 bootloader,优先改 rootfs,同时关闭校验选项,仅针对自己拥有或授权的设备做功能定制。
最实用的一招是观察串口打印。启动日志会直接暴露问题方向:kernel panic、crc mismatch、signed image verify fail 分别指向不同原因。日志信息往往比静态猜测高效得多。
3.4 重新打包:对齐、校验和与分区表
改完 rootfs 必须重新打包,这一步翻车率最高。常见校验有三类:长度对齐、头部 size 字段、CRC32。一个常用于处理头部 size 和尾部 CRC 的 Python 片段:
import binascii def pad_to(data, align): pad_len = (align - (len(data) % align)) % align return data + b'\x00' * pad_len def fix_size_at(data, offset, length_bytes=4, endian='little'): size = len(data) b = size.to_bytes(length_bytes, endian) data = bytearray(data) data[offset:offset + length_bytes] = b return bytes(data) def fix_crc32_tail(data, tail_offset=-4): crc = binascii.crc32(data[:tail_offset]) & 0xffffffff return data[:tail_offset] + crc.to_bytes(4, 'little')用之前一定要确认头部 size 字段的位置和 CRC 的计算范围。改完后先在测试机上验证,不要直接刷生产机。
4. 完整实操:一台老盒子从解包到改完刷回去
4.1 第一步:确认平台并导出固件
手里的update.zip解压后有boot.img、system.img、recovery.img和几个不带名字的 bin。先执行基础诊断:
file boot.img strings -n 10 boot.img | head -30 strings -n 10 system.img | grep -iE "squash|ext4|android|version" | head -20看到ANDROID!确认 boot 分区是 Android bootimage;看到hsqs确认 system 分区是 SquashFS;strings里出现的 “hi3798mv100” 直接锁定了平台。整个方向瞬间清晰。
4.2 第二步:分区识别与 system 分区解包
boot.img直接用abootimg或unpack_bootimg解开,得到 kernel 和 ramdisk。system.img先跑一遍binwalk找 SquashFS 偏移:
binwalk system.img dd if=system.img of=system.squashfs bs=512 skip=<偏移块数> unsquashfs -d rootfs system.squashfs解出目录后发现里面有preinstall、etc、framework等标准 Android 目录,说明整个分析思路走通了。顺便把recovery.img也解开,竟然在脚本里发现上一位维护者留的调试打印,侧面印证这块固件确实被人改过但没留任何记录。
4.3 第三步:修改内容并重新打包
在rootfs里替换开机动画、放好预装 APK,改掉build.prop的默认桌面配置,然后用 mksquashfs 重新打包:
mksquashfs rootfs system_new.squashfs -comp xz -b 131072注意原 SquashFS 的 block size 和压缩算法尽量保持一致,否则 bootloader 挂载参数会对不上。再按 profile 里记录的 size 字段位置,把新镜像长度写回头部,重算 CRC。最后按分区顺序拼回完整 bin。
4.4 第四步:刷写与验证
先把原始固件完整备份两遍,再烧录到测试芯片上,接串口看 log。第一次启动卡在 CRC mismatch,查下来是只改了头部 size 却忘了尾部补零到分区固定长度,导致 CRC 范围包含了旧数据。修正后再次启动,顺利进桌面。整个过程耗了一个晚上,如果一开始就有成熟模板,半小时内就能完成同平台改包。
5. 常见问题与排查技巧实录
5.1 刷机卡 logo 或校验失败怎么办
卡在 logo 说明 bootloader 已开始加载 kernel 或 system,但后续校验没过。优先看串口输出:crc mismatch就是校验没更新,sha mismatch是安全校验链断了,mount failed多半是 SquashFS 打包参数不一致。建议先在测试机上反复验证再上批量设备。
5.2 文件系统解不开、魔数对不上
binwalk扫不到特征时,用hexdump -C看分区开头几十字节,很多自定义头部会在固定位置保留明文标识。把前面的头剥掉,按推算偏移切出真正文件系统区段再file一次。极端情况就用strings在整分区里搜路径关键字,反向定位 inode 表位置。
5.3 修改后分区不够装
原厂 SquashFS 压缩率很高,稍微堆点新内容就容易超出分区固定大小。处理思路:清理 rootfs 里无用资源,比如预装商店、第三方输入法、多余字体;检查是否有重复库;再不行就调小 block size 提升压缩率。如果分区大小定义在头部且支持调整,可以连带调整后续分区偏移,但整体重算风险很高,新手不建议碰。
5.4 设备连烧录工具都连不上
先确认驱动和工具版本,瑞芯微平台要短接 maskrom 引脚或进入 loader 模式,海思平台用 HiTool 烧写模式,Amlogic 平台按住 reset 再插 USB。如果设备进的是正常系统,烧录工具必然识别不到,要从启动阶段入手。串口完全没有输出时,检查接线和波特率,很多盒子默认 115200,但部分 SoC 是 1500000,参数选错会满屏乱码。
5.5 一套顺手的工作流与避坑速查
现在处理新固件的顺序已经固定:先备份原版、跑 file 和 strings、再 binwalk 粗扫、按指纹定位分区、提取文件系统、修改、重打包、测试机验证。中间最容易踩的坑整理成一张速查表:
| 现象 | 常见原因 | 排查手段 |
|---|---|---|
| 刷后卡 logo | 校验值未更新或分区偏移错 | 串口看 crc/sha mismatch |
| 挂载失败 | SquashFS 打包参数不一致 | 保持原 block size 和压缩算法 |
| binwalk 无结果 | 自定义头或加密壳 | hexdump 头部找可见标识 |
| 分区不够装 | 原压缩率高于修改后 | 清理无用资源,调 block size |
| 烧录连不上 | 未进入对应烧录模式 | 短接或按键进入 loader 模式 |
| 串口乱码 | 波特率或接线错误 | 尝试 115200 / 1500000 |
最后说一点实际操作中的体会:不管工具做得再顺手,拿到任何新旧固件第一步永远是完整备份原镜像。很多人跳过这步直接改包,翻车后连原厂包都找不回来,只能对着砖头发呆。我的习惯是原版 bin 和拆出的每个分区各留两份、存到不同目录,这习惯已经帮我省了不知道多少时间。