免责声明:
本文所涉及的任何技术、信息或工具,仅供学习和参考之用。请勿利用本文提供的信息从事任何违法活动或不当行为。任何因使用本文所提供的信息或工具而导致的损失、后果或不良影响,均由使用者个人承担责任,与本文作者无关。作者不对任何因使用本文信息或工具而产生的损失或后果承担任何责任。使用本文所提供的信息或工具即视为同意本免责声明,并承诺遵守相关法律法规和道德规范。
了解过免杀的都知道,杀软会对敏感 api 进行 hook 操作,而我们通常有两种方式进行解决,,syscall 和 unhook,而我们在 syscall 的时候有时候会导致堆栈不完整,在杀软看来是一些异常的行为,比如下图 可以看到 RIP 指针直接已经在 Program 里面了,
(正常的情况如下图所示:
)
而我们在 unhook 时就完全不需要这种考虑,因为我们用的是一段新的 ntdll 或者其他 dll 的内存,调用 的发出在杀软看起来是合理的,接下来我们一起来学习一下。
1.从磁盘重载 ntdll
原理图如下:
可以看出来,其实就是从磁盘上 clean 的 ntdll 的.text 端覆盖内存中被 hook 的ntdll 的.text 端。我们 unhook 的流程如下,如果对 pe 文件结构有了解的话会看的比较轻松。
1. 将 ntdll.dll 的新副本从磁盘映射到进程内存
2. 查找被 hook 的 ntdll.dll的 .text 部分的虚拟地址
-
- 1. 获取ntdll.dll基址
- 2. 模块基址 + 模块的 .text 段 VirtualAddress
3. 查找新映射ntdll.dll的 .text 段的虚拟地址
4. 获取被 hook 的 ntdll .text 段的内存写的权限
5. 将新映射的ntdll.dll的 .text 段覆盖到被 hook 的 ntdll 的 .text 部分
6. 还原之前被 hook 的 ntdll .text 段的内存被原本的内存权限
下面是一个简单的 demo:
#include "pch.h" #include <iostream> #include <Windows.h> #include <winternl.h> #include <psapi.h> int main() { HANDLE process = GetCurrentProcess(); MODULEINFO mi = {}; HMODULE ntdllModule = GetModuleHandleA("ntdll.dll"); GetModuleInformation(process, ntdllModule, &mi, sizeof(mi)); LPVOID ntdllBase = (LPVOID)mi.lpBaseOfDll; HANDLE ntdllFile = CreateFileA("c:\\windows\\system32\\ntdll.dll", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); HANDLE ntdllMapping = CreateFileMapping(ntdllFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL); LPVOID ntdllMappingAddress = MapViewOfFile(ntdllMapping, FILE_MAP_READ, 0, 0, 0); PIMAGE_DOS_HEADER hookedDosHeader = (PIMAGE_DOS_HEADER)ntdllBase; PIMAGE_NT_HEADERS hookedNtHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)ntdllBase + hookedDosHeader->e_lfanew); for (WORD i = 0; i < hookedNtHeader->FileHeader.NumberOfSections; i++) { PIMAGE_SECTION_HEADER hookedSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(hookedNtHeader) + ((DWORD_PTR)IMAGE_SIZEOF_SECTION_HEADER * i)); if (!strcmp((char*)hookedSectionHeader->Name, (char*)".text")) { DWORD oldProtection = 0; bool isProtected = VirtualProtect((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader - > Misc.VirtualSize, PAGE_EXECUTE_READWRITE, &oldProtection); memcpy((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), (LPVOID) ((DWORD_PTR)ntdllMappingAddress + (DWORD_PTR)hookedSectionHeader - > VirtualAddress), hookedSectionHeader->Misc.VirtualSize); isProtected = VirtualProtect((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader - > Misc.VirtualSize, oldProtection, &oldProtection); } } CloseHandle(process); CloseHandle(ntdllFile); CloseHandle(ntdllMapping); FreeLibrary(ntdllModule); return 0; }
Fence1-1
这种方式是最简单的并且理论上可以对所有的 dll 进行 hook,但是缺点是需要读取磁盘上的 dll,而如果 杀软对读取系统 dll 的行为进行了监控,那么我们这种方式其实是不好使的。
2. PE 文件映射绕过 hook
这个思路是在https://idiotc4t.com/defense-evasion/load-ntdll-too 学到的,当我们通过CreateFileMapping , MapViewOfFile 等 api 进行文件映射时,果被打开文件是 PE格式,那么这个文件 会按照内存展开,那么我们猜想是不是这个被第二次载入内存的ntdll是不是就是一个干净的ntdll,能不能帮助我们绕过一些 hook。
demo 如下:
#include <Windows.h> #include <stdio.h> #define DEREF( name )*(UINT_PTR *)(name) #define DEREF_64( name )*(DWORD64 *)(name) #define DEREF_32( name )*(DWORD *)(name) #define DEREF_16( name )*(WORD *)(name) #define DEREF_8( name )*(BYTE *)(name) typedef NTSTATUS(NTAPI* pNtAllocateVirtualMemory)( HANDLE ProcessHandle, PVOID* BaseAddress, ULONG_PTR ZeroBits, PSIZE_T RegionSize, ULONG AllocationType, ULONG Protect); FARPROC WINAPI GetProcAddressR(HANDLE hModule, LPCSTR lpProcName) { UINT_PTR uiLibraryAddress = 0; FARPROC fpResult = NULL; if (hModule == NULL) return NULL; uiLibraryAddress = (UINT_PTR)hModule; __try { UINT_PTR uiAddressArray = 0; UINT_PTR uiNameArray = 0; UINT_PTR uiNameOrdinals = 0; PIMAGE_NT_HEADERS pNtHeaders = NULL; PIMAGE_DATA_DIRECTORY pDataDirectory = NULL; PIMAGE_EXPORT_DIRECTORY pExportDirectory = NULL; pNtHeaders = (PIMAGE_NT_HEADERS)(uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew); pDataDirectory = (PIMAGE_DATA_DIRECTORY)&pNtHeaders - > OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)(uiLibraryAddress + pDataDirectory->VirtualAddress); uiAddressArray = (uiLibraryAddress + pExportDirectory - > AddressOfFunctions); uiNameArray = (uiLibraryAddress + pExportDirectory->AddressOfNames); uiNameOrdinals = (uiLibraryAddress + pExportDirectory - > AddressOfNameOrdinals); if (((DWORD)lpProcName & 0xFFFF0000) == 0x00000000) { uiAddressArray += ((IMAGE_ORDINAL((DWORD)lpProcName) - pExportDirectory->Base) * sizeof(DWORD)); fpResult = (FARPROC)(uiLibraryAddress + DEREF_32(uiAddressArray)); } else { DWORD dwCounter = pExportDirectory->NumberOfNames; while (dwCounter--) { char* cpExportedFunctionName = (char*)(uiLibraryAddress + DEREF_32(uiNameArray)); if (strcmp(cpExportedFunctionName, lpProcName) == 0) { uiAddressArray += (DEREF_16(uiNameOrdinals) * sizeof(DWORD)); fpResult = (FARPROC)(uiLibraryAddress + DEREF_32(uiAddressArray)); break; } uiNameArray += sizeof(DWORD); uiNameOrdinals += sizeof(WORD); } } } __except (EXCEPTION_EXECUTE_HANDLER) { fpResult = NULL; } return fpResult; } int main() { HANDLE hNtdllfile = CreateFileA("c:\\windows\\system32\\ntdll.dll", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); HANDLE hNtdllMapping = CreateFileMapping(hNtdllfile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL); LPVOID lpNtdllmaping = MapViewOfFile(hNtdllMapping, FILE_MAP_READ, 0, 0, 0); pNtAllocateVirtualMemory NtAllocateVirtualMemory = (pNtAllocateVirtualMemory)GetProcAddressR((HMODULE)lpNtdllmaping, "NtAllocateVirtualMemory"); int err = GetLastError(); LPVOID Address = NULL; SIZE_T uSize = 0x1000; NTSTATUS status = NtAllocateVirtualMemory(GetCurrentProcess(), &Address, 0, &uSize, MEM_COMMIT, PAGE_READWRITE); return 0; };
Fence2-1
这种方式需要使用CreateFileMapping , MapViewOfFile等 api进行文件映射,此类 api也会被杀软关 注,并且我们无法保证打开哪些文件才可以获得干净的 ntdll,因此感觉这个方式的实战价值不算很高。
3.通过创建挂起的进程来获得干净的 ntdll
3.1前置条件
我们都知道,每个进程的内存里都会加载各种各样的 dll,每个程序不同,其加载的 dll也都不同,但是 每个进程都应该加载Kernel32.dll、Kernelbase.dll和 Ntdll.dll等,因为这些 DLL包含进程与操作系统交互所需的低级指令和 API调用。而我们发现,在同一个系统上的两个进程在相同基地址处加载了相同的 系统DLL。
并且系统 dll的每个模块也被加载到了相同的地址
3.2原理
我们来看一下当我们程序在加载的时候, edr的 dll 和系统 dll 被一起加载进来
此时,我们的进程是挂起的,我们去看一些 Nt函数时,会发现他们还没有被 hook
而当我们恢复挂起的进程之后,可以发现 Nt函数此时被 hook了
此时我们可以确定两件事情:
新挂起进程的内存是干净的,没有被 hook的
所有的系统 dll在被加载时的内存空间都是一样的
所以我们接下来要做的事情就是想办法从干净的内存读取 ntdll并且覆盖到当前进程被 hook的内存空间。
我们可以用 ReadProcessMemory这个 api来读取其他进程的内存,我们先提前计算好 ntdll在内存空 间中的位置,然后直接去读取就可以了, demo代码可以看https://github.com/dosxuz/PerunsFart, 并且 github 有一个应用此技术武器化的工具: https://github.com/optiv/Freeze
4. 通过自定义的跳转函数进行 unhook
我们都知道加载 dll的函数是 LoadLibrary,这个函数在 kernel32.dll里面,然而这个函数在 ntdll里面对应的函数时 LdrLoadDLL,而我们这个方法的主角就是 LdrLoadDLL。
在 x64 平台下,我们去查看这个函数的汇编指令
而我们就可以自实现一个函数,汇编如下:
其中第一条指令时 LdrLoadDLL的第一条指令,我们自己实现,防止此条指令被 hook,变成jmp指令。
address就是内存中 LdrLoadDLL第二条指令的位置,在 x64 下就是 address(LdrLoadDLL)+5
mov qword ptr[rsp + 10h] //原始的LdrLoadDll中汇编,使用我们自己的防止被hook
mov r11, address //address(LdrLoadDLL)+5
jmp rll
ret
Fence4-1
这里附上一张我在 vs调试时的反汇编,我们只需要将这些字节起来放到一起就可以了。
首先先完成了LdrLoadDLL的第一条指令,然后将address(LdrLoadDLL)+5 放到 r11 寄存器中,然后我们直接jmpr11 就可以了,因为 r11 里面的地址就是LdrLoadDLL第二条指令的地址,我们这样做也是 避免了LdrLoadDLL被 hook,第一条指令变成jmpedr.address。
并且我们这样做所有的函数发出都是从 ntdll里面发出的,如图:
这样我们就自己实现了一个跳转函数,demo代码可以参考https://github.com/trickster0/LdrLoadDll-Unhooking,原作者只提供了 x64 下的代码,我自己稍微改了一下兼容 x64 和 x86 ,地址:https://github.com/fdx-xdf/LdrLoadDll-Unhooking-x86-x64/详细的分析过程可参考: https://killer.wtf/2022/01/19/CustomJmpUnhook.html
加载类的过程通常涉及到Java反射机制。在Java中,反射机制允许程序在运行时检查和操作类、对象、属性以及方法。通过反射,我们可以动态地加载类、实例化对象、调用方法、访问/修改属性等。
ClassLoader负责将类的字节码加载到内存中,而反射机制允许我们在运行时检查和操作这些类。通过Class类及其相
5.参考文章:
https://www.ired.team/offensive-security/defense-evasion/how-to-unhook-a-dll-using-c++https://idiotc4t.com/defense-evasion/load-ntdll-too
https://www.optiv.com/insights/source-zero/blog/sacrificing-suspended-processeshttps://dosxuz.gitlab.io/post/perunsfart/
https://killer.wtf/2022/01/19/CustomJmpUnhook.html
广告
原文始发于微信公众号(影域实验室):免杀-Unhook
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论