ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

嵌入式固件分析实战:从零手写工具解析无人能讲的bin固件

嵌入式固件分析实战:从零手写工具解析无人能讲的bin固件 接到一个没人能讲清的固件大概是嵌入式这行最磨人的事之一。上季度我就摊上这么一单一款已经卖了几年的网络终端设备版本迭代了十几轮但老固件包没有任何配套文档源码服务器早就换了问了一圈唯一知道详情的人是两年前离职的同事他的交接文档里只写了一句话——用老版本工具刷别用新的。领导把那个几百MB的bin文件丢给我让我自己研究。固件这东西说穿了就是设备的灵魂可当灵魂没人解释的时候它看起来就是一堆没有规律的二进制。这篇文章就是记录我从零摸清这份固件、并顺手做了一个分析工具的全过程。如果你也遇到过手里有固件包但什么都讲不清的情况——不管是电视盒子、路由器、还是嵌入式工控板——这套思路应该有用。我会把工具怎么设计、哪些判断逻辑容易踩坑、怎么验证结果都摊开写争取让你看完能自己动手做一个。1. 一份讲不清的固件到底缺了什么1.1 三种最常见的讲不清我接手的这份固件属于三无状态无文档、无源码、无负责人。共享目录里躺着 update.zip.bak、mstar_5635_full.bin、backup_2021.img 这一堆文件单看文件名你根本不知道哪个是最新版本哪个是出厂救援包。更麻烦的是固件内部的构造完全是一个黑盒。动手之前我先把自己最想知道的事情列了出来其实就是三个核心信息缺口构建来源缺失不知道固件是从哪套SDK编出来的找不到build脚本后续没有人能复现一个一模一样的固件包版本之间到底改了什么只能靠猜。分区布局未知拆开之后我能看到kernel、rootfs这些东西但每个分区烧到什么地址、大小上限是多少、哪一段是uboot完全要靠逆向推断。安全策略成谜固件有没有整体加密哪个分区单独加密升级时校验的是CRC32还是RSA签名如果校验不通过会发生什么这些信息直接影响后面能不能安全刷机。这三个问题没解决之前这份固件就是一块烫手砖动一下都可能把设备刷成废铁。1.2 为什么这种事在嵌入式行业这么普遍你可能觉得哪有人这么交接项目的但我干这行久了发现这其实是常态不是意外。原因很简单嵌入式团队往往很小固件知识高度集中在一两个人的脑子里硬件项目预算紧的时候第一刀就砍在文档上。再加上很多设备从立项到量产就几个月能跑就行四个字能解释掉几乎所有流程漏洞。更要命的是固件不像应用代码它没有一个所有人都能访问的中央仓库。很多时候固件包就散落在工程师的个人硬盘、共享网盘、或者某个已经失效的CI目录里。版本管理靠文件名后缀时间一长谁也说不清哪个包对应哪块板子。等到第一任维护者离职第二任只改过UI配置第三任就是你——这种故事几乎每年都在整个行业重演。1.3 为什么不能只靠人工硬看我刚拿到bin文件时最先做的就是xxd打开一看毫无悬念的一堆随机字节翻了几百行光标就不知道跳哪去了。几百MB的固件人工排查头部结构、文件系统偏移、加密标识理论上可行实际上没人有这个耐心而且一次只能处理一个文件下次换个固件又得重新来一遍。我当时判断这活儿必须工具化。不是说我多爱写代码而是我知道这个问题不是一次性的后面大概率还有第二个、第三个固件要分析。与其每次都用十六进制编辑器跟二进制搏斗不如把识别、校验、报告的过程固化成一套工具以后每收到一个新固件跑一条命令就能出一份分析结果。这也是我做这个工具的初衷。2. 先摸个底我把固件从头到脚体检了一遍2.1 文件名和命名规则里藏着大量信息固件文件名看起来随意其实比你想的清醒得多。比如 e900v20d 这种前缀是运营商集采型号v20d一般是硬件版本号hg680ka 是代工厂加平台代号ax1800pro 这类路由器固件型号本身就直接标明平台定位。我拿到的文件里还有一个 update.zip.bak名字虽然是备份但zip扩展名说明它八成是一个标准升级包只是被改过后缀。我还习惯在拿到文件后先做一件事把目录里所有文件名按时间排序结合修改日期推断大致版本演变。这一步虽然不涉及任何二进制分析但能帮你建立对固件家族的初步认识后面分析具体文件时不容易被干扰。2.2 file、binwalk、strings 三件套先跑一轮接下来是标准三件套很多做固件分析的人都知道但这里我想强调一下顺序和细节。先看 file 的输出确认文件到底是什么格式file e900v20d_update.zip.bak # 结果类似Zip archive data, at least v2.0 to extract然后跑 binwalk。我建议先只用签名扫描不要急着解压binwalk -s e900v20d_update.zip.bakbinwalk 会按已知的魔数签名扫描列出偏移量、文件类型、可能的描述。这一步能很快告诉我固件里是不是嵌了 U-Boot、squashfs、gzip、jffs2 等内容。再配合 strings 找可打印字符串重点看几类关键词strings -n 10 backup_2021.img | grep -iE u-boot|kernel|rootfs|partition|factory|upgrade|model|version一次就能找到很多有用的文本比如分区表名字、uboot的版本号、rootfs挂载路径。这些信息虽然零散但拼起来已经能还原出固件大概的结构轮廓。2.3 用熵值判断固件有没有被加密二进制分析里有一个很关键的指标信息熵。简单理解熵越高数据越接近随机可预测性越低。普通代码和数据熵值通常在6到7之间经过压缩的数据熵值能到7.5以上加密后的密文熵值几乎逼近8。我写了一个小函数对固件分块计算熵值很快就知道哪些区域值得精看、哪些区域基本可以判定为不可读。import math from collections import Counter def block_entropy(data: bytes, block_size: int 4096): entropy_list [] for offset in range(0, len(data), block_size): block data[offset:offsetblock_size] if not block: break freq [0] * 256 for b in block: freq[b] 1 ent 0.0 for count in freq: if count 0: continue p count / len(block) ent - p * math.log2(p) entropy_list.append((offset, ent)) return entropy_list跑完之后把高熵区间标记出来再结合文件系统特征提取基本可以区分这里是压缩过的rootfs和这里可能是加密数据。这一步帮我省了非常多时间不用再把每个可疑区域都拖下来仔细看。2.4 把第一轮体检结果整理成表格分析工作最忌讳的就是看完就忘。每跑完一个文件我都把关键信息记成一张表后面写工具时的很多判断逻辑都是从这张表里提炼出来的。下面是当时工作笔记的简化版文件起始magic熵值区间识别结论备注update.zip.bakPK7.6-7.9Zip升级包内含多个img需二次解包mstar_5635_full.bin0x27 0x05 0x19 0x566.8-7.5U-Boot镜像头大端字段含CRCbackup_2021.img无已知魔数7.98疑似整体加密无文件系统特征熵极高这张表看着简单但它直接决定了工具的功能模块我需要一个能识别魔数和字节序的解析器一个能判断加密分区的检测器以及一个能做CRC校验的验证器。3. 把靠猜变成流程工具的功能清单和架构3.1 工具到底要解决什么问题很多人做固件工具容易犯一个毛病什么都要做结果什么都做不精。我给自己列了个范围只解决四个问题一键识别给它一个bin或zip立刻告诉我这是什么平台、什么类型的固件内部有没有U-Boot、kernel、rootfs。完整性校验算出的CRC/MD5和固件头里写的值能不能对上判断文件有没有被改动过、下载是否完整。加密分区提示把高熵且无文件系统特征的区域标出来提醒我刷机前必须确认密钥和工具链不要贸然暴力解包。多固件对比不同版本的固件之间哪个分区变了、哪个分区没动为排查问题和追版本提供依据。这四个功能做完已经能覆盖我90%的日常需求。至于解包、重打包、改固件这类的操作我一开始就没打算做进工具里因为那些动作本身有风险而且违背了我只是想把固件搞清楚的初衷。3.2 为什么用Python而不是C、Shell或现成框架我选Python原因很直接标准库够用、二进制处理方便、迭代速度快。结构体解析有struct压缩有zlib哈希有hashlib命令行有argparse不需要额外装任何第三方库就能跑。C当然性能更好但对这种批处理分析工具来说性能瓶颈在磁盘和固件本身Python的启动开销根本不是问题。Shell脚本也不是不行但正则和数据结构一复杂起来可维护性太差。至于那些现成的逆向框架我也有装但它们往往要求你先声明这是哪种固件、基于什么平台而这恰恰是我最不知道的部分。所以我的选择是自己写一个轻量工具必要时把binwalk、7z、openssl这些外部命令作为子进程调用组合起来用。3.3 CLI设计像git一样有子命令工具命名为 fwtool命令行设计得很简单四个子命令fwtool parse firmware.bin -o report.json fwtool verify firmware.bin fwtool diff old.bin new.bin fwtool report firmware.bin -f markdownparse 负责识别并输出结构化JSONverify 只跑完整性校验diff 做版本对比report 把结果渲染成容易看的markdown。这种设计让我在无人值守的批处理脚本里也能很方便地调用不需要依赖交互式界面。3.4 架构分层特征库、解析器、报告分离我的目录结构大概是这样的fwtool/ ├── cli.py # 命令行入口 ├── features.py # 魔数、分区名、关键词特征库 ├── parser.py # 主解析器调度各功能模块 ├── entropy_check.py # 熵值计算与加密判定 ├── integrity.py # CRC/MD5验证 └── report.py # JSON/Markdown输出每一层只干一件事。features.py 只存放特征数据比如U-Boot镜像头魔数为27 05 19 56squashfs文件系统标识为hsqsgzip压缩文件头为1f 8b等parser.py 负责把二进制切片喂给特征库匹配report.py 拿到解析结果后按照选定的格式输出。这样后面遇到新平台我只需要往特征库里加几行主流程一行都不用改。4. 解析器的核心逻辑魔数、压缩识别、加密判定、校验细节4.1 魔数解析和字节序最容易搞错的点固件头解析最坑的就是字节序。你在x86电脑上跑Python默认一肚子小端思维但很多老嵌入式平台尤其是MIPS、部分ARM SoC的固件头偏偏用大端。我第一个版本就栽在这里明明在偏移4的地方看到了版本号字段struct.unpack 按小端解出来却是个天文数字完全没法看。正确做法是先读魔数判断平台再动态决定后续字段的字节序。比如U-Boot镜像头的魔数是27 05 19 56紧跟其后的镜像大小字段在U-Boot标准里就是大端存储的import struct def parse_uboot_header(data: bytes): if not data.startswith(b\x27\x05\x19\x56): return None # U-Boot镜像头固定大端 image_size struct.unpack_from(I, data, 8)[0] crc_value struct.unpack_from(I, data, 12)[0] name data[32:64].decode(ascii, errorsignore).rstrip(\x00) return { type: U-Boot image header, size: image_size, header_crc: crc_value, name: name, }关键在于字节序不能拍脑袋要么从已知魔数推断要么做成可配置项。我在features.py里给每种平台都标了字节序解析器匹配到魔数后会连带拿到字节序信息后面所有字段解析都用同一个口径。4.2 压缩与文件系统特征的识别表固件里最常见的内容不是明文代码而是被压缩过、打包过的镜像。我在工具里维护了一张特征表是通过大量常见固件样本总结出来的内容类型特征字节说明gzip压缩1F 8B常见于单分区压缩段xz压缩FD 37 7A 58 5A 00新版固件常用squashfshsqs / sqsh / sqfs路由器和盒子的rootfs最爱jffs20x19 0x85 0x20 0x03老Flash文件系统U-Boot27 05 19 56bootloader镜像头Android bootimg以 ANDROID! 开头安卓设备boot分区匹配逻辑很简单扫描文件的所有偏移落在特征表里的偏移记录下来连同距离上一个特征的距离一起输出。这一步实际上是简化版binwalk但好处是我能完全控制输出格式后面diff的时候非常方便。4.3 加密判定不能只信熵值熵值高不代表加密这是一定要警惕的。压缩算法同样会产生高熵数据如果我只拿熵值做判断squashfs这种压缩文件系统几乎必然被误报成加密分区。所以我在工具里做了三重判定该区域熵值大于0.97该区域没有任何已知文件系统或压缩头特征该区域起始偏移不落在已有分区对齐规则上。只有同时满足这三个条件我才把该区域标记为疑似加密。这个逻辑老老实实解释了为什么很多所谓加密固件其实只是压缩数据而不是真的被人上了锁。还有一类情况是整体加密的固件从头到尾熵值都接近1没有任何可识别特征这种我会直接提示未找到可解析结构而不是强行解读。4.4 CRC完整性校验头部字段和实际内容对一对很多固件头会存一个CRC32或MD5值尤其是U-Boot镜像头。在校验模块里我读取出头部记录的哈希值再对相应数据段重新计算一遍对不上就直接报错。import zlib def verify_crc(data: bytes) - bool: # 假设前4字节是小端CRC32数据体从第4字节开始 stored_crc struct.unpack_from(I, data, 0)[0] actual_crc zlib.crc32(data[4:]) 0xffffffff return stored_crc actual_crc但要注意不是所有固件都把CRC放在最开头。有些放在末尾有些用MD5有些干脆没有。所以校验逻辑也做成了插件式一个校验策略专门负责描述哈希字段在哪个偏移、覆盖哪段数据、用什么算法features.py里加策略即可。这样做的好处是碰到一个新固件我只要先手工找出它的校验方式然后把它写进特征库以后这个系列就能自动校验。4.5 输出一份结构化报告最终解析结果统一输出成JSON方便脚本和后续diff程序消费。下面是一个简化后的报告示例{ file: backup_2021.img, platform: unknown, structures: [ { offset: 0, type: U-Boot image header, size_field: 16777216, crc_valid: true, name: sdk_build_v33 }, { offset: 262144, type: squashfs, size: 11800000 } ], suspicious_crypto: [ { offset: 12582912, entropy: 0.986, reason: high entropy no signature } ] }看到这个报告我基本就能决定下一步动作哪里可以直接解包提取文件哪里需要找原厂工具链哪里可疑但要先对比完再下结论。5. 测试验证拿三类真实固件跑了一遍踩了三个坑5.1 选测试对象的原则工具写完自己测肯定不够我特意挑了三种不同类型固件来验证电视盒子类类似 e900v20d、hg680ka、创维8h26 这种常见命名路由器类ax1800pro 这类还有一个存储主控的量产工具包sm2258xt、fc1178bc 这类。选这三种是因为它们的结构差异很大盒子固件往往是zip包套多个img路由器固件经常是squashfs套内核量产工具直接就是PC端软件加密壳加固件包。能同时处理好这三类才说明工具不是针对某一家格式硬编码的。5.2 坑一大小端判断错位第一次跑路由器固件时parse 报告显示U-Boot头的大小字段是0x10000000我一眼就看出不对这明显是个小端识别错误。原来这个固件用的是大端结构而我的布局文件里没有写明字节序默认按小端跑结果就是所有数值都被调了个儿。修复其实很简单把平台字节序加进特征库就完了。但这件事给我提了个醒工具可以自动化但运行结果必须经过一次人工抽查尤其是数值字段明显不合理时十有八九是字节序问题。5.3 坑二squashfs被误判成疑似加密另一个印象深刻的问题是加密误报。第一次跑固件扫描时我把一个squashfs分区标记成了疑似加密原因是这个分区的熵值高达0.986而且由于rootfs内部再压缩起始偏移并不在上一轮分区表预期位置正好撞上了我三重判定的条件。发现问题是因为我用binwalk对比了一遍发现同一位置明明有sqsh特征。我马上意识到工具缺少一个环节高熵区域的特征二次确认不能只看起始偏移还要在区域内搜索已知文件系统的子特征。修复方法是对候选加密区域再做一次小范围特征扫描只要在区域内找到任何已知结构就降级为高熵但可识别。这之后误报率明显下降。5.4 坑三全量包链接解析并没有想象中简单做diff功能时我想把官网上的固件最新版下载下来做对比。一开始想着用正则提取下载页里的链接就行结果页面是用JavaScript渲染的直接抓HTML文本根本匹配不到完整URL。后来我换成先找页面背后的接口地址再模拟请求拿到真实下载链接。这个踩坑过程其实很多人都会遇到全量包链接解析听着是个小事实际涉及网站如何渲染、接口如何鉴权、文件如何重定向完全没有捷径。我在fwtool里加了一个小命令专门接受固件下载页URL和一个可选的referer先把动态内容转成静态响应再从中提取出直链。这个功能跑通以后做固件版本对比就方便多了至少不用每天去浏览器里手动找链接。5.5 金标准以刷机结果回测工具准确性工具输出的报告到底对不对我最终用最笨也最可靠的标准来验证刷机。挑一台测试设备按工具解出来的分区布局进行刷写能正常启动、升级成功、功能正常说明报告基本正确变砖或者启动循环说明某个分区判断有问题。当然刷机本身就是有风险的动作我在验证前会确认设备有可恢复手段比如烧录器、量产模式、或者出厂救援包绝不用唯一一台主力设备冒险。用结果回测的过程让我发现了不少问题比如某个文件头部识别成gzip其实内部是LZMA二次压缩还有个别分区我在JSON报告里标成rootfs刷机实测才发现那其实是一个只读配置分区。这些问题不刷机根本发现不了所以我的结论是工具能让你快十倍但最终还是要靠真机闭环验证。6. 工具做好之后把固件知识变成团队资产6.1 不让分析结论只停留在我的笔记本工具本身是一段代码但真正有价值的是工具产出的报告和我在验证过程中积累的知识。我把每个固件的分析结果都整理成一份markdown档案内容包括文件hash、识别到的分区表、校验结论、以及这个系列固件的坑在哪里。新同事接手时不需要再摸索两天直接跑一条fwtool report命令再翻一下档案马上就能进入状态。6.2 给同类工具的三点建议第一不要一上来就追求全自动。先手动分析一个固件把你在分析过程中做的每一个判断写成步骤再逐步把这些步骤代码化否则你写出来的工具只会处理你已经知道的东西。第二凡是涉及刷机或者解包后重打包的功能一定要做危险操作确认别在命令行里一言不合就真写分区。第三加密固件的检测逻辑尽量保守误报一个压缩分区是小事但漏报一个加密分区可能让你在刷机时直接面临整个设备变砖的风险。6.3 工具的边界感最后想说一下边界。固件分析工具不是用来破解加密的也不应该拿来做绕过保护的事情。我在工具里只做识别和提示一旦发现某个固件是整体加密且没有厂商授权的工具链我会停在这里去找正规的供应商渠道要资料而不是尝试逆向密钥。原因很实在能破解的人当然有但多数情况下维护产品、修bug、做升级才是我的核心工作没必要为了一个老老实实加密的固件去蹚进法律和安全的浑水里。如果你也要给遗留固件做工具我的建议是别一上来就想着写得多完美。先用手动方法跑通一个案子把你每一步的判断写下来再把这些判断变成代码。你会发现工具的意义不在于能自动解包多少个固件而在于下一次再有人递给你一个没人讲得清的bin时你是有底气说稍等我先跑一下报告的人。
返回列表