Windows PE结构

4、导入表解析

1、课程目标

  1. 理解PE导入机制的工作原理
  2. 掌握导入表结构及其解析方法
  3. 理解IAT的作用和工作方式
  4. 学会修改和添加导入表项

2、名词解释

名词 全称 解释
Import Table 导入表 记录程序依赖的外部函数
IID Import Descriptor 导入描述符
INT Import Name Table 导入名称表/查找表
IAT Import Address Table 导入地址表
Thunk - 函数地址/名称条目
Ordinal 序号 通过序号导入函数
Hint 提示 加速函数查找的索引
Bound Import 绑定导入 预先计算的函数地址

3、使用工具

  • CFF Explorer
  • PE-bear
  • Dependency Walker
  • x64dbg

4、技术原理

4.1、导入表结构

导入表结构图:

IMAGE_DATA_DIRECTORY[1]
     |
     v
+-------------------+
| Import Descriptor |  --> kernel32.dll
+-------------------+
| Import Descriptor |  --> user32.dll
+-------------------+
| Import Descriptor |  --> ntdll.dll
+-------------------+
| NULL (结束标志)    |
+-------------------+

每个Import Descriptor:
+------------------+
| OriginalFirstThunk| ---> INT (Import Name Table)
| TimeDateStamp    |             |
| ForwarderChain   |             v
| Name             | --+    +------------+
| FirstThunk       |   |    | Thunk 1    | -> Hint + Name
+------------------+   |    | Thunk 2    | -> Ordinal
       |               |    | NULL       |
       |               |    +------------+
       |               |
       |               +--> DLL名称字符串
       |
       +---> IAT (Import Address Table)
                  |
                  v
             +------------+
             | 加载前: INT|  (与INT相同)
             | 加载后: VA |  (实际函数地址)
             +------------+

4.2、导入描述符结构

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;
        DWORD   OriginalFirstThunk;  // RVA to INT (Import Name Table)
    };
    DWORD   TimeDateStamp;           // 时间戳(绑定导入用)
    DWORD   ForwarderChain;          // 转发链
    DWORD   Name;                    // RVA to DLL name
    DWORD   FirstThunk;              // RVA to IAT
} IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;

// Thunk数据(32位)
typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;      // 转发器字符串RVA
        DWORD Function;             // 函数地址
        DWORD Ordinal;              // 序号(高位为1)
        DWORD AddressOfData;        // RVA to IMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA32;

// Thunk数据(64位)
typedef struct _IMAGE_THUNK_DATA64 {
    union {
        ULONGLONG ForwarderString;
        ULONGLONG Function;
        ULONGLONG Ordinal;
        ULONGLONG AddressOfData;
    } u1;
} IMAGE_THUNK_DATA64;

// 导入函数名称
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;       // 函数索引提示
    CHAR    Name[1];    // 函数名(变长)
} IMAGE_IMPORT_BY_NAME;

// 判断是否按序号导入
#define IMAGE_SNAP_BY_ORDINAL32(Ordinal) ((Ordinal & 0x80000000) != 0)
#define IMAGE_SNAP_BY_ORDINAL64(Ordinal) ((Ordinal & 0x8000000000000000) != 0)
#define IMAGE_ORDINAL32(Ordinal) (Ordinal & 0xFFFF)
#define IMAGE_ORDINAL64(Ordinal) (Ordinal & 0xFFFF)

5、代码实现

5.1、解析导入表

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

// 解析导入表
void ParseImportTable(LPVOID fileData) {
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)fileData;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(
        (PBYTE)fileData + dosHeader->e_lfanew
    );
    
    BOOL is64 = (ntHeaders->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC);
    
    // 获取导入表数据目录
    PIMAGE_DATA_DIRECTORY importDir;
    if (is64) {
        PIMAGE_OPTIONAL_HEADER64 opt = (PIMAGE_OPTIONAL_HEADER64)&ntHeaders->OptionalHeader;
        importDir = &opt->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
    } else {
        PIMAGE_OPTIONAL_HEADER32 opt = (PIMAGE_OPTIONAL_HEADER32)&ntHeaders->OptionalHeader;
        importDir = &opt->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
    }
    
    if (importDir->VirtualAddress == 0) {
        printf("[-] 没有导入表\n");
        return;
    }
    
    printf("\n========== Import Table ==========\n");
    printf("RVA: 0x%08X, Size: 0x%08X\n\n", 
           importDir->VirtualAddress, importDir->Size);
    
    // 获取导入描述符数组
    DWORD importFoa = RvaToFoa(fileData, importDir->VirtualAddress);
    PIMAGE_IMPORT_DESCRIPTOR importDesc = (PIMAGE_IMPORT_DESCRIPTOR)(
        (PBYTE)fileData + importFoa
    );
    
    int dllIndex = 0;
    
    // 遍历所有导入的DLL
    while (importDesc->Name != 0) {
        // 获取DLL名称
        DWORD nameFoa = RvaToFoa(fileData, importDesc->Name);
        char* dllName = (char*)((PBYTE)fileData + nameFoa);
        
        printf("[%d] %s\n", dllIndex, dllName);
        printf("    OriginalFirstThunk: 0x%08X\n", importDesc->OriginalFirstThunk);
        printf("    TimeDateStamp:      0x%08X\n", importDesc->TimeDateStamp);
        printf("    ForwarderChain:     0x%08X\n", importDesc->ForwarderChain);
        printf("    Name:               0x%08X -> %s\n", importDesc->Name, dllName);
        printf("    FirstThunk (IAT):   0x%08X\n", importDesc->FirstThunk);
        
        // 解析函数列表
        ParseImportFunctions(fileData, importDesc, is64);
        
        printf("\n");
        importDesc++;
        dllIndex++;
    }
    
    printf("\n[*] 共导入 %d 个DLL\n", dllIndex);
}

// 解析导入函数
void ParseImportFunctions(LPVOID fileData, 
                          PIMAGE_IMPORT_DESCRIPTOR importDesc,
                          BOOL is64) {
    // 优先使用OriginalFirstThunk(INT),如果为0则使用FirstThunk(IAT)
    DWORD thunkRva = importDesc->OriginalFirstThunk;
    if (thunkRva == 0) {
        thunkRva = importDesc->FirstThunk;
    }
    
    DWORD thunkFoa = RvaToFoa(fileData, thunkRva);
    DWORD iatRva = importDesc->FirstThunk;
    
    int funcIndex = 0;
    
    if (is64) {
        PIMAGE_THUNK_DATA64 thunk = (PIMAGE_THUNK_DATA64)(
            (PBYTE)fileData + thunkFoa
        );
        
        while (thunk->u1.AddressOfData != 0) {
            printf("    [%3d] ", funcIndex);
            
            if (IMAGE_SNAP_BY_ORDINAL64(thunk->u1.Ordinal)) {
                // 按序号导入
                WORD ordinal = (WORD)IMAGE_ORDINAL64(thunk->u1.Ordinal);
                printf("Ordinal: %5d", ordinal);
            } else {
                // 按名称导入
                DWORD hintNameFoa = RvaToFoa(fileData, (DWORD)thunk->u1.AddressOfData);
                PIMAGE_IMPORT_BY_NAME hintName = (PIMAGE_IMPORT_BY_NAME)(
                    (PBYTE)fileData + hintNameFoa
                );
                printf("Hint: %4d  Name: %s", hintName->Hint, hintName->Name);
            }
            
            printf("  (IAT RVA: 0x%08X)\n", iatRva + funcIndex * 8);
            
            thunk++;
            funcIndex++;
        }
    } else {
        PIMAGE_THUNK_DATA32 thunk = (PIMAGE_THUNK_DATA32)(
            (PBYTE)fileData + thunkFoa
        );
        
        while (thunk->u1.AddressOfData != 0) {
            printf("    [%3d] ", funcIndex);
            
            if (IMAGE_SNAP_BY_ORDINAL32(thunk->u1.Ordinal)) {
                WORD ordinal = (WORD)IMAGE_ORDINAL32(thunk->u1.Ordinal);
                printf("Ordinal: %5d", ordinal);
            } else {
                DWORD hintNameFoa = RvaToFoa(fileData, thunk->u1.AddressOfData);
                PIMAGE_IMPORT_BY_NAME hintName = (PIMAGE_IMPORT_BY_NAME)(
                    (PBYTE)fileData + hintNameFoa
                );
                printf("Hint: %4d  Name: %s", hintName->Hint, hintName->Name);
            }
            
            printf("  (IAT RVA: 0x%08X)\n", iatRva + funcIndex * 4);
            
            thunk++;
            funcIndex++;
        }
    }
    
    printf("    Total: %d functions\n", funcIndex);
}

5.2、查找特定导入

// 查找导入的DLL
PIMAGE_IMPORT_DESCRIPTOR FindImportDll(LPVOID fileData, const char* dllName) {
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)fileData;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(
        (PBYTE)fileData + dosHeader->e_lfanew
    );
    
    BOOL is64 = (ntHeaders->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC);
    
    PIMAGE_DATA_DIRECTORY importDir;
    if (is64) {
        importDir = &((PIMAGE_OPTIONAL_HEADER64)&ntHeaders->OptionalHeader)
                    ->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
    } else {
        importDir = &ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
    }
    
    if (importDir->VirtualAddress == 0) {
        return NULL;
    }
    
    DWORD importFoa = RvaToFoa(fileData, importDir->VirtualAddress);
    PIMAGE_IMPORT_DESCRIPTOR importDesc = (PIMAGE_IMPORT_DESCRIPTOR)(
        (PBYTE)fileData + importFoa
    );
    
    while (importDesc->Name != 0) {
        DWORD nameFoa = RvaToFoa(fileData, importDesc->Name);
        char* name = (char*)((PBYTE)fileData + nameFoa);
        
        if (_stricmp(name, dllName) == 0) {
            return importDesc;
        }
        
        importDesc++;
    }
    
    return NULL;
}

// 查找导入的函数
DWORD FindImportFunction(LPVOID fileData, 
                         const char* dllName, 
                         const char* funcName) {
    PIMAGE_IMPORT_DESCRIPTOR importDesc = FindImportDll(fileData, dllName);
    if (!importDesc) {
        return 0;
    }
    
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)fileData;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(
        (PBYTE)fileData + dosHeader->e_lfanew
    );
    
    BOOL is64 = (ntHeaders->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC);
    
    DWORD thunkRva = importDesc->OriginalFirstThunk;
    if (thunkRva == 0) {
        thunkRva = importDesc->FirstThunk;
    }
    
    DWORD thunkFoa = RvaToFoa(fileData, thunkRva);
    DWORD iatRva = importDesc->FirstThunk;
    
    int index = 0;
    
    if (is64) {
        PIMAGE_THUNK_DATA64 thunk = (PIMAGE_THUNK_DATA64)(
            (PBYTE)fileData + thunkFoa
        );
        
        while (thunk->u1.AddressOfData != 0) {
            if (!IMAGE_SNAP_BY_ORDINAL64(thunk->u1.Ordinal)) {
                DWORD hintFoa = RvaToFoa(fileData, (DWORD)thunk->u1.AddressOfData);
                PIMAGE_IMPORT_BY_NAME hintName = (PIMAGE_IMPORT_BY_NAME)(
                    (PBYTE)fileData + hintFoa
                );
                
                if (strcmp(hintName->Name, funcName) == 0) {
                    return iatRva + index * 8;  // 返回IAT条目的RVA
                }
            }
            thunk++;
            index++;
        }
    } else {
        PIMAGE_THUNK_DATA32 thunk = (PIMAGE_THUNK_DATA32)(
            (PBYTE)fileData + thunkFoa
        );
        
        while (thunk->u1.AddressOfData != 0) {
            if (!IMAGE_SNAP_BY_ORDINAL32(thunk->u1.Ordinal)) {
                DWORD hintFoa = RvaToFoa(fileData, thunk->u1.AddressOfData);
                PIMAGE_IMPORT_BY_NAME hintName = (PIMAGE_IMPORT_BY_NAME)(
                    (PBYTE)fileData + hintFoa
                );
                
                if (strcmp(hintName->Name, funcName) == 0) {
                    return iatRva + index * 4;
                }
            }
            thunk++;
            index++;
        }
    }
    
    return 0;
}

5.3、运行时Hook IAT

// IAT Hook结构
typedef struct _IAT_HOOK {
    LPVOID      originalFunc;
    LPVOID      hookFunc;
    LPVOID*     iatEntry;
    char        dllName[64];
    char        funcName[128];
} IAT_HOOK;

// 安装IAT Hook
BOOL InstallIATHook(HMODULE hModule, 
                    const char* dllName, 
                    const char* funcName,
                    LPVOID hookFunc,
                    IAT_HOOK* hook) {
    // 获取模块基址
    LPBYTE base = (LPBYTE)hModule;
    
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)base;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(
        base + dosHeader->e_lfanew
    );
    
    PIMAGE_DATA_DIRECTORY importDir = &ntHeaders->OptionalHeader
                                       .DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
    
    if (importDir->VirtualAddress == 0) {
        return FALSE;
    }
    
    // 遍历导入描述符
    PIMAGE_IMPORT_DESCRIPTOR importDesc = (PIMAGE_IMPORT_DESCRIPTOR)(
        base + importDir->VirtualAddress
    );
    
    while (importDesc->Name) {
        char* name = (char*)(base + importDesc->Name);
        
        if (_stricmp(name, dllName) == 0) {
            // 找到目标DLL
            PIMAGE_THUNK_DATA originalThunk = (PIMAGE_THUNK_DATA)(
                base + importDesc->OriginalFirstThunk
            );
            PIMAGE_THUNK_DATA firstThunk = (PIMAGE_THUNK_DATA)(
                base + importDesc->FirstThunk
            );
            
            while (originalThunk->u1.AddressOfData) {
                if (!IMAGE_SNAP_BY_ORDINAL(originalThunk->u1.Ordinal)) {
                    PIMAGE_IMPORT_BY_NAME hintName = (PIMAGE_IMPORT_BY_NAME)(
                        base + originalThunk->u1.AddressOfData
                    );
                    
                    if (strcmp(hintName->Name, funcName) == 0) {
                        // 找到目标函数
                        hook->originalFunc = (LPVOID)firstThunk->u1.Function;
                        hook->hookFunc = hookFunc;
                        hook->iatEntry = (LPVOID*)&firstThunk->u1.Function;
                        strcpy(hook->dllName, dllName);
                        strcpy(hook->funcName, funcName);
                        
                        // 修改内存保护
                        DWORD oldProtect;
                        VirtualProtect(hook->iatEntry, sizeof(LPVOID), 
                                      PAGE_READWRITE, &oldProtect);
                        
                        // 替换函数地址
                        *hook->iatEntry = hookFunc;
                        
                        // 恢复保护
                        VirtualProtect(hook->iatEntry, sizeof(LPVOID), 
                                      oldProtect, &oldProtect);
                        
                        printf("[+] IAT Hook: %s!%s\n", dllName, funcName);
                        printf("    Original: 0x%p\n", hook->originalFunc);
                        printf("    Hook:     0x%p\n", hookFunc);
                        
                        return TRUE;
                    }
                }
                
                originalThunk++;
                firstThunk++;
            }
        }
        
        importDesc++;
    }
    
    return FALSE;
}

// 卸载IAT Hook
BOOL UninstallIATHook(IAT_HOOK* hook) {
    if (!hook->iatEntry) {
        return FALSE;
    }
    
    DWORD oldProtect;
    VirtualProtect(hook->iatEntry, sizeof(LPVOID), 
                  PAGE_READWRITE, &oldProtect);
    
    *hook->iatEntry = hook->originalFunc;
    
    VirtualProtect(hook->iatEntry, sizeof(LPVOID), 
                  oldProtect, &oldProtect);
    
    printf("[+] IAT Hook removed: %s!%s\n", hook->dllName, hook->funcName);
    
    return TRUE;
}

// 示例:Hook MessageBoxA
typedef int (WINAPI* PFN_MessageBoxA)(HWND, LPCSTR, LPCSTR, UINT);
IAT_HOOK g_MessageBoxHook = {0};

int WINAPI HookedMessageBoxA(HWND hWnd, LPCSTR lpText, 
                             LPCSTR lpCaption, UINT uType) {
    printf("[Hook] MessageBoxA called!\n");
    printf("       Text: %s\n", lpText);
    printf("       Caption: %s\n", lpCaption);
    
    // 调用原函数(修改内容)
    PFN_MessageBoxA original = (PFN_MessageBoxA)g_MessageBoxHook.originalFunc;
    return original(hWnd, "[HOOKED]", lpCaption, uType);
}

6、课后作业

  1. 编写完整的导入表解析器

    • 列出所有导入的DLL
    • 列出每个DLL的所有函数
    • 支持序号导入和名称导入
  2. 实现IAT Hook框架

    • 支持Hook任意导入函数
    • 支持保存和调用原函数
    • 实现自动卸载功能
  3. 分析PE导入表安全问题

    • 研究导入表注入技术
    • 分析绑定导入的影响
    • 了解DLL劫持原理