利用硬件断点逃避端点检测和响应 (EDR) 平台及其他控制系统的监控并非新鲜事。威胁行为者和研究人员都曾利用断点注入命令并执行恶意操作。
使用 Windows 事件跟踪 (ETW) 和 Windows 反恶意软件扫描接口 (AMSI) 的概念验证 (PoC) 技术已经存在一段时间了。其中包括一些深入的研究成果,创建TamperingSyscalls和EthicalChaos创建的进程内无补丁 AMSI 绕过技术 - 但这两种攻击都是通过钩住当前进程内存中的特定函数来操纵内存,从而达到非预期目的。
Cymulate 攻击研究小组将该方法扩展为一种名为“Blindside”的新技术,使其能够更广泛地发挥作用。Blindside技术并非挂钩特定函数,而是加载一个不受监控且未挂钩的 DLL,并利用调试技术来运行任意代码。
硬件断点和调试寄存器(DR0-DR7)概述
由于 Blindside 依赖于硬件断点,因此了解这些断点和调试寄存器的功能至关重要。
什么是硬件断点和调试寄存器?
x86 和 x64 处理器均支持硬件断点,包含八个调试寄存器:DR0 – DR7。这些寄存器的长度为 32 位或 64 位,具体取决于处理器类型,用于控制调试操作的监控。
与 Windows 开发人员更熟悉的软件断点不同,硬件断点允许设置“内存断点”,当指令尝试读取、写入或执行指定的内存地址(基于断点配置)时触发。然而,硬件断点的一个限制是,同一时刻只能激活少量的硬件断点。
调试寄存器(DR0-DR7)的功能
-
DR0-DR3:保存断点的线性地址,称为调试地址寄存器。当指令与其中一个寄存器中的地址匹配时,断点就会触发。
-
DR4-DR5:这些是保留的调试寄存器,在本技术中未使用。
-
DR6:称为调试状态寄存器,它报告上次异常期间采样的调试条件。
-
DR7:调试控制寄存器在 Blindside 技术中至关重要,因为它控制每个断点及其条件。
这些调试寄存器的主要功能是设置和监控最多 4 个编号为 0 到 3 的寄存器。对于每个断点,可以指定以下信息:
-
断点发生的线性地址。
-
断点位置的长度(1、2 或 4 个字节)。
-
该操作将在生成调试异常的地址处执行。
-
断点是否启用。
-
生成调试异常时断点条件是否存在。
调试异常
说到硬件断点中的异常,这里有两种后果:调试异常 (#DB) 和断点异常 (#BP)。就 Blindside 技术而言,调试异常 (#DB) 至关重要。断点触发后,执行将被重定向到一个处理程序——通常是调试器程序或更广泛软件系统的一部分。需要注意的是,Blindside 技术中的异常只有在单步异常时才会触发。
设置盲区技术
步骤 1:断点处理程序
在准备使用该技术时,首先需要建立一个断点处理程序。以下是 C++ 中处理程序的示例:
该函数首先检查 EXCEPTION_POINTERS 结构体的 ExceptionRecord 成员中的异常代码是否为 EXCEPTION_SINGLE_STEP,这表明发生了单步异常。如果是,函数会检查该结构体的 ContextRecord 成员中的指令指针 (Rip) 是否等于第一个调试寄存器 (Dr0) 的值。如果也等于,函数会打印一些关于异常的信息,包括异常地址、某些寄存器的值以及堆栈指针 (Rsp) 的值。
最后,函数设置恢复标志 (RF),并返回 EXCEPTION_CONTINUE_EXECUTION 以指示应继续执行。如果异常代码不是 EXCEPTION_SINGLE_STEP,则函数返回 EXCEPTION_CONTINUE_SEARCH 以指示应继续查找处理程序。
技术设置第 2 部分:设置断点
配置处理程序来处理异常后,准备的下一步是创建实际的断点。
像以前一样使用 C++,下面是断点配置的示例:
此函数接受两个参数。第一个参数是系统应设置断点的地址,第二个参数是启用或禁用断点。然后,该技术获取正在执行的指定线程的当前上下文,并将其存储在上下文变量中。如果 setBP 变量为 true,代码会将 DR0 设置为攻击者希望中断的地址。请注意,如果需要,该技术也可以使用 Dr1、Dr2 或 Dr3 来存储地址。接下来,执行将 Dr7 的第一位设置为 1 以启用断点,并清除第 16 位和第 17 位以中断执行。
相反,如果 setBP 变量为 false,代码将清除 Dr0,并对 Dr7 的第一位执行相同操作。最后,代码设置线程的上下文以便更新。
利用盲区
在研究此主题时,Cymulate 回顾了众多研究人员创建的通用方法论的重要成果。Cymulate 攻击研究小组意识到,一种与已知技术不同的技术可以在调试模式下创建一个新进程,在 LdrLoadDll 上设置断点,并强制仅加载 ntdll.dll。这样一来,攻击者就能得到一个干净的 ntdll 版本,而无需任何钩子。攻击者随后可以将干净的 ntdll 的内存复制到现有进程,并解除所有先前钩住的系统调用。
进程首次创建时,ntdll.dll 会自动加载,但其他 dll 也会加载。利用这种技术,断点会通过钩住 LdrLoadDLL 来阻止其他 dll 的加载,并创建一个仅包含 ntdll 的独立、未钩住状态的进程。
穿越盲区技术
从整个过程来看,Blindside 技术的应用可以允许未受监控的进程在 Windows 会话的上下文中运行,如下所示:
步骤 1:在调试模式下创建一个新进程
步骤2:找到LdrLoadDll的进程地址
由于创建的进程是目标子进程,因此它将具有相同的 ntdll 基址和相同的 LdrLoadDll 地址。这意味着必须识别 LdrLoadDll 的地址。
步骤3:设置断点
找到LdrLoadDll地址后,下一个需要的功能就是在远程进程上放置断点。
该函数有两个参数:应设置断点的地址和应设置断点的线程的句柄。
该函数首先初始化一个 CONTEXT 结构,并将其 ContextFlags 成员设置为 CONTEXT_DEBUG_REGISTERS 和 CONTEXT_INTEGER 的按位或值,这指定该结构应填充线程的当前调试寄存器和整数寄存器。然后,它将第一个调试寄存器(Dr0)的值设置为指定的地址,并设置 Dr7 寄存器的第 0 位以启用断点。
步骤 4:等待断点触发
接下来,该函数调用 SetThreadContext() 函数将更新后的上下文应用于线程。然后,函数进入无限循环,使用 WaitForDebugEvent() 函数等待调试事件。当收到调试事件时,该函数会检查它是否是异常代码为 EXCEPTION_SINGLE_STEP 的异常调试事件。如果是,该函数将使用 GetThreadContext() 函数检索线程的当前上下文,并检查异常地址是否与指定的地址匹配。
如果异常地址与指定地址匹配,该函数将重置 Dr0、Dr6 和 Dr7 寄存器,并且不返回任何内容,这样做是为了阻止 LdrLoadDll 加载其他 DLL。否则,它将重置断点并通过调用带有 DBG_CONTINUE 参数的 ContinueDebugEvent() 函数继续执行。此循环持续到 WaitForDebugEvent() 返回 0(表示没有更多可用的调试事件)。
步骤5:内存加载和解除挂钩
然后需要将 ntdll 的内存复制到目标进程中并解除所有系统调用。
此函数有一个参数,即创建的调试进程的句柄。调试进程的基址将与 ntdll 的基址相同。使用 NtReadVirtualMemory 读取 ntdll 的内存后,freshNtdll(分配的缓冲区)将存储该内存信息。现在可以安全地终止原始进程,因为不再需要它了。
步骤 6:覆盖钩子
接下来,需要遍历所有内容以找到 ntdll 的 .text 部分的虚拟地址,将保护更改为 PAGE_EXECUTE_READWRITE,并将新映射缓冲区(freshNtdll)的 .text 部分复制到原始挂钩版本的 ntdll,这将导致钩子被覆盖。
步骤 7:清理
完成此技术的最后一步是恢复原始保护。
原文始发于微信公众号(Khan安全团队):EDR 规避:利用硬件断点的新技术 – Blindside
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论