4. 解析固件文件头

任务目标

编写一个独立可运行的解析器,读取固件卷(FV)中的EFI文件原始字节,解析其EFI_FILE_HEADER结构,提取并输出文件状态(State)、文件类型(Type)、文件大小(Size)以及头部校验和与数据校验和。

核心结构定义

EFI文件头共32字节,布局如下(与EFI_FILE_HEADER一致):

偏移  长度  字段
0x00  16   Name(GUID,此处不展开解析)
0x10  4    IntegrityHeader(UINT32,保留)
0x14  4    Type(UINT32)
0x18  4    Attributes(UINT32)
0x1C  4    Size(UINT32,含头)
0x20  2    State(UINT16)
0x22  1    HeaderChecksum(UINT8)
0x23  1    FileChecksum(UINT8)
0x24  8    保留

文件总大小以Size字段为准,且按8字节对齐。校验和算法:HeaderChecksum为文件头前32字节(含校验和字段本身,计算时置0)的8位累加和取反;FileChecksum为整个文件(含头,计算时两个校验和字段均置0)的8位累加和取反。

完整实现代码

以下Python脚本直接读取FV二进制文件,扫描其中所有EFI文件并解析头部信息:

import struct
import sys

def parse_efi_file(data, offset):
    """解析单个EFI文件头,返回(信息字典, 文件结束偏移)或None"""
    if offset + 0x28 > len(data):
        return None
    header = data[offset:offset+0x28]
    name_guid = header[0:16]
    type_val = struct.unpack_from('<I', header, 0x14)[0]
    attrs = struct.unpack_from('<I', header, 0x18)[0]
    size = struct.unpack_from('<I', header, 0x1C)[0]
    state = struct.unpack_from('<H', header, 0x20)[0]
    hdr_cksum = header[0x22]
    file_cksum = header[0x23]

    # 校验和验证(计算时校验和字段置0)
    hdr_copy = bytearray(header)
    hdr_copy[0x22] = 0
    hdr_copy[0x23] = 0
    calc_hdr = (~sum(hdr_copy) & 0xFF)
    calc_file = 0
    if offset + size <= len(data):
        file_copy = bytearray(data[offset:offset+size])
        file_copy[0x22] = 0
        file_copy[0x23] = 0
        calc_file = (~sum(file_copy) & 0xFF)

    # 状态位解析(低4位有效)
    state_names = []
    if state & 0x01: state_names.append('VALID')
    if state & 0x02: state_names.append('MARKED_ERASE')
    if state & 0x04: state_names.append('DELETED')
    if state & 0x08: state_names.append('HEADER_VALID')
    if not state_names:
        state_names.append('EMPTY/UNKNOWN')

    info = {
        'offset': offset,
        'guid': name_guid.hex(),
        'type': type_val,
        'attrs': attrs,
        'size': size,
        'state_raw': state,
        'state': '|'.join(state_names),
        'hdr_cksum': hdr_cksum,
        'file_cksum': file_cksum,
        'hdr_cksum_ok': (hdr_cksum == calc_hdr),
        'file_cksum_ok': (file_cksum == calc_file) if offset + size <= len(data) else None,
    }
    # 文件按8字节对齐
    next_off = offset + ((size + 7) & ~7)
    return info, next_off

def scan_firmware_volume(fv_path):
    with open(fv_path, 'rb') as f:
        data = f.read()

    # 跳过FV头(假设标准FV头为0x48字节,含扩展头则需额外处理)
    # 此处从偏移0x48开始扫描,适用于无扩展头的标准FV
    offset = 0x48
    files = []
    while offset + 0x28 <= len(data):
        # 检查是否为全0xFF(空白区域)
        if data[offset] == 0xFF and data[offset+1] == 0xFF:
            break
        result = parse_efi_file(data, offset)
        if result is None:
            break
        info, next_off = result
        files.append(info)
        if next_off <= offset:  # 防止死循环
            break
        offset = next_off
    return files

if __name__ == '__main__':
    if len(sys.argv) != 2:
        print('用法: python fv_parser.py <固件卷文件>')
        sys.exit(1)
    files = scan_firmware_volume(sys.argv[1])
    print(f'共发现 {len(files)} 个EFI文件:')
    for f in files:
        print(f'偏移0x{f["offset"]:08X} | GUID {f["guid"]} | 类型0x{f["type"]:02X} | '
              f'大小0x{f["size"]:X} | 状态0x{f["state_raw"]:02X}({f["state"]}) | '
              f'头校验和0x{f["hdr_cksum"]:02X}{"(OK)" if f["hdr_cksum_ok"] else "(BAD)"} | '
              f'文件校验和0x{f["file_cksum"]:02X}{"(OK)" if f["file_cksum_ok"] else "(BAD)"}')

运行与验证

依赖:仅需Python 3.6+,无第三方库。

运行方式:

python fv_parser.py <你的固件卷文件.bin>

验证方法:

  1. 准备一个包含EFI文件的固件卷二进制文件(可从UEFI固件中提取,或用UEFITool导出)。
  2. 运行脚本,观察输出中每个文件的类型、大小、状态字段是否与已知固件内容相符。
  3. 校验和标记为OK表示计算值与文件头存储值一致;若出现BAD,说明文件可能已损坏或校验和算法不匹配(某些平台使用其他校验规则)。
  4. 构造一个已知EFI文件(如DXE驱动)的二进制数据,手动修改一个字节后重新运行,确认校验和变为BAD,验证检测能力。

预期输出示例(输入为包含2个文件的标准FV):

共发现 2 个EFI文件:
偏移0x00000048 | GUID 5b1b9a0a... | 类型0x0B | 大小0x1000 | 状态0x0F(VALID|MARKED_ERASE|DELETED|HEADER_VALID) | 头校验和0x2A(OK) | 文件校验和0x1C(OK)
偏移0x00001048 | GUID 8c8c8c8c... | 类型0x09 | 大小0x800 | 状态0x0F(VALID|MARKED_ERASE|DELETED|HEADER_VALID) | 头校验和0x15(OK) | 文件校验和0xE3(OK)

注意:上述输出为格式示例,实际GUID和校验和值取决于输入文件内容。

常见问题

  • 扫描起始偏移:脚本默认从0x48开始扫描,适用于标准48字节FV头且无扩展头的情况。若FV头含扩展头(ExtHeaderOffset非零),需先读取FV头中的扩展头偏移量并跳过。
  • 文件类型值:常见类型包括0x01(RAW)、0x02(FREEFORM)、0x07(PEI)、0x09(DXE)、0x0B(PEI_DXE_COMBO)等,可按需映射为名称。
  • 状态字段:实际固件中状态通常为0x0F(所有位均置1),表示文件有效且未被删除。