目录
一、整体加载流程概览
二、详细实现步骤(汇编思路)
阶段0: OEP环境设置引用等
确定OEP加载程序
需要加载的程序大小
编译器链接器解决基地址(到时候在看看)
申请LoadPE所需要的空间
加载的过程
包引用环境变量等
查看基地址是否在400000地址加载
定义全局变量
三、阶段分析
阶段 1:打开目标文件
阶段 2 & 3:文件映射到虚拟内存
阶段 4:解析 PE 结构(核心阶段)
阶段 5:跳到目标程序运行
四、汇编实现的核心特点
五、总结
一、整体加载流程概览
LoadPE 的加载过程主要分为5 个大阶段,共 10 余个具体步骤:
打开文件
映射虚拟内存
获取虚拟内存
解析 PE 结构(最核心、最复杂的一步)
跳到目标程序运行
二、详细实现步骤(汇编思路)
阶段0: OEP环境设置引用等
确定OEP加载程序
首先要确定加载的程序的入口点也就是OEP
但是加载到目标主机 可能0x40000被占用
所以我们loadPE需要解决这个问题
需要加载的程序大小
可选头里面有个sizeofImage 394000也就是这个大小
编译器链接器解决基地址(到时候在看看)
工程选项 设置链接器
申请LoadPE所需要的空间
也就是在程序入口点code的位置后面
申请394000H 字节的空间给我的 LoadPE函数加载的程序
IMAGE_SIZE EQU 394000H .code ORG IMAGE_SIZE ; 也就是LoadPE函数的其实位置是394000H + 1H LoadPE PROC ; Function ret LoadPE endp加载的过程
包引用环境变量等
使用的模式
.386 .model flat,c option casemap:none引用的宏静态库
.const ; constant IMAGE_SIZE EQU 394000H .data ; data g_szfile db "PlantsVsZombies.exe" ; file Name .code ORG IMAGE_SIZE LoadPE PROC ; Function ret LoadPE endp start: ; main INVOKE LoadPE ; call function end start查看基地址是否在400000地址加载
这也就说明成功加载到了0x00400000的位置
400000以后的位置都预留好了 ---> 394000
定义全局变量
LOCAL @hFILE:HANDLE LOCAL @hFileMap:HANDLE LOCAL @szPeBuffer:LPVOID LOCAL @pDosHeader:PTR IMAGE_DOS_HEADER LOCAL @pNtHeader:PTR IMAGE NT HEADERS LOCAL @pSecetionHeader:PTR IMAGE_SECTION_HEADER LOCAL @dwNumberSec:DWORD LOCAL @dwSizeOfHeader:DWORD LOCAL @dwEP:DWORD LOCAL @plmagelmpHeader:PTR IMAGE_IMPORT_DESCRIPTOR LOCAL @ZeroHeadImp: IMAGE IMPORT DESCRIPTOR LOCAL @dwImageBase:DWORD LOCAL @dwMod:HANDLE三、阶段分析
阶段 1:打开目标文件
使用
CreateFileAPI 打开目标程序文件(例如PlantsVsZombies.exe)。以只读方式打开,获取文件句柄(
@hFILE)。
阶段 2 & 3:文件映射到虚拟内存
调用
CreateFileMapping,创建一个文件映射对象。调用
MapViewOfFile,将文件内容映射到进程的虚拟内存空间,得到内存指针(@szPeBuffer)。此时目标 PE 文件的内容以只读的形式存在于内存中,供后续解析使用。
阶段 4:解析 PE 结构(核心阶段)
这是整个 LoadPE 中最重要、最体现汇编功底的部分,需要一步步手动解析 PE 文件格式:
4.1 获取 DOS 头
将映射的内存首地址赋值给
@pDosHeader。通过
e_lfanew字段定位到 PE 头的位置。
4.2 获取 NT 头
根据 DOS 头的
e_lfanew偏移,找到IMAGE_NT_HEADERS。
4.3 获取文件头大小(SizeOfHeaders)
从 NT 头的 OptionalHeader 中读取
SizeOfHeaders,用于后续拷贝头部。
4.4 获取节数量(NumberOfSections)
从 FileHeader 中读取节的数量,后续用于遍历所有节。
4.5 获取程序入口点(AddressOfEntryPoint)
读取 OptionalHeader 中的
AddressOfEntryPoint(RVA),并加上 ImageBase,得到最终入口点地址。
4.6 获取导入表(Import Directory)
从 DataDirectory 中找到导入表的位置(IMAGE_DIRECTORY_ENTRY_IMPORT)。
由于植物大战僵尸是需要api函数,由于我们是脱了操作系统,那就没人帮我获取请api,需要自己手动解析导入表
4.7 获取名称表和 IAT 表
遍历
IMAGE_IMPORT_DESCRIPTOR结构,分别获取 DLL 名称 RVA 和 FirstThunk(IAT)、OriginalFirstThunk(INT)。
4.8 加载所需的 DLL
对于每个需要导入的 DLL,调用 LoadLibrary 将其加载到当前进程。填充IAT表
4.9 修复导入地址表(IAT)
遍历 IAT 中的每个函数:
如果是序号导入,则直接使用序号。
如果是名称导入,则通过 GetProcAddress 获取函数地址。
将获取到的真实函数地址填充回目标程序的 IAT 表中,使其能正常调用系统 API。
阶段 5:跳到目标程序运行
对目标加载地址(通常为
00400000)进行VirtualProtect,修改内存保护属性为PAGE_EXECUTE_READWRITE。将解析好的 PE 头部和所有节数据拷贝到目标地址空间。
完成所有准备工作后,使用
JMP指令直接跳转到目标程序的入口点(@dwEP)。此时控制权完全交给目标程序,LoadPE 的使命完成。
四、汇编实现的核心特点
全程手动操作:几乎所有 PE 结构字段都需要通过寄存器偏移手动读取和计算。
寄存器密集:由于汇编无法直接进行内存到内存的操作,大量使用
MOV、ADD、LEA等指令,以及ASSUME伪指令来模拟结构体。内存精确控制:通过
VirtualProtect+crt_memcpy实现对内存的精准拷贝和权限管理。地址固定技巧:使用
ORG IMAGE_SIZE将自身代码推后,为目标程序的 ImageBase(00400000)预留足够空间。
五、总结
用汇编实现 LoadPE 的本质是:
自己当一次 Windows Loader—— 手动打开文件、映射内存、解析 PE 头、拷贝数据、修复导入表,最后把执行权交给目标程序。
整个过程虽然代码量较大、逻辑繁琐,但赋予了开发者对加载流程的完全控制权,为后续的免杀优化(例如手动导出表解析、减少敏感 API 调用、直接 syscall 等)提供了坚实的基础。
这就是为什么许多高隐蔽 Loader 都倾向于使用汇编来实现核心加载逻辑。