内存直接加载运行
- 病毒木马具有模拟PE加载器的功能, 它们可以把DLL或者exe等PE文件从内存中直接加载到病毒木马的内存中去执行, 不需要通过LoadLibrary等现成的API函数去操作;
- 假如程序需要动态调用DLL文件, 内存加载运行技术可以把DLL作为资源插入到自己的程序中, 此时可直接在内存中加载运行即可;
- 内存加载技术的核心就在于模拟PE加载器PE文件, 也就是对导入表、导出表、重定位表的操作过程;
实现原理
首先, 需要将DLL文件加载到内存中, 按照映像大小进行对齐后映射到内存中, 然后根据重定位表修改硬编码数据, 最后根据导出表函数地址修正导入表;
流程总结
- 根据映像大小SizeOfImage申请可读可写可执行的内存空间, 首地址即DLL加载基址;
- 获取其映像对齐大小
SectionAlignment
并复制到该内存空间中(FileBuffer => ImageBuffer); - 修正重定位表;
- 修正导入表, 根据PE结构中的导入表, 加载所需的Dll, 并获取导入函数的地址将其写入导入表中;
- 修改DLL的加载基址ImageBase;
- 获取DLL入口地址, 构造DllMain函数实现加载;
对于exe文件, 重定位表不是必须的。因为对于exe进程来说, 进程最早加载的模块是exe模块, 所以它可以按照默认加载基址加载到内存中; exe和Dll唯一的区别在于构造入口函数的差别, exe不需要构造入口函数, 而是根据PE结构获取exe的入口地址偏移AddressOfEntryPoint并计算出入口地址, 然后直接跳转。
1. 将Dll文件读入内存
1 | wchat_t szFileName[MAX_PATH] = L"C:\\Users\\dell\\OneDrive\\桌面\\哔哩哔哩学习\\PE Learn\\PELoader_Demo\\Debug\\TestDLL_01.dll"; |
创建或打开文件或I/O设备, 此函数区分多字节和Unicode两种模式
1 | // UNICODE |
- 如果函数成功, 则返回值是指定文件、设备、命名管道或邮件槽的打开句柄;
- 如果函数失败, 则返回值为INVALID_HANDLE_VALUE;
检索指定文件的大小(以字节为单位)
1 | DWORD |
- 如果函数成功, 则返回值为文件大小的低位双字, 如果lpFileSizeHigh为非NULL, 则该函数会将文件大小的高位双字放入该参数指向的变量中;
- 如果函数失败且lpFileSizeHigh为NULL, 则返回值INVALID_FILE_SIZE;
从指定的文件或输入/输出 (I/O) 设备读取数据
1 | BOOL |
- 如果函数成功,则返回值为非零 (TRUE);
- 如果函数失败或正在异步完成,则返回值为零 (FALSE);
2. FileBuffer => ImageBuffer
将FileBuffer转换成ImageBuffer可以分为两步:
- 先将PE头部复制至内存中;
- 然后循环将节内容复制过去;
代码示例
1 | BOOL MmMapFile(LPVOID lpData, LPVOID lpBaseAddress) |
3. 修正重定位表
重定位表
- Relocation(重定位)是一种将程序中的一些地址修正为运行时可用的实际地址的机制。在程序编译过程中, 由于程序中使用了各种全局变量和函数, 这些变量和函数的地址还没有确定, 因此它们的地址只能暂时使用一个相对地址。当程序被加载到内存中运行时, 这些相对地址需要被修正为实际的绝对地址, 这个过程就是重定位;
- 在Windows操作系统中, 程序被加载到内存中运行时, 需要将程序中的各种内存地址进行重定位, 以使程序能正确运行。Windows系统使用PE(Portable Executable)文件格式来存储可执行程序, 其中包括重定位信息。当程序被加载到内存中时, 系统会解析这些重定位信息, 并将程序中的各种内存地址进行重定位。
- 重定位表一般出现在
Dll
中, 因为Dll
都是动态加载, 所以地址不固定, Dll的入口点在整个执行过程中至少要执行2次, 一次时在开始时执行初始化工作, 一次则是在结束时做最后的收尾工作, 重定位表则是解决Dll的地址问题;
重定位表的结构
代码示例
1 | BOOL DoRelocationTable(LPVOID lpBaseAddress) |
4. 修正导入表
PE加载器在加载PE的时候会将导入函数的地址填入导入地址表中, 导入表结构如下:
1 | typedef struct _IMAGE_IMPORT_DESCRIPTOR { |
主要用到的是OriginalFirstThunk和FirstThunk; 这两个表用到的结构体是一样的(IMAGE_THUNK_DATA):
1 | typedef struct _IMAGE_THUNK_DATA32 { |
如果指向导入名称表; 则内容是AddressOfData, 指向IMAGE_IMPORT_BY_NAME结构体;
如果指向导入地址表:
- 序号导入的话, Ordinal首位是1, 低4位是导入序号;
- 名称导入的话, Function的值是函数地址;
这里我们需要将导入函数的地址填入导入地址表中, 所以需要知道这个函数是怎么导入的, 然后通过GetProcAddress API获取函数地址, 然后将函数地址填入导入地址表中;
通过GetProcAddress获取函数地址, 需要知道Dll名称, 通过Dll名称获取模块句柄
所以代码流程是:
修正导入表流程
- 先获取导入表数组的数量和第一个成员的地址;
- 根据导入表的数量, 进行循环遍历;
- 获取导入名称表;
- 获取导入地址表;
- 进行导入名称表的遍历(导入名称表数组以0作为最后一个成员结束);
- 获取导入函数的名称或序号;
- 加载这个Dll, 通过名称或序号, 获取其函数地址;
- 将地址填入导入地址表;
- 进入下一次循环;
- 进入下一次循环;
- 两次遍历完成后, 导入表就已经完成了修正;
代码示例
1 | BOOL DoImportTable(LPVOID lpBaseAddress) |
5. 修改Dll的加载基址ImageBase
- PE加载器在加载PE的时候会将进程分配的基地址填入扩展头的ImageBase中;
1 | BOOL SetImageBase(LPVOID lpBaseAddress) |
6. 修改DllMain入口点
- 调用Dll的入口函数DllMain, 函数地址则是PE文件的入口点;
1 | BOOL CallDllMain(LPVOID lpBaseAddress) |
7. 获取Dll导出函数
导出表
- PE文件运行, 需要依赖Dll; 系统Dll包括Kernel32.dll、User32.dll等;
- 导出表时当前PE文件提供了哪些函数给别人使用;
- 不管是exe还是Dll, 本质都是PE文件; exe文件也可以导出函数给别人使用; 一般exe没有, 但不是不可以;
导出表结构
1 | typedef struct _IMAGE_EXPORT_DIRECTORY { |
遍历流程
- 获取导出函数名称;
- 比较是否是要找函数名称;
- 如果是, 则获取函数序号(2字节);
- 根据函数序号获取函数地址;
1 | LPVOID MmGetProcAddress(LPVOID lpBaseAddress, wchar_t* lpszFuncName) |
8. 释放内存加载的Dll
释放资源
1 | BOOL MmFreeLibrary(LPVOID lpBaseAddress) |
内存加载Dll执行演示
示例Dll源代码
1 | // dllmain.cpp : 定义 DLL 应用程序的入口点。 |
PELoader_Demo.cpp
1 | // PELoader_Demo.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 |
调用导出函数
内存加载exe执行
待补充…
x64位手工模拟PE加载器
x64位手工模拟PE加载器
实现原理和x32位相同, 需注意的是在修复重定位时判断是否进行修复时: x64位修复项等于0x10; x86位(IMAGE_REL_BASED_HIGHLOW)等于0x3;
代码实现
1 | // LoadPE_Demo_x64.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 |
仓库地址
参考资料
参考书籍
- 《Windwos黑客编程技术详解》第4章第3节
参考Blog