nouveau的文章

  • EXE加载模拟器直接在内存中加载运行EXE不通过API创建进程运行

    背景在网上搜索了很多病毒木马的分析报告,看了一段时间后,发现还是有很多病毒木马都能够模拟PE加载器,把DLL或者是EXE等PE文件,直接从内存中直接加载到自己的内存中执行,不需要通过API函数去操作,以此躲过一些杀软的检测。
    在看到这些技术的描述后,虽然没有详细的实现思路,但是凭借自己的知识积累,我也大概知道是怎么做了。后来,就自己动手写了这么一个程序,实现了从内存中直接加载并运行EXE,不需要通过API函数创建另一个进程启动该EXE。暂时还没有想清楚这种技术有什么积极的一面,不管了,既然都把程序写出来了,那就当作是对PE结构以及编程水平的一次锻炼吧
    现在,把实现的思路和实现过程,写成文档,分享给大家。
    程序实现原理要想完全理解透彻这个程序的技术,需要对PE文件格式有比较详细的了解才行,起码要了解PE格式的导入表、导出表以及重定位表的具体操作过程。
    这个程序和 “DLL加载模拟器直接在内存中加载DLL不通过API加载” 这篇文章原理是一样的,只是一个是EXE,一个是DLL,本质上都是PE文件加载模拟器。
    EXE加载到内存的过程并运行实现原理
    首先,在EXE文件中,根据PE结构格式获取其加载映像的大小SizeOfImage,并根据SizeOfImage在自己的程序中申请一块可读、可写、可执行的内存,那么这块内存的首地址就是EXE程序的加载基址
    然后,根据EXE中的PE结构格式获取其映像对齐大小SectionAlignment,然后把EXE文件数据按照SectionAlignment对齐大小拷贝到上述申请的可读、可写、可执行的内存中
    接着,根据PE结构的重定位表,重新对重定位表进行修正
    接着,根据PE结构的导入表,加载所需的DLL,并获取导入表导入函数的地址并写入导入表中
    接着,修改EXE的加载基址ImageBase
    最后,根据PE结构获取EXE的入口地址AddressOfEntryPoint,然后跳转到入口地址处继续执行

    这样,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;}
    程序测试在 main 函数中调用上述封装好的函数,加载EXE程序进行测试。main 函数为:
    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;}
    测试结果:
    运行程序后,成功显示“快压”安装程序的对话框界面,而且还显示了“快压”安装程序正在向 “i.kpzip.com” 使用HTTP发送“GET”数据请求:

    所以,程序测试成功。
    总结这个程序你只要熟悉PE格式结构的话,这个程序理解起来会比较容易。其中,需要特别注意的一点是:并不是所有的EXE都是用于本文介绍的方法。因为,对于那些没有重定位表的EXE程序来说,它们加载运行时的加载基址只能是固定的,因为没有重定位表的缘故,所以无法重定位数据,势必不能成功运行程序。同时,对一些MFC程序也不支持,目前正在改进当中。如果遇到不成功的,那就多换几个有重定位表的EXE试试就好。
    参考参考自《Windows黑客编程技术详解》一书
    8  留言 2019-01-02 09:49:15
eject