Windows PE结构

7、重定位表解析

1、课程目标

  1. 理解PE重定位的原理和必要性
  2. 掌握重定位表的结构和解析方法
  3. 学会手动进行重定位修复
  4. 理解重定位在Shellcode和注入中的应用

2、名词解释

名词 全称 解释
Relocation 重定位 调整代码中的地址引用
Base Relocation 基址重定位 PE加载到非首选地址时的修复
Delta 差值 实际加载地址与首选地址的差
ASLR Address Space Layout Randomization 地址空间布局随机化
Fixup 修复项 需要重定位的位置
IMAGE_REL_BASED_HIGHLOW - 32位绝对地址重定位
IMAGE_REL_BASED_DIR64 - 64位绝对地址重定位

3、使用工具

  • CFF Explorer
  • PE-bear
  • x64dbg
  • IDA Pro

4、技术原理

4.1、为什么需要重定位

场景:DLL编译时ImageBase = 0x10000000

代码中的绝对地址:
    mov eax, [0x10001234]  ; 访问全局变量
    call 0x10002000        ; 调用函数

如果DLL被加载到0x20000000:
    Delta = 0x20000000 - 0x10000000 = 0x10000000
    
需要修正为:
    mov eax, [0x20001234]  ; 0x10001234 + Delta
    call 0x20002000        ; 0x10002000 + Delta

4.2、重定位表结构

// 重定位块
typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;     // 页面RVA(4KB对齐)
    DWORD   SizeOfBlock;        // 块大小(包括头)
    // 后跟 WORD 数组,每个 WORD 描述一个重定位项
} IMAGE_BASE_RELOCATION, *PIMAGE_BASE_RELOCATION;

// 重定位项格式(WORD)
// 高4位:重定位类型
// 低12位:页内偏移

// 重定位类型
#define IMAGE_REL_BASED_ABSOLUTE         0   // 无操作(用于对齐)
#define IMAGE_REL_BASED_HIGH             1   // 高16位
#define IMAGE_REL_BASED_LOW              2   // 低16位
#define IMAGE_REL_BASED_HIGHLOW          3   // 完整32位地址 ★常用★
#define IMAGE_REL_BASED_HIGHADJ          4   // 高16位调整
#define IMAGE_REL_BASED_MIPS_JMPADDR     5   // MIPS跳转地址
#define IMAGE_REL_BASED_ARM_MOV32        5   // ARM MOV32
#define IMAGE_REL_BASED_THUMB_MOV32      7   // Thumb MOV32
#define IMAGE_REL_BASED_MIPS_JMPADDR16   9   // MIPS16跳转
#define IMAGE_REL_BASED_DIR64            10  // 64位地址 ★常用★

4.3、重定位表布局

+-------------------------+
| VirtualAddress = 0x1000 |
| SizeOfBlock = 0x0014    |
| Entry[0] = 0x3010       |  Type=3(HIGHLOW), Offset=0x010
| Entry[1] = 0x3024       |  Type=3(HIGHLOW), Offset=0x024
| Entry[2] = 0x3038       |  Type=3(HIGHLOW), Offset=0x038
| Entry[3] = 0x0000       |  Type=0(ABSOLUTE), 用于对齐
+-------------------------+
| VirtualAddress = 0x2000 |
| SizeOfBlock = 0x000C    |
| Entry[0] = 0x3100       |
| Entry[1] = 0x3200       |
+-------------------------+
| VirtualAddress = 0x0000 |  结束标志
| SizeOfBlock = 0x0000    |
+-------------------------+

5、代码实现

5.1、解析重定位表

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

// 获取重定位类型名称
const char* GetRelocTypeName(WORD type) {
    switch (type) {
        case IMAGE_REL_BASED_ABSOLUTE: return "ABSOLUTE";
        case IMAGE_REL_BASED_HIGH:     return "HIGH";
        case IMAGE_REL_BASED_LOW:      return "LOW";
        case IMAGE_REL_BASED_HIGHLOW:  return "HIGHLOW";
        case IMAGE_REL_BASED_HIGHADJ:  return "HIGHADJ";
        case IMAGE_REL_BASED_DIR64:    return "DIR64";
        default: return "UNKNOWN";
    }
}

// 解析重定位表
void ParseRelocationTable(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 relocDir;
    ULONGLONG imageBase;
    
    if (is64) {
        PIMAGE_OPTIONAL_HEADER64 opt = (PIMAGE_OPTIONAL_HEADER64)&ntHeaders->OptionalHeader;
        relocDir = &opt->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
        imageBase = opt->ImageBase;
    } else {
        PIMAGE_OPTIONAL_HEADER32 opt = (PIMAGE_OPTIONAL_HEADER32)&ntHeaders->OptionalHeader;
        relocDir = &opt->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
        imageBase = opt->ImageBase;
    }
    
    if (relocDir->VirtualAddress == 0) {
        printf("[-] 没有重定位表\n");
        return;
    }
    
    printf("\n========== Relocation Table ==========\n");
    printf("RVA: 0x%08X, Size: 0x%08X\n", 
           relocDir->VirtualAddress, relocDir->Size);
    printf("ImageBase: 0x%llX\n\n", imageBase);
    
    DWORD relocFoa = RvaToFoa(fileData, relocDir->VirtualAddress);
    PIMAGE_BASE_RELOCATION relocBlock = (PIMAGE_BASE_RELOCATION)(
        (PBYTE)fileData + relocFoa
    );
    
    int blockIndex = 0;
    int totalEntries = 0;
    
    // 遍历所有重定位块
    while (relocBlock->VirtualAddress != 0) {
        printf("Block #%d: PageRVA=0x%08X, BlockSize=0x%04X\n",
               blockIndex, relocBlock->VirtualAddress, relocBlock->SizeOfBlock);
        
        // 计算条目数量
        DWORD numEntries = (relocBlock->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
        WORD* entries = (WORD*)(relocBlock + 1);
        
        for (DWORD i = 0; i < numEntries; i++) {
            WORD type = entries[i] >> 12;
            WORD offset = entries[i] & 0x0FFF;
            
            // 跳过ABSOLUTE类型(用于对齐)
            if (type == IMAGE_REL_BASED_ABSOLUTE) {
                continue;
            }
            
            DWORD targetRva = relocBlock->VirtualAddress + offset;
            
            printf("  [%3d] Type: %d (%s), Offset: 0x%03X, TargetRVA: 0x%08X\n",
                   i, type, GetRelocTypeName(type), offset, targetRva);
            
            // 显示需要重定位的原始值
            DWORD targetFoa = RvaToFoa(fileData, targetRva);
            if (type == IMAGE_REL_BASED_HIGHLOW) {
                DWORD* ptr = (DWORD*)((PBYTE)fileData + targetFoa);
                printf("         Original Value: 0x%08X\n", *ptr);
            } else if (type == IMAGE_REL_BASED_DIR64) {
                ULONGLONG* ptr = (ULONGLONG*)((PBYTE)fileData + targetFoa);
                printf("         Original Value: 0x%016llX\n", *ptr);
            }
            
            totalEntries++;
        }
        
        printf("  Entries in block: %d\n\n", numEntries);
        
        // 移动到下一个块
        relocBlock = (PIMAGE_BASE_RELOCATION)(
            (PBYTE)relocBlock + relocBlock->SizeOfBlock
        );
        blockIndex++;
    }
    
    printf("[*] 共 %d 个重定位块, %d 个重定位项\n", blockIndex, totalEntries);
}

5.2、执行重定位

// 重定位信息
typedef struct _RELOC_INFO {
    DWORD   rva;        // 需要修复的位置RVA
    WORD    type;       // 重定位类型
} RELOC_INFO;

// 收集所有重定位信息
int CollectRelocations(LPVOID fileData, RELOC_INFO* relocArray, int maxCount) {
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)fileData;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(
        (PBYTE)fileData + dosHeader->e_lfanew
    );
    
    PIMAGE_DATA_DIRECTORY relocDir = &ntHeaders->OptionalHeader
                                      .DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
    
    if (relocDir->VirtualAddress == 0) {
        return 0;
    }
    
    DWORD relocFoa = RvaToFoa(fileData, relocDir->VirtualAddress);
    PIMAGE_BASE_RELOCATION relocBlock = (PIMAGE_BASE_RELOCATION)(
        (PBYTE)fileData + relocFoa
    );
    
    int count = 0;
    
    while (relocBlock->VirtualAddress != 0 && count < maxCount) {
        DWORD numEntries = (relocBlock->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
        WORD* entries = (WORD*)(relocBlock + 1);
        
        for (DWORD i = 0; i < numEntries && count < maxCount; i++) {
            WORD type = entries[i] >> 12;
            WORD offset = entries[i] & 0x0FFF;
            
            if (type != IMAGE_REL_BASED_ABSOLUTE) {
                relocArray[count].rva = relocBlock->VirtualAddress + offset;
                relocArray[count].type = type;
                count++;
            }
        }
        
        relocBlock = (PIMAGE_BASE_RELOCATION)(
            (PBYTE)relocBlock + relocBlock->SizeOfBlock
        );
    }
    
    return count;
}

// 对内存中的PE映像执行重定位
BOOL ApplyRelocations(LPBYTE imageBase, ULONGLONG newBase, ULONGLONG oldBase) {
    LONGLONG delta = newBase - oldBase;
    
    if (delta == 0) {
        return TRUE;  // 不需要重定位
    }
    
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)imageBase;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(
        imageBase + dosHeader->e_lfanew
    );
    
    PIMAGE_DATA_DIRECTORY relocDir = &ntHeaders->OptionalHeader
                                      .DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
    
    if (relocDir->VirtualAddress == 0) {
        printf("[-] 没有重定位表,无法重定位\n");
        return FALSE;
    }
    
    PIMAGE_BASE_RELOCATION relocBlock = (PIMAGE_BASE_RELOCATION)(
        imageBase + relocDir->VirtualAddress
    );
    
    int totalFixed = 0;
    
    while (relocBlock->VirtualAddress != 0) {
        DWORD numEntries = (relocBlock->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
        WORD* entries = (WORD*)(relocBlock + 1);
        
        for (DWORD i = 0; i < numEntries; i++) {
            WORD type = entries[i] >> 12;
            WORD offset = entries[i] & 0x0FFF;
            
            LPBYTE targetAddr = imageBase + relocBlock->VirtualAddress + offset;
            
            switch (type) {
                case IMAGE_REL_BASED_ABSOLUTE:
                    // 无操作
                    break;
                    
                case IMAGE_REL_BASED_HIGHLOW: {
                    // 32位修复
                    DWORD* ptr = (DWORD*)targetAddr;
                    *ptr += (DWORD)delta;
                    totalFixed++;
                    break;
                }
                
                case IMAGE_REL_BASED_DIR64: {
                    // 64位修复
                    ULONGLONG* ptr = (ULONGLONG*)targetAddr;
                    *ptr += delta;
                    totalFixed++;
                    break;
                }
                
                case IMAGE_REL_BASED_HIGH: {
                    WORD* ptr = (WORD*)targetAddr;
                    *ptr += HIWORD(delta);
                    totalFixed++;
                    break;
                }
                
                case IMAGE_REL_BASED_LOW: {
                    WORD* ptr = (WORD*)targetAddr;
                    *ptr += LOWORD(delta);
                    totalFixed++;
                    break;
                }
                
                default:
                    printf("[!] 未知重定位类型: %d\n", type);
                    break;
            }
        }
        
        relocBlock = (PIMAGE_BASE_RELOCATION)(
            (PBYTE)relocBlock + relocBlock->SizeOfBlock
        );
    }
    
    printf("[+] 重定位完成,修复了 %d\n", totalFixed);
    printf("    Delta: 0x%llX\n", delta);
    
    return TRUE;
}

5.3、手动PE加载器中的重定位

// 简单PE加载器(包含重定位)
typedef BOOL (WINAPI* DllMain_t)(HINSTANCE, DWORD, LPVOID);

LPVOID ManualMapDll(LPBYTE dllData) {
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)dllData;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(
        dllData + dosHeader->e_lfanew
    );
    
    BOOL is64 = (ntHeaders->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC);
    
    DWORD imageSize;
    ULONGLONG preferredBase;
    
    if (is64) {
        imageSize = ((PIMAGE_OPTIONAL_HEADER64)&ntHeaders->OptionalHeader)->SizeOfImage;
        preferredBase = ((PIMAGE_OPTIONAL_HEADER64)&ntHeaders->OptionalHeader)->ImageBase;
    } else {
        imageSize = ntHeaders->OptionalHeader.SizeOfImage;
        preferredBase = ntHeaders->OptionalHeader.ImageBase;
    }
    
    // 尝试在首选地址分配内存
    LPBYTE imageBase = (LPBYTE)VirtualAlloc(
        (LPVOID)preferredBase,
        imageSize,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE
    );
    
    // 如果失败,在任意地址分配
    if (!imageBase) {
        imageBase = (LPBYTE)VirtualAlloc(
            NULL,
            imageSize,
            MEM_COMMIT | MEM_RESERVE,
            PAGE_EXECUTE_READWRITE
        );
    }
    
    if (!imageBase) {
        printf("[-] 无法分配内存\n");
        return NULL;
    }
    
    printf("[+] 映像分配地址: 0x%p (首选: 0x%llX)\n", 
           imageBase, preferredBase);
    
    // 复制PE头
    memcpy(imageBase, dllData, ntHeaders->OptionalHeader.SizeOfHeaders);
    
    // 复制区段
    PIMAGE_SECTION_HEADER sections = IMAGE_FIRST_SECTION(ntHeaders);
    for (WORD i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++) {
        if (sections[i].SizeOfRawData > 0) {
            memcpy(
                imageBase + sections[i].VirtualAddress,
                dllData + sections[i].PointerToRawData,
                sections[i].SizeOfRawData
            );
        }
    }
    
    // 执行重定位
    if ((ULONGLONG)imageBase != preferredBase) {
        printf("[*] 需要重定位...\n");
        if (!ApplyRelocations(imageBase, (ULONGLONG)imageBase, preferredBase)) {
            VirtualFree(imageBase, 0, MEM_RELEASE);
            return NULL;
        }
    }
    
    // 处理导入表(略)
    // ...
    
    // 调用DllMain
    DWORD entryPoint = ntHeaders->OptionalHeader.AddressOfEntryPoint;
    if (entryPoint) {
        DllMain_t DllMain = (DllMain_t)(imageBase + entryPoint);
        DllMain((HINSTANCE)imageBase, DLL_PROCESS_ATTACH, NULL);
    }
    
    return imageBase;
}

5.4、重定位在Shellcode中的应用

// 位置无关代码(PIC)技术
// 通过获取当前EIP/RIP来计算相对地址

// x86 获取EIP
__declspec(naked) DWORD GetEIP32() {
    __asm {
        call next
    next:
        pop eax
        ret
    }
}

// x64 获取RIP(使用LEA)
ULONGLONG GetRIP64() {
    ULONGLONG rip;
    // lea rax, [rip]
    // 在实际shellcode中使用内联汇编
    return rip;
}

// 运行时重定位shellcode
void RelocateShellcode(LPBYTE shellcode, DWORD size, 
                       ULONGLONG oldBase, ULONGLONG newBase) {
    LONGLONG delta = newBase - oldBase;
    
    // 扫描shellcode中的绝对地址并修复
    // 这需要了解shellcode的结构
    
    // 示例:修复内嵌的字符串指针
    for (DWORD i = 0; i < size - sizeof(ULONGLONG); i++) {
        ULONGLONG* ptr = (ULONGLONG*)(shellcode + i);
        
        // 检查是否看起来像旧基址范围内的地址
        if (*ptr >= oldBase && *ptr < oldBase + size) {
            *ptr += delta;
        }
    }
}

6、课后作业

  1. 编写重定位表解析器

    • 显示所有重定位块
    • 显示每个重定位项详情
    • 统计各类型数量
  2. 实现PE手动加载器

    • 加载PE到内存
    • 执行重定位
    • 解析导入表
    • 调用入口点
  3. 分析重定位与安全的关系

    • 研究ASLR实现原理
    • 分析无重定位表的PE
    • 了解重定位表滥用技术