Windows PE结构

6、资源表解析

1、课程目标

  1. 理解PE资源的组织结构
  2. 掌握资源表的三级目录结构
  3. 学会提取和修改PE资源
  4. 了解资源在木马开发中的应用

2、名词解释

名词 全称 解释
Resource Table 资源表 存储程序资源的数据结构
Resource Directory 资源目录 资源的层级结构
RT_ICON - 图标资源类型
RT_BITMAP - 位图资源类型
RT_STRING - 字符串资源类型
RT_VERSION - 版本信息资源
RT_MANIFEST - 清单资源
RT_RCDATA - 原始数据资源

3、使用工具

  • Resource Hacker
  • CFF Explorer
  • PE-bear
  • ResEdit

4、技术原理

4.1、资源表三级结构

资源表结构(三级目录):

Level 1: 资源类型(Type)
    |
    +-- RT_ICON (3)
    |       |
    +-- RT_BITMAP (2)
    |       |
    +-- RT_VERSION (16)
            |
Level 2: 资源名称/ID(Name/ID)
    |
    +-- ID: 1
    |       |
    +-- ID: 2
    |       |
    +-- Name: "MYICON"
            |
Level 3: 资源语言(Language)
    |
    +-- 0x0409 (English-US)
    |       |
    +-- 0x0804 (Chinese-Simplified)
            |
            v
    IMAGE_RESOURCE_DATA_ENTRY
            |
            v
        实际资源数据

4.2、资源目录结构

// 资源目录
typedef struct _IMAGE_RESOURCE_DIRECTORY {
    DWORD   Characteristics;        // 特征(通常为0)
    DWORD   TimeDateStamp;          // 时间戳
    WORD    MajorVersion;           // 主版本
    WORD    MinorVersion;           // 次版本
    WORD    NumberOfNamedEntries;   // 命名条目数
    WORD    NumberOfIdEntries;      // ID条目数
    // 后跟 IMAGE_RESOURCE_DIRECTORY_ENTRY 数组
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;

// 资源目录条目
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
    union {
        struct {
            DWORD NameOffset:31;    // 名称偏移(相对资源段)
            DWORD NameIsString:1;   // 1=名称,0=ID
        };
        DWORD   Name;
        WORD    Id;                 // 资源ID
    };
    union {
        DWORD   OffsetToData;       // 数据条目偏移
        struct {
            DWORD OffsetToDirectory:31;  // 子目录偏移
            DWORD DataIsDirectory:1;     // 1=目录,0=数据
        };
    };
} IMAGE_RESOURCE_DIRECTORY_ENTRY;

// 资源数据条目
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
    DWORD   OffsetToData;   // 数据RVA
    DWORD   Size;           // 数据大小
    DWORD   CodePage;       // 代码页
    DWORD   Reserved;       // 保留
} IMAGE_RESOURCE_DATA_ENTRY;

// 资源名称字符串
typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
    WORD    Length;         // 字符数(非字节数)
    WCHAR   NameString[1];  // Unicode字符串
} IMAGE_RESOURCE_DIR_STRING_U;

4.3、标准资源类型

#define RT_CURSOR           1
#define RT_BITMAP           2
#define RT_ICON             3
#define RT_MENU             4
#define RT_DIALOG           5
#define RT_STRING           6
#define RT_FONTDIR          7
#define RT_FONT             8
#define RT_ACCELERATOR      9
#define RT_RCDATA           10
#define RT_MESSAGETABLE     11
#define RT_GROUP_CURSOR     12
#define RT_GROUP_ICON       14
#define RT_VERSION          16
#define RT_DLGINCLUDE       17
#define RT_PLUGPLAY         19
#define RT_VXD              20
#define RT_ANICURSOR        21
#define RT_ANIICON          22
#define RT_HTML             23
#define RT_MANIFEST         24

5、代码实现

5.1、解析资源表

#include <windows.h>
#include <stdio.h>

// 资源基地址(用于计算偏移)
LPBYTE g_ResourceBase = NULL;

// 获取资源类型名称
const char* GetResourceTypeName(DWORD type) {
    switch (type) {
        case 1:  return "RT_CURSOR";
        case 2:  return "RT_BITMAP";
        case 3:  return "RT_ICON";
        case 4:  return "RT_MENU";
        case 5:  return "RT_DIALOG";
        case 6:  return "RT_STRING";
        case 7:  return "RT_FONTDIR";
        case 8:  return "RT_FONT";
        case 9:  return "RT_ACCELERATOR";
        case 10: return "RT_RCDATA";
        case 11: return "RT_MESSAGETABLE";
        case 12: return "RT_GROUP_CURSOR";
        case 14: return "RT_GROUP_ICON";
        case 16: return "RT_VERSION";
        case 17: return "RT_DLGINCLUDE";
        case 19: return "RT_PLUGPLAY";
        case 20: return "RT_VXD";
        case 21: return "RT_ANICURSOR";
        case 22: return "RT_ANIICON";
        case 23: return "RT_HTML";
        case 24: return "RT_MANIFEST";
        default: return "UNKNOWN";
    }
}

// 获取资源名称
void GetResourceName(PIMAGE_RESOURCE_DIRECTORY_ENTRY entry, 
                     char* buffer, int bufferSize) {
    if (entry->NameIsString) {
        // 名称是字符串
        PIMAGE_RESOURCE_DIR_STRING_U nameStr = (PIMAGE_RESOURCE_DIR_STRING_U)(
            g_ResourceBase + entry->NameOffset
        );
        
        // 转换Unicode到ANSI
        WideCharToMultiByte(CP_ACP, 0, nameStr->NameString, nameStr->Length,
                           buffer, bufferSize, NULL, NULL);
        buffer[nameStr->Length] = '\0';
    } else {
        // 名称是ID
        sprintf(buffer, "#%d", entry->Id);
    }
}

// 解析资源数据条目
void ParseResourceDataEntry(PIMAGE_RESOURCE_DATA_ENTRY dataEntry, 
                            int indent) {
    char indentStr[64];
    memset(indentStr, ' ', indent * 2);
    indentStr[indent * 2] = '\0';
    
    printf("%sData RVA: 0x%08X, Size: 0x%08X (%d bytes)\n",
           indentStr, dataEntry->OffsetToData, dataEntry->Size, dataEntry->Size);
    printf("%sCodePage: %d\n", indentStr, dataEntry->CodePage);
}

// 递归解析资源目录
void ParseResourceDirectory(PIMAGE_RESOURCE_DIRECTORY dir, 
                            int level, 
                            const char* parentName) {
    char indent[64];
    memset(indent, ' ', level * 2);
    indent[level * 2] = '\0';
    
    int totalEntries = dir->NumberOfNamedEntries + dir->NumberOfIdEntries;
    
    printf("%sDirectory: Named=%d, ID=%d, Total=%d\n",
           indent, dir->NumberOfNamedEntries, dir->NumberOfIdEntries, totalEntries);
    
    // 条目紧跟在目录结构后面
    PIMAGE_RESOURCE_DIRECTORY_ENTRY entries = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)(dir + 1);
    
    for (int i = 0; i < totalEntries; i++) {
        PIMAGE_RESOURCE_DIRECTORY_ENTRY entry = &entries[i];
        
        char name[256];
        GetResourceName(entry, name, sizeof(name));
        
        // 第一级是类型
        if (level == 0) {
            if (!entry->NameIsString) {
                printf("%s[%d] Type: %s (%s)\n", 
                       indent, i, name, GetResourceTypeName(entry->Id));
            } else {
                printf("%s[%d] Type: %s (Custom)\n", indent, i, name);
            }
        }
        // 第二级是名称/ID
        else if (level == 1) {
            printf("%s[%d] Name/ID: %s\n", indent, i, name);
        }
        // 第三级是语言
        else if (level == 2) {
            printf("%s[%d] Language: 0x%04X\n", indent, i, entry->Id);
        }
        
        // 检查是目录还是数据
        if (entry->DataIsDirectory) {
            // 子目录
            PIMAGE_RESOURCE_DIRECTORY subDir = (PIMAGE_RESOURCE_DIRECTORY)(
                g_ResourceBase + entry->OffsetToDirectory
            );
            ParseResourceDirectory(subDir, level + 1, name);
        } else {
            // 数据条目
            PIMAGE_RESOURCE_DATA_ENTRY dataEntry = (PIMAGE_RESOURCE_DATA_ENTRY)(
                g_ResourceBase + entry->OffsetToData
            );
            ParseResourceDataEntry(dataEntry, level + 1);
        }
    }
}

// 解析资源表
void ParseResourceTable(LPVOID fileData) {
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)fileData;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(
        (PBYTE)fileData + dosHeader->e_lfanew
    );
    
    PIMAGE_DATA_DIRECTORY rsrcDir = &ntHeaders->OptionalHeader
                                    .DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE];
    
    if (rsrcDir->VirtualAddress == 0) {
        printf("[-] 没有资源表\n");
        return;
    }
    
    printf("\n========== Resource Table ==========\n");
    printf("RVA: 0x%08X, Size: 0x%08X\n\n", 
           rsrcDir->VirtualAddress, rsrcDir->Size);
    
    DWORD rsrcFoa = RvaToFoa(fileData, rsrcDir->VirtualAddress);
    g_ResourceBase = (LPBYTE)fileData + rsrcFoa;
    
    PIMAGE_RESOURCE_DIRECTORY rootDir = (PIMAGE_RESOURCE_DIRECTORY)g_ResourceBase;
    
    ParseResourceDirectory(rootDir, 0, "ROOT");
}

5.2、提取资源

// 资源信息
typedef struct _RESOURCE_ENTRY {
    DWORD   type;
    DWORD   id;
    WCHAR   name[256];
    WORD    language;
    DWORD   dataRva;
    DWORD   dataSize;
    LPBYTE  data;
} RESOURCE_ENTRY;

// 查找特定资源
BOOL FindResource(LPVOID fileData, DWORD type, DWORD id, 
                  WORD language, RESOURCE_ENTRY* result) {
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)fileData;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(
        (PBYTE)fileData + dosHeader->e_lfanew
    );
    
    PIMAGE_DATA_DIRECTORY rsrcDir = &ntHeaders->OptionalHeader
                                    .DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE];
    
    if (rsrcDir->VirtualAddress == 0) {
        return FALSE;
    }
    
    DWORD rsrcFoa = RvaToFoa(fileData, rsrcDir->VirtualAddress);
    LPBYTE rsrcBase = (LPBYTE)fileData + rsrcFoa;
    
    // 第一级:类型
    PIMAGE_RESOURCE_DIRECTORY typeDir = (PIMAGE_RESOURCE_DIRECTORY)rsrcBase;
    int totalTypes = typeDir->NumberOfNamedEntries + typeDir->NumberOfIdEntries;
    PIMAGE_RESOURCE_DIRECTORY_ENTRY typeEntries = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)(typeDir + 1);
    
    for (int i = 0; i < totalTypes; i++) {
        if (!typeEntries[i].NameIsString && typeEntries[i].Id == type) {
            // 找到类型
            if (!typeEntries[i].DataIsDirectory) continue;
            
            // 第二级:名称/ID
            PIMAGE_RESOURCE_DIRECTORY idDir = (PIMAGE_RESOURCE_DIRECTORY)(
                rsrcBase + typeEntries[i].OffsetToDirectory
            );
            int totalIds = idDir->NumberOfNamedEntries + idDir->NumberOfIdEntries;
            PIMAGE_RESOURCE_DIRECTORY_ENTRY idEntries = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)(idDir + 1);
            
            for (int j = 0; j < totalIds; j++) {
                if (!idEntries[j].NameIsString && idEntries[j].Id == id) {
                    // 找到ID
                    if (!idEntries[j].DataIsDirectory) continue;
                    
                    // 第三级:语言
                    PIMAGE_RESOURCE_DIRECTORY langDir = (PIMAGE_RESOURCE_DIRECTORY)(
                        rsrcBase + idEntries[j].OffsetToDirectory
                    );
                    int totalLangs = langDir->NumberOfNamedEntries + langDir->NumberOfIdEntries;
                    PIMAGE_RESOURCE_DIRECTORY_ENTRY langEntries = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)(langDir + 1);
                    
                    for (int k = 0; k < totalLangs; k++) {
                        if (language == 0 || langEntries[k].Id == language) {
                            // 找到资源
                            PIMAGE_RESOURCE_DATA_ENTRY dataEntry = (PIMAGE_RESOURCE_DATA_ENTRY)(
                                rsrcBase + langEntries[k].OffsetToData
                            );
                            
                            result->type = type;
                            result->id = id;
                            result->language = langEntries[k].Id;
                            result->dataRva = dataEntry->OffsetToData;
                            result->dataSize = dataEntry->Size;
                            
                            DWORD dataFoa = RvaToFoa(fileData, dataEntry->OffsetToData);
                            result->data = (LPBYTE)fileData + dataFoa;
                            
                            return TRUE;
                        }
                    }
                }
            }
        }
    }
    
    return FALSE;
}

// 提取资源到文件
BOOL ExtractResource(LPVOID fileData, DWORD type, DWORD id, 
                     const char* outputPath) {
    RESOURCE_ENTRY entry;
    
    if (!FindResource(fileData, type, id, 0, &entry)) {
        printf("[-] 未找到资源: Type=%d, ID=%d\n", type, id);
        return FALSE;
    }
    
    printf("[+] 找到资源: Type=%d, ID=%d, Size=%d bytes\n",
           entry.type, entry.id, entry.dataSize);
    
    // 写入文件
    HANDLE hFile = CreateFileA(
        outputPath,
        GENERIC_WRITE,
        0,
        NULL,
        CREATE_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        NULL
    );
    
    if (hFile == INVALID_HANDLE_VALUE) {
        return FALSE;
    }
    
    DWORD written;
    WriteFile(hFile, entry.data, entry.dataSize, &written, NULL);
    CloseHandle(hFile);
    
    printf("[+] 资源已提取到: %s\n", outputPath);
    
    return TRUE;
}

// 提取图标组
BOOL ExtractIconGroup(LPVOID fileData, DWORD groupId, 
                      const char* outputPath) {
    // 首先获取GROUP_ICON资源
    RESOURCE_ENTRY groupEntry;
    if (!FindResource(fileData, 14, groupId, 0, &groupEntry)) { // RT_GROUP_ICON = 14
        return FALSE;
    }
    
    // 解析GRPICONDIR结构
    #pragma pack(push, 2)
    typedef struct {
        WORD idReserved;
        WORD idType;
        WORD idCount;
    } GRPICONDIR;
    
    typedef struct {
        BYTE bWidth;
        BYTE bHeight;
        BYTE bColorCount;
        BYTE bReserved;
        WORD wPlanes;
        WORD wBitCount;
        DWORD dwBytesInRes;
        WORD nID;
    } GRPICONDIRENTRY;
    #pragma pack(pop)
    
    GRPICONDIR* iconDir = (GRPICONDIR*)groupEntry.data;
    GRPICONDIRENTRY* entries = (GRPICONDIRENTRY*)(iconDir + 1);
    
    printf("[+] 图标组包含 %d 个图标\n", iconDir->idCount);
    
    // 创建ICO文件...
    // (需要将多个RT_ICON资源组合成ICO格式)
    
    return TRUE;
}

5.3、资源在木马开发中的应用

// 从资源加载ShellCode
BOOL LoadShellcodeFromResource(DWORD resourceId, LPVOID* shellcode, DWORD* size) {
    HRSRC hResource = FindResourceA(NULL, MAKEINTRESOURCEA(resourceId), RT_RCDATA);
    if (!hResource) {
        return FALSE;
    }
    
    HGLOBAL hGlobal = LoadResource(NULL, hResource);
    if (!hGlobal) {
        return FALSE;
    }
    
    *shellcode = LockResource(hGlobal);
    *size = SizeofResource(NULL, hResource);
    
    return TRUE;
}

// 从资源加载加密的PE
BOOL LoadEncryptedPEFromResource(DWORD resourceId, 
                                  const BYTE* key, 
                                  LPVOID* decrypted, 
                                  DWORD* size) {
    LPVOID encData;
    DWORD encSize;
    
    if (!LoadShellcodeFromResource(resourceId, &encData, &encSize)) {
        return FALSE;
    }
    
    // 分配解密缓冲区
    *decrypted = VirtualAlloc(NULL, encSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (!*decrypted) {
        return FALSE;
    }
    
    // XOR解密
    BYTE* src = (BYTE*)encData;
    BYTE* dst = (BYTE*)*decrypted;
    int keyLen = strlen((char*)key);
    
    for (DWORD i = 0; i < encSize; i++) {
        dst[i] = src[i] ^ key[i % keyLen];
    }
    
    *size = encSize;
    
    return TRUE;
}

// 修改版本信息
BOOL ModifyVersionInfo(LPVOID fileData, 
                       const char* companyName,
                       const char* productName,
                       const char* fileDescription) {
    RESOURCE_ENTRY entry;
    
    if (!FindResource(fileData, 16, 1, 0, &entry)) { // RT_VERSION = 16
        printf("[-] 未找到版本信息\n");
        return FALSE;
    }
    
    // 版本信息格式复杂,需要解析VS_VERSIONINFO结构
    // 这里只是示例框架
    
    printf("[+] 找到版本信息,大小: %d bytes\n", entry.dataSize);
    
    return TRUE;
}

6、课后作业

  1. 编写资源提取工具

    • 列出所有资源
    • 提取指定类型的资源
    • 支持提取图标和位图
  2. 实现资源嵌入器

    • 向PE文件添加资源
    • 替换现有资源
    • 修改版本信息
  3. 分析资源隐写应用

    • 在资源中隐藏数据
    • 从资源加载ShellCode
    • 研究常见木马的资源使用方式