0%

绕过用户态API hooking

参考文章:A practical guide to bypassing userland API Hooking

​ 这里主要是对这篇文章提到的绕过技术进行一个翻译总结。

shellcode 编码或加密

​ Cobalt Strike shellcode 使用带有静态密钥的简单 XOR 编码进行编码足以绕过 EDR 静态分析。

检测API挂钩

​ API 挂钩是许多 EDR 或防病毒供应商使用的一种技术,用于实时监控进程或代码执行是否存在恶意行为。要检测 EDR 是否实现 API 挂钩,我们可以简单地查看可能被 EDR 挂钩的函数调用的前几条指令。这些被挂钩的函数通常是一些进程注入调用的函数,例如 NtOpenProcess、NtCreateThread 或 NtCreateUserProcess。

​ 为了识别所有潜在的挂钩函数调用,有一个开源工具可以在执行期间将函数调用指令的前几个字节与系统 DLL 中相同函数调用的干净版本进行比较。

避免使用已被hook的API

​ 一旦我们确定了 EDR 可以拦截哪些函数调用,我们就可以使用代码执行或进程注入技术来避免这些函数调用。比如可以使用CreateThreadpoolWait函数实现shellcode执行技术。

  • 使用CreateEvent函数创建一个signaled的事件对象(否则无法触发回调)
  • 使用VirtualAlloc给shellcode分配内存空间
  • 使用RtlMoveMemory写入shellcode到当前进程指定内存空间中
  • 使用CreateThreadPoolWait函数创建一个线程池等待回调,其中回调函数指向我们的shellcode(在本文演示过程中,由于设置了事件状态为signaled,因此会立即触发回调并执行shellcode)
  • 使用SetThreadPoolWait设置线程池需要监听的事件,此时将监听步骤1创建的事件
  • 使用WaitForSingleObject对步骤1的事件进行等待(防止进程直接退出)
1
2
3
4
5
6
7
8
HANDLE event = CreateEvent(NULL, FALSE, TRUE, NULL);
LPVOID shellcodeAddress = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
RtlMoveMemory(shellcodeAddress, shellcode, sizeof(shellcode));

PTP_WAIT threadPoolWait = CreateThreadpoolWait((PTP_WAIT_CALLBACK)shellcodeAddress, NULL, NULL);
SetThreadpoolWait(threadPoolWait, event, NULL);
WaitForSingleObject(event, INFINITE);

修补被hook的API

​ 通过修补已被 EDR 覆盖的指令的几个字节来恢复函数调用的原始指令。以文中提到的NtOpenProcess为例。

​ 下图分别为被挂钩的和未被挂钩的函数,用于 NtOpenProcess 函数调用 (7FFD749CD220) 的内存地址的前 16 个字节被 EDR 覆盖,以将函数的执行流程重定向到 EDR 的代码。如果我们可以使用与图中所示的原始指令相同的指令来修补这些内存地址,我们可以防止 EDR 将执行流程重定向到 EDR。

image-20220420153331438

image-20220420153343628

​ 修补代码如下

image-20220420153628939

bypass bitdefender这篇博客也用了这样的方法,不过到最后发现还有一点被查杀是因为用的是http检测到了流量,最终使用https解决问题。

将整个DLL脱钩

​ 因为需要修补的函数可能不止一个,所以可以简单地使用该技术。

这种技术本质上依赖于将驻留在内存中(并已被 EDR 篡改)的 ntdll 代码替换为存储在磁盘上的版本,其中包含原始指令。

image-20220420154729825

代码如下:

image-20220420155151799

Cobalt Strike 反射 DLL 注入

.net程序集–D/Invoke

​ 动态调用(D/Invoke)是一种动态调用非托管代码而不使用P/Invoke逃避API hook的技术。P/Invoke 是一种从非托管代码中调用托管代码的技术,通常被恶意软件开发人员用NET 语言来执行shellcode。

​ 动态调用(DInvoke)在运行时加载 DLL 并使用指向其在内存中位置的指针调用函数,而不是使用 PInvoke 静态导入 API 调用。可以从内存中调用任意非托管代码(同时传递参数),从而以各种方式绕过 API 挂钩并反射性地执行利用后的有效负载。这也避免了通过 .NET 程序集的 PE 标头中的导入地址表查找可疑 API 调用的导入的检测。

DInvoke

.net程序集–Execute-Assembly

​ 关于这个可以看一下这一篇文章Excute-Assembly