← 返回目录课程:《工业控制单片机固件逆向工程案例实战教程》
04

04 固件二进制分析:Binwalk 文件系统扫描与自定义 LZSS 解压

使用Binwalk识别固件中嵌入的文件系统,并编写脚本解压私有LZSS压缩段
章节导航 难度:入门类型:概念框架
目标结果

把固件二进制分析落到固件二进制分析的触发条件、可观察状态和复查材料上,先判断问题从哪里收窄。

关键线索
接口配置流程状态日志
交付材料
架构图代码片段排障表检查清单

固件解剖:从 Binwalk 扫描到私有 LZSS 解压

固件二进制分析固件解剖:从Binw识别对象配置与验证链路

在工业控制单片机固件逆向工程中,第一步往往是识别固件中嵌入的文件系统。面对一个二进制固件,我们通常不知道其内部结构。本节将演示如何使用 Binwalk 扫描固件,定位文件系统边界,并针对厂商自定义的 LZSS(Lempel-Ziv-Storer-Szymanski) 压缩段编写解压脚本。核心目标是掌握从“黑盒”二进制到可分析文件系统的完整链路

实操步骤

拿到固件后,不要直接拖入反汇编器。先用 binwalk -Me firmware.bin 自动扫描并提取已知签名。如果发现未知压缩段,再手动分析熵值变化,定位私有算法边界

固件二进制分析

指标对照卡:固件二进制分析

  • 关键对象:实际技术应用场景里的固件二进制分析
  • 问题现象:实际技术应用场景里固件二进制分析一开始改动很快,输入需求增加后日志记录显示状态外泄、循环依赖和接口膨胀。
  • 验证动作:先标出谁持有状态、谁发起调用、谁记录日志和失败返回,再按输入条件、阈值标准和处理动作决定接口是否需要拆分。第04节记录前后变化点时,优先确认示例片段、状态记录和固件二进制分析是否指向同一个原因。
  • 工程判断:调整前后调用方向更清楚,失败返回、日志记录和输出结果可追踪,旧入口仍可回退,才说明边界收住了。

固件二进制分析落地前要同时核对固件二进制分析的文件系统扫描、失败路径和自定义。

概念块:固件二进制分析

固件二进制分析用固件二进制分析、文件系统扫描、自定义连接到固件二进制分析的调试、验证和交付动作。

  • 固件二进制分析
  • 文件系统扫描
  • 自定义
  • 并编写脚本解压私有LZSS压缩段

工业控制单片机固件通常包含引导加载程序、主程序、文件系统和配置参数。文件系统可能是 SquashFS、CramFS 或 JFFS2,但也可能是厂商定制的私有格式。Binwalk 通过扫描已知文件头签名(Magic Number)来识别这些区域。然而,当遇到私有 LZSS(Lempel-Ziv-Storer-Szymanski) 压缩段时,Binwalk 无法直接识别,只能看到高熵数据块。此时,我们需要结合熵值分析和固件结构知识,手动定位压缩段的起始与结束地址

本节目标:通过一个真实案例,展示如何用 Binwalk 识别标准文件系统,并编写 Python 脚本解压私有 LZSS 段,最终还原出完整的 步骤 固件分区

真实项目场景

某工业 PLC 固件升级包 plc_v2.1.bin 大小为 4 MB。Binwalk 扫描后识别出前 512 KB 为 U-Boot,中间 2 MB 为 SquashFS 文件系统,但末尾 1.5 MB 显示为高熵数据,无已知签名。通过分析固件中的分区表(位于偏移 0x1000 处),发现该区域标记为 步骤,压缩算法为私有 LZSS 变种。我们需要提取并解压此分区,以分析其内部配置文件

对象配置与验证链路:Binwalk 扫描与自定义 LZSS 解压

实现路径分为两步:先用 Binwalk 扫描获取分区偏移,再编写解压脚本处理私有 LZSS 段

Binwalk 扫描与分区定位

执行 binwalk -Me plc_v2.1.bin,输出显示 SquashFS 文件系统位于偏移 0x80000,大小为 0x200000。对于末尾的高熵区域,我们使用 binwalk -E plc_v2.1.bin 查看熵值图。熵值接近 1.0 的区域即为压缩或加密数据。结合分区表(偏移 0x1000 处读取到的 struct partition),确认该区域起始偏移为 0x280000,大小为 0x180000

关键字段:分区表结构中的 magic(0xAA55)、type(0x03 表示 LZSS 压缩)、start_lba 和 size。这些信息是编写解压脚本的输入参数

自定义 LZSS 解压脚本实现

私有 LZSS 变种通常修改了字典大小、匹配长度或标志位格式。通过逆向分析固件中的解压函数(位于 U-Boot 中),我们还原出算法参数:字典大小为 4096 字节,匹配长度最小为 3,标志位为 8 位(每 8 个压缩块共用 1 字节标志)。以下脚本实现了该变种的解压逻辑:

import struct
def lzss_decompress(data, dict_size=4096, min_match=3):
    """解压私有 LZSS 数据块"""
    output = []
    dictionary = [0] * dict_size
    dict_index = 0
    i = 0
    while i < len(data):
        # 读取 8 位标志位
        flags = data[i]
        i += 1
        for bit in range(8):
            if i >= len(data):
                break
            if (flags >> bit) & 1:
                # 未压缩字节
                byte = data[i]
                output.append(byte)
                dictionary[dict_index] = byte
                dict_index = (dict_index + 1) % dict_size
                i += 1
            else:
                # 压缩引用:2 字节 (偏移 + 长度)
                if i + 1 >= len(data):
                    break
                match_offset = data[i] | ((data[i+1] & 0xF0) << 4)
                match_length = (data[i+1] & 0x0F) + min_match
                i += 2
                for j in range(match_length):
                    byte = dictionary[(match_offset + j) % dict_size]
                    output.append(byte)
                    dictionary[dict_index] = byte
                    dict_index = (dict_index + 1) % dict_size
    return bytes(output)
# 使用示例:读取固件中的压缩段
with open('plc_v2.1.bin', 'rb') as f:
    f.seek(0x280000)
    compressed_data = f.read(0x180000)
decompressed_data = lzss_decompress(compressed_data)
with open('步骤.bin', 'wb') as f:
    f.write(decompressed_data)
print(f"解压完成,输出大小: {len(decompressed_data)} 字节")

这段配置属于“问题代码/原始配置”,核对键名、默认值、取值范围和修改后影响的行为。

关键字段解释:flags 的每一位控制后续 8 个数据块的执行侧式;match_offset 和 match_length 从字典中复制历史数据。脚本中的 dict_size 和 min_match 必须与固件中的解压函数一致,否则解压结果会出错

关键判断

如果解压后输出乱码,首先检查 dict_size 和 min_match 参数。常见错误是字典大小误设为 2048 或 8192,导致偏移计算错误。通过对比固件中已知的未压缩数据(如字符串表)来验证参数

结果校验

交付前复查固件二进制分析,重点不是重复步骤,而是确认固件二进制分析、固件二进制分析和文件系统扫描能不能互相解释。若指标对照卡、关键对象或外部条件变化,原来的结果就要重新验收,不能把旧记录当作新场景的证明。下一步处理可以先锁定实际技术应用场景里的固件二进制分析,再选择补实验、补日志、补参数对照或补一条清晰的失败边界。交接这节内容时,把现场判断当作观察入口,把配置验收当作边界条件,避免只沿用固件二进制分析的名称而忽略结果变化。第04节如果迁移到新环境,先重做固件二进制分析的最小验证,再决定配置验收是否需要调整。