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

验证方法

  1. 准备一个包含UEFI固件卷的镜像文件(可从主板BIOS更新包或OVMF.fd中获取)。
  2. 运行上述命令,观察输出:
  • 程序会扫描整个镜像,找到所有签名正确且长度有效的固件卷;
  • 每个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字节对齐,代码已处理此情况。