4. 解析UEFI固件卷
任务目标
编写一个Python工具,解析UEFI固件镜像中的固件卷(Firmware Volume, FV),提取其中的文件系统信息,包括FV头、文件系统GUID、FV大小以及FV内包含的文件列表。
UEFI固件卷结构概述
UEFI固件镜像由固件卷组成,每个FV以EFI_FIRMWARE_VOLUME_HEADER结构开始(共0x48字节),关键字段如下:
- ZeroVector(偏移0x00,16字节):固定为0
- FileSystemGuid(偏移0x10,16字节):标识FV内文件系统类型,标准FFS为
7A9354D9-0468-444A-81CE-0BF617D890DF - FvLength(偏移0x28,8字节):FV总大小
- Signature(偏移0x38,4字节):固定为
_FVH(0x4856465F) - HeaderLength(偏移0x48,2字节):FV头长度,通常为0x48
FV头之后是文件(EFI_FFS_FILE_HEADER),每个文件头0x18字节,包含文件GUID、类型、大小(3字节,含头)和状态。文件按8字节对齐,文件之间可能存在填充(0xFF)。
完整实现代码
import struct
import uuid
import sys
# 标准FFS文件系统GUID
FFS_GUID = uuid.UUID('7A9354D9-0468-444A-81CE-0BF617D890DF')
FVH_SIGNATURE = 0x4856465F # '_FVH'
# FFS文件类型
FILE_TYPES = {
0x01: 'RAW',
0x02: 'FREEFORM',
0x03: 'SECURITY_CORE',
0x04: 'PEI_CORE',
0x05: 'DXE_CORE',
0x06: 'PEIM',
0x07: 'DRIVER',
0x08: 'COMBINED_PEIM_DRIVER',
0x09: 'APPLICATION',
0x0A: 'FIRMWARE_VOLUME_IMAGE',
0x0B: 'COMBINED_SMM_DXE',
0x0C: 'SMM',
0x0D: 'FIRMWARE_VOLUME_IMAGE',
0x0E: 'SMM_CORE',
0x0F: 'MM_CORE',
0x10: 'MM_STANDALONE',
0x11: 'MM_CORE_STANDALONE',
0x12: 'ACPI',
0x13: 'SMBIOS',
0x14: 'RAW',
0xE0: 'FFS_PAD',
0xFE: 'FFS_HEADER_EXTENSION'
}
def parse_fv(data, fv_offset, fv_length):
"""解析单个固件卷,返回文件列表"""
fv_data = data[fv_offset:fv_offset + fv_length]
if len(fv_data) < 0x48:
return None, 'FV头不足0x48字节'
# 解析FV头
zero = fv_data[0:16]
fs_guid = uuid.UUID(bytes_le=fv_data[0x10:0x20])
fv_len = struct.unpack_from('<Q', fv_data, 0x28)[0]
sig = struct.unpack_from('<I', fv_data, 0x38)[0]
header_len = struct.unpack_from('<H', fv_data, 0x48)[0]
if sig != FVH_SIGNATURE:
return None, f'签名错误: 0x{sig:08X}'
if fv_len != fv_length:
return None, f'FV长度不匹配: 头声明{fv_len}, 实际{fv_length}'
files = []
offset = header_len
while offset + 0x18 <= fv_len:
# 检查是否为填充字节
if fv_data[offset] == 0xFF and fv_data[offset+1] == 0xFF:
# 跳过填充到8字节对齐
offset = (offset + 7) & ~7
continue
# 解析FFS文件头
file_guid = uuid.UUID(bytes_le=fv_data[offset:offset+16])
file_type = fv_data[offset+0x11]
size_3bytes = fv_data[offset+0x14:offset+0x17]
file_size = size_3bytes[0] | (size_3bytes[1] << 8) | (size_3bytes[2] << 16)
state = fv_data[offset+0x17]
if file_size == 0 or file_size > fv_len - offset:
break # 无效大小,停止解析
files.append({
'offset': fv_offset + offset,
'guid': str(file_guid),
'type': FILE_TYPES.get(file_type, f'未知(0x{file_type:02X})'),
'size': file_size,
'state': f'0x{state:02X}'
})
# 下一个文件按8字节对齐
offset += (file_size + 7) & ~7
return files, None
def main(image_path):
with open(image_path, 'rb') as f:
data = f.read()
print(f'镜像大小: {len(data)} 字节 (0x{len(data):X})')
print('=' * 70)
# 扫描整个镜像查找FV签名
found_any = False
pos = 0
while pos < len(data) - 0x48:
# 检查签名 '_FVH' (小端)
if data[pos+0x38:pos+0x3C] == b'_FVH':
# 读取FV长度
fv_len = struct.unpack_from('<Q', data, pos + 0x28)[0]
if fv_len > 0 and pos + fv_len <= len(data):
files, err = parse_fv(data, pos, fv_len)
if err:
print(f'偏移 0x{pos:X}: 解析失败 - {err}')
else:
found_any = True
fs_guid = uuid.UUID(bytes_le=data[pos+0x10:pos+0x20])
print(f'\n固件卷 @ 偏移 0x{pos:X}')
print(f' 文件系统GUID: {fs_guid}')
print(f' FV大小: {fv_len} 字节 (0x{fv_len:X})')
print(f' 头长度: {struct.unpack_from("<H", data, pos+0x48)[0]}')
print(f' 文件数量: {len(files)}')
if files:
print(f' {"偏移":>10} {"类型":<24} {"大小":>8} GUID')
print(' ' + '-' * 80)
for f in files:
print(f' 0x{f["offset"]:>8X} {f["type"]:<24} {f["size"]:>8} {f["guid"]}')
pos += fv_len # 跳到下一个FV
continue
pos += 1
if not found_any:
print('未找到有效的固件卷。')
if __name__ == '__main__':
if len(sys.argv) != 2:
print(f'用法: python {sys.argv[0]} <固件镜像文件>')
sys.exit(1)
main(sys.argv[1])
运行方式
python parse_fv.py firmware.bin
验证方法
- 准备一个包含UEFI固件卷的镜像文件(可从主板BIOS更新包或
OVMF.fd中获取)。 - 运行上述命令,观察输出:
- 程序会扫描整个镜像,找到所有签名正确且长度有效的固件卷;
- 每个FV应显示文件系统GUID(标准FFS为
7a9354d9-0468-444a-81ce-0bf617d890df)、FV大小和文件列表; - 文件列表中每个文件应包含偏移、类型(如
PEI_CORE、DXE_CORE、DRIVER等)、大小和GUID。
通过条件:程序能正确识别至少一个FV,且解析出的文件类型和大小与用UEFITool等已知工具打开同一镜像的结果一致。
常见问题
- 签名正确但解析失败:可能是FV头中的长度字段与实际数据不符,或镜像包含嵌套FV(FV内嵌FV镜像文件),此时外层解析会停止,内嵌FV需递归处理。
- 文件类型显示“未知”:某些固件使用非标准文件类型值,不影响解析正确性。
- 填充字节处理:FFS规范要求文件间填充0xFF并8字节对齐,代码已处理此情况。