Python如何直接运行内存中加载的EXE文件而无需保存到磁盘
结论
可以实现,但Windows系统原生没有提供直接从内存加载EXE执行的公开API,你需要手动完成PE格式解析、内存布局映射、导入表修复、重定位修正等系统加载器的工作,或者使用封装好相关逻辑的第三方工具库。
常见实现方案
方案1:手动调用Windows API完成PE加载(仅适用Windows平台)
你可以通过ctypes调用Windows原生API,逐步骤完成PE文件的加载逻辑,核心步骤如下:- 解析PE头获取镜像大小、节区信息、导入表、重定位表等元数据
- 调用
VirtualAlloc在当前进程/目标进程申请合适权限的内存空间 - 按照节区属性将PE内容复制到对应内存地址
- 修复导入表,加载依赖的系统DLL并填充函数地址
- 处理重定位表,修正基地址不匹配的地址偏移
- 调用
CreateThread或者跳转到入口点执行代码
对应的简化实现示例代码如下:
import ctypes from ctypes import wintypes kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) def run_exe_from_memory(exe_data: bytes): # 解析PE头 pe_offset = int.from_bytes(exe_data[0x3C:0x40], byteorder='little') image_base = int.from_bytes(exe_data[pe_offset+0x34:pe_offset+0x38], byteorder='little') image_size = int.from_bytes(exe_data[pe_offset+0x50:pe_offset+0x54], byteorder='little') entry_point = int.from_bytes(exe_data[pe_offset+0x28:pe_offset+0x2C], byteorder='little') # 申请内存 mem = kernel32.VirtualAlloc( wintypes.LPVOID(image_base), wintypes.DWORD(image_size), wintypes.DWORD(0x3000), # MEM_COMMIT | MEM_RESERVE wintypes.DWORD(0x40) # PAGE_EXECUTE_READWRITE ) if not mem: mem = kernel32.VirtualAlloc( None, wintypes.DWORD(image_size), wintypes.DWORD(0x3000), wintypes.DWORD(0x40) ) delta = mem - image_base # 复制PE头 ctypes.memmove(wintypes.LPVOID(mem), exe_data, int.from_bytes(exe_data[pe_offset+0x54:pe_offset+0x58], byteorder='little')) # 复制节区 num_sections = int.from_bytes(exe_data[pe_offset+0x6:pe_offset+0x8], byteorder='little') section_header_offset = pe_offset + 0xF8 for i in range(num_sections): sec_offset = section_header_offset + i * 0x28 virt_addr = int.from_bytes(exe_data[sec_offset+0xC:sec_offset+0x10], byteorder='little') raw_size = int.from_bytes(exe_data[sec_offset+0x10:sec_offset+0x14], byteorder='little') raw_offset = int.from_bytes(exe_data[sec_offset+0x14:sec_offset+0x18], byteorder='little') ctypes.memmove( wintypes.LPVOID(mem + virt_addr), exe_data[raw_offset:raw_offset+raw_size], wintypes.DWORD(raw_size) ) # 重定位修复(简化版,仅处理基础重定位) if delta != 0: reloc_dir_offset = pe_offset + 0xA0 reloc_virt = int.from_bytes(exe_data[reloc_dir_offset:reloc_dir_offset+4], byteorder='little') reloc_size = int.from_bytes(exe_data[reloc_dir_offset+4:reloc_dir_offset+8], byteorder='little') reloc_ptr = mem + reloc_virt end_ptr = reloc_ptr + reloc_size while reloc_ptr < end_ptr: base_page = int.from_bytes(ctypes.string_at(reloc_ptr, 4), byteorder='little') block_size = int.from_bytes(ctypes.string_at(reloc_ptr+4, 4), byteorder='little') num_entries = (block_size - 8) // 2 entries_ptr = reloc_ptr + 8 for j in range(num_entries): entry = int.from_bytes(ctypes.string_at(entries_ptr + j*2, 2), byteorder='little') entry_type = entry >> 12 entry_offset = entry & 0xFFF if entry_type == 3: # IMAGE_REL_BASED_HIGHLOW addr = mem + base_page + entry_offset orig_val = int.from_bytes(ctypes.string_at(addr, 4), byteorder='little') new_val = orig_val + delta ctypes.memmove(addr, new_val.to_bytes(4, byteorder='little'), 4) reloc_ptr += block_size # 修复导入表(简化版,仅处理基础依赖) import_dir_offset = pe_offset + 0x80 import_virt = int.from_bytes(exe_data[import_dir_offset:import_dir_offset+4], byteorder='little') import_size = int.from_bytes(exe_data[import_dir_offset+4:import_dir_offset+8], byteorder='little') import_ptr = mem + import_virt while True: name_rva = int.from_bytes(ctypes.string_at(import_ptr + 12, 4), byteorder='little') if name_rva == 0: break dll_name = ctypes.string_at(mem + name_rva).decode('utf-8') dll_handle = kernel32.LoadLibraryA(dll_name.encode('utf-8')) thunk_rva = int.from_bytes(ctypes.string_at(import_ptr + 16, 4), byteorder='little') thunk_ptr = mem + thunk_rva while True: thunk_val = int.from_bytes(ctypes.string_at(thunk_ptr, 4), byteorder='little') if thunk_val == 0: break if thunk_val & 0x80000000: func_addr = kernel32.GetProcAddress(dll_handle, wintypes.LPCSTR(thunk_val & 0x7FFFFFFF)) else: func_name = ctypes.string_at(mem + thunk_val + 2).decode('utf-8') func_addr = kernel32.GetProcAddress(dll_handle, func_name.encode('utf-8')) ctypes.memmove(thunk_ptr, func_addr.to_bytes(4, byteorder='little'), 4) thunk_ptr += 4 import_ptr += 20 # 执行入口点 entry_func = ctypes.CFUNCTYPE(wintypes.DWORD)(mem + entry_point) entry_func() # 调用示例 if __name__ == "__main__": with open("example.exe", "rb") as f: exe_data = f.read() run_exe_from_memory(exe_data)注意:上述代码是简化实现,仅能运行无复杂依赖的32位PE文件,64位PE、有动态链接依赖、需要运行时参数的EXE需要额外扩展逻辑,同时部分杀毒软件会对内存加载PE的行为报毒。
你可以将上述逻辑封装为独立的Python模块,即可实现你问题中提到的直接调用example_run_exe_file_from_variable.run(full_file)执行内存中EXE的效果。方案2:使用内存盘中转
如果不想手动处理PE加载逻辑,可以创建一个内存虚拟盘,把EXE内容写入内存盘后再用subprocess.Popen运行,本质上还是走磁盘加载逻辑但实际读写都在内存中,速度快且不会留下持久化磁盘文件,实现难度更低。
注意事项
- 上述所有方案都仅适用于Windows平台,Linux/macOS的ELF/Mach-O文件内存加载逻辑完全不同,需要单独实现
- 内存加载EXE的行为属于敏感操作,大部分安全软件都会拦截此类行为,仅建议在本地测试、自己可控的环境下使用
内容的提问来源于stack exchange,提问作者mal
相关产品推荐
相关产品推荐

