添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

手工模拟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;
    		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章
  •