3. 提取特定 GUID 的文件
任务目标
在固件卷(FV)中,根据指定的文件 GUID,定位并提取对应的 EFI 文件内容。本案例实现一个命令行工具,输入 FV 镜像文件路径与目标 GUID,输出匹配文件的内容到磁盘。
核心实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#pragma pack(push, 1)
typedef struct {
uint8_t ZeroVector[16];
uint32_t FileSystemGuid[4];
uint64_t FvLength;
uint32_t Signature;
uint32_t Attributes;
uint16_t HeaderLength;
uint16_t Checksum;
uint8_t ExtHeaderOffset;
uint8_t Reserved;
uint8_t Revision;
} FIRMWARE_VOLUME_HEADER;
typedef struct {
uint8_t Name[16];
uint32_t Offset;
uint32_t Length;
uint32_t State;
uint32_t Type;
uint32_t Attributes;
} EFI_FFS_FILE_HEADER;
#pragma pack(pop)
static int guid_match(const uint8_t *a, const uint8_t *b) {
return memcmp(a, b, 16) == 0;
}
static void print_guid(const uint8_t *g) {
printf("%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X\n",
g[3], g[2], g[1], g[0], g[5], g[4], g[7], g[6],
g[8], g[9], g[10], g[11], g[12], g[13], g[14], g[15]);
}
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "用法: %s <FV镜像文件> <目标GUID(16字节原始数据)>\n", argv[0]);
return 1;
}
FILE *fp = fopen(argv[1], "rb");
if (!fp) { perror("打开文件失败"); return 1; }
FIRMWARE_VOLUME_HEADER fv;
if (fread(&fv, sizeof(fv), 1, fp) != 1) {
fprintf(stderr, "读取FV头失败\n"); fclose(fp); return 1;
}
if (fv.Signature != 0x4856465A) { // '_FVH'
fprintf(stderr, "不是有效的固件卷镜像\n"); fclose(fp); return 1;
}
uint16_t header_len = fv.HeaderLength;
uint64_t fv_len = fv.FvLength;
uint8_t *target_guid = (uint8_t *)argv[2];
// 定位第一个FFS文件
fseek(fp, header_len, SEEK_SET);
uint32_t offset = header_len;
int found = 0;
while (offset + sizeof(EFI_FFS_FILE_HEADER) <= fv_len) {
EFI_FFS_FILE_HEADER ffs;
fseek(fp, offset, SEEK_SET);
if (fread(&ffs, sizeof(ffs), 1, fp) != 1) break;
// 检查文件头有效性(State位)
if ((ffs.State & 0x03) == 0) break; // 无效状态,结束
uint32_t file_len = ffs.Length & 0xFFFFFF;
if (file_len == 0) break;
if (guid_match(ffs.Name, target_guid)) {
// 提取文件内容(跳过文件头)
uint32_t data_offset = offset + sizeof(EFI_FFS_FILE_HEADER);
uint32_t data_len = file_len - sizeof(EFI_FFS_FILE_HEADER);
// 对齐到8字节
uint32_t padded = (file_len + 7) & ~7;
if (data_len > padded - sizeof(EFI_FFS_FILE_HEADER))
data_len = padded - sizeof(EFI_FFS_FILE_HEADER);
char out_name[64];
snprintf(out_name, sizeof(out_name), "extracted_%08X.bin",
*(uint32_t *)&target_guid[0]);
FILE *out = fopen(out_name, "wb");
if (!out) { perror("创建输出文件失败"); fclose(fp); return 1; }
fseek(fp, data_offset, SEEK_SET);
uint8_t buf[4096];
uint32_t remaining = data_len;
while (remaining > 0) {
size_t chunk = remaining > sizeof(buf) ? sizeof(buf) : remaining;
if (fread(buf, 1, chunk, fp) != chunk) break;
fwrite(buf, 1, chunk, out);
remaining -= (uint32_t)chunk;
}
fclose(out);
printf("找到文件 GUID: ");
print_guid(ffs.Name);
printf("类型: 0x%02X, 大小: %u 字节\n", ffs.Type, data_len);
printf("已提取到: %s\n", out_name);
found = 1;
break;
}
// 下一个文件(8字节对齐)
offset += (file_len + 7) & ~7;
}
if (!found) {
printf("未找到匹配 GUID 的文件\n");
printf("目标 GUID: ");
print_guid(target_guid);
}
fclose(fp);
return found ? 0 : 1;
}
编译与运行
# 编译
gcc -o fv_extract fv_extract.c
# 运行(GUID 以16字节原始二进制传入)
./fv_extract firmware.fv "$(printf '\xXX\xXX...\xXX')"
验证方法
- 准备一个包含已知文件的 FV 镜像(可用 UEFI 固件工具生成或从真实固件提取)。
- 使用
fv_extract工具,传入目标文件的 GUID(16字节原始数据)。 - 程序输出匹配文件的类型、大小,并生成
extracted_*.bin文件。 - 通过条件:输出文件大小与 FV 中记录的文件数据长度一致,且文件内容可用十六进制工具检查,与预期相符。
关键说明
- FFS 文件头中
Length字段为 24 位,需与Attributes高位配合获取完整长度;本实现按标准 FV 格式处理。 - 文件 GUID 在 FFS 头中按 EFI 规范以小端序存储,
print_guid已做字节序转换以便阅读。 - 若 FV 包含扩展头(
ExtHeaderOffset非零),本工具跳过扩展头直接扫描 FFS 文件,适用于标准 FV 布局。 - 实际固件中 FV 可能包含填充区域(0xFF),扫描遇到无效状态即停止,符合 FV 规范。