PE Loader重定位功能实现失败求助:代码错误排查与修复
背景
我正在学习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

