简介:面向信息安全从业者与CTF MISC方向选手的binwalk工具资源包,聚焦Windows环境下实现Linux二进制分析能力的实践需求。内容围绕固件、镜像与隐藏数据挖掘场景,涵盖Cygwin、WSL等替代方案及FLOSS等跨平台工具的思路,并延伸至binwalk基于Python的扫描解析原理与源码定制方向,适合具备一定Linux基础、希望补齐Windows侧分析链路的中级学习者。压缩包共189个文件、约81.74MB,以89个Python源码文件为主体,辅以sample样本、binwalk相关脚本与bat批处理入口,以及lzma、gzip、squashfs、jffs2、cpio等固件与压缩格式样本,另有head、conf、md等配置与说明文件,目录按archives、firmware、filesystems等模块划分,便于按格式检索验证。已有3042人学习下载。读者可据此获得一套可本地运行的二进制分析环境素材,结合样本与源码理解文件头识别、字节模式匹配机制,并借助批处理入口在Windows命令行中复现扫描流程,为CTF解题与固件逆向提供可操作的参考。
1. 拆开一个固件包之前,先想清楚 binwalk 到底在替你做什么
拿到一个来路不明的固件镜像,或者 CTF 里那张看着像图片、实际塞了七八层压缩包的文件,很多人的第一反应是直接binwalk -e一把梭。结果要么解出来一堆_文件名.extracted目录,里面全是碎片,要么卡在某个偏移量上反复报错,最后连原始文件长什么样都忘了。binwalk 这个工具在信息安全、Linux 取证和 CTF MISC 题里出场率极高,但它本质上不是「解压器」,而是一个签名扫描器 + 提取调度器:它按文件头魔数在二进制流里找偏移,再调用对应的外部工具去落地。理解这一点,你才知道为什么有时候它「找到了却提不出来」,为什么-e之后还要手动补刀。这篇笔记围绕一份 binwalk 工具包资源展开,讲清楚它适合谁、怎么装、怎么用、参数怎么调,以及那些我踩过的坑。
2. 签名扫描的底层逻辑:魔数、偏移与提取调度
2.1 binwalk 靠什么「看见」文件
binwalk 的核心是一张签名表(magic signature),每条记录包含一个魔数模式、一个描述、以及可选的提取规则。扫描时它把目标文件按字节流读入,逐偏移比对魔数,命中就记录下偏移量和类型。这解释了为什么它对未压缩、未加密、头部完整的嵌入文件特别灵,而对被截断、被异或、被自定义头包裹的数据基本无能为力。
常见做法是先做一次纯扫描,不加任何提取参数,看清楚里面到底有什么:
# 只扫描不提取,输出偏移、十六进制值、描述 binwalk firmware.bin # 提高签名匹配的熵阈值敏感度,适合找被弱加密/压缩的段 binwalk -E firmware.bin # 输出熵图数据,用于判断哪些区间是高熵(压缩/加密)哪些是低熵(明文/填充) binwalk -E --save firmware.bin第一行是最基础的签名扫描,输出三列:十进制偏移、十六进制偏移、识别到的文件类型。第二行的-E是熵分析模式,它不依赖签名表,而是按块计算香农熵,高熵区通常意味着压缩或加密数据,低熵区往往是明文、填充或代码段。第三行把熵数据落盘,方便后续用脚本画图定位边界。参数上,-E的块大小可以用--block调整,默认值在大多数固件上够用,但遇到块内混合内容时调小能提高分辨率。
2.2 提取阶段到底发生了什么
-e不是 binwalk 自己解压,而是它根据签名表里的提取规则,调用系统里已有的工具(如unsquashfs、tar、gzip、7z)去处理对应偏移的数据。所以「提取失败」十有八九不是 binwalk 的问题,而是外部工具缺失或版本不匹配。这也是为什么同一份固件在别人机器上能解、在你机器上只出空目录。
# 提取并自动递归处理已知格式 binwalk -e firmware.bin # 指定提取目录,避免污染当前工作区 binwalk -e -C /tmp/fw_out firmware.bin # 只提取指定类型的签名,减少无关落地 binwalk -e --include='gzip' --include='squashfs' firmware.bin-C指定输出根目录,递归提取会在其下生成_firmware.bin.extracted子目录。--include是白名单,只对列出的格式执行提取,适合你已经通过扫描知道目标类型、不想被一堆小文件干扰的场景。反过来--exclude是黑名单。这两个参数在 CTF 里很实用,因为题目经常故意塞大量诱饵文件头,全量提取会浪费大量时间。
提示:
-e的递归深度默认是 8 层,遇到嵌套压缩包时可以用-M配合--depth控制,但深度过大容易触发误报循环,建议先手动确认层级再放开。
2.3 签名表与自定义规则
binwalk 的签名表是文本格式,位于安装目录的magic/下。当你遇到厂商自定义头(比如某路由器固件用私有魔数包裹 squashfs),标准签名表扫不出来,这时可以追加自定义规则。常见做法是复制一份现有 magic 文件,按格式加一行魔数模式,再用-m指定自定义表:
# 使用自定义签名表扫描 binwalk -m /path/to/custom.magic firmware.bin自定义规则的写法遵循 libmagic 语法,魔数用\x转义,偏移用&或相对偏移描述。这一步对新手偏重,但对做固件取证的从业者几乎是必备技能——厂商私有格式不会等你更新签名库。参数上注意-m是替换而非追加,想保留内置规则就把原表内容合并进去。
3. 从扫描到落地:一套可复现的固件拆解流程
3.1 环境准备与依赖补齐
binwalk 本身是 Python 工具,但提取能力高度依赖系统工具链。在 Debian/Ubuntu 系上,一套能覆盖大多数固件格式的依赖大致如下:
# 基础依赖:压缩、文件系统、归档 sudo apt install -y python3-pip squashfs-tools mtd-utils gzip bzip2 tar \ p7zip-full cpio zlib1g-dev liblzma-dev liblzo2-dev # 安装 binwalk(优先用发行版包,版本稳定) sudo apt install -y binwalk # 若需较新特性,用 pip 安装并确认入口 pip3 install binwalk第一段装的是提取阶段真正干活的工具:squashfs-tools处理最常见的路由器根文件系统,mtd-utils处理 NAND/NOR 镜像,p7zip-full兜底各种压缩格式。第二段用发行版包管理器安装 binwalk 本体,版本经过打包测试,比 pip 直装更少踩依赖冲突。第三段是备选,pip 装完后要确认binwalk命令在 PATH 里,否则可能是用户级安装路径没进环境变量。
注意:不要在同一台机器上混用 apt 版和 pip 版 binwalk,两者的签名表路径和 Python 依赖可能互相覆盖,表现为「命令能跑但提取规则全丢」。
3.2 标准拆解四步法
我一般把固件拆解固定成四步,每步都有明确的产出物,避免一上来就递归提取导致现场混乱。
第一步,全量扫描并保存报告:
binwalk firmware.bin > scan.txt binwalk -E firmware.bin > entropy.txtscan.txt是签名命中清单,entropy.txt是熵分布。两份对照看,能快速判断哪些偏移是真实文件、哪些是误报(高熵区里的签名命中更可信,低熵区里的孤立命中多半是巧合)。
第二步,按类型定向提取:
# 先只提最可能的根文件系统 binwalk -e --include='squashfs' firmware.bin第三步,对提取出的文件系统做二次扫描,因为很多固件是「外层 cpio + 内层 squashfs」的套娃结构:
# 进入提取目录后对每个子文件再扫 find _firmware.bin.extracted -type f -size +1k -exec binwalk {} \;-size +1k过滤掉明显无意义的小文件,-exec对每个候选再跑一次签名扫描。这一步是套娃固件的关键,跳过它你会以为只解出一层就完事了。
第四步,验证落地完整性:
# 检查 squashfs 是否能正常挂载/解包 unsquashfs -l _firmware.bin.extracted/squashfs-root.squashfs | head-l只列目录不落地,能快速确认文件系统结构是否完整。如果这里报错,说明提取出的 squashfs 被截断或偏移不对,需要回到第一步重新核对偏移量。
3.3 参数组合与场景对照
不同场景下参数选择差别很大,下面这张表是我实际用下来比较稳的组合:
| 场景 | 推荐参数 | 说明 |
|---|---|---|
| 首次摸底 | binwalk file | 纯扫描,零副作用 |
| 判断加密/压缩段 | binwalk -E file | 熵分析,不依赖签名 |
| 定向提取 | binwalk -e --include='类型' file | 减少无关落地 |
| 套娃递归 | binwalk -Me file | 递归提取,注意深度 |
| 自定义格式 | binwalk -m custom.magic file | 替换签名表 |
| 指定输出目录 | binwalk -e -C /path file | 隔离工作区 |
-M是递归提取开关,配合-e使用;-C控制输出根目录。表格里没列-d(深度)是因为默认值在多数场景够用,只有明确知道嵌套层数时才手动调,否则容易在误报循环里打转。
4. 避坑与排查:那些让提取结果变成空目录的瞬间
4.1 现象:-e跑完只有空目录,没有任何落地文件
原因通常是外部提取工具缺失或签名表里的提取命令路径不对。binwalk 在提取阶段会 fork 子进程调用unsquashfs等工具,工具不存在时它只在 stderr 打一行警告,不中断整体流程,于是你看到的就是「目录建了、文件没出」。
解决:先看提取时的 stderr 输出,确认是哪个工具报的command not found,补装对应包;再用binwalk -e -v开详细日志,定位到具体偏移和调用命令。补装后重跑,别在旧目录上叠加,先清空输出目录。
4.2 现象:签名扫到了 squashfs,但unsquashfs报「not a valid squashfs」
原因是偏移量不准。binwalk 报的偏移是魔数起始位置,但某些固件在魔数前还有头部字段,直接按报的偏移切会切歪。另一种情况是固件被截断,squashfs 尾部元数据缺失。
解决:用dd按十六进制偏移手动切一段出来验证:
# 从指定偏移切出候选段,skip 用十进制 dd if=firmware.bin of=candidate.squashfs bs=1 skip=1048576 count=4194304 unsquashfs -l candidate.squashfsskip是起始偏移(十进制,binwalk 输出第一列就是),count是切多长。先切一段试,能列目录再扩大范围。如果手动切也报错,基本可以判定是截断或加密,不是偏移问题。
4.3 现象:递归提取陷入死循环,目录越套越深
原因是签名误报——某段数据恰好匹配了某个魔数,提取出来的「文件」又包含同样的魔数,递归就停不下来。CTF 题里这是常见陷阱。
解决:限制递归深度--depth,或者干脆关掉-M改手动逐层提取。手动虽然慢,但每层都能确认产出物,比让脚本在误报里空转强。我一般对来源不明的文件默认不开递归,先看第一层结果再决定。
4.4 现象:熵图显示大片高熵,但签名一个都没命中
高熵只说明数据被压缩或加密,不代表 binwalk 能识别。自定义压缩、异或混淆、AES 加密段都会呈现高熵且无签名。这时候签名扫描帮不上忙,得换思路:看高熵区的起止偏移,用dd切出来单独分析,或者用strings找残留明文线索。
解决:binwalk -E的输出里记录了每个块的熵值,找到熵值突变的边界,那往往就是加密段和明文段的分界。切出来后先跑strings -n 8看有没有可读字符串,再决定是爆破、逆向还是放弃。
4.5 现象:提取出的文件系统能列目录,但读文件报 I/O 错误
原因是 squashfs 的压缩算法不被当前unsquashfs支持,比如用了较新的 zstd 或 lz4 变体,而系统里的 squashfs-tools 版本偏旧。
解决:升级squashfs-tools到较新版本,或者用unsquashfs -comp指定解压器。确认压缩算法可以看 squashfs 超级块里的 compression id,用file命令或十六进制查看器读头部字段。版本不匹配这类问题没有捷径,只能对齐工具链。
5. 进阶:用脚本把扫描结果变成可复现的提取清单
单次拆解靠手敲命令没问题,但当你一周要处理十几个固件样本时,手工记录偏移和类型会疯。我的习惯是把 binwalk 的扫描输出解析成结构化清单,再用清单驱动提取,这样每一步都可追溯、可重跑。
import re import subprocess # 解析 binwalk 标准输出,提取偏移、十六进制、描述 def parse_scan(path): out = subprocess.run(["binwalk", path], capture_output=True, text=True).stdout items = [] for line in out.splitlines(): # 匹配 "十进制 十六进制 描述" 三列格式 m = re.match(r"^(\d+)\s+(0x[0-9A-Fa-f]+)\s+(.+)$", line.strip()) if m: items.append({ "offset": int(m.group(1)), "hex": m.group(2), "desc": m.group(3).strip(), }) return items # 按描述关键字过滤出需要提取的条目 def pick(items, keywords): return [i for i in items if any(k in i["desc"].lower() for k in keywords)] if __name__ == "__main__": entries = parse_scan("firmware.bin") targets = pick(entries, ["squashfs", "gzip", "cpio"]) for t in targets: print(f"{t['hex']} {t['desc']}")这段脚本做三件事:调用 binwalk 拿原始输出、用正则把三列格式解析成字典、按关键字筛出目标条目。parse_scan里的正则锚定了行首十进制和十六进制两列,避免把表头或警告行误收进来;pick的关键字列表按你的固件类型调整,比如纯路由器固件只留squashfs和cpio。产出是一份偏移清单,后续可以用dd按清单逐条切,也可以喂给提取脚本批量处理。
这套做法的价值在于可复现:同一份固件,任何人拿到这份清单都能切出同样的段,排查问题时不用靠记忆复述「我当时大概在哪个偏移」。我一般会把清单和原始扫描输出一起存档,命名带上样本哈希前缀,避免不同样本的清单混在一起。
再补一个验证习惯:每次提取完,对落地文件做一次反向校验——用sha256sum记录原始段和提取产物的哈希,确认提取过程没有静默截断。这个动作看起来多余,但在处理被故意破坏的样本时,它能帮你区分「文件本来就不完整」和「提取工具吃掉了尾部」。
从那以后我每次拆固件都强制先存扫描报告再动手,哪怕只是临时看一眼,也先落一份scan.txt。这个习惯救过我好几次——当提取结果和预期对不上时,回头翻报告比重新扫一遍快得多。希望帮到你。
本文还有配套的精品资源,点击获取