尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
进程空中技术:在内存中直接加载并运行exe的完整实现
简介这份资源围绕进程空中技术RunPE / Process Hollowing展开面向安全研究人员、逆向工程师以及希望深入理解 Windows 底层机制的学习者帮助解决在不将 exe 写入磁盘的前提下于内存中动态加载并执行代码的问题。压缩包内共 1 个文件为 cpp 源码整体约 2KB属于轻量级示例工程便于直接阅读与二次实验。内容涉及承载进程选择、目标 exe 节区读取、内存状态保存与清空、入口点重设以及 NtResumeThread 恢复执行等关键环节并延伸至隐蔽执行、逆向工程与动态调试等应用场景同时提示了异常进程行为检测等防御思路。目前已有 1209 人学习下载适合具备一定 PE 文件格式与 Windows API 基础、希望动手复现并理解 RunPE 完整流程的读者参考。1. 进程空中技术把 exe 从磁盘里“抠”出来直接塞进内存跑你有没有遇到过这种场景手头有个 exe不想让它落地到磁盘上或者磁盘上那个文件已经被删了、被锁了但进程还得跑起来。常规做法是CreateProcess传个路径让系统自己去加载器里走一遍。但如果我们把 PE 文件整个读进内存自己解析、自己映射、自己修导入表、自己跳过去执行呢这就是“进程空中技术”要干的事——在内存中加载 exe不依赖磁盘路径不经过系统加载器的常规入口。它适合谁适合想理解 Windows PE 加载机制、想做内存加载器、或者单纯想搞清楚“一个 exe 到底是怎么被系统跑起来”的开发者。下面我按实际拆项目的节奏从整体设计一路讲到怎么改、怎么避坑。2. 整体架构一个内存加载器需要拆成哪几块2.1 从 PE 文件到可执行映像的映射链路一个 exe 在磁盘上和在内存里长得不一样。磁盘上是按文件对齐FileAlignment常见 0x200排的内存里是按节对齐SectionAlignment常见 0x1000排的。所以第一步不是直接memcpy而是按节表把每个节从文件偏移搬到内存偏移。核心逻辑是读 DOS 头拿e_lfanew读 NT 头拿OptionalHeader遍历节表对每个节做VirtualAlloc或直接往已分配的大块内存里按VirtualAddress拷贝SizeOfRawData字节。如果SizeOfRawData小于VirtualSize剩余部分补零——这一步很多人漏掉导致 BSS 段数据是脏的。// 简化版按节映射 PE 到内存 PIMAGE_DOS_HEADER dos (PIMAGE_DOS_HEADER)raw; PIMAGE_NT_HEADERS nt (PIMAGE_NT_HEADERS)(raw dos-e_lfanew); BYTE* image (BYTE*)VirtualAlloc(NULL, nt-OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); // 先拷贝头部 memcpy(image, raw, nt-OptionalHeader.SizeOfHeaders); // 逐节拷贝 PIMAGE_SECTION_HEADER sec IMAGE_FIRST_SECTION(nt); for (int i 0; i nt-FileHeader.NumberOfSections; i) { if (sec[i].SizeOfRawData 0) continue; memcpy(image sec[i].VirtualAddress, raw sec[i].PointerToRawData, sec[i].SizeOfRawData); }参数说明SizeOfImage是整个映像在内存中的总大小SizeOfHeaders是头部区域大小VirtualAddress是节在内存中的 RVAPointerToRawData是节在文件中的偏移。注意PAGE_EXECUTE_READWRITE是为了后续能执行实际项目里可以按节权限细分但内存加载场景下先给足权限最省事。2.2 重定位表为什么你的 exe 换个基址就崩exe 默认有首选基址ImageBase常见 0x400000。如果你VirtualAlloc拿到的地址不是这个值所有写死的绝对地址就全错了。这时候必须处理重定位表.reloc节。逻辑是遍历重定位块每个块覆盖 4KB 页面块内每个条目高 4 位是类型IMAGE_REL_BASED_HIGHLOW最常见低 12 位是页内偏移。实际修正值 目标地址 (实际基址 - 首选基址)。// 重定位修正 ULONG_PTR delta (ULONG_PTR)image - nt-OptionalHeader.ImageBase; if (delta ! 0) { PIMAGE_DATA_DIRECTORY relocDir nt-OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; if (relocDir-Size 0) { PIMAGE_BASE_RELOCATION reloc (PIMAGE_BASE_RELOCATION)(image relocDir-VirtualAddress); while (reloc-VirtualAddress ! 0) { DWORD count (reloc-SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD); WORD* entries (WORD*)((BYTE*)reloc sizeof(IMAGE_BASE_RELOCATION)); for (DWORD i 0; i count; i) { if ((entries[i] 12) IMAGE_REL_BASED_HIGHLOW) { DWORD* patch (DWORD*)(image reloc-VirtualAddress (entries[i] 0xFFF)); *patch (DWORD)delta; } } reloc (PIMAGE_BASE_RELOCATION)((BYTE*)reloc reloc-SizeOfBlock); } } }这里有个血泪经验64 位 exe 要用IMAGE_REL_BASED_DIR64修正的是 8 字节别拿 32 位逻辑硬套。另外如果 exe 编译时关了重定位/FIXED这个节可能不存在那就只能祈祷分配到的地址正好是 ImageBase否则直接翻车。2.3 导入表解析把外部 DLL 的函数地址填进去exe 自己不会实现MessageBoxA它得从user32.dll里拿。导入表就是一张“我需要哪些 DLL、每个 DLL 里我要哪些函数”的清单。加载器要做的是对每个IMAGE_IMPORT_DESCRIPTORLoadLibraryA对应的 DLL 名然后遍历OriginalFirstThunkINT和FirstThunkIAT按名字或序号GetProcAddress把结果写进 IAT。PIMAGE_IMPORT_DESCRIPTOR imp (PIMAGE_IMPORT_DESCRIPTOR)(image nt-OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress); while (imp-Name ! 0) { HMODULE hDll LoadLibraryA((char*)(image imp-Name)); PIMAGE_THUNK_DATA orig (PIMAGE_THUNK_DATA)(image imp-OriginalFirstThunk); PIMAGE_THUNK_DATA iat (PIMAGE_THUNK_DATA)(image imp-FirstThunk); while (orig-u1.AddressOfData ! 0) { FARPROC fn; if (orig-u1.Ordinal IMAGE_ORDINAL_FLAG) { fn GetProcAddress(hDll, (char*)(orig-u1.Ordinal 0xFFFF)); } else { PIMAGE_IMPORT_BY_NAME name (PIMAGE_IMPORT_BY_NAME)(image orig-u1.AddressOfData); fn GetProcAddress(hDll, name-Name); } iat-u1.Function (ULONG_PTR)fn; orig; iat; } imp; }注意OriginalFirstThunk可能为 0这时候要回退用FirstThunk当 INT 用但那样会破坏 IAT 原始数据所以最好在映射后先备份一份 IAT。另外LoadLibraryA会触发 DLL 的 DllMain如果目标 DLL 有副作用这一步就可能出问题。2.4 入口点跳转TLS 回调、异常表和 Cookie 别漏导入修完理论上可以跳AddressOfEntryPoint了。但实际项目里还有几块TLS 回调IMAGE_DIRECTORY_ENTRY_TLS要在入口前调用否则某些 CRT 初始化会挂异常表.pdata在 64 位下要注册RtlAddFunctionTable不然抛异常直接崩还有LoadConfig里的 Security Cookie如果 exe 开了/GS不初始化 Cookie 可能被检查拦下。常见做法是先跑 TLS 回调再注册异常表最后((void(*)())entry)()。3. 手把手跑通从零编译一个最小内存加载器3.1 环境准备与项目文件清单你需要一台 Windows 机器装 Visual Studio带 C 桌面开发负载或者 MinGW-w64 也行。项目就三个文件loader.c加载器主逻辑、target.c被加载的测试 exe 源码、build.bat编译脚本。target.c写个最简单的MessageBoxA(NULL, hi, mem, MB_OK)就行编译成 exe 后当输入。加载器本身编译成控制台程序读文件、映射、修表、跳转。# build.bat 示例VS 开发者命令行 cl /nologo /O2 loader.c /link /SUBSYSTEM:CONSOLE /OUT:loader.exe cl /nologo /O2 target.c /link /SUBSYSTEM:WINDOWS /OUT:target.exe参数说明/SUBSYSTEM:CONSOLE让加载器有控制台方便看输出/SUBSYSTEM:WINDOWS让目标 exe 不弹黑框。注意目标 exe 最好关掉 ASLR 相关优化或者确保重定位表存在否则加载器分配地址对不上就白干。3.2 读取文件到内存并校验 PE 结构第一步不是急着映射而是把文件读进来校验IMAGE_DOS_SIGNATURE0x5A4D和IMAGE_NT_SIGNATURE0x00004550。如果签名不对后面所有偏移都是垃圾。读文件用CreateFileAGetFileSizeReadFile或者简单点fopen/fread也行。FILE* fp fopen(target.exe, rb); fseek(fp, 0, SEEK_END); long size ftell(fp); fseek(fp, 0, SEEK_SET); BYTE* raw (BYTE*)malloc(size); fread(raw, 1, size, fp); fclose(fp); PIMAGE_DOS_HEADER dos (PIMAGE_DOS_HEADER)raw; if (dos-e_magic ! IMAGE_DOS_SIGNATURE) { printf(bad dos\n); return 1; } PIMAGE_NT_HEADERS nt (PIMAGE_NT_HEADERS)(raw dos-e_lfanew); if (nt-Signature ! IMAGE_NT_SIGNATURE) { printf(bad nt\n); return 1; } printf(image size: 0x%X, entry: 0x%X\n, nt-OptionalHeader.SizeOfImage, nt-OptionalHeader.AddressOfEntryPoint);这里e_lfanew是 NT 头相对文件开头的偏移不是固定值必须从 DOS 头里读。SizeOfImage决定后面VirtualAlloc要多大AddressOfEntryPoint是 RVA跳转时要加上实际基址。3.3 映射、重定位、修导入、跳入口的完整调用顺序把前面几块串起来顺序不能乱先VirtualAlloc拿内存再拷贝头部和节然后做重定位因为后面修导入时地址已经变了再修导入表最后处理 TLS 和异常表跳入口。如果顺序反了比如先修导入再重定位IAT 里填的地址会被重定位逻辑当成普通指针再改一遍直接错乱。// 完整流程伪代码 BYTE* image map_sections(raw, nt); apply_relocations(image, nt); resolve_imports(image, nt); run_tls_callbacks(image, nt); register_exception_table(image, nt); void (*entry)() (void(*)())(image nt-OptionalHeader.AddressOfEntryPoint); entry();注意entry的调用约定要和目标 exe 匹配exe 入口通常是void __stdcall无参形式但 CRT 入口可能带参数实际项目里可以按IMAGE_NT_HEADERS的Subsystem判断或者干脆用CreateThread起个线程跑避免栈布局问题。3.4 验证加载结果用调试器看 IAT 和入口跑起来后怎么确认真的加载成功了两个办法一是在entry()前下断点用 WinDbg 或 x64dbg attach 上去看image基址处的 PE 头是否完整IAT 里是否填了user32.dll的函数地址二是在目标 exe 里弹个 MessageBox弹出来就说明导入表修对了。如果弹不出来但进程没崩大概率是入口跳转后卡在某个未初始化状态检查 TLS 回调和 Security Cookie。4. 避坑与常见问题内存加载 exe 最容易翻车的几个点4.1 现象跳入口后直接访问违例崩在 0x00000000原因通常是导入表没修全某个 IAT 项还是 0代码一调用就跳空。解决在resolve_imports里对每个GetProcAddress返回值做判空如果为空打印 DLL 名和函数名别默默写 0。另外有些 API 是转发导出forwarded exportGetProcAddress能处理但如果你手动解析导出表就会漏。4.2 现象32 位 exe 在 64 位加载器里跑不起来原因位数不匹配64 位进程没法直接执行 32 位代码。解决加载器位数必须和目标 exe 一致。如果你要在一个进程里加载不同位数的 exe那得走 WOW64 或者另起进程内存加载本身不解决跨位数问题。4.3 现象重定位后数据段里的指针还是旧值原因只处理了.reloc节但有些 exe 把重定位信息放在其他节或者用了IMAGE_REL_BASED_HIGHLOW之外的类型没处理。解决遍历时把所有类型都打出来至少处理HIGHLOW32 位和DIR6464 位遇到ABSOLUTE类型直接跳过。4.4 现象目标 exe 带/GS编译跳入口后触发 security cookie 检查失败原因LoadConfig目录里的 Cookie 没初始化或者__security_init_cookie没被调用。解决在跳入口前手动调用目标 exe 里的__security_init_cookie如果导出了的话或者更简单——编译目标 exe 时加/GS-关掉。内存加载场景下安全 Cookie 的防护意义不大关掉最省事。4.5 现象加载后 MessageBox 弹了但程序退出时崩原因目标 exe 的 CRT 在退出时要做清理但内存加载没有完整的进程环境atexit链或者堆销毁可能出问题。解决加载完跑完逻辑后直接ExitProcess别让目标 exe 自己走正常返回路径。或者用CreateThread跑入口主线程等信号后强杀。5. 二次开发改造把加载器变成你自己的工具链5.1 从“加载 exe”扩展到“加载 DLL 并调导出函数”exe 和 DLL 的加载流程 90% 一样区别在 DLL 有导出表而且入口点DllMain带三个参数HINSTANCE、DWORD reason、LPVOID reserved。改造思路把map_sections、apply_relocations、resolve_imports抽成通用函数然后加一个resolve_exports遍历IMAGE_DIRECTORY_ENTRY_EXPORT把函数名和地址存到哈希表里。调用时按名字查表拿函数指针传参执行。这样你就能在内存里跑插件了磁盘上不留 DLL 文件。// 导出表解析核心 PIMAGE_EXPORT_DIRECTORY exp (PIMAGE_EXPORT_DIRECTORY)(image nt-OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); DWORD* names (DWORD*)(image exp-AddressOfNames); WORD* ords (WORD*)(image exp-AddressOfNameOrdinals); DWORD* funcs (DWORD*)(image exp-AddressOfFunctions); for (DWORD i 0; i exp-NumberOfNames; i) { char* fname (char*)(image names[i]); void* addr image funcs[ords[i]]; // 存入你自己的符号表 }参数说明AddressOfNames是函数名 RVA 数组AddressOfNameOrdinals是名字到函数索引的映射AddressOfFunctions是函数 RVA 数组。注意导出可能按序号导出NumberOfFunctions NumberOfNames按序号调用时直接查funcs数组。5.2 加一层加密壳让磁盘上的 exe 不是明文 PE内存加载的一个典型用途是配合加密壳。你可以把目标 exe 用 AES 加密后存成二进制块加载器读进来先解密到内存再走映射流程。改造点在fread之后、map_sections之前插入解密步骤。密钥可以硬编码在加载器里也可以从外部传入。注意解密后的缓冲区要可写别用只读内存。// 解密示例伪代码实际用 AES 库 aes_decrypt(raw, size, key, iv, decrypted); // 然后用 decrypted 替代 raw 走后续流程这里有个坑加密后的文件大小可能和原始 PE 不一样填充块解密后要按 PE 头里的SizeOfImage截断别把填充数据当节内容映射进去。5.3 验证改造是否成功三个可量化的检查点改完之后怎么确认没改坏我一般会跑三个检查第一用dumpbin /headers target.exe看原始 exe 的ImageBase和SizeOfImage和加载器打印的值对比第二在entry()前 dump 前 64 字节 IAT确认非零项占比超过 90%第三跑一个带printf的测试 exe看输出是否和直接双击运行一致。三个都过基本就稳了。5.4 一个我踩过的坑别在 DllMain 里做重活有一次我把加载逻辑封装成 DLL在DllMain的DLL_PROCESS_ATTACH里直接调LoadLibraryA去修导入表结果死锁了。原因是DllMain里加载其他 DLL 会触发加载器锁而LoadLibraryA又要拿同一把锁。从那以后我每次把内存加载逻辑放到独立线程或者显式初始化函数里绝不在DllMain里碰加载器。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

MyBatis-Plus多数据源实战:动态数据源配置与原理详解

MyBatis-Plus多数据源实战:动态数据源配置与原理详解

0. 开场:如果项目里只有一个数据库,这篇文章可以直接关掉先说个直白的判断:如果你的业务系统只连一个库,那多数据源配置对你来说就是纯粹的增加复杂度,别被“轻松实现”这四个字忽悠着盲上。但如果你遇到的是这些场景&…

📅 2026/10/20 7:44:38
IDA Pro高效逆向:5个隐藏功能助你告别sub_xxx信息废墟

IDA Pro高效逆向:5个隐藏功能助你告别sub_xxx信息废墟

用IDA Pro做逆向分析的人里,十有八九都卡在同一个地方:不是反汇编窗口看不懂,而是信息量太大,代码里全是sub_xxx、dword_xxx,工作台上一片灰色废墟。很多朋友来跟我聊效率问题,说别人几十分钟能讲清楚一条调…

📅 2026/10/20 7:44:38
CircuitPython驱动SSD1675墨水屏:从环境搭建到三个实战案例

CircuitPython驱动SSD1675墨水屏:从环境搭建到三个实战案例

我最近在折腾一块 250122 的墨水屏,驱动包正是 adafruit-circuitpython-ssd1675。这个小东西解决了一个非常具体的问题:在 CircuitPython 生态里,用十几行代码就能把 SSD1675 电子纸点亮并显示内容。相比裸写寄存器、对着 datasheet 翻 VCOM、…

📅 2026/10/20 7:44:38
MORE NEWS

更多资讯

📰

无人机河道漂浮检测数据集构建实战:五类目标识别全流程解析

做无人机河道巡查的朋友应该都有同感:跑完几十公里航线,导出的素材动辄几十个G,熬夜看完视频,发现真正需要上报的漂浮物没几处,而水面上最扎眼的往往是乱七八糟的枯枝、白色垃圾、废旧船只。单纯靠人眼去盯&#xff0c…

📰

AI教材写作如何避开查重雷区?从底层原理到实操降重技巧

2. 为什么AI写教材容易“撞车”:查重系统的底层逻辑3.1 别急着怪AI,先搞清楚“查重”查的是什么“我明明是自己写的,为什么一查重就红?”这是我被问过最多的一句话。尤其是在AI工具刚普及那两年,很多人把大模型生成的内…

📰

国产AI会议助手深度拆解:语音识别、信创适配与私有化部署全解析

这两年因为工作关系,我接触了不少会议纪要工具。说实话,一开始我对“国产AI会议助手”这个说法是有点疑虑的,毕竟底层用的模型、算力、框架,很多都是国外的开源方案。但真正深入去部署、去调优、去做信创环境适配之后,…

📰

Ubuntu 20.04 Docker部署FATE联邦学习框架实操指南

1. 先回答问题:为什么在 Ubuntu 20.04 上要选择 Docker 部署 FATE1.1 FATE 联邦学习框架到底解决什么问题FATE 联邦学习框架的核心场景,是让多个参与方在不直接暴露原始数据的前提下,共同完成模型训练和评估。传统机器学习通常会先把各方数据…

📰

电力通信协议实战组合包:DL/T 698.45与Q/GDW 1376.1可执行解析工具

简介:本资源是面向电力系统自动化工程师、智能电表开发与测试人员的通信协议实践套件,聚焦DL/T 645、Q/GDW 1376.1–1376.3及Q/GDW 698等核心行业协议的解析、模拟与验证需求。资源包含89个文件,涵盖37个协议文本说明(txt&#xf…

📰

比 PyTorch 快 4 倍?map-anything-apache-fp16 精度与速度基准测试完整报告

【免费下载链接】map-anything-apache-fp16 项目地址: https://ai.gitcode.com/hf_mirrors/mlx-community/map-anything-apache-fp16 点击查看 免费下载 map-anything-apache-fp16 是 Meta 团队 MapAnything 3D 重建模型的 float16 精度 MLX 移植版,可在…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬