内存直接加载运行DLL文件
前言:
将DLL文件作为资源插入到自己程序中的方法,前面已经说过了。附上链接:MFC —— 资源文件释放(为了程序更简洁) 程序需要动态调用DLL文件,内存加载运行技术可以把这些DLL作为资源插入到自己的程序中。此时直接在内存中加载运行即可,不需要再将DLL释放到本地。
实现原理:
将资源加载到内存,然后把DLL文件按照映像对齐大小映射到内存中,切不可直接将DLL文件数据存储到内存中。因为根据PE结构的基础知识可知,PE文件有两个对齐字段,一个是映像对齐大小SectionAlignment,另一个是文件对齐大小FileAlignment。其中,映像对齐大小是PE文件加载到内存中所用的对齐大小,而文件对齐大小是PE文件存储在本地磁盘所用的对齐大小。一般文件对齐大小会比映像对齐大小要小,这样文件会变小,以此节省磁盘空间。然而,成功映射内存数据之后,在DLL程序中会存在硬编码数据,硬编码都是以默认的加载基址作为基址来计算的。由于DLL可以任意加载到其他进程空间中,所以DLL的加载基址并非固定不变。当改变加载基址的时候,硬编码也要随之改变,这样DLL程序才会计算正确。但是,如何才能知道需要修改哪些硬编码呢?换句话说,如何知道硬编码的位置?答案就藏在PE结构的重定位表中,重定位表记录的就是程序中所有需要修改的硬编码的相对偏移位置。根据重定位表修改硬编码数据后,这只是完成了一半的工作。DLL作为一个程序,自然也会调用其他库函数,例如MessageBox。那么DLL如何知道MessageBox函数的地址呢?它只有获取正确的调用函数地址后,方可正确调用函数。PE结构使用导入表来记录PE程序中所有引用的函数及其函数地址。在DLL映射到内存之后,需要根据导入表中的导入模块和函数名称来获取调用函数的地址。若想从导入模块中获取导出函数的地址,最简单的方式是通过GetProcAddress函数来获取(此次采用的方法)。但是为了避免调用敏感的WIN32 API函数而被杀软拦截检测,采用直接遍历PE结构导出表的方式来获取导出函数地址。
实现流程:
(1).将资源形式的dll文件加载到内存
(2).根据PE结构获取其文件映像大小
(3).根据文件映像大小再申请一块可读、可写、可执行的内存
(4).将内存DLL数据按映像对齐大小(SectionAlignment)映射到刚刚申请的内存中
(5).根据PE结构的重定位表,对需要重定位的数据进行修正
(6).根据PE结构的导入表,加载所需的DLL,获取函数地址并写入导入地址表
(7).修改DLL的加载基址为第(3)步申请的空间的首地址
(8).获取Dll的入口地址并构造DllMain函数,然后调用DllMain函数
实现代码:
//************************************ // 函数名: CStartDlg::LoadMyResource // 返回类型: LPVOID // 功能: 加载资源到内存 // 参数1: UINT uiResourceName 资源名 // 参数1: char * lpszResourceType 资源类型 //************************************ LPVOID CStartDlg::LoadMyResource(UINT uiResourceName, char* lpszResourceType) { //获取指定模块里的资源 HRSRC hRsrc = FindResource(GetModuleHandle(NULL), MAKEINTRESOURCE(uiResourceName), (LPCWSTR)lpszResourceType); if (NULL == hRsrc) { MessageBox(L"获取资源失败!"); return FALSE; } //获取资源大小 DWORD dwSize = SizeofResource(NULL, hRsrc); if (dwSize <= 0) { MessageBox(L"获取资源大小失败!"); return FALSE; } //将资源加载到内存里 HGLOBAL hGlobal = LoadResource(NULL, hRsrc); if (NULL == hGlobal) { MessageBox(L"资源加载到内存失败!"); return FALSE; } //锁定资源 LPVOID lpVoid = LockResource(hGlobal); if (NULL == lpVoid) { MessageBox(L"锁定资源失败!"); return FALSE; } return lpVoid; } //************************************ // 函数名: CStartDlg::MmLoadLibrary // 返回类型: LPVOID // 功能: 模拟LoadLibrary加载内存文件到进程中 // 参数1: LPVOID lpData 文件基址 // 参数2: BOOL IsExe 文件属性标志,TRUE表示exe文件,FALSE表示dll文件 //************************************ LPVOID CStartDlg::MmLoadLibrary(LPVOID lpData,BOOL IsExe) { LPVOID lpBaseAddress = NULL; // 获取映像大小 DWORD dwSizeOfImage = GetSizeOfImage(lpData); // 在进程中申请一个可读、可写、可执行的内存块 lpBaseAddress = VirtualAlloc(NULL, dwSizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (NULL == lpBaseAddress) { MessageBox(L"申请空间失败!"); return NULL; } // 将申请的空间的数据全部置0 RtlZeroMemory(lpBaseAddress, dwSizeOfImage); // 将内存DLL数据按映像对齐大小(SectionAlignment)映射到刚刚申请的内存中 if (FALSE == MmMapFile(lpData, lpBaseAddress)) { MessageBox(L"区段映射到内存失败!"); return NULL; } // 修改PE文件的重定位表信息 if (FALSE == DoRelocationTable(lpBaseAddress)) { MessageBox(L"修复重定位失败!"); return NULL; } // 填写PE文件的导入表信息 if (FALSE == DoImportTable(lpBaseAddress)) { MessageBox(L"导入表填写失败!"); return NULL; } // 修改PE文件的加载基址IMAGE_NT_HEADERS.OptionalHeader.ImageBase if (FALSE == SetImageBase(lpBaseAddress)) { MessageBox(L"修改加载机制失败!"); return NULL; } // 调用DLL的入口函数DllMain,函数地址即为PE文件的入口点AddressOfEntryPoint if (FALSE == CallDllMain(lpBaseAddress,IsExe)) { MessageBox(L"调用入口函数失败!"); return NULL; } return lpBaseAddress; } //************************************ // 函数名: CStartDlg::GetSizeOfImage // 返回类型: DWORD // 功能: 获取文件映像大小 // 参数1: LPVOID lpData 文件基址 //************************************ DWORD CStartDlg::GetSizeOfImage(LPVOID lpData) { //获取Dos头 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpData; //判断是否是有效的PE文件 0x5A4D if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) { MessageBox(L"这不是一个PE文件!"); return 0; } //获取NT头 PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((DWORD)pDosHeader+pDosHeader->e_lfanew); //判断是否是有效的PE文件 0x00004550 if (pNtHeader->Signature != IMAGE_NT_SIGNATURE) { MessageBox(L"这不是一个PE文件!"); return 0; } //获取文件映像大小 return pNtHeader->OptionalHeader.SizeOfImage; } //************************************ // 函数名: CStartDlg::MmMapFile // 返回类型: BOOL // 功能: 将内存DLL数据按映像对齐大小(SectionAlignment)映射到刚刚申请的内存中 // 参数1: LPVOID lpData 文件基址 // 参数2: LPVOID lpBaseAddress 申请的内存的首地址 //************************************ BOOL CStartDlg::MmMapFile(LPVOID lpData, LPVOID lpBaseAddress) { //获取Dos头 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpData; //获取NT头 PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((DWORD)pDosHeader + pDosHeader->e_lfanew); //获取所有头部+区段表的大小 DWORD dwSizeOfHeaders = pNtHeader->OptionalHeader.SizeOfHeaders; //获取区段数量 WORD wNumberOfSections = pNtHeader->FileHeader.NumberOfSections; //获取区段表数组的首元素 PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNtHeader); //将头部(包括区段表)拷贝到内存 RtlCopyMemory(lpBaseAddress, lpData, dwSizeOfHeaders); LPVOID lpSrcMem = NULL; LPVOID lpDestMem = NULL; DWORD dwSizeOfRawData = 0; //循环加载所有区段 for (WORD i = 0; i < wNumberOfSections; i++) { //过滤掉无效区段 if (0 == pSectionHeader->VirtualAddress || 0 == pSectionHeader->SizeOfRawData) { pSectionHeader++; continue; } //获取区段在文件中的位置 lpSrcMem = (LPVOID)((DWORD)lpData + pSectionHeader->PointerToRawData); //获取区段映射到内存中的位置 lpDestMem = (LPVOID)((DWORD)lpBaseAddress + pSectionHeader->VirtualAddress); //获取区段在文件中的大小 dwSizeOfRawData = pSectionHeader->SizeOfRawData; //将区段数据拷贝到内存中 RtlCopyMemory(lpDestMem, lpSrcMem, dwSizeOfRawData); //获取下一个区段头(属性) pSectionHeader++; } return TRUE; } //************************************ // 函数名: CStartDlg::DoRelocationTable // 返回类型: BOOL // 功能: 修改PE文件的重定位表信息 // 参数1: LPVOID lpBaseAddress 映像对齐后的文件基址 //************************************ BOOL CStartDlg::DoRelocationTable(LPVOID lpBaseAddress) { //获取Dos头 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress; //获取NT头 PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((DWORD)pDosHeader + pDosHeader->e_lfanew); //获取重定位表的地址 PIMAGE_BASE_RELOCATION pReloc = (PIMAGE_BASE_RELOCATION)((DWORD)pDosHeader + pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress); //注意重定位表的位置可能和硬盘文件中的偏移地址不同,应该使用加载后的地址 //判断是否有重定位表 if ((PVOID)pReloc == (PVOID)pDosHeader) { //没有重定位表 return TRUE; } int nNumberOfReloc = 0; WORD* pRelocData = NULL; DWORD* pAddress = NULL; //开始修复重定位 while (pReloc->VirtualAddress != 0 && pReloc->SizeOfBlock != 0) { //计算本区域(每一个描述了4KB大小的区域的重定位信息)需要修正的重定位项的数量 nNumberOfReloc = (pReloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD); for (int i = 0; i < nNumberOfReloc; i++) { //获取IMAGE_BASE_RELOCATION结构后面的数据的地址 pRelocData = (WORD*)((DWORD)pReloc + sizeof(IMAGE_BASE_RELOCATION)); //每个WORD由两部分组成,高4位指出了重定位的类型,WINNT.H中的一系列IMAGE_REL_BASED_xxx定义了重定位类型的取值。 //大部分重定位属性值都是0x3 //低12位是相对于IMAGE_BASE_RELOCATION中第一个元素VirtualAddress描述位置的偏移 //找出需要修正的地址 if ((WORD)(pRelocData[i] & 0xF000) == 0x3000) { //获取需要修正数据的地址, 按位与计算优先级比加减乘除低 pAddress = (DWORD*)((DWORD)pDosHeader + pReloc->VirtualAddress + (pRelocData[i] & 0x0FFF)); //进行修改 *pAddress += (DWORD)pDosHeader - pNtHeader->OptionalHeader.ImageBase; } } //下一个重定位块 pReloc = (PIMAGE_BASE_RELOCATION)((DWORD)pReloc + pReloc->SizeOfBlock); } return TRUE; } //************************************ // 函数名: CStartDlg::DoImportTable // 返回类型: BOOL // 功能: 填写PE文件的导入表信息 // 参数1: LPVOID lpBaseAddress 映像对齐后的文件基址 //************************************ BOOL CStartDlg::DoImportTable(LPVOID lpBaseAddress) { //获取Dos头 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress; //获取NT头 PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((DWORD)pDosHeader + pDosHeader->e_lfanew); //获取导入表地址 PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pDosHeader + pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress); char* pDllName = nullptr; HMODULE hDll = NULL; PIMAGE_THUNK_DATA pIat = NULL; FARPROC pFuncAddress = NULL; PIMAGE_IMPORT_BY_NAME pImportByName = NULL; //循环遍历导入表 while (pImport->Name) { //获取导入表中的Dll名称 pDllName = (char*)((DWORD)pDosHeader + pImport->Name); //检索Dll模块获取模块句柄 hDll = GetModuleHandleA(pDllName); //获取失败 if (NULL == hDll) { //加载Dll模块获取模块句柄 hDll = LoadLibraryA(pDllName); //加载失败 if (NULL == hDll) { pImport++; continue; } } //获取IAT pIat = (PIMAGE_THUNK_DATA)((DWORD)pDosHeader + pImport->FirstThunk); //遍历IAT中函数 while (pIat->u1.Ordinal) { //判断导入的函数是名称导入还是序号导入 //判断最高位是否为1,如果是1那么是序号导入 if (pIat->u1.Ordinal & 0x80000000) { //获取函数地址 pFuncAddress = GetProcAddress(hDll, (LPCSTR)(pIat->u1.Ordinal & 0x7FFFFFFF)); } //名称导入 else { //获取IMAGE_IMPORT_BY_NAME结构 pImportByName = (PIMAGE_IMPORT_BY_NAME)((DWORD)pDosHeader + pIat->u1.AddressOfData); //获取函数地址 pFuncAddress = GetProcAddress(hDll, pImportByName->Name); } //将函数地址填入到IAT中 pIat->u1.Function = (DWORD)pFuncAddress; pIat++; } pImport++; } return TRUE; } //************************************ // 函数名: CStartDlg::SetImageBase // 返回类型: BOOL // 功能: 修改PE文件的加载基址IMAGE_NT_HEADERS.OptionalHeader.ImageBase // 参数1: LPVOID lpBaseAddress 映像对齐后的文件基址 //************************************ BOOL CStartDlg::SetImageBase(LPVOID lpBaseAddress) { //获取Dos头 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress; //获取NT头 PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((DWORD)pDosHeader + pDosHeader->e_lfanew); //修改默认加载基址 pNtHeader->OptionalHeader.ImageBase = (DWORD)lpBaseAddress; return TRUE; } //************************************ // 函数名: CStartDlg::CallDllMain // 返回类型: BOOL // 功能: 调用PE文件的入口函数 // 参数1: LPVOID lpBaseAddress 映像对齐后的文件基址 // 参数2: BOOL IsExe 文件属性标志,TRUE表示exe文件,FALSE表示dll文件 //************************************ BOOL CStartDlg::CallDllMain(LPVOID lpBaseAddress,BOOL IsExe) { //定义函数指针变量 typedef_DllMain DllMain = NULL; typedef_wWinMain MyWinMain = NULL; //获取Dos头 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress; //获取NT头 PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((DWORD)pDosHeader + pDosHeader->e_lfanew); BOOL bRet = TRUE; //如果是exe文件 if (IsExe) { MessageBox(_T("有问题,待解决")); //MyWinMain = (typedef_wWinMain)((DWORD)pDosHeader + pNtHeader->OptionalHeader.AddressOfEntryPoint+0xF8D); //bRet = MyWinMain((HINSTANCE)lpBaseAddress, NULL, NULL, SW_SHOWNORMAL); } //dll 文件 else { DllMain = (typedef_DllMain)((DWORD)pDosHeader + pNtHeader->OptionalHeader.AddressOfEntryPoint); //调用入口函数,附加进程DLL_PROCESS_ATTACH bRet = DllMain((HINSTANCE)lpBaseAddress, DLL_PROCESS_ATTACH, NULL); } return bRet; }