前言:
将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;
}