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')"

验证方法

  1. 准备一个包含已知文件的 FV 镜像(可用 UEFI 固件工具生成或从真实固件提取)。
  2. 使用 fv_extract 工具,传入目标文件的 GUID(16字节原始数据)。
  3. 程序输出匹配文件的类型、大小,并生成 extracted_*.bin 文件。
  4. 通过条件:输出文件大小与 FV 中记录的文件数据长度一致,且文件内容可用十六进制工具检查,与预期相符。

关键说明

  • FFS 文件头中 Length 字段为 24 位,需与 Attributes 高位配合获取完整长度;本实现按标准 FV 格式处理。
  • 文件 GUID 在 FFS 头中按 EFI 规范以小端序存储,print_guid 已做字节序转换以便阅读。
  • 若 FV 包含扩展头(ExtHeaderOffset 非零),本工具跳过扩展头直接扫描 FFS 文件,适用于标准 FV 布局。
  • 实际固件中 FV 可能包含填充区域(0xFF),扫描遇到无效状态即停止,符合 FV 规范。