
1. 接手即地狱这份固件为什么没人讲得清去年我接过一个挺尴尬的活儿某台设备在现场跑着出了问题场测同事把固件包丢给我丢下一句你看一下里面是什么能不能救。结果这个所谓固件包总共只有三个文件一个没有后缀的二进制一个名字叫update.zip的压缩包还有一份字段对不上、连表头都写错的 CSV 设备表。原项目组早就解散了能找到的文档只有一条半年前的群聊记录截图里还写着final 版本别乱动。接手这种没人讲得清的固件第一反应肯定是骂人但骂完还得干活。我后来发现固件分析这种工作本质上和考古非常像你拿到的不是完整的答案而是一堆残片需要通过类型识别、结构扫描、内容提取、交叉验证逐步把系统原来的样子还原出来。也正是在这个过程中我写了一个专门用来做固件考古的小工具把之前靠肉眼和一堆零散命令完成的工作固化成了流水线。先说为什么会走到没人讲得清这一步。大部分嵌入式或网络设备项目固件从来都不是一个人写的而是 bootloader 一个人、内核一个人、文件系统一个人、打包另一个人等设备量产、方案迭代、人员流动之后真正知道这个 bin 是怎么拼起来的的人就没了。交接给下一任的时候往往只剩一个编译脚本、一个 release 目录和几个半残的文档。这时候你能依靠的不是记忆而是固件本身留下的痕迹。1.1 没有文档、没有标签、没有售后我手上的这个二进制文件叫firmware_v2.3_final.bin大小 32 MB看起来像是从 SPI Flash 里整片读出来的镜像。旁边的update.zip是增量升级包里面大概只有 script 和几个 patch 文件CSV 表格原本应该是设备型号-主板版本-固件版本的对照表但里面型号重复、版本号格式混乱甚至有一行填的日期比文件修改时间还晚两年。典型的遗留项目烂摊子就是这样版本号不可信final不一定 final文件名不可信可能只是某个人随手保存的名字元数据不可信因为没人维护。唯一可信的是文件本身的内容。这种场景下常规打开看看的思路是走不通的。你不能双击运行不能像看普通软件一样看安装包不能假设它一定是某种已知格式。固件可能有自定义头部、有加密段、有分区表偏移、有大端小端混合、有未擦除干净的旧数据、还有厂商偷偷塞进去的调试入口。能救你的只有一套可复现的分析方法也就是取证式的处理流程。1.2 固件分析不是玄学是取证很多人一听说分析固件就觉得高深但其实它的底层逻辑很简单一切都要有证据。固件里每一个字节都有其存在的理由你要做的是找到这一段是什么这一段从哪里开始到哪里结束它的内容为什么是这样。这不像是写业务代码那种顶层设计工作更像是法医做现场还原。你首先得知道手里这个东西属于哪一类设备然后找它的 bootloader 是谁再找内核镜像在哪个偏移最后把文件系统切出来看里面跑着什么程序、存着什么配置、留了什么坑。我决定做一个工具并不是因为现有的 binwalk 不好用而是因为标准工具给的是线索我要的是结论。固件现场太乱靠一条命令一次扫描永远只能看局部而一个合格的考古工具应该能自动把指纹识别、分区定位、文件系统提取、内容解析、报告输出串起来把每次分析的依据都记录下来让下一个人看到报告就能明白全貌。2. 正式动手前的固件建档先弄清楚手里是什么任何分析工具都是建立在你对目标的理解之上的。在做工具之前我先花了一个下午用最原始的方式给这个固件建档确认文件类型、确认结构、确认分区边界。这个步骤很枯燥但绝对不能省因为你一旦在错误的前提上写自动化逻辑后面全是在给自己埋雷。2.1 先从 file 和 hexdump 开始第一步永远是file它会告诉你这个文件在操作系统的常识库里是什么。但这个firmware_v2.3_final.bin当时输出的是data意思就是文件头没有匹配到任何已知格式。不意外嵌入式固件经常这样很多厂家会去掉头部魔数或者直接在裸镜像上套一个自己的私有头。接着用hexdump看文件最开头的字节hexdump -C firmware_v2.3_final.bin | head -20如果开头全是ff或者00有两种可能要么这段区域是 flash 的空白擦除区要么是引导 ROM 在启动后跳过的保留区域。我拿到的文件开头 16 个字节是ff ff ff ff ff ff ff ff ...一直延续到 0x100 附近才出现其他内容。这说明文件开头有一段未使用区域真正的引导代码从后面的某个偏移才开始比如 0x100 或者 0x200 这种对齐边界。当时我直接跳过开头从 0x100 开始找规律才发现一个 U-Boot 的踪迹。这里有一个我踩过很多次的坑千万不要用xxd看一两个扇区就下结论。固件不像磁盘镜像不一定按 512 字节扇区对齐很多厂商会在 flash 前面留 256 字节、1 KB、甚至 64 KB 的启动头。你需要看的是更大范围的布局而不是盯着开头几个字节纠结。2.2 binwalk 扫描但别信它的默认结论binwalk 是固件分析的标配工具但很多人用它的方式有问题直接跑binwalk firmware_v2.3_final.bin看到一堆结果就当结论抄下来。这样一定会被骗。正确姿势是先做签名扫描binwalk --signature firmware_v2.3_final.binbinwalk 的原理是拿文件内容跟内置签名库做匹配所以它对看起来很像是压缩数据的随机内容也会报警。比如字库、图片资源、加密后的密文段都可能伪装成gzip或zlib数据。我当时扫描出来 30 多条记录里面有 mkimage、gzip、squashfs、jffs2 的痕迹但其中至少一半是误报。判断方法是交叉验证把疑似段用dd切出来再单独跑file和strings看内容是否能对得上。签名说的是这段的头部像某个格式不代表这段真的是那个格式。如果你直接binwalk -e自动提取很可能会解出一个全是乱码的假分区白白浪费时间。2.3 分区与文件系统从反推布局开始如果 binwalk 扫出了 U-Boot、内核、文件系统这几个关键点那布局基本已经清晰了。常见的 SPI Flash 固件布局一般是这样的区域典型偏移内容Bootloader0x00000000U-Boot 或厂商私有 bootEnvironment0x00040000flash 环境变量、启动参数内核区0x00080000zImage / Image / FIT imageDTB 区0x00100000设备树文件根文件系统0x00200000SquashFS / JFFS2 / CramFS用户数据区0x01000000配置、日志、应用数据当然这只是常见情况具体偏移以固件实际结构为准。为了判断文件系统类型可以查魔数文件系统魔数字节序相关说明SquashFShsqs最为常见只读压缩CramFS0x28CD3D45老设备常见JFFS20x1985 0x2003需要擦除块信息UBIFS0x06101831NAND 设备上常见EXT2/3/40x53EF很少直接出现在固件里拿到这些信息后我已经能画出这个固件最粗略的骨架。接下来才进入工具开发阶段。3. 工具的主干一条固件考古流水线按传统做法分析完这个文件我可能就写一篇这次是怎么解的备忘录然后把文件归档等下次遇到类似问题再重新手工走一遍。但当时的项目状况让我意识到如果我不把方法沉淀成一个工具下次再有人拿到这种固件依然会从hexdump开始抠半天。所以我做了一个专门针对脏固件无文档固件的分析工具名字就叫fw-praetorian也就是固件考古学的意思。它不是一个完全自动化的一键破解工具而是一条把人工分析流程固化的流水线保证每一步都有记录、可复现、可交接。3.1 为什么不用现成工具而要自己组装市面上确实有很多优秀工具binwalk、firmware-mod-kit、dd、srecord、squashfs-tools、jefferson 等等。但它们的问题是每一个都只解决一个环节。binwalk 负责扫描squashfs-tools 负责解包srecord 负责处理 S19 记录但没有一个工具能告诉整个固件是什么结构分区边界在哪里哪个文件可疑哪个版本信息对不上。我需要的东西更像一条加工流水线输入任意 bin/img/zip/S19 文件甚至是一整个目录输出结构化的分析报告 自动提取出来的分区镜像 可复现的日志而且现成工具对异常情况的容忍度很低。你给它一个厂商加了 16 字节私有头的 SquashFS它可能直接报错退出。我自己的工具可以按照规则先剥离头部、再判断字节序、再做签名修正把人眼能看出来但工具不认的脏数据修掉。3.2 流水线输入输出与处理步骤工具的输入不局限于二进制镜像。我接到过的固件载体至少有三类裸镜像.bin、.img直接从 flash 读取的完整或部分镜像升级包update.zip、.tar.gz带脚本和差异补丁烧录记录Motorola S-record 即.s19文件文本格式的地址和数据记录所以工具的入口层先做了自动识别根据文件头、扩展名、内容特征判断到底是哪种载体。如果检测到是update.zip先解压解析updater-script看它究竟在升级时做了什么如果检测到是 S19先解析记录类型、地址字段、校验和把数据段重组成二进制镜像再进入后续分析流程。处理阶段分五步指纹识别提取文件头、文件尾、大小、熵值、可疑字符串形成初始档案。分区扫描用签名加滑动窗口熵值找出所有可疑边界。按边界切割把每个候选分区dd出来保存为独立文件记录偏移和大小。内容解析对每个候选分区做文件系统识别、内核识别、版本字符串提取。报告生成输出包含证据链的 Markdown/JSON 报告并把所有提取产物统一归档到一个目录。每一步的分析结果都写入一个 JSON 日志节点。日志是自解释的它记录我在偏移 0x80000 发现了一个 zImage判断依据是file命令识别为 Linux kernel ARM boot executable zImage并且以 ML 头x55 魔数开头。为什么强调日志因为你做分析时觉得理所当然的判断半年后的自己不一定能想起来更别说接手的人。3.3 技术选型Python 为主Shell 为辅工具主体我用 Python 3 写原因是 binwalk 本身就是一个 Python 库我可以直接调用它的签名扫描函数而不是在子进程里解析命令行输出。S19 解析用了 pyserial 的辅助逻辑配合自写的记录解析器。外部命令用subprocess调用因为 unsquashfs、jefferson、unar 这类解析器没有完整的 Python 接口与其重复造轮子不如稳稳定定调用成熟实现。核心类的骨架长这样import mmap class FirmwareProbe: def __init__(self, path): self.path path self.fp open(path, rb) self.mmap mmap.mmap(self.fp.fileno(), 0, accessmmap.ACCESS_READ) self.finds [] def scan_signatures(self): # 基于 binwalk 的签名库做扫描返回所有候选区域 pass def entropy_map(self, granularity256): # 滑窗计算 Shamon 熵用于识别压缩/加密数据段 pass def carve(self, offset, size, name): # 从 mmap 中切出指定区间保存到 artifacts/{name}.bin pass def generate_report(self): # 汇总 finds、切割产物、解析结果输出 JSON Markdown pass选择 Python 还有一个原因固件分析经常要跟 Env 工具链、交叉编译的产物打交道传统 Shell 脚本在 Windows/WSL/Linux 之间跑容易踩环境差异Python 在这方面的坑相对少一些。当然工具最终还是要往项目仓库里放不能做成只有我能用的脚本堆否则我自己就变成下一个没人讲得清的源头了。4. 模块拆解分区识别、文件系统提取与痕迹挖掘工具的最终价值体现在几个关键模块上。很多人在分析固件时喜欢上来就抓关键点比如直接找 rootfs 里有没有密码、有没有后门但其实缺少前置步骤很容易漏掉重要信息。我按照实际分析顺序把模块拆成了三层。4.1 分区边界识别熵值、魔数与结构线索分区边界识别是这套工具最核心的模块。它解决的问题是拿到一个连分区表都没有的裸镜像怎么知道哪里是一段的结束、哪里是另一段的开始方法有两个一个是签名一个是熵值。签名法很好理解找到 U-Boot、zImage、squashfs 的魔数就知道这里大概率是一个分区的起点。但签名法有一个致命盲区如果厂商对数据做了加密、压缩或混淆魔数可能完全找不到。这时候就需要熵值。熵值衡量的是数据的混乱程度。一段纯英文配置的熵值很低大概在 2 到 4 之间而压缩过或者加密过的数据段熵值通常接近 8。所以我在工具里做了一个滑动窗口熵图以 256 字节为窗口每次滑 128 字节计算每个窗口的香农熵然后画出熵值曲线。熵值曲线的突降和突升往往就是一个分区边界。举个例子压缩文件系统部分熵值接近 7.9到了未使用空白区熵值突然降到 0.2那你基本可以在这个位置划定边界。实际操作中我会把熵图和 binwalk 签名结果叠加在一起看两个证据都有才动手切。纯熵值切出来的段还可能是固件包里的媒体资源所以切割后必须再走一轮内容验证。4.2 文件系统提取与挂载中的坑确定候选分区后下一步是提取文件系统。这部分我踩了不少坑每个都值得单独说。第一个坑是 SquashFS 的字节序和版本差异。SquashFS 分为大端和小端两种老固件里经常出现大端版本而现代 Linux 工具默认可能按小端处理。识别方式是在超级块里读s_magic常见值有0x73717368sqsh小端和0x68737173hsqs小端。如果你发现工具报bad magic先看是不是字节序反了。第二个坑是偏移不对齐。很多固件不会把 rootfs 放在完美对齐的块边界上比如它可能从 0x801000 开始而不是 0x800000中间夹杂了厂商的一个 4 KB 私有头。这种情况直接挂载会失败需要先手动找到 superblock 的精确位置。我的工具里做了一个自动搜索遍历候选区域的前 64 KB逐个位置尝试匹配超级块魔数找到后把该位置作为真正的开头。这个逻辑救了我很多次。第三个坑是 JFFS2 和 UBIFS。JFFS2 没有固定的超级块它的起始位置判断依赖于扫描擦除块而擦除块大小又因 flash 型号而异通常在 64 KB 到 128 KB 之间。UBIFS 则更依赖 NAND 的坏块管理直接从完整镜像里切割经常不完整。处理这两个格式时我的原则是不要强行恢复先切出来再用 jefferson 等专门工具尝试如果不能解就不要乱填参数。这里给一个安全的切割示例dd iforiginal.bin ofrootfs.squashfs skip2097152 bs1 count8388608 unsquashfs -s rootfs.squashfsunsquashfs -s只查看信息不实际解包适合先确认正确性。确认无误后再unsquashfs -d output_dir rootfs.squashfs解包。4.3 版本信息、启动参数和埋雷痕迹文件系统提出来了固件的内脏才算真正暴露。我会优先看几个关键文件/etc/version、/etc/os-release明确的软件版本/proc/cmdline或者 U-Boot 里的bootargs启动参数能看出内核期望的 flash 分区布局、控制台参数/etc/inittab、/etc/init.d/*启动流程里有没有额外动作各种.conf、.cfg设备核心配置/usr/bin、/usr/sbin下的可执行文件通过字符串搜索确认用途搜索可疑痕迹时我常用一组命令组合strings rootfs.squashfs | grep -iE password|passwd|token|secret|pwd strings rootfs.squashfs | grep -iE wget|curl|tftp|scp|/tmp/为什么要找/tmp/很多老固件会在启动时从网络下载脚本放到/tmp/然后执行这类行为往往在/etc/init.d/里的一个不起眼的S99xxxx脚本里比如写死wget http://192.168.1.10/patch.sh -O /tmp/patch.sh。看到这种内容必须警觉它可能只是开发期调试代码也可能是被恶意利用的入口。这些痕迹不一定会被工具自动标记为漏洞但至少会被记录下来形成一份这个固件有哪些可疑点的清单。安全审计的视角是先知道有什么再判断要不要处理。5. 实战复盘把一份半残固件翻了个底朝天工具写完后的第一次正经实战就是开头那台设备的firmware_v2.3_final.bin。整个过程比想象中顺利但也让我意识到工具的价值不在全自动而在帮我省掉重复劳动并且不遗漏可疑证据。5.1 拿到手的是什么东西先跑工具入口层输入文件是那个 32 MB 的.bin。自动识别阶段输出了以下信息文件头 0x00000000 - 0x00000100空白区熵接近 00x00000100 附近出现U-Boot字符串0x00040000 附近出现bootcmd、bootargs等环境变量0x00080000 附近出现 zImage 结构0x00200000 附近检测到 SquashFS 魔数hsqs尾部有大片 0xff 空白区这说明它基本符合典型的 SPI Flash 布局。再用熵图确认边界0x00080000 前熵值较低0x00080000 到 0x001FFFFF 熵值很高0x00200000 之后熵值也很高但没有明显突变。分区边界基本可定。5.2 工具跑完之后的逐项核对工具自动切出来的分区如下分区编号偏移大小判定00x000001000x0003FF00U-Boot bootloader10x000400000x0003C000环境变量区20x000800000x00100000内核 zImage30x001800000x00040000DTB 设备树40x002000000x00C00000SquashFS rootfs50x00E00000剩余数据/保留区但我不直接信任自动结果还是手动验了一遍把 DTB 段切出来用fdtdump打开看到里面确实有设备型号、memory 节点、串口配置把内核段切出来用file识别为Linux kernel ARM boot executable zImage再用extract-vmlinux解出内核读取版本字符串Linux version 3.10.108。rootfs 解包后看到/etc/version写着v2.3-build-20210901这和文件名里的 v2.3 对上了但实际编译日期比 CSV 表格里的记录晚了整整四个月。这些交叉验证才是最终结论的基石。5.3 还原出的启动链和关键配置从 U-Boot 环境变量区里我读到了完整的启动流程bootargsconsolettyS0,115200 root/dev/mtdblock5 rootfstypesquashfs mtdpartsspi0.0:1m(u-boot),1m(env),4m(kernel),2m(dtb),12m(rootfs),rest(data) bootcmdsf probe; sf read 0x82000000 0x80000 0x100000; bootz 0x82000000 - 0x83000000这些信息比什么文档都值钱串口频率是 115200对应调试终端的设置mtdparts明确了分区表和工具自动识别出的分区几乎一致bootcmd表示从 SPI Flash 读取内核到内存地址 0x82000000并传递 DTB 地址 0x83000000 启动有了这条链我甚至不需要重新刷机就能知道设备的启动机制。之后如果需要修改启动参数、切换 rootfs、增加调试串口直接改环境变量区就可以了。更重要的是这份信息写进了工具生成的分析报告后人再也不用自己一个字节一个字节地找。6. 工具的边界与维护心得工具跑通了固件也还原了但我不想把故事停在成功这个高潮上。很多类似的开发项目最后会烂尾就是因为作者把工具做出来之后就能用就行地放在某个角落既没文档也没维护下一任接手时又开始骂人。6.1 它能做什么不能做什么我做的fw-praetorian能处理的是未加密或未经深度混淆的固件。它擅长识别常见文件系统、定位分区、提取配置、还原启动参数也能解析 S19 和 update.zip 这类标准烧录载体。但有几个场景是它搞不定的也是任何这种工具都搞不定的全加密固件如果厂商用了 AES、RSA 对整片固件加密没有密钥熵图和签名都只是告诉你这里很混乱救不出来。带坏块管理的 NAND 镜像raw dump 和实际运行时访问的布局不一致OOB 区域也没办法靠普通镜像解析。深度定制的私有文件系统魔数不是标准值结构也不符合任何已知格式只能靠人工逆向。业务层逻辑漏洞工具能发现/usr/sbin/xxx里有可疑字符串但无法确定这个程序是不是有缓冲区溢出、能不能被利用。所以这个工具的正确用法是帮你把 80% 的重复性分析工作自动化剩下 20% 的脑筋还是要你自己动。它不是一键破解器而是一个考古现场记录员。在选择依赖它的方便时务必保留它对人工判断的约束它输出的是候选信息和证据链不是最终结论。6.2 给同样处境的朋友几条建议如果你也接到了类似的烂摊子我有几条具体建议永远在副本上操作。分析固件时复制一份到analysis/目录用副本扫描、切割、提取原始文件只读。宁可占点磁盘也不要搞到一半把原始文件搞坏了。记录每一条命令。不是让你格式化地写文档而是把你执行过的命令、输出片段、关键发现按时间顺序追加到一个NOTES.md里。哪怕写得很乱也比没写强。我在实际项目里经常通过翻NOTES.md找到之前灵光一现的推断。对final保持高度怀疑。文件名里的final、latest、v2.3只能代表某个时间点的快照真正的版本要以固件内/etc/version、内核版本串、bootloader 编译时间三点交叉确认。不要把鸡蛋放在一个工具里。binwalk 误报率不低SquashFS 版本又不统一我建议至少同时用 binwalk 自定义签名搜索 熵图确认三重验证再决定切割边界。给工具留好交接文档。哪怕只写 200 字的使用说明和设计思路也比别人拿到源代码后两眼一抹黑要好得多。我做完的fw-praetorian自带README.md里面明确写了环境依赖、支持格式、已知限制。只有工具的作者先做好交接下一代人接手时才不会再次陷入没人讲得清的状态。工具本身不复杂真正值钱的是那个分析流程。大部分做嵌入式的人都能在三天内写出一个扫描脚本但只有真正踩过 binwalk 误报、倒腾过字节序、在坏块边缘试探过的人才会知道记录证据链比导出结论更重要。这份教训比我分析出来的那个固件分区表有价值得多。