3环下要想隐藏dll,仅仅靠断链和抹去PE头信息是不够的;这样做能骗过同样在3环运行的调试器,但是骗不过在0环通过驱动做检测的PChunter、Process Hacker等工具;要想彻底隐藏,需要更进一步搞定驱动层的各种检测,下面会详细介绍隐藏的细节原理和操作方法!
1、VAD 虚拟内存管理
内存分两种:物理内存和虚拟内存;操作系统和进程共享物理内存,进程独享虚拟内存;物理内存可以通过CR3在进程之间互相隔离,确保进程之间互不侵犯;那么进程内部的虚拟地址该怎么管理了? 32位下,每个进程独享4GB内存,怎么知道哪些内存已经使用过,哪些没用过? 已经使用的内存,是可读可写可执行的么? 还是只读的了? 该怎么记录这些关键信息了?windwos采用一种叫做virtual address descripot的自平衡二叉树来管理虚拟内存,低端的内存地址放在根节点左子树,高端内存地址放根节点右子树,大致的结构如下:每当进程调用virtualAlloc分配虚拟内存时,操作系统会先遍历这个树,看看还有哪些地方的虚拟内存还未使用,然后返回给开发人员:
windbg能查到每个进程VAD的root节点:
VAD里面也记录了该进程dll的使用情况。
当内存使用完毕,建议立即调用VirtualFree,将这段虚拟内存从VAD从抹去,后续再次遍历时才能继续使用!正常情况下,如果要卸载dll,可以调用windwos提供的freeLibrary接口,里面有关键的函数:ZwUnmapViewOfSection,可以直接把dll对应的内存从VAD中删除(这里多说两句:ZwUnmapViewOfSection 功能很强大,可以替换进程的代码,让其称为傀儡执行恶意的代码)。
2、之前分享过一个驱动隐藏的思路(https://www.cnblogs.com/theseventhson/p/13170445.html): 让driver entry返回false,操作系统会认为驱动加载失败,不会记录。但在driverentry里面把自己想要执行的代码拷贝到堆上,然后将代码入口点作为imageLoad回调函数的入口点。虽然驱动加载“失败”,但代码已经拷贝到堆,并且注册成为了回调函数,dll隐藏也可以借鉴类似的思路:
- 先重新申请一个新空间,把需要隐藏的dll拷贝到新空间备份
- 用freelibrary释放需要隐藏的dll,VAD中会删除这个dll的。此时如果eip跳转到dll执行,肯定报错
- 重新用virtualAlloc申请原dll地址,再把第一步备份的原dll代码拷贝到这次申请的地址(其实就是dll原来加载的地址)
- 此时如果eip跳转到这个地址执行代码是ok的
这么做的本质是:把dll从vad的记录中抹去,重新申请内存来存放dll的代码。虽说在vad还是有内存的使用记录,但因为并未使用loadlibrary,所以也不会在vad中留下dll的记录(这是本质是把dll变相当成shellcode在用,至于全局变量、导入函数、重定位这些,由编译器和操作系统都做好了,不需要开发人员操心);核心代码如下:
/************************************************************************/ /* 把当前进程的所有DLL(除开需要隐藏的那个)都使用LoadLibrary再次加载一边,增加引用计数, */ /* 使得Free时对应的DLL资源不释放 */ /************************************************************************/ void LockAllModules() { HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId()); if (hSnapshot != INVALID_HANDLE_VALUE) { MODULEENTRY32 me = { sizeof(me) }; BOOL fOk = Module32First(hSnapshot, &me); for (fOk = Module32Next(hSnapshot, &me); fOk; fOk = Module32Next(hSnapshot, &me)) { //跳过第一个(自身) CString wInfo; wInfo.Format(_T("%s"), me.szModule); wInfo.MakeLower(); if (wInfo != _T("dlls.dll"))LoadLibrary(me.szModule);//加载除了dlls.dll以外的所有内存 } } } BOOL CopycatAndHide(HMODULE hDll) { // 整体思路:先把DLL加载到当前进程,然后将该加载的DLL再备份到当前进程空间; // 接下来该DLL再Free了,此时进程再访问该DLL的话会出错; // Free后,再把预先备份的DLL数据还原,而且还原的数据地址是原先DLL加载的地址 // 如此,进程内再调用该DLL的话,由于数据完整,一切OK DWORD g_dwImageSize = 0; VOID* g_lpNewImage = NULL; IMAGE_DOS_HEADER* pDosHeader; IMAGE_NT_HEADERS* pNtHeader; IMAGE_OPTIONAL_HEADER* pOptionalHeader; LPVOID lpBackMem = 0; DWORD dwOldProtect; DWORD dwCount = 30; pDosHeader = (IMAGE_DOS_HEADER*)hDll; pNtHeader = (IMAGE_NT_HEADERS*)(pDosHeader->e_lfanew + (DWORD)hDll); pOptionalHeader = (IMAGE_OPTIONAL_HEADER*)&pNtHeader->OptionalHeader; LockAllModules(); // 找一块内存把需要隐藏而且已经加载到内存的DLL备份 // SizeOfImage,4个字节,表示程序调入后占用内存大小(字节),等于所有段的长度之和。 lpBackMem = VirtualAlloc(0, pOptionalHeader->SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (!lpBackMem) return FALSE; if (!VirtualProtect((LPVOID)hDll, pOptionalHeader->SizeOfImage, PAGE_EXECUTE_READWRITE, &dwOldProtect)) return FALSE; g_dwImageSize = pOptionalHeader->SizeOfImage; memcpy(lpBackMem, (LPVOID)hDll, g_dwImageSize); // 抹掉PE头 //memset(lpBackMem, 0, 0x200); *((PBYTE)hDll + pOptionalHeader->AddressOfEntryPoint) = (BYTE)0xc3; // DWORD dwRet =0; // Free掉DLL do { dwCount--; } while (FreeLibrary(hDll) && dwCount); // 把备份的DLL数据还原回来,使得预先引用该DLL的程序能够继续正常运行 g_lpNewImage = VirtualAlloc((LPVOID)hDll, g_dwImageSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (g_lpNewImage != (LPVOID)hDll) return FALSE; memcpy(g_lpNewImage, lpBackMem, g_dwImageSize); VirtualFree(lpBackMem, 0, MEM_RELEASE); return TRUE; }
参考:
1、https://wenku.baidu.com/view/439526b369dc5022aaea0077 内存管理
2、https://bbs.pediy.com/thread-257179.htm VC黑防日记(二):DLL隐藏和逆向
3、https://blog.csdn.net/arbboter/article/details/38260973 DLL隐藏技术