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字节明文长度(小端序)。
加密识别流程
- 使用
file fw_sample.bin查看文件类型,若显示data而非已知文件系统/内核镜像,则高度怀疑加密或压缩。 - 执行
hexdump -C fw_sample.bin | head -20检查头部:若头部存在可读ASCII字符串(如"uboot"、"kernel"等)但后续数据熵值极高(无明显ASCII),则判定为部分加密或整体加密。 - 计算文件熵值:若整个文件熵值接近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')
运行与验证
- 将上述脚本保存为
decrypt_fw.py,与fw_sample.bin置于同一目录。 - 执行
python3 decrypt_fw.py,观察输出是否提示解密成功。 - 验证解密结果:
file fw_plain.bin应显示为Linux kernel ARM boot executable zImage或类似已知镜像类型(本样本为ARM Linux内核)。 - 进一步验证:
hexdump -C fw_plain.bin | head -5应看到内核头部魔数(如0x016f2818)或可读字符串。
通过条件:解密后文件能被file命令正确识别为内核镜像,且头部魔数与预期一致,即判定解密成功。
常见问题
- 熵值高但解密失败:可能密钥错误或加密模式不是CBC,尝试ECB模式或检查IV是否为前16字节密文。
- 明文长度异常:引导头偏移可能不同,尝试扫描0x5AA5魔数定位真实头部。
- 填充错误:部分固件使用Zero Padding而非PKCS7,解密后直接截断至明文长度即可。