巧妙的EDR绕过技术及其实现方法

本文详细介绍了三种绕过EDR监控的技术方法,包括利用竞争条件、硬件断点和故意触发异常,通过实际代码演示如何绕过Sophos Intercept X的检测机制。

巧妙的EDR绕过技术及其实现方法

最近我在测试一些EDR检测间接系统调用的能力时,想到了一个独特的绕过方法。

直接与间接系统调用的问题

直接和间接系统调用的一个缺点是,从调用堆栈中可以明显看出你绕过了EDR的用户模式钩子。以下是直接、间接和常规调用的示例调用堆栈:

  • 直接系统调用的调用堆栈
  • 间接系统调用的调用堆栈
  • 常规挂钩Nt函数调用的调用堆栈

从最后一张图片可以看出,当通过挂钩函数进行调用时,EDR钩子的返回地址会出现在调用堆栈中(在我的案例中是hmpalert)。

这是一个有趣的困境:我们不想调用挂钩函数,因为那可能触发检测;但如果我们完全绕过钩子,也可能触发检测。

第一个想法:TOCTOU

时间检查到时间使用(TOCTOU)是软件利用中常用的技术。当对对象执行安全检查,但在检查时间和使用时间之间无法阻止修改该对象时,就会出现漏洞。

我的初步想法是利用类似的竞争条件来对抗EDR的钩子:使用良性参数调用挂钩函数,然后在调用过程中快速将其替换为恶意参数。

第二个想法:硬件断点

这个想法更简单:选择一个我想调用的被EDR挂钩的ntdll函数,然后在syscall指令上设置硬件断点。

通过在执行syscall指令上设置执行断点,我们能够在EDR完成检查之后、系统调用发生之前拦截执行。这基本上允许我们挂钩EDR的钩子,并将任何合法调用转换为自定义系统调用。

我们可以使用不会触发检测的良性参数调用挂钩函数,然后在EDR已经检查调用后将参数替换为恶意参数。我们甚至可以根据需要更改系统调用号,以调用与EDR认为我们正在进行的不同的系统调用。

寻找合适的目标

为了测试我的想法,我想出了一个会立即触发检测事件的函数调用。最终,我选择使用旧的进程注入代码。

该代码的工作原理类似于进程空心化:它创建一个处于挂起状态的新进程,将自身注入到挂起的进程中,然后使用SetThreadContext()将主线程的入口点更改为恶意代码的入口点。

我选择的目标是Sophos Intercept X,因为它宣传能够检测进程空心化攻击。

使用硬件断点绕过检查

对于我们的第一个示例,我们只需在NtSetContextThread()的syscall和retn指令上设置断点。

以下是查找这些指令的示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
BOOL FindSyscallInstruction(LPVOID nt_func_addr, LPVOID* syscall_addr, LPVOID* syscall_ret_addr) {
    BYTE* ptr = (BYTE*)nt_func_addr;

    for (int i = 0; i < 1024; i++) {
        if (*&ptr[i] == 0x0F && *&ptr[i + 1] == 0x05) {
            printf("Found syscall opcode at %llx\n", (DWORD64)&ptr[i]);
            *syscall_addr = (LPVOID)&ptr[i];
            *syscall_ret_addr = (LPVOID)&ptr[i + 2];
            break;
        }
    }

    if (!*syscall_addr) {
        printf("error: syscall instruction not found\n");
        return FALSE;
    }

    if (**(BYTE**)syscall_ret_addr != 0xc3) {
        printf("Error: syscall instruction not followed by ret\n");
        return FALSE;
    }

    return TRUE;
}

在断点处理程序中,我们只需更改RCX和RDX寄存器,它们包含NtSetContextThread()的参数1和参数2。

结果

成功!代码能够将自身注入到notepad中并显示消息框。

第三个想法:故意触发异常

不使用硬件断点,我们将尝试引发CPU异常。常规异常可以以与断点异常完全相同的方式处理,但我们不需要调用NtSetContextThread()来设置它们。

我们知道EDR在调用NtSetContextThread()时会检查上下文结构,所以让我们利用这一点。大多数软件在尝试读取地址之前会检查地址是否为NULL,但如果它既不是NULL也不是有效地址呢?

如果我们将上下文地址设置为0x1337会发生什么?

1
2
HANDLE thread_handle = CreateThread(NULL, 0, test_thread, NULL, CREATE_SUSPENDED, 0);
SetThreadContext(thread_handle, (CONTEXT*)0x1337);

当我们运行它时…EDR的钩子尝试读取无效内存并使进程崩溃。

现在我们有了一个无需任何硬件断点即可触发异常的简单方法。

结论

我们有两种绕过EDR钩子而不实际绕过EDR钩子的方法。虽然我不确定将强制异常方法转化为通用EDR绕过的实际可行性或难易程度。

第一种方法更通用,但可能也更容易被检测到。由于异常处理程序允许我们在不使用NtSetContextThread()的情况下更改线程的上下文,我们可能会结合这两种方法。

这只是一个有趣的周末副项目,我觉得应该发布出来。希望有人会发现这些信息有用。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计