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>
验证方法:
- 准备一个包含EFI文件的固件卷二进制文件(可从UEFI固件中提取,或用UEFITool导出)。
- 运行脚本,观察输出中每个文件的类型、大小、状态字段是否与已知固件内容相符。
- 校验和标记为
OK表示计算值与文件头存储值一致;若出现BAD,说明文件可能已损坏或校验和算法不匹配(某些平台使用其他校验规则)。 - 构造一个已知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),表示文件有效且未被删除。