前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >edr对抗技术1-api unhook output

edr对抗技术1-api unhook output

作者头像
Gamma实验室
发布2024-05-14 14:24:03
730
发布2024-05-14 14:24:03
举报
文章被收录于专栏:Gamma安全实验室Gamma安全实验室

0x01 关于对抗api hook技术

auth:cmrex

文章灵感来自于:阿里云先知fdx师傅

出现场景

杀软会对敏感的api事先进行hook操作,例如:

代码语言:javascript
复制
VirtualAlloc
memcpy
CreateRemoteThreadEx

等诸如此类的api函数。所以就可以使用下列几种办法来对抗:

  1. syscall实现apihook绕过
  2. unhook技术

那么他们的特点分别是:

  1. 在 syscall 的时候有时候会导致堆栈不完整,在杀软看来是一些异常的行为,比如下图可以看到 RIP 指针直接已经在 Program 里面了。
  2. 通过一些办法,让我们获得一个没有被hook的ntdl。

syscall实现apihook绕过

unhook技术

重载ntdll的代码段

这里借用别人的一张图,大概是这样:因为刚开始是有一个ntdll被载入内存,然后杀软对其hook,自然也就是修改了代码段。然后这个时候,我们用新的ntdll的代码段覆盖被hook的代码段,实现ntdll重载。

  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 段的内存被原本的内存权限

劣势

这种方式是最简单的并且理论上可以对所有的 dll 进行 hook,但是缺点是需要读取磁盘上的 dll,而如果杀软对读取系统 dll 的行为进行了监控,那么我们这种方式其实是不好使的。

PE 文件映射绕过 hook

如果被打开文件是PE格式,那么这个文件会按照内存展开,那么我们猜想是不是这个被第二次载入内存的ntdll是不是就是一个干净的ntdll,能不能帮助我们绕过一些inline hook。这种其实是载入第二个ntdl来绕过hook。流程:

  1. 使用CreateFileMapping->MapViewOfFile映射一个ntdll
  2. 自己实现一个GetProcAddress函数
  3. 使用自写GetProcAddress函数获取nt函数其实就是打开一个新的pe格式的ntdll,然后摄取这个ntdll里的函数直接使用。这种也会被专门设置的防止ntdll重载规则拦截。

如何使用

这里是使用了NtAllocateVirtualMemory,从新的ntdll创建的

代码语言:javascript
复制
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);

直接跳转

我们都知道加载 dll 的函数是 LoadLibrary,这个函数在 kernel32.dll 里面,然而这个函数在 ntdll 里面对应的函数时 LdrLoadDLL,而我们这个方法的主角就是 LdrLoadDLL。 在 x64 平台下,我们去查看这个函数的汇编指令

而我们就可以自实现一个函数,汇编如下:

其中第一条指令时 LdrLoadDLL 的第一条指令,我们自己实现,防止此条指令被 hook,变成 jmp 指令。

address 就是内存中 LdrLoadDLL 第二条指令的位置,在 x64 下就是 address(LdrLoadDLL)+5

代码语言:javascript
复制
mov qword ptr[rsp + 10h]  //原始的LdrLoadDll中汇编,使用我们自己的防止被hook
mov r11,address     //address(LdrLoadDLL)+5
jmp rll
ret

这样我们就自己实现了一个跳转函数,demo 代码可以参考

挂起的进程获取干净的ntdll

当一个进程还没有被运行,刚开始加载若干dll的时候,会记载没有被hook的ntdll,以及edr的dll。这个时候这里的ntdll是干净的ntdll。

没有被hook

如果是被hook了,这里会出现一个跳转,跳转到edr的dll中去。所以我们里的思路是:

  • 新挂起进程的内存是干净的,没有被 hook 的
  • 所有的系统 dll 在被加载时的内存空间都是一样的启动一个进程,挂起它,读取他的干净的ntdll,然后自己使用。

首先创建一个进程挂起

代码语言:javascript
复制
```cpp
STARTUPINFOA* si = new STARTUPINFOA();
PROCESS_INFORMATION* pi = new PROCESS_INFORMATION();
//BOOL stat = CreateProcessA_p(nullptr, (LPSTR)"C:\\Windows\\System32\\svchost.exe", nullptr, nullptr, FALSE, CREATE_SUSPENDED, nullptr, nullptr, si, pi);
BOOL stat = CreateProcessA_p(nullptr, (LPSTR)"cmd.exe", nullptr, nullptr, FALSE, CREATE_SUSPENDED | CREATE_NEW_CONSOLE, nullptr, "C:\\Windows\\System32\\", si, pi);

HANDLE hProcess = pi->hProcess;
printf("PID : %d\n", pi->dwProcessId);

通过遍历进程中加载的模块来查找ntdll.dll的基地址

为此,我们使用了paranoidninja的自定义函数。这里它被命名为GetDll()

代码语言:javascript
复制
WCHAR findname[] = L"ntdll.dll\x00";
PVOID ntdllBase = GetDll(findname);
printf("ntdll.dll base address : 0x%p\n", ntdllBase);

我们传递要定位其基地址的DLL的名称(在本例中为ntdll.DLL),函数返回其基地址。以下是GetDll()函数的代码

代码语言:javascript
复制
PVOID GetDll(PWSTR FindName)
{
  _PPEB ppeb = (_PPEB)__readgsqword(0x60);
  ULONG_PTR pLdr = (ULONG_PTR)ppeb->pLdr;
  ULONG_PTR val1 = (ULONG_PTR)((PPEB_LDR_DATA)pLdr)->InMemoryOrderModuleList.Flink;
  PVOID dllBase = nullptr;

  ULONG_PTR val2;
  while (val1)
  {
    PWSTR DllName = ((PLDR_DATA_TABLE_ENTRY)val1)->BaseDllName.pBuffer;
    dllBase = (PVOID)((PLDR_DATA_TABLE_ENTRY)val1)->DllBase;
    if (wcscmp(FindName, DllName) == 0)
    {
      break;
    }
    val1 = DEREF_64(val1);
  }
  return dllBase;
}

上面的函数使用内在的__readgsqword()从gs寄存器中读取0x60字节。这将为我们提供指向PEB(过程环境块)的指针

从PEB结构中,我们可以找到Loader数据或Ldr的地址

然后,我们使用Ldr遍历加载的模块,以获得所需的ntdll基础

通过Loader数据提取加载的模块

demo

代码语言:javascript
复制
https://github.com/dosxuz/PerunsFart
https://github.com/optiv/Freeze

0x02 api hook的不同杀软分析

天擎的hook

tq对于api的hook分析

这里研究天擎hook从何而来?首先看一个程序的dll列表:

随后这里我们可以看见很多dll,然后找到tq的dll:

代码语言:javascript
复制
C:\Program Files (x86)\Qianxin\Tianqing\dlp\x64\qmdlphook64_31915f.dll
C:\Program Files (x86)\Qianxin\Tianqing\watermark\x64\watermarkhook64.dll
C:\Program Files (x86)\Qianxin\Tianqing\hookbase\x64\ghhlp64.dll
C:\Program Files (x86)\Qianxin\Tianqing\hookbase\x64\QmHookHelper64.dll

随后我判断可能是tq的这个dll实现了hook相关的功能:

ghhlp64.dll

然后我们看这个dll的导入表

kernel32.VirtualAlloc的跟踪hook点

我们可以很清晰的看见,调用了virtualalloc后,来到了ghhlp64.dll中继续运行,很明显这里被tq给hook了。

在有tq的环境下,这里调用的流程是:

程序运行前:

加载若干dll

ghhlp64->调用virtualalloc->kernel32->kernelbase(实际没有分配空间所以没到ntdll中)->ghhlp64

运行的时候也就正常的走到了kernel32,ntdll中,没有其他的操作了。

总结:其实就是杀软仅仅只是在程序启动的时候检测一下导入表而已,运行的时候并没有动态的检测导入表。

尝试使用动态导入表

这个时候我们发现,还是被检测到了,这里是这样的:

代码语言:javascript
复制
LHShield64.dll

这里发现被检测到了,但是仔细一看,发现这个是这个dll自己的操作,和我们程序没有任何关系。我发现了这个dll给我们的进程创建了两个线程,理论上来讲应该是他对我们的进程进行的监控的线程应该是。

tq首先会对进程创建两个线程监控,随后线程退出,监测结束。那能不能结束这个进程呢?做不到,因为这个线程是在我的main函数之前被创建的。走到main的时候,这个线程就结束了。应该是对我们起来的程序代码段检测的。所以加一下壳子,自己写的壳子,杀软就可能看不到了?

但是值得一提的是,tq只是在程序运行之前检测一下导入表而已,然后对于具体执行的东西比如virtualalloc的内容并没hook,如下:

这里是加载完tq的dll后,ntdll这里没有任何的跳转。至于tq如何检测的,那就是别的技术了。

卡巴斯基

而这里我们再看看卡巴斯基环境下的内容:

可以看出来卡巴斯基并没有对这个dll的这个api进行拦截hook,反而是另一款程序冰盾主动防御在这里拦截了(iMonitorH2K.i64)但是卡巴斯基在程序走到ntdll的内存分配的那函数后也就出现了爆毒。应该是卡巴斯基的内存查杀给它干了。

结论:卡巴斯基没有hook api

冰盾

实验一:没有动态导入表直接测试加载器

大致情况类似上面的tq,在这里我放出hook的流程:

初始化:

加载了若干dll

加载了iMonitorH2K.i64{

这里直接检测了virtualalloc,并且监控

}

木马程序:

来到了木马的入口点

调用virtualalloc来到kernel32,调用ntdll

ntdll继续分配空间,返回到程序中

随后执行直接到了wininet模块准备联网

实验二:有动态导入表直接测试加载器

初始化:

加载了若干dll

加载了iMonitorH2K.i64{

这里没有检测virtualalloc!

只是对iMonitorH2K.i64初始化,走个流程

}

木马程序:

来到了木马的入口点

调用virtualalloc来到kernel32,调用ntdll

ntdll继续分配空间,返回到程序中

随后执行直接到了wininet模块准备联网

总结

杀软会在程序运行后加载一些基础dll的时候,让程序加载他自己的dll。然后检测一下是否存在敏感api,然后使用消息队列发送。然后在运行到具体的地方的时候,例如virtualalloc的时候,再去进行更加深入(内核中)的操作。

0x03 对于冰盾的HOOK额外研究

之前我们看到的无论是tq还是卡巴斯基都不会对程序hook api。他们只是开始载入自己的dll到程序中。手头也没有edr程序来研究。于是我们借助了冰盾这个工具来研究吧。

配置

这里我们首先配置冰盾的参数:

设置不允许读取其他进程空间,就相当于对于ReadProcessMemory这个api进行hook。随后我们开始调试

调试分析

首先还是正常来到kernel32中,然后调用ReadProcessMemory

然后来到了kernelbase中的NtReadVirtualMemory,目前一切正常

然后我们进去NtReadVirtualMemory,发现就已经被hook了

这里直接来到了imonitorh2k中。那么下一步就是考虑绕过这个,所以就需要重写ntdll程序。

重写实验成功

这里是程序中,此刻我们利用了pe文件映射之后在这里,加载了ntdll中NtReadVirtualMemory函数

然后尝试去打开别的进程,这里没有走到imonitorh2k中去,而是直接来到了这里:

跟刚才的被hook的完全不一样了

绕过冰盾hook代码

参考PE文件映射unhook

0x04 参考

代码语言:javascript
复制
https://xz.aliyun.com/t/14310
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-05-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Gamma安全实验室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0x01 关于对抗api hook技术
  • 出现场景
  • syscall实现apihook绕过
  • unhook技术
  • 重载ntdll的代码段
  • 劣势
  • PE 文件映射绕过 hook
  • 如何使用
  • 直接跳转
  • 挂起的进程获取干净的ntdll
  • 首先创建一个进程挂起
  • 通过Loader数据提取加载的模块
  • demo
  • 0x02 api hook的不同杀软分析
  • 天擎的hook
  • tq对于api的hook分析
  • kernel32.VirtualAlloc的跟踪hook点
  • 尝试使用动态导入表
  • 卡巴斯基
  • 冰盾
  • 实验一:没有动态导入表直接测试加载器
  • 实验二:有动态导入表直接测试加载器
  • 总结
  • 0x03 对于冰盾的HOOK额外研究
  • 配置
  • 调试分析
  • 重写实验成功
  • 绕过冰盾hook代码
  • 0x04 参考
相关产品与服务
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档