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隱藏技術