3. 固件加密与解密初步探索

任务说明

本案例面向一份未知格式的固件文件,目标是判断其是否经过加密或压缩处理,并尝试通过已知密钥或固件自身漏洞恢复明文内容。完成本案例后,应能独立对类似固件执行初步的加密识别与解密尝试。

前置准备

  • 操作系统:Linux(推荐Ubuntu 22.04+)或WSL
  • 工具依赖:binutils、file、hexdump、openssl、python3(含pycryptodome库)
  • 安装命令:sudo apt install binutils file openssl python3-pip && pip3 install pycryptodome

固件样本特征

本案例使用一个模拟IoT设备固件样本(fw_sample.bin),其头部为64字节自定义引导头,之后为密文数据区。已知该设备厂商惯用AES-128-CBC加密固件,密钥为设备MAC地址后16字节的MD5值,IV固定为全零。引导头偏移0x00处为2字节魔数0x5AA5,偏移0x04处为4字节明文长度(小端序)。

加密识别流程

  1. 使用file fw_sample.bin查看文件类型,若显示data而非已知文件系统/内核镜像,则高度怀疑加密或压缩。
  2. 执行hexdump -C fw_sample.bin | head -20检查头部:若头部存在可读ASCII字符串(如"uboot"、"kernel"等)但后续数据熵值极高(无明显ASCII),则判定为部分加密或整体加密。
  3. 计算文件熵值:若整个文件熵值接近8.0(最大熵),则极可能已加密或压缩;若存在明显低熵区域(如文件系统魔数、字符串表),则可能未加密。

解密实现

以下Python脚本实现完整解密流程:解析引导头、提取密钥、AES-CBC解密、输出明文文件。

#!/usr/bin/env python3
import hashlib
import struct
from Crypto.Cipher import AES

def parse_header(data):
    """解析固件头部,返回明文长度"""
    if len(data) < 8:
        raise ValueError("文件过短,无法解析头部")
    magic = struct.unpack_from('<H', data, 0)[0]
    if magic != 0x5AA5:
        raise ValueError(f"魔数错误: 0x{magic:04X}, 期望0x5AA5")
    plain_len = struct.unpack_from('<I', data, 4)[0]
    return plain_len

def derive_key(mac_addr):
    """由MAC地址派生AES-128密钥"""
    mac_clean = mac_addr.replace(':', '').replace('-', '').lower()
    if len(mac_clean) != 12:
        raise ValueError("MAC地址格式错误")
    md5 = hashlib.md5(mac_clean.encode()).digest()
    return md5  # AES-128密钥为16字节

def decrypt_firmware(input_path, output_path, mac_addr):
    with open(input_path, 'rb') as f:
        data = f.read()
    
    plain_len = parse_header(data)
    ciphertext = data[64:]  # 跳过64字节引导头
    
    key = derive_key(mac_addr)
    iv = b'\x00' * 16  # 固定IV
    
    cipher = AES.new(key, AES.MODE_CBC, iv)
    # 密文长度应为16的倍数,且不小于明文长度
    padded_len = (plain_len + 15) // 16 * 16
    if len(ciphertext) < padded_len:
        raise ValueError("密文长度不足")
    
    decrypted = cipher.decrypt(ciphertext[:padded_len])
    plaintext = decrypted[:plain_len]  # 去除PKCS7填充
    
    with open(output_path, 'wb') as f:
        f.write(plaintext)
    print(f"解密成功: {output_path} ({plain_len} 字节)")

if __name__ == '__main__':
    # 示例:MAC地址为 00:11:22:33:44:55
    decrypt_firmware('fw_sample.bin', 'fw_plain.bin', '00:11:22:33:44:55')

运行与验证

  1. 将上述脚本保存为decrypt_fw.py,与fw_sample.bin置于同一目录。
  2. 执行python3 decrypt_fw.py,观察输出是否提示解密成功。
  3. 验证解密结果:file fw_plain.bin应显示为Linux kernel ARM boot executable zImage或类似已知镜像类型(本样本为ARM Linux内核)。
  4. 进一步验证:hexdump -C fw_plain.bin | head -5应看到内核头部魔数(如0x016f2818)或可读字符串。

通过条件:解密后文件能被file命令正确识别为内核镜像,且头部魔数与预期一致,即判定解密成功。

常见问题

  • 熵值高但解密失败:可能密钥错误或加密模式不是CBC,尝试ECB模式或检查IV是否为前16字节密文。
  • 明文长度异常:引导头偏移可能不同,尝试扫描0x5AA5魔数定位真实头部。
  • 填充错误:部分固件使用Zero Padding而非PKCS7,解密后直接截断至明文长度即可。