You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PE Loader重定位功能实现失败求助:代码错误排查与修复

PE加载器重定位实现错误排查与IAT疑问

背景

我正在学习PE格式及PE加载器工作原理,选用了一个不支持重定位的FilelessPELoader示例。原加载器仅能在PE文件指定的ImageBase地址成功分配可执行内存时才能加载运行,否则会失败,原关键代码如下:

preferAddr = (LPVOID)ntHeader->OptionalHeader.ImageBase;
...
pImageBase = (BYTE*)VirtualAlloc(
    preferAddr,
    ntHeader->OptionalHeader.SizeOfImage,
    MEM_COMMIT | MEM_RESERVE,
    PAGE_EXECUTE_READWRITE);

我尝试自行实现重定位功能但未达预期,修改后的pe_loader()函数代码如下:

void pe_loader(char* data, DWORD datasize) {

    masquerade_cmdline();

    DWORD chksum = 0;
    for (DWORD i = 0; i < datasize; i++) {
        chksum = data[i] * i + chksum / 3;
    };

    BYTE* pImageBase = NULL;
    LPVOID preferAddr = 0;
    DWORD OldProtect = 0;

    IMAGE_NT_HEADERS* ntHeader = (IMAGE_NT_HEADERS*)get_nt_header(data);
    if (!ntHeader) {
        exit(0);
    }

    IMAGE_DATA_DIRECTORY* relocDir = get_pe_directory(data, IMAGE_DIRECTORY_ENTRY_BASERELOC);
    preferAddr = (LPVOID)ntHeader->OptionalHeader.ImageBase;

    HMODULE dll = LoadLibraryA("ntdll.dll");
    NtUnmapViewOfSectionProc NtUnmapViewOfSection = (NtUnmapViewOfSectionProc)
        GetProcAddress(dll, "NtUnmapViewOfSection");
    NtUnmapViewOfSection((HANDLE)-1, (LPVOID)ntHeader->OptionalHeader.ImageBase);

    pImageBase = (BYTE*)VirtualAlloc(NULL,
        ntHeader->OptionalHeader.SizeOfImage,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE);
    if (!pImageBase)
    {
        exit(0);
    }

    // Copy the PE file in allocated memory
    ntHeader->OptionalHeader.ImageBase = (size_t)pImageBase;
    memcpy(pImageBase, data, ntHeader->OptionalHeader.SizeOfHeaders);

    LPVOID lpRelocHdr = NULL;
    IMAGE_SECTION_HEADER* SectionHeaderArr =
        (IMAGE_SECTION_HEADER*)(size_t(ntHeader) + sizeof(IMAGE_NT_HEADERS));

    for (int i = 0; i < ntHeader->FileHeader.NumberOfSections; i++)
    {
        if (strcmp((char*)SectionHeaderArr[i].Name, ".reloc") == 0)
        {
            lpRelocHdr = LPVOID(size_t(pImageBase) + SectionHeaderArr[i].VirtualAddress);
        }

        memcpy(LPVOID(size_t(pImageBase) + SectionHeaderArr[i].VirtualAddress),
            LPVOID(size_t(data) + SectionHeaderArr[i].PointerToRawData),
            SectionHeaderArr[i].SizeOfRawData);
    }


    printf("Executable memory : %p\n", pImageBase);
    printf("RW memory : %p\n", data);

    DWORD DeltaImageBase = (DWORD)pImageBase - ntHeader->OptionalHeader.ImageBase;
    IMAGE_BASE_RELOCATION* ImageBaseReloc = (IMAGE_BASE_RELOCATION*)lpRelocHdr;
    cout << hex << ImageBaseReloc << endl;
    
    while (ImageBaseReloc->VirtualAddress != 0)
    {
        DWORD RelocEntries = (ImageBaseReloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / 2;
        
        cout << "Virtual Address : " << hex << ImageBaseReloc->VirtualAddress << endl;
        cout << "Relocation Entries : " << hex << RelocEntries << endl;
        cout << "Size of Relocation Header : " << hex << (ImageBaseReloc->SizeOfBlock) << endl;
        cout << hex << ImageBaseReloc << endl;

        WORD* ImageRelocEntry = (WORD*)((char*)ImageBaseReloc + sizeof(IMAGE_BASE_RELOCATION));
        for (int i = 0; i < RelocEntries; i++)
        {
            //type is the first 4 bits of the relocation word
            int type = ImageRelocEntry[i] >> 12;
            // offset is the last 12 bits
            int offset = ImageRelocEntry[i] & 0x0fff;
            cout << type << "\t" << offset << endl;
            if (type == 0)
                continue;

            DWORD* ChangeAddr = (DWORD*)(pImageBase + ImageBaseReloc->VirtualAddress + offset);
            *ChangeAddr = DeltaImageBase;
        }

        ImageBaseReloc = (IMAGE_BASE_RELOCATION*)(((char*)ImageBaseReloc + ImageBaseReloc->SizeOfBlock));
    }
    

    // Fix the PE Import addr table
    repair_iat(pImageBase);

    // AddressOfEntryPoint
    size_t ret_addr = (size_t)(pImageBase)+ntHeader->OptionalHeader.AddressOfEntryPoint;

    // Jumping to the EntryPoint of the loaded PE
    std::cout << "  - Jumping..." << std::endl;

    EnumThreadWindows(0, (WNDENUMPROC)ret_addr, 0);

    return;
}

代码错误与修复方案

1. DeltaImageBase计算完全错误

你先修改了ntHeader->OptionalHeader.ImageBase = (size_t)pImageBase,之后计算DeltaImageBase = (DWORD)pImageBase - ntHeader->OptionalHeader.ImageBase,结果必然为0,完全失去了基址偏移的意义。

修复:
先保存原ImageBase的值,再修改ntHeader的ImageBase,同时同步内存中的PE结构:

// 保存原基址
DWORD originalImageBase = (DWORD)ntHeader->OptionalHeader.ImageBase;
// 修改原始数据中的PE头基址
ntHeader->OptionalHeader.ImageBase = (size_t)pImageBase;
// 拷贝PE头到分配的内存
memcpy(pImageBase, data, ntHeader->OptionalHeader.SizeOfHeaders);
// 重新获取内存中的NT头,确保后续操作基于实际加载的PE结构
IMAGE_NT_HEADERS* memNtHeader = (IMAGE_NT_HEADERS*)get_nt_header((char*)pImageBase);
// 计算正确的基址偏移
DWORD DeltaImageBase = (DWORD)pImageBase - originalImageBase;

2. 重定位地址修正逻辑错误

你直接将*ChangeAddr = DeltaImageBase,这是硬覆盖地址,正确的逻辑是给原地址加上基址偏移——重定位表记录的是PE文件中硬编码的、基于原ImageBase的地址,需要加上实际分配地址与原地址的差值,才能指向正确的内存位置。

修复:

DWORD* ChangeAddr = (DWORD*)(pImageBase + ImageBaseReloc->VirtualAddress + offset);
// 替换为加法操作,修正硬编码地址
*ChangeAddr += DeltaImageBase;

3. 重定位类型处理不严谨

虽然你跳过了type=0,但PE重定位中只有部分类型需要处理(比如IMAGE_REL_BASED_HIGHLOW即type=3是最常见的32位地址重定位类型),建议明确判断需要处理的类型,避免无效操作:

// 提前定义或引用Windows头文件中的常量
#define IMAGE_REL_BASED_HIGHLOW 3

// 只处理需要修正32位地址的重定位类型
if (type != IMAGE_REL_BASED_HIGHLOW)
    continue;

4. NtUnmapViewOfSection调用冗余

你直接分配随机地址,调用NtUnmapViewOfSection去释放原ImageBase地址没有意义,反而可能在原地址未被占用时触发错误。可以直接移除该调用,或者仅在尝试分配原地址失败时再执行。

5. PE头修改未同步到内存

你修改的是原始data中的ntHeader,但拷贝到内存中的PE头还是原来的ImageBase,虽然重定位不依赖内存中的头,但后续操作(比如获取内存中的重定位目录)应该基于内存中的PE结构,所以建议拷贝头之后重新获取内存中的NT头(如修复方案1中所示)。

IAT修复与重定位的关系

解析重定位表和修复IAT是完全独立的操作,两者都必须执行:

  • 重定位的作用是修正PE文件内部所有硬编码的、基于原ImageBase的地址(比如全局变量指针、函数跳转地址等),确保这些地址指向实际加载后的内存位置。
  • IAT修复的作用是填充导入函数的实际内存地址,PE文件中仅存储导入函数的名称或序号,需要通过系统API查询并填充到IAT中,才能让PE正确调用外部函数。

两者功能互补,缺一不可,不存在替代关系。

内容的提问来源于stack exchange,提问作者David

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 17:25:55