能否将任意程序用作库?ELF及Windows平台跳转执行方案问询
针对你需要先在自定义代码中准备变量,再跳转至编译好的ELF/Windows PE程序指定位置的需求,以下是两种平台下的可行方案,替代反编译重编译的低效方法:
ELF 平台(Linux/Unix)
方案1:动态加载目标ELF(适用于有导出符号的场景)
利用dlopen/dlsym系列函数加载目标ELF,直接获取目标变量和函数的地址,赋值后跳转执行。这种方法无需手动解析ELF结构,适合有符号导出的程序。
#include <dlfcn.h> #include <stdio.h> int main() { // 1. 在自定义代码中准备变量 int prepared_val = 42; // 2. 加载目标ELF(支持PIE/非PIE) void* elf_handle = dlopen("./target_program.elf", RTLD_NOW); if (!elf_handle) { fprintf(stderr, "加载失败: %s\n", dlerror()); return 1; } // 3. 获取目标程序的全局变量地址并赋值 int* target_global_var = dlsym(elf_handle, "target_var"); if (!target_global_var) { fprintf(stderr, "获取变量地址失败: %s\n", dlerror()); dlclose(elf_handle); return 1; } *target_global_var = prepared_val; // 4. 获取目标函数/标签地址并跳转执行 void (*target_entry)() = dlsym(elf_handle, "target_function"); if (!target_entry) { fprintf(stderr, "获取入口地址失败: %s\n", dlerror()); dlclose(elf_handle); return 1; } target_entry(); // 直接调用等价于跳转至目标位置 dlclose(elf_handle); return 0; }
注意:如果目标ELF是非PIE且未导出符号,此方法不适用,需用下面的手动加载方案。
方案2:手动解析并加载ELF(适用于无符号的场景)
直接解析ELF的头部、程序段,用mmap将代码/数据段加载到进程空间,手动定位变量和目标地址后跳转。适合非PIE或无符号导出的程序。
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys/mman.h> #include <elf.h> int main() { int fd = open("./non_pie_target.elf", O_RDONLY); if (fd == -1) { perror("打开文件失败"); return 1; } // 读取ELF头部 Elf64_Ehdr elf_hdr; read(fd, &elf_hdr, sizeof(elf_hdr)); Elf64_Phdr prog_hdr; void* code_segment = NULL; void* data_segment = NULL; // 遍历程序段,加载代码和数据段到对应虚拟地址 for (int i = 0; i < elf_hdr.e_phnum; i++) { lseek(fd, elf_hdr.e_phoff + i * sizeof(prog_hdr), SEEK_SET); read(fd, &prog_hdr, sizeof(prog_hdr)); if (prog_hdr.p_type == PT_LOAD) { if (prog_hdr.p_flags & PF_X) { // 加载代码段(非PIE使用固定虚拟地址) code_segment = mmap((void*)prog_hdr.p_vaddr, prog_hdr.p_memsz, PROT_READ | PROT_EXEC | PROT_WRITE, MAP_FIXED | MAP_PRIVATE, fd, prog_hdr.p_offset); if (code_segment == MAP_FAILED) { perror("映射代码段失败"); return 1; } } else if (prog_hdr.p_flags & PF_W) { // 加载数据段 data_segment = mmap((void*)prog_hdr.p_vaddr, prog_hdr.p_memsz, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_PRIVATE, fd, prog_hdr.p_offset); if (data_segment == MAP_FAILED) { perror("映射数据段失败"); return 1; } } } } close(fd); // 1. 准备变量:假设目标变量在数据段偏移0x1234处 int* target_var = (int*)((char*)data_segment + 0x1234); *target_var = 42; // 2. 跳转至目标地址:假设目标标签在代码段偏移0x5678处 void (*target_jump)() = (void*)((char*)code_segment + 0x5678); target_jump(); return 0; }
注意:MAP_FIXED会强制覆盖指定地址的内存,需确保该地址未被当前进程占用;PIE程序需计算基址偏移,调整加载地址。
Windows 平台
方案1:加载PE文件并调用导出符号(适用于有导出符号的场景)
使用LoadLibrary/GetProcAddress加载目标PE(EXE/DLL),获取变量和函数地址后操作,逻辑和ELF的动态加载类似。
#include <windows.h> #include <stdio.h> int main() { // 1. 准备变量 int prepared_val = 42; // 2. 加载目标PE(EXE需显式导出符号,推荐用DLL) HMODULE pe_module = LoadLibraryA("./target_program.exe"); if (!pe_module) { printf("加载失败: %lu\n", GetLastError()); return 1; } // 3. 获取目标变量地址并赋值 int* target_global_var = (int*)GetProcAddress(pe_module, "target_var"); if (!target_global_var) { printf("获取变量地址失败: %lu\n", GetLastError()); FreeLibrary(pe_module); return 1; } *target_global_var = prepared_val; // 4. 跳转至目标函数执行 void (*target_entry)() = (void(*)())GetProcAddress(pe_module, "target_function"); if (!target_entry) { printf("获取入口地址失败: %lu\n", GetLastError()); FreeLibrary(pe_module); return 1; } target_entry(); FreeLibrary(pe_module); return 0; }
注意:Windows EXE默认不会导出符号,需在编译时用__declspec(dllexport)显式导出目标变量和函数,或改用DLL文件。
方案2:手动解析并加载PE(适用于无符号的场景)
解析PE的DOS头、NT头和节表,用VirtualAlloc分配内存,复制代码/数据段并修复重定位,最后跳转至指定地址。
#include <windows.h> #include <stdio.h> int main() { HANDLE file_handle = CreateFileA("./target_program.exe", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (file_handle == INVALID_HANDLE_VALUE) { printf("打开文件失败: %lu\n", GetLastError()); return 1; } DWORD file_size = GetFileSize(file_handle, NULL); LPBYTE file_buf = (LPBYTE)malloc(file_size); ReadFile(file_handle, file_buf, file_size, NULL, NULL); CloseHandle(file_handle); // 解析PE结构 PIMAGE_DOS_HEADER dos_hdr = (PIMAGE_DOS_HEADER)file_buf; PIMAGE_NT_HEADERS nt_hdr = (PIMAGE_NT_HEADERS)(file_buf + dos_hdr->e_lfanew); PIMAGE_SECTION_HEADER sec_hdrs = IMAGE_FIRST_SECTION(nt_hdr); // 分配内存(优先尝试固定基址,失败则用系统分配的动态基址) LPVOID image_base = VirtualAlloc((LPVOID)nt_hdr->OptionalHeader.ImageBase, nt_hdr->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (!image_base) { image_base = VirtualAlloc(NULL, nt_hdr->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (!image_base) { printf("分配内存失败: %lu\n", GetLastError()); free(file_buf); return 1; } } // 复制PE头和节内容 memcpy(image_base, file_buf, nt_hdr->OptionalHeader.SizeOfHeaders); for (int i = 0; i < nt_hdr->FileHeader.NumberOfSections; i++) { PIMAGE_SECTION_HEADER sec = &sec_hdrs[i]; memcpy((LPBYTE)image_base + sec->VirtualAddress, file_buf + sec->PointerToRawData, sec->SizeOfRawData); } // 修复重定位(动态基址时需要) if (nt_hdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size > 0) { PIMAGE_BASE_RELOCATION reloc = (PIMAGE_BASE_RELOCATION)((LPBYTE)image_base + nt_hdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress); DWORD delta = (DWORD)((LPBYTE)image_base - nt_hdr->OptionalHeader.ImageBase); while (reloc->VirtualAddress != 0) { DWORD* relocs = (DWORD*)((LPBYTE)reloc + sizeof(IMAGE_BASE_RELOCATION)); int count = (reloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD); for (int j = 0; j < count; j++) { WORD rel_info = ((WORD*)relocs)[j]; if ((rel_info >> 12) == IMAGE_REL_BASED_HIGHLOW) { DWORD* addr = (DWORD*)((LPBYTE)image_base + reloc->VirtualAddress + (rel_info & 0xFFF)); *addr += delta; } } reloc = (PIMAGE_BASE_RELOCATION)((LPBYTE)reloc + reloc->SizeOfBlock); } } // 1. 准备变量:假设目标变量在数据节偏移0x1234处 int* target_var = (int*)((LPBYTE)image_base + sec_hdrs[1].VirtualAddress + 0x1234); *target_var = 42; // 2. 跳转至目标地址:假设目标标签在代码节偏移0x5678处 void (*target_jump)() = (void(*)())((LPBYTE)image_base + sec_hdrs[0].VirtualAddress + 0x5678); target_jump(); VirtualFree(image_base, 0, MEM_RELEASE); free(file_buf); return 0; }
注意:手动加载PE需处理导入表修复(加载依赖DLL并替换函数地址),上述代码为简化版,实际使用需完善该部分逻辑。
关于反编译重编译方案的说明
你之前尝试的反编译ELF为.s再重新编译的方法,存在天然缺陷:反编译工具无法100%还原原始指令(尤其是涉及绝对地址硬编码的非PIE程序),且工具本身的bug会导致编译后程序地址偏移,最终无法正常跳转。因此更推荐上述直接加载进程空间的方案。
内容的提问来源于stack exchange,提问作者Romain

