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隐藏技术
4、https://bbs.pediy.com/thread-153508.htm 进程替换