1. 固件文件系统识别

固件文件系统识别 学习卡片

任务目标

给定一个未知的固件镜像文件,通过特征签名识别其文件系统类型(SquashFS、CramFS、JFFS2等),为后续提取做准备。本案例使用一个已知的OpenWrt固件片段作为输入,完成从镜像到文件系统类型判定的完整闭环。

环境依赖

需要binwalk(固件分析工具)和file命令。安装:apt install binwalk file。若binwalk的magic数据库过旧,可先执行binwalk --update更新签名库。

实现方案

识别文件系统类型有三种递进手段:特征字符串匹配(快速预判)、binwalk签名扫描(标准做法)、结构体解析(精确验证)。本案例提供完整脚本,对输入镜像依次执行三种检测并汇总结果。

关键完整代码

#!/usr/bin/env python3
import os
import re
import subprocess
import sys
import struct

MAGIC_PATTERNS = {
    b"hsqs": "SquashFS (little-endian, v4)",
    b"sqsh": "SquashFS (big-endian, v4)",
    b"shsq": "SquashFS (big-endian, v3)",
    b"sqsh"[::-1]: "SquashFS (little-endian, v3)",
    b"\x45\x3d\xcd\x28": "CramFS (little-endian)",
    b"\x28\xcd\x3d\x45": "CramFS (big-endian)",
    b"\x19\x85\x20\x03": "JFFS2 (little-endian)",
    b"\x85\x19\x03\x20": "JFFS2 (big-endian)",
}

def check_magic(data):
    """在文件头部和偏移0x1000处检查魔数"""
    results = []
    for offset in (0, 0x1000):
        if offset + 4 > len(data):
            continue
        chunk = data[offset:offset+4]
        for magic, name in MAGIC_PATTERNS.items():
            if chunk == magic:
                results.append((offset, name, "magic"))
    return results

def check_binwalk(path):
    """调用binwalk扫描文件系统签名"""
    try:
        out = subprocess.check_output(
            ["binwalk", path], stderr=subprocess.DEVNULL, timeout=30
        ).decode(errors="replace")
    except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
        return []
    results = []
    for line in out.splitlines():
        if any(k in line for k in ("SquashFS", "CramFS", "JFFS2")):
            parts = line.split()
            if parts and parts[0].isdigit():
                results.append((int(parts[0]), line.strip(), "binwalk"))
    return results

def parse_squashfs_superblock(data, offset):
    """解析SquashFS v4超级块关键字段"""
    if offset + 96 > len(data):
        return None
    magic, inode_cnt, mod_time, block_size, frag_cnt = struct.unpack(
        "<IIIII", data[offset:offset+20]
    )
    comp_id = struct.unpack("<H", data[offset+20:offset+22])[0]
    comp_map = {1: "gzip", 2: "lzma", 3: "lzo", 4: "xz", 5: "lz4", 6: "zstd"}
    return {
        "magic_hex": hex(magic),
        "inode_count": inode_cnt,
        "block_size": block_size,
        "compression": comp_map.get(comp_id, f"unknown({comp_id})"),
    }

def main():
    if len(sys.argv) != 2:
        print("用法: python3 fs_identify.py <固件镜像路径>")
        sys.exit(1)
    path = sys.argv[1]
    if not os.path.isfile(path):
        print(f"错误: 文件不存在 - {path}")
        sys.exit(1)

    with open(path, "rb") as f:
        data = f.read()

    print(f"=== 分析文件: {path} (大小: {len(data)} 字节) ===\n")

    # 1. 魔数预检
    magic_hits = check_magic(data)
    if magic_hits:
        print("[1] 魔数匹配:")
        for off, name, _ in magic_hits:
            print(f"    偏移 0x{off:08x}: {name}")
    else:
        print("[1] 魔数匹配: 未找到已知文件系统魔数")

    # 2. binwalk扫描
    bw_hits = check_binwalk(path)
    print("\n[2] binwalk签名扫描:")
    if bw_hits:
        for off, desc, _ in bw_hits:
            print(f"    偏移 {off}: {desc}")
    else:
        print("    未发现文件系统签名")

    # 3. 结构体验证(仅对SquashFS做精确解析)
    print("\n[3] SquashFS超级块解析:")
    parsed = False
    for off, name, src in magic_hits + bw_hits:
        if "SquashFS" in name and src == "magic":
            info = parse_squashfs_superblock(data, off)
            if info:
                print(f"    偏移 0x{off:08x}: inode数={info['inode_count']}, "
                      f"块大小={info['block_size']}, 压缩={info['compression']}")
                parsed = True
                break
    if not parsed:
        print("    未解析到有效的SquashFS超级块")

    # 4. 汇总判定
    print("\n=== 判定结果 ===")
    all_hits = magic_hits + bw_hits
    if not all_hits:
        print("无法识别文件系统类型,可能不是常见的SquashFS/CramFS/JFFS2镜像。")
        print("建议: 尝试 'binwalk -A' 或 'binwalk -E' 进一步分析。")
    else:
        # 取偏移最小的命中作为主类型
        primary = min(all_hits, key=lambda x: x[0])
        print(f"主文件系统类型: {primary[1]} (偏移 {primary[0]})")
        if len(all_hits) > 1:
            print(f"共发现 {len(all_hits)} 处签名,可能存在嵌套文件系统。")

if __name__ == "__main__":
    main()

运行方式

# 准备测试镜像(若无现成固件,可下载OpenWrt官方固件或使用以下方式生成最小SquashFS测试镜像)
# 生成最小SquashFS测试镜像(需root权限):
# mkdir -p /tmp/rootfs && echo "test" > /tmp/rootfs/hello.txt
# mksquashfs /tmp/rootfs test.squashfs -comp gzip

# 运行识别脚本
python3 fs_identify.py test.squashfs

验证方法与通过条件

使用上述生成的test.squashfs作为输入,预期输出应包含:

  • 魔数匹配:偏移 0x00000000 处命中 SquashFS (little-endian, v4)(对应hsqs魔数)
  • binwalk扫描:报告 SquashFS filesystem, little endian, version 4.0 等描述
  • 超级块解析:显示inode数≥1、块大小通常为131072(128KB)、压缩方式为gzip
  • 判定结果:主类型为SquashFS

判定通过条件:脚本输出的主类型与实际文件系统一致,且超级块解析字段与mksquashfs默认参数吻合。若使用真实固件(如OpenWrt的*-squashfs.bin),同样应识别出SquashFS及其偏移位置。

常见问题

  • binwalk无输出:尝试binwalk --update更新签名库,或使用binwalk -A(opcode扫描)辅助判断。
  • 魔数偏移非0:固件常带有bootloader头部,文件系统通常从偏移0x10000或更高地址开始,脚本已覆盖0和0x1000两个常见位置,实际使用时以binwalk扫描结果为准。
  • JFFS2无固定魔数偏移:JFFS2的魔数0x19852003出现在每个擦除块起始处,binwalk会报告多个命中,取第一个即可。