Windows PE结构

7、重定位表解析

1、课程目标

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

2、名词解释

名词全称解释
Relocation重定位调整代码中的地址引用
Base Relocation基址重定位PE加载到非首选地址时的修复
Delta差值实际加载地址与首选地址的差
ASLRAddress 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
    • 了解重定位表滥用技术