把固件二进制分析落到固件二进制分析的触发条件、可观察状态和复查材料上,先判断问题从哪里收窄。
在工业控制单片机固件逆向工程中,第一步往往是识别固件中嵌入的文件系统。面对一个二进制固件,我们通常不知道其内部结构。本节将演示如何使用 Binwalk 扫描固件,定位文件系统边界,并针对厂商自定义的 LZSS(Lempel-Ziv-Storer-Szymanski) 压缩段编写解压脚本。核心目标是掌握从“黑盒”二进制到可分析文件系统的完整链路
拿到固件后,不要直接拖入反汇编器。先用 binwalk -Me firmware.bin 自动扫描并提取已知签名。如果发现未知压缩段,再手动分析熵值变化,定位私有算法边界
固件二进制分析落地前要同时核对固件二进制分析的文件系统扫描、失败路径和自定义。
固件二进制分析用固件二进制分析、文件系统扫描、自定义连接到固件二进制分析的调试、验证和交付动作。
工业控制单片机固件通常包含引导加载程序、主程序、文件系统和配置参数。文件系统可能是 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 -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 变种通常修改了字典大小、匹配长度或标志位格式。通过逆向分析固件中的解压函数(位于 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节如果迁移到新环境,先重做固件二进制分析的最小验证,再决定配置验收是否需要调整。