DLL加载模拟器直接在内存中加载DLL不通过API加载

Naiiive

发布日期: 2019-01-01 13:24:04 浏览量: 1292
评分:
star star star star star star star star star star
*转载请注明来自write-bug.com

背景

在网上搜索了很多病毒木马的分析报告,看了一段时间后,发现还是有很多病毒木马都能够模拟PE加载器,把DLL或者是EXE等PE文件,直接从内存中直接加载到自己的内存中执行,不需要通过API函数去操作,以此躲过一些杀软的检测。

在看到这些技术的描述后,虽然没有详细的实现思路,但是凭借自己的知识积累,我也大概知道是怎么做了。后来,就自己动手写了这么一个程序,实现了从内存中直接加载DLL,并获取DLL的导出函数,调用并执行。当然,这种技术并非没有积极的一面。如果你的程序需要很多DLL文件进行动态调用,你完全可以把这些DLL作为资源插入到自己的程序中,然后使用这个内存加载运行DLL的技术,直接在内存中加载运行就好,不需要再将DLL释放到本地上。这样做的好处应该不言而喻了,就是你的程序会因此变得很整洁,只有一个EXE程序,其他的DLL均包含在EXE程序里了。

现在,把实现的思路和实现过程,写成文档,分享给大家。

程序实现原理

要想完全理解透彻这个程序的技术,需要对PE文件格式有比较详细的了解才行,起码要了解PE格式的导入表、导出表以及重定位表的具体操作过程。

1. DLL加载到内存的过程实现原理

  • 首先,从DLL文件中,根据PE结构格式获取其加载映像的大小SizeOfImage,并根据SizeOfImage在自己的程序中申请一块可读、可写、可执行的内存,那么这块内存的首地址就是DLL的加载基址

  • 然后,根据DLL中的PE结构格式获取其映像对齐大小SectionAlignment,然后把DLL文件数据按照SectionAlignment对齐大小拷贝到上述申请的可读、可写、可执行的内存中

  • 接着,根据PE结构的重定位表,重新对重定位表进行修正

  • 接着,根据PE结构的导入表,加载所需的DLL,并获取导入表导入函数的地址并写入导入表中

  • 接着,修改DLL的加载基址ImageBase

  • 最后,根据PE结构获取DLL的入口地址,然后构造并调用 DllMain 函数,实现DLL的加载

2. 获取DLL导出函数的实现过程

  • 首先,根据DLL的加载基址以及PE结构,获取DLL导出表

  • 然后,获取导出表的信息,如NumberOfNames、AddressOfNames等信息

  • 接着,遍历导出表的导出函数名字列表,与请求获取的导出函数名称进行匹配

  • 匹配成功,则获取导出函数的地址,并返回

3. DLL的释放

DLL的释放就比较好理解了,就是直接释放模拟加载DLL时候申请的可读、可写、可执行的内存即可。

编码实现

DLL加载过程函数

  1. // 模拟LoadLibrary加载内存DLL文件到进程中
  2. // lpData: 内存DLL文件数据的基址
  3. // dwSize: 内存DLL文件的内存大小
  4. // 返回值: 内存DLL加载到进程的加载基址
  5. LPVOID MmLoadLibrary(LPVOID lpData, DWORD dwSize)
  6. {
  7. LPVOID lpBaseAddress = NULL;
  8. // 获取镜像大小
  9. DWORD dwSizeOfImage = GetSizeOfImage(lpData);
  10. // 在进程中开辟一个可读、可写、可执行的内存块
  11. lpBaseAddress = ::VirtualAlloc(NULL, dwSizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
  12. if (NULL == lpBaseAddress)
  13. {
  14. ShowError("VirtualAlloc");
  15. return NULL;
  16. }
  17. ::RtlZeroMemory(lpBaseAddress, dwSizeOfImage);
  18. // 将内存DLL数据按SectionAlignment大小对齐映射到进程内存中
  19. if (FALSE == MmMapFile(lpData, lpBaseAddress))
  20. {
  21. ShowError("MmMapFile");
  22. return NULL;
  23. }
  24. // 修改PE文件重定位表信息
  25. if(FALSE == DoRelocationTable(lpBaseAddress))
  26. {
  27. ShowError("DoRelocationTable");
  28. return NULL;
  29. }
  30. // 填写PE文件导入表信息
  31. if (FALSE == DoImportTable(lpBaseAddress))
  32. {
  33. ShowError("DoImportTable");
  34. return NULL;
  35. }
  36. //修改页属性。应该根据每个页的属性单独设置其对应内存页的属性。
  37. //统一设置成一个属性PAGE_EXECUTE_READWRITE
  38. DWORD dwOldProtect = 0;
  39. if (FALSE == ::VirtualProtect(lpBaseAddress, dwSizeOfImage, PAGE_EXECUTE_READWRITE, &dwOldProtect))
  40. {
  41. ShowError("VirtualProtect");
  42. return NULL;
  43. }
  44. // 修改PE文件加载基址IMAGE_NT_HEADERS.OptionalHeader.ImageBase
  45. if (FALSE == SetImageBase(lpBaseAddress))
  46. {
  47. ShowError("SetImageBase");
  48. return NULL;
  49. }
  50. // 调用DLL的入口函数DllMain,函数地址即为PE文件的入口点IMAGE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint
  51. if (FALSE == CallDllMain(lpBaseAddress))
  52. {
  53. ShowError("CallDllMain");
  54. return NULL;
  55. }
  56. return lpBaseAddress;
  57. }

获取DLL导出函数

  1. // 模拟GetProcAddress获取内存DLL的导出函数
  2. // lpBaseAddress: 内存DLL文件加载到进程中的加载基址
  3. // lpszFuncName: 导出函数的名字
  4. // 返回值: 返回导出函数的的地址
  5. LPVOID MmGetProcAddress(LPVOID lpBaseAddress, PCHAR lpszFuncName)
  6. {
  7. LPVOID lpFunc = NULL;
  8. // 获取导出表
  9. PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress;
  10. PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG32)pDosHeader + pDosHeader->e_lfanew);
  11. PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((DWORD)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
  12. // 获取导出表的数据
  13. PDWORD lpAddressOfNamesArray = (PDWORD)((DWORD)pDosHeader + pExportTable->AddressOfNames);
  14. PCHAR lpFuncName = NULL;
  15. PWORD lpAddressOfNameOrdinalsArray = (PWORD)((DWORD)pDosHeader + pExportTable->AddressOfNameOrdinals);
  16. WORD wHint = 0;
  17. PDWORD lpAddressOfFunctionsArray = (PDWORD)((DWORD)pDosHeader + pExportTable->AddressOfFunctions);
  18. DWORD dwNumberOfNames = pExportTable->NumberOfNames;
  19. DWORD i = 0;
  20. // 遍历导出表的导出函数的名称, 并进行匹配
  21. for (i = 0; i < dwNumberOfNames; i++)
  22. {
  23. lpFuncName = (PCHAR)((DWORD)pDosHeader + lpAddressOfNamesArray[i]);
  24. if (0 == ::lstrcmpi(lpFuncName, lpszFuncName))
  25. {
  26. // 获取导出函数地址
  27. wHint = lpAddressOfNameOrdinalsArray[i];
  28. lpFunc = (LPVOID)((DWORD)pDosHeader + lpAddressOfFunctionsArray[wHint]);
  29. break;
  30. }
  31. }
  32. return lpFunc;
  33. }

DLL释放

  1. // 释放从内存加载的DLL到进程内存的空间
  2. // lpBaseAddress: 内存DLL数据按SectionAlignment大小对齐映射到进程内存中的内存基址
  3. // 返回值: 成功返回TRUE,否则返回FALSE
  4. BOOL MmFreeLibrary(LPVOID lpBaseAddress)
  5. {
  6. BOOL bRet = FALSE;
  7. if (NULL == lpBaseAddress)
  8. {
  9. return bRet;
  10. }
  11. bRet = ::VirtualFree(lpBaseAddress, 0, MEM_RELEASE);
  12. lpBaseAddress = NULL;
  13. return bRet;
  14. }

程序测试

我们编写一个用来测试的DLL程序TestDll,导出函数的代码如下所示:

  1. BOOL ShowMessage(char *lpszText, char *lpszCaption)
  2. {
  3. ::MessageBox(NULL, lpszText, lpszCaption, MB_OK);
  4. return TRUE;
  5. }

在 main 函数中,调用上述封装好的函数接口进行测试。main 函数为:

  1. int _tmain(int argc, _TCHAR* argv[])
  2. {
  3. char szFileName[MAX_PATH] = "TestDll.dll";
  4. // 打开DLL文件并获取DLL文件大小
  5. HANDLE hFile = ::CreateFile(szFileName, GENERIC_READ | GENERIC_WRITE,
  6. FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
  7. FILE_ATTRIBUTE_ARCHIVE, NULL);
  8. if (INVALID_HANDLE_VALUE == hFile)
  9. {
  10. ShowError("CreateFile");
  11. return 1;
  12. }
  13. DWORD dwFileSize = ::GetFileSize(hFile, NULL);
  14. // 申请动态内存并读取DLL到内存中
  15. BYTE *lpData = new BYTE[dwFileSize];
  16. if (NULL == lpData)
  17. {
  18. ShowError("new");
  19. return 2;
  20. }
  21. DWORD dwRet = 0;
  22. ::ReadFile(hFile, lpData, dwFileSize, &dwRet, NULL);
  23. // 将内存DLL加载到程序中
  24. LPVOID lpBaseAddress = MmLoadLibrary(lpData, dwFileSize);
  25. if (NULL == lpBaseAddress)
  26. {
  27. ShowError("MmLoadLibrary");
  28. return 3;
  29. }
  30. printf("DLL加载成功\n");
  31. // 获取DLL导出函数并调用
  32. typedef BOOL(*typedef_ShowMessage)(char *lpszText, char *lpszCaption);
  33. typedef_ShowMessage ShowMessage = (typedef_ShowMessage)MmGetProcAddress(lpBaseAddress, "ShowMessage");
  34. if (NULL == ShowMessage)
  35. {
  36. ShowError("MmGetProcAddress");
  37. return 4;
  38. }
  39. ShowMessage("CDIY - www.coderdiy.com - 专注计算机技术交流分享\n", "CDIY");
  40. // 释放从内存加载的DLL
  41. BOOL bRet = MmFreeLibrary(lpBaseAddress);
  42. if (FALSE == bRet)
  43. {
  44. ShowError("MmFreeLirbary");
  45. }
  46. // 释放
  47. delete[] lpData;
  48. lpData = NULL;
  49. ::CloseHandle(hFile);
  50. system("pause");
  51. return 0;
  52. }

测试结果:

直接成功弹窗!

所以,DLL文件直接在内存中成功被加载并执行。

总结

这个程序对于初学者来说,理解起来比较复杂。但是,你只要熟悉PE格式结构的话,这个程序理解起来会比较容易。上面讲解中,对于重定位表、导入表以及导出表部分的具体操作并没有细讲,如果你没有了解PE结构,那么理解起来会有困难。如果你了解过PE结构,那么那部分知识应该也不用细讲的。

上传的附件 cloud_download RunDllInMem_Test.7z ( 218.43kb, 28次下载 )

发送私信

我在一个迷惘的故事里,看着自己的人生演绎着百态

11
文章数
14
评论数
最近文章
eject