前言
手工模拟PE加载器,也就是内存加载技术,做一遍这个可谓真是把我仅有的PE结构的知识用到了个遍,算是作为PE入门阶段的“期末考试”吧,哈哈哈
本来想写好几篇关于PE结构的笔记文章来分享对PE结构常见表结构(导入表,导出表,重定位表)的分析,完成这一篇之后,我觉得没必要了
因为我觉得这一篇基本上概括了对所有常用表结构的分析与实践,理论结合实践才是最好的学习方式;
这里将从头到尾的来介绍如何手工模拟PE加载器加载PE文件并执行(其实是学习笔记哈哈哈),希望能帮到需要的人
内存加载运行介绍
把DLL或exe加载到内存中去执行,而不需要通过LoadLibrary等API函数去操作,当程序需要动态调用DLL的时候,内存加载技术可以将DLL作为资源插入程序,然后内存加载,这样就不需要将DLL释放到本地了
内存加载技术的核心就在于模拟PE加载器加载PE文件,也就是对导入表,导出表,重定位表的操作过程
内存加载技术的原理&实现
首先,将DLL文件加载到内存当中,需要先将DLL按照映像对齐大小映射到内存中,然后根据重定位表修改硬编码数据,最后根据导出表函数地址来修改导入表函数地址(当然,也可以通过GetProcAddress来实现)
具体PE原理在之前的文章或笔记里都有详细提到,这里就不提了
加载到内存中后,需要获取DLL文件的入口地址,然后跳转执行来完成启动
总的来说操作流程如下:
- 根据映像大小SizeOfImage在自己程序内申请可读可写可执行的空间,首地址就是DLL的加载基址
- 将DLL文件按照映像对齐大小复制到上述空间
- 修正重定位表
- 修正导入表
- 修改DLL的加载基址ImageBase
- 获取DLL的入口地址,构造DLLMain函数实现加载
对于EXE文件,重定位表不是必须的,exe和dll加载的原理唯一的区别在于构造入口函数,exe不需要构造入口函数,根据PE结构获得入口地址偏移AddressOfEntryPoint并计算出入口地址即可,然后跳转
下面来分别实现一下两种内存加载技术:
1,将DLL文件读入内存
char szFileName[MAX_PATH] = "C:\\Users\\selph\\source\\WinHackBook\\04-LoadPE\\Debug\\TestDll1.dll";
// 打开DLL文件并获取DLL文件大小
HANDLE hFile = ::CreateFileA(szFileName, GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_ARCHIVE, NULL);
DWORD dwFileSize = ::GetFileSize(hFile, NULL);
// 申请动态内存并读取DLL到内存中
BYTE* lpData = new BYTE[dwFileSize];
DWORD dwRet = 0;
::ReadFile(hFile, lpData, dwFileSize, &dwRet, NULL);
// 将内存DLL加载到程序中
LPVOID lpBaseAddress = MemLoadLibrary(lpData, dwFileSize);
printf("DLL加载成功\n");
通过CreateFile API 打开PE文件(这里是DLL文件):
HANDLE CreateFileA(
LPCSTR lpFileName, //文件名
DWORD dwDesiredAccess, //请求的访问,常用的是GENERIC_READ | GENERIC_WRITE
DWORD dwShareMode, //共享模式,读/写/删除,FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE
LPSECURITY_ATTRIBUTES lpSecurityAttributes, //安全描述符,一般NULL
DWORD dwCreationDisposition, //对存在或者不存在进行的操作,OPEN_EXISTING为打开已经存在的文件
DWORD dwFlagsAndAttributes, //文件或设备的属性,不设置属性是FILE_ATTRIBUTE_NORMAL,文件应该被存档,标记要备份或者删除的文件用FILE_ATTRIBUTE_ARCHIVE
HANDLE hTemplateFile //访问模板,NULL
);
成功则返回文件的打开句柄
https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea
然后通过GetFileSize API 获取其大小:
DWORD GetFileSize(
HANDLE hFile, //文件句柄
LPDWORD lpFileSizeHigh //返回文件大小的高位双字,不需要可NULL
);
返回值为文件大小的低位双字(文件大于4GB,一个DWORD就不够表示其大小,所以文件小于4GB,只用低位双字即可)
https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfilesize
有了文件句柄和文件大小,接下来就可以把文件读到内存缓冲区了,通过new申请一个这么大的内存缓冲区
//动态申请数组类型:
int *iArr = new int[3];
//释放:
delete [] iArr;
然后使用ReadFile API进行读入:
BOOL ReadFile(
HANDLE hFile, //文件句柄
LPVOID lpBuffer, //内存缓冲区
DWORD nNumberOfBytesToRead,//要读取的最大字节数
LPDWORD lpNumberOfBytesRead, //读取的实际字节数
LPOVERLAPPED lpOverlapped //使用FILE_FLAG_OVERLAPPED打开文件时才用,这里NULL即可
);
返回TRUE即为成功,到这里文件内容全部复制到缓冲区里了,lpData就是文件内容的缓冲区指针,接下来通过MemLoadLibrary函数进行文件到映像的各种转换
https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile
2,获取映像大小
现在已经打开了文件,获取了文件内容,接下来要把文件版的PE转化为映像版的PE,首先第一步,为映像申请空间,这就需要知道需要的映像大小是多少
DWORD GetSizeOfImage(LPVOID lpData){
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpData;
PIMAGE_NT_HEADERS32 pNt = (PIMAGE_NT_HEADERS32)(pDos->e_lfanew + (DWORD)pDos);
DWORD dwSizeOfImage = pNt->OptionalHeader.SizeOfImage;
return dwSizeOfImage;
}
PE文件的映像大小写在了PE头的扩展头的SizeOfImage里,只需要将它读取出来即可
在内存中和在文件中,PE文件的headers都是一样的
3,申请内存空间
LPVOID lpBaseAddress = ::VirtualAllocEx(GetCurrentProcess(),NULL,dwSizeOfImage,MEM_COMMIT|MEM_RESERVE,PAGE_EXECUTE_READWRITE);
这就不用封装成函数了,因为一句话搞定哈哈哈,用的是VirtualAllocEx API:
LPVOID VirtualAllocEx(
HANDLE hProcess, //进程句柄
LPVOID lpAddress, //指定要分配内存的起始位置,如NULL,则函数来确定起始位置
SIZE_T dwSize, //申请空间的大小
DWORD flAllocationType, //申请权限类型,这里要一步一步保存和提交修改,所以选择MEM_COMMIT|MEM_RESERVE
DWORD flProtect //保护类型,可读可写可执行:PAGE_EXECUTE_READWRITE
);
函数会将分配的内存初始化为0,不用手动再初始化了,GetCurrentProcess() 函数可以返回当前进程的伪句柄(不用释放的那种)
https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex
4,按照内存对齐大小读取文件
现在有为映像准备的空间了,接下来把文件读取成映像吧
通过PELoad可以很方便查看到文件和内存的对齐大小,简单来说,内存中每个部分分配1000字节,文件中则分配200个字节,现在要把文件的200字节复制到内存中的1000字节中去,多余的空位用0填充
借用网上的图片来看,就比较好理解:
代码如下:参数分别是文件缓冲区的指针(基地址),映像缓冲区的指针(基地址)
BOOL MemMapFile(LPVOID lpData, LPVOID lpBaseAddress){
//读取NT头
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpData;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((DWORD)pDos + pDos->e_lfanew);
//读取所有头+区段头的大小
DWORD dwSizeOfHeaders = pNt->OptionalHeader.SizeOfHeaders;
//获取区段数量
WORD dwNumOfSections = pNt->FileHeader.NumberOfSections;
//加载所有头+区段头的大小
::RtlCopyMemory(lpBaseAddress,lpData,dwSizeOfHeaders);
//对齐SectionAlignment循环加载区段
//获得第一个区段的文件位置(NT头之后紧接着就是区段)
PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pNt + sizeof(IMAGE_NT_HEADERS));
for (int i = 0; i < dwNumOfSections; i++){
//判断区段存不存在于文件dll中,不存在就跳过
if ((pSectionHeader->VirtualAddress == 0) || (pSectionHeader->SizeOfRawData == 0)) {
pSectionHeader++;
continue;
}
LPVOID lpSrcMem = (LPVOID)(pSectionHeader->PointerToRawData + (DWORD)lpData);
LPVOID lpDstMem = (LPVOID)(pSectionHeader->VirtualAddress + (DWORD)lpBaseAddress);
DWORD dwSizeOfRawData = pSectionHeader->SizeOfRawData;
::RtlCopyMemory(lpDstMem,lpSrcMem,dwSizeOfRawData);
pSectionHeader++;
}
return TRUE;
}
首先从PE结构中读取头的大小,然后将头部分直接复制到映像中去,API RtlCopyMemory:
VOID RtlMoveMemory(
_Out_ VOID UNALIGNED *Destination, //目的存储块
_In_ const VOID UNALIGNED *Source, //源头存储块
_In_ SIZE_T Length //长度
);
https://docs.microsoft.com/en-us/windows/win32/devnotes/rtlmovememory
接下来获取区段数量(在PE头的文件头里NumberOfSections)和第一个区段头的位置就是PE头结束后的位置:
这两个头是紧挨着的,有这两样就可以开始循环填充了,循环区段数量次:
先进行判断,判断区段存不存在,如果区段不存在,则区段头里的几个值(文件中大小,内存中大小)都会是0;
如果区段存在,分别获取文件中和映像该区段的首地址,然后按照区段在文件中的大小向映像中进行复制,复制完检查下一个区段头,直到将全部区段都映射到映像中去
5,修正重定位表
到此,映像已经映射出来了,只需要稍作修改就能让映像运行起来了:
PE加载器在加载PE的时候会修正重定位表中的地址中的硬编码信息,将硬编码进行重定位(也就是把原来的VA转换成RVA,再转换成现在的VA)
先了解一下重定位表的结构:
重定位表是一个结构体数组,以全0作为数组最后一项结尾,数组里有3个内容,VirtualAddress是映像中存在硬编码的部分的基地址的偏移,SizeOfBlock是结构体的大小,Block数组记录了存在硬编码的地址RVA:
举个例子:
这里第一个成员的值是360F,这里WORD类型是2字节,这2个字节16位里,前4位记录的是类型,一般来说只要是x86是3,x64是A,就需要进行重定位,这里用的是x86的软件,所以是3
后12位记录的是偏移量,这里的后12位是60F,则存在硬编码的地址应该在内存中的基址+11000h+60Fh的位置,换算成文件位置则是:400h+60Fh = A0Fh的位置:
定位到这个位置之后,把这个位置DWORD值 - PE的ImageBase + 实际的ImageBase 就是修正后的结果了
所以就先把重定位表中的重定位信息修正了,根据以上原理去看代码就不难理解:
//修改重定位表,这里的参数是内存中的基址
BOOL DoRelocationTable(LPVOID lpBaseAddress){
//定位到重定位表
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpBaseAddress;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((DWORD)lpBaseAddress + pDos->e_lfanew);
PIMAGE_BASE_RELOCATION pLoc = (PIMAGE_BASE_RELOCATION)((DWORD)lpBaseAddress+
pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
//判断是否有重定位表,数据目录表不存在的时候,VirtualAddress为0,也就是指向映像基址
if ((LPVOID)pLoc == lpBaseAddress) {
return TRUE;
}
//开始扫描重定位表,重定位表VirtualAddress和SizeOfBlock都为0表示重定位表结束
while ((pLoc->VirtualAddress + pLoc->SizeOfBlock) != 0) {
//重定位数据,位于IMAGE_BASE_RELOCATION表开头8字节之后
PWORD pLocData = (PWORD)((DWORD)pLoc + sizeof(IMAGE_BASE_RELOCATION));
//计算本节需要修正的重定位项(地址)的数目,每个数据都是16字节(4+12字节,高4位指出重定位类型,低12位是RVA)
//sizeOfBlock的值包括了SizeOfBlock和VirtualAddress的大小,8字节,所以需要减去
DWORD dwNumOfReloc = (pLoc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
for (DWORD i = 0; i < dwNumOfReloc; i++){
//高4位是类型,如果等于3则表示需要修正
if ((DWORD)((pLocData[i] & 0x0000F000) == 0x00003000)) {
//需要修正的数据
//修改重定位表的数据,重定位表记录的是存在硬编码的地址,以基址+偏移的形式
//存在硬编码的地址 = 重定位基址+重定位表数据偏移 = 基址+重定位地址+重定位数据(数据后12位)
PDWORD pAddress = (PDWORD)((PBYTE)pDos + pLoc->VirtualAddress + (pLocData[i] & 0x0FFF));
//重定位地址 = 硬编码地址0 - ImageBase + 实际基地址
// = 实际基地址 - ImageBase + 硬编码地址
//这种写法也行:*pAddress = *pAddress - pNt->OptionalHeader.ImageBase + (DWORD)pDos
DWORD dwDelta = (DWORD)pDos - pNt->OptionalHeader.ImageBase;
*pAddress += dwDelta;
}
}
//转移到下一个重定位区段进行处理
pLoc = (PIMAGE_BASE_RELOCATION)((PBYTE)pLoc + pLoc->SizeOfBlock);
}
return TRUE;
}
6,修正导入地址表
PE加载器在加载PE的时候会将导入函数的地址填入导入地址表中,导入表结构如下:是个导入表结构体数组
主要要用到的项是OriginalFirstThunk和FirstThunk,前者是导入名称表,后者是导入地址表
这两个表用到的结构体是一样的,都是IMAGE_THUNK_DATA:
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
这个结构体里装了个联合体,
如果指向导入名称表:则内容是AddressOfData,指向PIMAGE_IMPORT_BY_NAME的结构体
如果指向导入地址表:
-
序号导入的话,Ordinal首位是1,低4位是导入序号,
-
名称导入的话,Function的值是函数地址
这里我们要将导入函数的地址填入导入地址表中,所以需要知道这个函数是怎么导入的,然后通过GetProcAddress API获取其函数地址,然后将函数地址填入导入地址表中。
通过GetProcAddress获取函数地址,还需要知道dll名称,通过dll名称获取模块句柄
所以这里的代码流程是:
- 先获取导入表数组的数量和第一个成员的地址
- 根据导入表的数量,进行循环遍历
- 获取导入名称表地址
- 获取导入地址表地址
- 进行导入名称表的遍历(导入名称表数组以0作为最后一个成员结束)
- 获取导入函数的名称或序号
- 加载这个dll,通过名称或序号,获取其函数地址
- 将这个地址填入导入地址表
- 进入下一次循环
- 进入下一次循环
- 两次遍历完成后,导入表就已经完成了修正
结合以上原理,下面的代码应该不难看懂:
//向导入表中填入导入表地址
BOOL DoImportTable(LPVOID lpBaseAddress){
//定位导入表
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpBaseAddress;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((DWORD)lpBaseAddress + pDos->e_lfanew);
PIMAGE_IMPORT_DESCRIPTOR pImp = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)lpBaseAddress +
pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
//遍历DLL导入表中的DLL及获取导入表中的函数地址
char* lpDllName;
HMODULE hDll = NULL;
DWORD i = 0;
PIMAGE_THUNK_DATA lpImpName = NULL;
PIMAGE_IMPORT_BY_NAME lpName = NULL;
PIMAGE_THUNK_DATA lpImpAddr = NULL;
FARPROC lpFunAddress = NULL;
while (TRUE) {
if (0 == pImp->OriginalFirstThunk) {
break;
}
//获取导入表中的DLL名称并加载DLL
lpDllName = (char*)((DWORD)pDos + pImp->Name);
hDll = ::GetModuleHandleA(lpDllName);
if (hDll == NULL) {
hDll = ::LoadLibraryA(lpDllName);
if (hDll == NULL) {
pImp++;
continue;
}
}
i = 0;
//IMAGE_THUNK_DATA指向一个4字节的联合体,内容是某数组的RVA
//获取INT首地址
lpImpName = (PIMAGE_THUNK_DATA)(pImp->OriginalFirstThunk + (DWORD)pDos);
//获取IAT首地址
lpImpAddr = (PIMAGE_THUNK_DATA)(pImp->FirstThunk + (DWORD)pDos);
while (TRUE) {
if (lpImpName[i].u1.AddressOfData == 0) {
break;
}
//获取导入函数名称结构
lpName = (PIMAGE_IMPORT_BY_NAME)(lpImpName[i].u1.AddressOfData +(DWORD)pDos);
//判断是名称导出还是序号导出,如果是序号导出,则Ordinal最高位为1,低4位为序号
if (0x80000000 & lpImpName[i].u1.Ordinal) {
//序号导出
//当IMAGE_THUNK_DATA最高位为1时,表示函数以序号方式导入
//????序号导入长啥样
lpFunAddress = ::GetProcAddress(hDll,(LPCSTR)(lpImpName[i].u1.Ordinal & 0x0000FFFF));
}
else {
lpFunAddress = ::GetProcAddress(hDll, lpName->Name);
}
lpImpAddr[i].u1.Function = (DWORD)lpFunAddress;
i++;
}
pImp++;
}
return TRUE;
}
7,修改加载基地址
PE加载器在加载PE的时候会将进程分配的基地址填入到扩展头的ImageBase里去,我们也这么干:
// 修改PE文件加载基址IMAGE_NT_HEADERS.OptionalHeader.ImageBase
BOOL SetImageBase(LPVOID lpBaseAddress){
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpBaseAddress;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((DWORD)lpBaseAddress + pDos->e_lfanew);
pNt->OptionalHeader.ImageBase = (DWORD)lpBaseAddress;
return TRUE;
}
8,修改内存页属性
因为我们加载到内存中的映像是需要执行的,所以给这块内存加上可执行的属性
//修改内存页属性
DWORD dwoldProtect = 0;
::VirtualProtectEx(GetCurrentProcess(), lpBaseAddress, dwSizeOfImage, PAGE_EXECUTE_READWRITE, &dwoldProtect);
9,修改Dllmain入口点
调用DLL的入口函数DllMain,函数地址即为PE文件的入口点IMAGE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint
BOOL CallDllMain(LPVOID lpBaseAddress){
typedef_DllMain DllMain = NULL;
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpBaseAddress;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((DWORD)pDos + pDos->e_lfanew);
typedef BOOL(__stdcall* typedef_DllMain)(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved);
DllMain = (typedef_DllMain)((DWORD)pDos + pNt->OptionalHeader.AddressOfEntryPoint);
BOOL bRet = DllMain((HINSTANCE)lpBaseAddress, DLL_PROCESS_ATTACH, NULL);
return bRet;
}
这里定义入口函数,然后获取入口函数地址,进行调用执行
到这里,这个dll已经成功加载到内存中了
接下来我们可以获取dll的导出函数来调用
10,获取Dll导出函数
因为这个DLL是我们手工加载的,所以我们也需要手工来获取函数地址(主要是不了解GetProcAddress具体是怎么操作的哈哈哈),那么我们就来模拟一个GetProcAddress函数:
这里涉及的知识是PE结构体的导出表,我们通过遍历导出表,通过参数传入的名称和导出表的函数名称相比较,来获取指定的函数地址
导出表结构如图,导出表内部重要的几个成员分别是:
- 函数名称数组AddressOfNames
- 函数地址数组AddressOfFunctions
- 函数序号数组AddressOfNameOrdinals
- 函数名称数量NumberOfNames
- 函数地址数量NumberOfFunctions
上面3个数组里装的都是DWORD类型的RVA,RVA分别指向函数名称,函数地址,函数序号
由于这三个数组是对齐的,所以序号要同时往后进行遍历
遍历流程如下:
- 获取导出函数名称
- 比较是否是我们要找的函数名称
- 如果是,则获取函数序号(2字节)
- 根据函数序号获取函数地址
//模拟GetProcAddress函数
LPVOID MemGetProcAddress(LPVOID lpBaseAddress, PCHAR lpszFuncName){
//定位导出表
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpBaseAddress;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((DWORD)lpBaseAddress + pDos->e_lfanew);
PIMAGE_EXPORT_DIRECTORY pExp = (PIMAGE_EXPORT_DIRECTORY)((DWORD)pDos
+ pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
//获得导出表数据
PDWORD lpAddressOfNames = (PDWORD)(pExp->AddressOfNames + (DWORD)lpBaseAddress);
DWORD dwNumOfNames = pExp->NumberOfNames;
PCHAR pFunName = NULL;
PDWORD lpAddressOfFunction = (PDWORD)((DWORD)pDos + pExp->AddressOfFunctions);
PWORD lpAddressOfNameOrdinals = (PWORD)((DWORD)pDos + pExp->AddressOfNameOrdinals);
LPVOID lpFunc = NULL;
//遍历导出表寻找函数
for (int i = 0; i < dwNumOfNames; i++){
pFunName = (PCHAR)((DWORD)pDos + lpAddressOfNames[i]);//导出函数名数组
if (0 == ::lstrcmpA(pFunName, lpszFuncName)) {
//0表示相同
WORD wHint = lpAddressOfNameOrdinals[i];
lpFunc = (LPVOID)((DWORD)pDos + lpAddressOfFunction[wHint]);
break;
}
}
return lpFunc;
}
11,释放内存加载的DLL
到这里关于加载DLL及其调用导出函数已经结束了,剩下的内容就是释放资源了
//释放加载到内存中的dll的空间
BOOL MemFreeLibrary(LPVOID lpBaseAddress){
BOOL bRet = FALSE;
if (lpBaseAddress == NULL) {
return bRet;
}
bRet = ::VirtualFreeEx(GetCurrentProcess(),lpBaseAddress,0,MEM_RELEASE);
lpBaseAddress = NULL;
return bRet;
}
12,释放读取的DLL文件
//释放读取的DLL文件
delete[] lpData;
lpData = NULL;
::CloseHandle(hFile);
内存加载DLL执行演示
示例DLL源代码
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
extern "C" __declspec(dllexport)
void ShowMessage() {
MessageBoxA(NULL, "I'm DLL File", "HELLO", MB_OK);
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
调用导出函数
内存加载EXE执行
内存加载DLL和加载EXE基本上流程是一样的,不同的地方在于入口点的操作
判断文件是exe还是dll
在PE头的标准头里的Characteristic里,0x2000有值表示是DLL,否则是exe,所以可以通过这个进行判断
//判断是exe(TRUE)还是dll(FALSE)
BOOL IsExeorDll(LPVOID lpBaseAddress){
//exe和dll的区别在PE头的标准头的特征里有
//读取NT头
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpBaseAddress;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((DWORD)pDos + pDos->e_lfanew);
if (pNt->FileHeader.Characteristics & 0x00002000 == 1) {
return FALSE;
}
return TRUE;
}
如果是EXE,那么入口点的操作要进行一点点更变:
修改EXE入口点
exe文件只需要直接跳转到入口点即可
// 执行exe,直接跳转到PE文件的入口点IMAGE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint即可
BOOL CallExeEntry(LPVOID lpBaseAddress){
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpBaseAddress;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((DWORD)pDos + pDos->e_lfanew);
LPVOID lpExeEntry = (LPVOID)((DWORD)pDos + pNt->OptionalHeader.AddressOfEntryPoint);
// 跳转到入口点处执行
__asm
{
mov eax, lpExeEntry
jmp eax
}
return TRUE;
}
到此为止,EXE和DLL加载的区别已经讲完了,对前面的代码稍作修改即可完成对exe文件的内存加载运行
还有一点不同就是exe通常来说,重定位是不必要的,如果exe程序没有重定位表的话,那就需要将exe文件加载到默认加载基址上,不然会加载失败
内存加载EXE执行演示
示例EXE源代码
// TestExe.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
#include<Windows.h>
int main()
{
MessageBoxA(NULL,"This is Loaded Exe 哦","阳光正好",MB_OK);
return 0;
}
内存加载执行
可通过暴力枚举PE结构特征头的方法来枚举进程加载的所有模块,通过与正常方法获得的模块进行进行比对,可以判断是否存在可以的PE文件
参考资料
- 《逆向工程核心原理》第13,16章
- 《Windows黑客编程技术详解》第4章
- 《C++黑客编程揭秘与防范》第6章