背景
在网上搜索了很多病毒木马的分析报告,看了一段时间后,发现还是有很多病毒木马都能够模拟PE加载器,把DLL或者是EXE等PE文件,直接从内存中直接加载到自己的内存中执行,不需要通过API函数去操作,以此躲过一些杀软的检测。
程序实现原理(以exe 为例,dll 同理)
- 首先,在EXE文件中,根据PE结构格式获取其加载映像的大小SizeOfImage,并根据SizeOfImage在自己的程序中申请一块可读、可写、可执行的内存,那么这块内存的首地址就是EXE程序的加载基址
- 然后,根据EXE中的PE结构格式获取其映像对齐大小SectionAlignment,然后把EXE文件数据按照SectionAlignment对齐大小拷贝到上述申请的可读、可写、可执行的内存中
- 接着,根据PE结构的重定位表,重新对重定位表进行修正
- 接着,根据PE结构的导入表,加载所需的DLL,并获取导入表导入函数的地址并写入导入表中
- 接着,修改EXE的加载基址ImageBase
- 最后,根据PE结构获取EXE的入口地址AddressOfEntryPoint,然后跳转到入口地址处继续执行
这样,EXE就成功加载到程序中并运行起来了。要注意的一个问题就是,并不是所有的EXE都有重定位表,对于没有重定位表的EXE程序,那就不适用于本文介绍的方法。
编码实现(exe)
// 模拟PE加载器加载内存EXE文件到进程中
// lpData: 内存EXE文件数据的基址
// dwSize: 内存EXE文件的内存大小
// 返回值: 内存EXE加载到进程的加载基址
LPVOID MmRunExe(LPVOID lpData, DWORD dwSize)
{
LPVOID lpBaseAddress = NULL;
// 获取镜像大小
DWORD dwSizeOfImage = GetSizeOfImage(lpData);
// 在进程中开辟一个可读、可写、可执行的内存块
lpBaseAddress = ::VirtualAlloc(NULL, dwSizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (NULL == lpBaseAddress)
{
ShowError("VirtualAlloc");
return NULL;
}
::RtlZeroMemory(lpBaseAddress, dwSizeOfImage);
// 将内存PE数据按SectionAlignment大小对齐映射到进程内存中
if (FALSE == MmMapFile(lpData, lpBaseAddress))
{
ShowError("MmMapFile");
return NULL;
}
// 修改PE文件重定位表信息
if (FALSE == DoRelocationTable(lpBaseAddress))
{
ShowError("DoRelocationTable");
return NULL;
}
// 填写PE文件导入表信息
if (FALSE == DoImportTable(lpBaseAddress))
{
ShowError("DoImportTable");
return NULL;
}
//修改页属性。应该根据每个页的属性单独设置其对应内存页的属性。
//统一设置成一个属性PAGE_EXECUTE_READWRITE
DWORD dwOldProtect = 0;
if (FALSE == ::VirtualProtect(lpBaseAddress, dwSizeOfImage, PAGE_EXECUTE_READWRITE, &dwOldProtect))
{
ShowError("VirtualProtect");
return NULL;
}
// 修改PE文件加载基址IMAGE_NT_HEADERS.OptionalHeader.ImageBase
if (FALSE == SetImageBase(lpBaseAddress))
{
ShowError("SetImageBase");
return NULL;
}
// 跳转到PE的入口点处执行, 函数地址即为PE文件的入口点IMAGE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint
if (FALSE == CallExeEntry(lpBaseAddress))
{
ShowError("CallExeEntry");
return NULL;
}
return lpBaseAddress;
}
int _tmain(int argc, _TCHAR* argv[])
{
char szFileName[] = "KuaiZip_Setup_2.8.28.8.exe";
// 打开EXE文件并获取EXE文件大小
HANDLE hFile = CreateFile(szFileName, GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_ARCHIVE, NULL);
if (INVALID_HANDLE_VALUE == hFile)
{
ShowError("CreateFile");
return 1;
}
DWORD dwFileSize = GetFileSize(hFile, NULL);
// 申请动态内存并读取DLL到内存中
BYTE *pData = new BYTE[dwFileSize];
if (NULL == pData)
{
ShowError("new");
return 2;
}
DWORD dwRet = 0;
ReadFile(hFile, pData, dwFileSize, &dwRet, NULL);
CloseHandle(hFile);
// 判断有无重定位表
if (FALSE == IsExistRelocationTable(pData))
{
printf("[FALSE] IsExistRelocationTable\n");
system("pause");
return 0;
}
// 将内存DLL加载到程序中
LPVOID lpBaseAddress = MmRunExe(pData, dwFileSize);
if (NULL == lpBaseAddress)
{
ShowError("MmRunExe");
return 3;
}
system("pause");
return 0;
}
效果图
编码实现(dll)
// 模拟LoadLibrary加载内存DLL文件到进程中
// lpData: 内存DLL文件数据的基址
// dwSize: 内存DLL文件的内存大小
// 返回值: 内存DLL加载到进程的加载基址
LPVOID MmLoadLibrary(LPVOID lpData, DWORD dwSize)
{
LPVOID lpBaseAddress = NULL;
// 获取镜像大小
DWORD dwSizeOfImage = GetSizeOfImage(lpData);
// 在进程中开辟一个可读、可写、可执行的内存块
lpBaseAddress = ::VirtualAlloc(NULL, dwSizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (NULL == lpBaseAddress)
{
ShowError("VirtualAlloc");
return NULL;
}
::RtlZeroMemory(lpBaseAddress, dwSizeOfImage);
// 将内存DLL数据按SectionAlignment大小对齐映射到进程内存中
if (FALSE == MmMapFile(lpData, lpBaseAddress))
{
ShowError("MmMapFile");
return NULL;
}
// 修改PE文件重定位表信息
if(FALSE == DoRelocationTable(lpBaseAddress))
{
ShowError("DoRelocationTable");
return NULL;
}
// 填写PE文件导入表信息
if (FALSE == DoImportTable(lpBaseAddress))
{
ShowError("DoImportTable");
return NULL;
}
//修改页属性。应该根据每个页的属性单独设置其对应内存页的属性。
//统一设置成一个属性PAGE_EXECUTE_READWRITE
DWORD dwOldProtect = 0;
if (FALSE == ::VirtualProtect(lpBaseAddress, dwSizeOfImage, PAGE_EXECUTE_READWRITE, &dwOldProtect))
{
ShowError("VirtualProtect");
return NULL;
}
// 修改PE文件加载基址IMAGE_NT_HEADERS.OptionalHeader.ImageBase
if (FALSE == SetImageBase(lpBaseAddress))
{
ShowError("SetImageBase");
return NULL;
}
// 调用DLL的入口函数DllMain,函数地址即为PE文件的入口点IMAGE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint
if (FALSE == CallDllMain(lpBaseAddress))
{
ShowError("CallDllMain");
return NULL;
}
return lpBaseAddress;
}
// 模拟GetProcAddress获取内存DLL的导出函数
// lpBaseAddress: 内存DLL文件加载到进程中的加载基址
// lpszFuncName: 导出函数的名字
// 返回值: 返回导出函数的的地址
LPVOID MmGetProcAddress(LPVOID lpBaseAddress, PCHAR lpszFuncName)
{
LPVOID lpFunc = NULL;
// 获取导出表
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress;
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG32)pDosHeader + pDosHeader->e_lfanew);
PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((DWORD)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
// 获取导出表的数据
PDWORD lpAddressOfNamesArray = (PDWORD)((DWORD)pDosHeader + pExportTable->AddressOfNames);
PCHAR lpFuncName = NULL;
PWORD lpAddressOfNameOrdinalsArray = (PWORD)((DWORD)pDosHeader + pExportTable->AddressOfNameOrdinals);
WORD wHint = 0;
PDWORD lpAddressOfFunctionsArray = (PDWORD)((DWORD)pDosHeader + pExportTable->AddressOfFunctions);
DWORD dwNumberOfNames = pExportTable->NumberOfNames;
DWORD i = 0;
// 遍历导出表的导出函数的名称, 并进行匹配
for (i = 0; i < dwNumberOfNames; i++)
{
lpFuncName = (PCHAR)((DWORD)pDosHeader + lpAddressOfNamesArray[i]);
if (0 == ::lstrcmpi(lpFuncName, lpszFuncName))
{
// 获取导出函数地址
wHint = lpAddressOfNameOrdinalsArray[i];
lpFunc = (LPVOID)((DWORD)pDosHeader + lpAddressOfFunctionsArray[wHint]);
break;
}
}
return lpFunc;
}
// 释放从内存加载的DLL到进程内存的空间
// lpBaseAddress: 内存DLL数据按SectionAlignment大小对齐映射到进程内存中的内存基址
// 返回值: 成功返回TRUE,否则返回FALSE
BOOL MmFreeLibrary(LPVOID lpBaseAddress)
{
BOOL bRet = FALSE;
if (NULL == lpBaseAddress)
{
return bRet;
}
bRet = ::VirtualFree(lpBaseAddress, 0, MEM_RELEASE);
lpBaseAddress = NULL;
return bRet;
}
我们编写一个用来测试的DLL程序TestDll,导出函数的代码如下所示:
BOOL ShowMessage(char *lpszText, char *lpszCaption)
{
::MessageBox(NULL, lpszText, lpszCaption, MB_OK);
return TRUE;
}
在 main 函数中,调用上述封装好的函数接口进行测试。main 函数为:
int _tmain(int argc, _TCHAR* argv[])
{
char szFileName[MAX_PATH] = "TestDll.dll";
// 打开DLL文件并获取DLL文件大小
HANDLE hFile = ::CreateFile(szFileName, GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_ARCHIVE, NULL);
if (INVALID_HANDLE_VALUE == hFile)
{
ShowError("CreateFile");
return 1;
}
DWORD dwFileSize = ::GetFileSize(hFile, NULL);
// 申请动态内存并读取DLL到内存中
BYTE *lpData = new BYTE[dwFileSize];
if (NULL == lpData)
{
ShowError("new");
return 2;
}
DWORD dwRet = 0;
::ReadFile(hFile, lpData, dwFileSize, &dwRet, NULL);
// 将内存DLL加载到程序中
LPVOID lpBaseAddress = MmLoadLibrary(lpData, dwFileSize);
if (NULL == lpBaseAddress)
{
ShowError("MmLoadLibrary");
return 3;
}
printf("DLL加载成功\n");
// 获取DLL导出函数并调用
typedef BOOL(*typedef_ShowMessage)(char *lpszText, char *lpszCaption);
typedef_ShowMessage ShowMessage = (typedef_ShowMessage)MmGetProcAddress(lpBaseAddress, "ShowMessage");
if (NULL == ShowMessage)
{
ShowError("MmGetProcAddress");
return 4;
}
ShowMessage("CDIY - www.coderdiy.com - 专注计算机技术交流分享\n", "CDIY");
// 释放从内存加载的DLL
BOOL bRet = MmFreeLibrary(lpBaseAddress);
if (FALSE == bRet)
{
ShowError("MmFreeLirbary");
}
// 释放
delete[] lpData;
lpData = NULL;
::CloseHandle(hFile);
system("pause");
return 0;
}