EDRSandBlast 是一个用 C 编写的工具,它利用一个有漏洞的已签名驱动程序来绕过 EDR 检测(通知例程回调、对象回调和 ETW TI 提供程序)以及 LSASS 保护。还实现了多种用户态脱钩技术,以规避用户态监控。
在发布时,结合了用户态(--usermode)和内核态(--kernelmode)技术,在 EDR 监视下转储了 LSASS 内存,既没有被阻止,也没有在产品(云)控制台中生成与“OS Credential Dumping”相关的事件。测试在 3 种不同的 EDR 产品上进行,并且在每种情况下都成功了。
EDR 产品使用 Windows 上的内核“通知例程”回调,以便内核通知它们系统活动,例如进程和线程的创建以及映像(exe / DLL)的加载。
这些内核回调是在内核态定义的,通常由实现回调的驱动程序使用一些有文档记录的 API(nt!PsSetCreateProcessNotifyRoutine、nt!PsSetCreateThreadNotifyRoutine 等)定义。这些 API 将驱动程序提供的回调例程添加到内核空间中没有文档记录的例程数组中:
PspCreateProcessNotifyRoutine 用于进程创建PspCreateThreadNotifyRoutine 用于线程创建PspLoadImageNotifyRoutine 用于映像加载EDRSandBlast 枚举这些数组中定义的例程,并移除链接到预定义 EDR 驱动程序列表(支持超过 1000 个安全产品的驱动程序,请参见 EDR 驱动程序检测部分)的任何回调例程。枚举和移除是通过利用由有漏洞驱动程序提供的任意内核内存读/写原语实现的(请参见 有漏洞驱动程序部分)。
上述数组的偏移量通过多种技术恢复,请参见 偏移量部分。
EDR(甚至 EPP)产品经常通过使用 nt!ObRegisterCallbacks 内核 API 注册“对象回调”。这些回调允许安全产品在特定对象类型(Windows 现在支持进程、线程和桌面相关的对象回调)的每次句柄生成时得到通知。句柄生成可能发生在打开对象(调用 OpenProcess、OpenThread 等)以及复制句柄(调用 DuplicateHandle 等)时。
通过在每个此类操作上得到内核的通知,安全产品可以分析句柄创建的合法性(例如,未知进程试图打开 LSASS),如果检测到威胁甚至可以阻止它。
每次使用 ObRegisterCallbacks 注册回调时,都会向 _OBJECT_TYPE 对象中的 CallbackList 双向链表添加一个新项,该对象描述了受回调影响的对象类型(进程、线程或桌面)。不幸的是,这些项由一个未记录且 Microsoft 未在符号文件中发布的结构描述。然而,从不同版本的 ntoskrnl.exe 研究似乎表明,该结构在(至少)Windows 10 内部版本 10240 和 22000(从 2015 年到 2022 年)之间没有变化。
提到的表示对象回调注册的结构如下:```C typedef struct OB_CALLBACK_ENTRY_t { LIST_ENTRY CallbackList; // linked element tied to _OBJECT_TYPE.CallbackList OB_OPERATION Operations; // bitfield : 1 for Creations, 2 for Duplications BOOL Enabled; // self-explanatory OB_CALLBACK* Entry; // points to the structure in which it is included POBJECT_TYPE ObjectType; // points to the object type affected by the callback POB_PRE_OPERATION_CALLBACK PreOperation; // callback function called before each handle operation POB_POST_OPERATION_CALLBACK PostOperation; // callback function called after each handle operation KSPIN_LOCK Lock; // lock object used for synchronization } OB_CALLBACK_ENTRY;
上面提到的`OB_CALLBACK`结构体也没有文档记录,并且定义
如下:```C
typedef struct OB_CALLBACK_t {
USHORT Version; // usually 0x100
USHORT OperationRegistrationCount; // number of registered callbacks
PVOID RegistrationContext; // arbitrary data passed at registration time
UNICODE_STRING AltitudeString; // used to determine callbacks order
struct OB_CALLBACK_ENTRY_t EntryItems[1]; // array of OperationRegistrationCount items
WCHAR AltitudeBuffer[1]; // is AltitudeString.MaximumLength bytes long, and pointed by AltitudeString.Buffer
} OB_CALLBACK;
为了禁用EDR注册的对象回调,EDRSandblast中实现了三种技术;但目前只启用了一种。
OB_CALLBACK_ENTRY 的 Enabled 字段这是 EDRSandblast 中启用的默认技术。为了检测和禁用与EDR相关的对象回调,会遍历位于与进程和线程类型关联的 _OBJECT_TYPE 对象中的 CallbackList 列表。两个 _OBJECT_TYPE 都指向内核中的公共全局符号 PsProcessType 和 PsThreadType。
列表中的每个项目都被假定符合上述 OB_CALLBACK_ENTRY 结构(此假设至少在撰写时的所有Windows 10构建中似乎成立)。查找 PreOperation 和 PostOperation 字段中定义的函数,检查它们是否属于EDR驱动程序,如果是,则通过切换 Enabled 标志来简单地禁用回调。
虽然这是一种相当安全的技术,但它依赖于未文档化的结构;为了降低不安全操作此结构的风险,会执行基本检查以验证某些字段是否具有预期值:
Enabled 为 TRUE 或 FALSE(别笑,BOOL 是 int,因此它可能是 1 或 0 以外的任何值);Operations 为 OB_OPERATION_HANDLE_CREATE、OB_OPERATION_HANDLE_DUPLICATE 或两者兼有;ObjectType 指向 PsProcessType 或 PsThreadType。CallbackList 链接另一种不依赖未文档化结构(因此在理论上更能抵抗NT内核变化)的策略是取消进程和线程的整个 CallbackList 的链接。_OBJECT_TYPE 对象如下:```C
struct _OBJECT_TYPE {
LIST_ENTRY TypeList;
UNICODE_STRING Name;
[...]
_OBJECT_TYPE_INITIALIZER TypeInfo;
[...]
LIST_ENTRY CallbackList;
}
使 `CallbackList` 的 `LIST_ENTRY` 的 `Flink` 和 `Blink` 指针指向 `LIST_ENTRY` 自身实际上使列表为空。由于 `_OBJECT_TYPE` 结构在内核符号中已发布,该技术不依赖于硬编码的偏移量/结构。然而,它有一些缺点。
第一个是无法仅禁用来自 EDR 的回调;实际上,该技术影响可能由“合法”软件注册的所有对象回调。尽管如此,应注意对象回调在 Windows 10 上不被任何预装组件使用(在撰写本文时),因此禁用它们不应影响机器稳定性(如果禁用只是暂时的,更是如此)。
第二个缺点是进程或线程句柄操作在操作系统正常功能中非常频繁(几乎是连续不断的)。因此,如果使用的内核写入原语无法“原子地”执行 `QWORD` 写入,那么内核很可能会在覆盖 `_OBJECT_TYPE.CallbackList.Flink` 指针的过程中访问它。例如,MSI 有漏洞的驱动程序 `RTCore64.sys` 一次只能执行 `DWORD` 写入,因此需要 2 个不同的 IOCTL 来覆盖指针,在这两个 IOCTL 之间,内核有很高的概率使用该指针(导致崩溃)。另一方面,有漏洞的 DELL 驱动程序 `DBUtil_2_3.sys` 可以在一个 IOCTL 中执行任意大小的写入,因此使用此方法不会导致崩溃风险。
#### 完全禁用对象回调
我们发现的最后一种技术是完全禁用线程和进程的对象回调支持。在与进程和线程类型对应的 `_OBJECT_TYPE` 结构内部,有一个 `TypeInfo` 字段,遵循文档化的 `_OBJECT_TYPE_INITIALIZER` 结构。后者包含一个 `ObjectTypeFlags` 位字段,其 `SupportsObjectCallbacks` 标志确定所描述的对象类型(进程、线程、桌面、令牌、文件等)是否支持对象回调注册。如前所述,在撰写本文时,Windows 安装上只有进程、线程和桌面对象类型支持这些回调。
由于 `SupportsObjectCallbacks` 位在读取 `CallbackList` 之前(当然也在执行回调之前)由 `ObpCreateHandle` 或 `ObDuplicateObject` 检查,在内核运行时翻转该位实际上禁用了所有对象回调的执行。
该方法的主要缺点是 *KPP*(“*PatchGuard*”)监控一些(所有?)`_OBJECT_TYPE` 结构的完整性,并触发 [`0x109 错误检查`](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/bug-check-0x109---critical-structure-corruption),其参数 4 等于 `0x8`,表示对象类型结构已被更改。
然而,足够快地执行禁用/重新启用(以及中间的“恶意”操作)应该足以与 *PatchGuard* 竞速(除非你不走运,周期检查恰好在错误时刻执行)。
### 通过取消链接微过滤器回调绕过 EDR
Windows 筛选器管理器系统允许 EDR 加载“微过滤器”驱动程序并注册回调,以便接收 I/O 操作(如文件打开、读取、写入等)的通知。
以下是筛选器管理器使用的不同内部结构的快速总结:
- 筛选器管理器建立一个“框架”(`_FLTP_FRAME`)作为其根结构;
- 为筛选器管理器管理的每个“磁盘”实例化一个“卷”结构(`_FLT_VOLUME`)(可以是分区、卷影副本或对应于命名管道或远程文件系统的特殊卷);
- 每个注册的微过滤器驱动程序对应一个“过滤器”结构(`_FLT_FILTER`),描述各种属性,例如其支持的操作;
- 这些微过滤器并非全部附加到每个卷;创建“实例”(`_FLT_INSTANCE`)结构来标记每个
过滤器<->卷关联;
- 微过滤器注册回调函数,这些函数在特定操作(文件打开、写入、读取等)之前和/或之后执行。这些回调在 `_CALLBACK_NODE` 结构中描述,可以通过不同方式访问:
- 每个微过滤器实例实现的所有 `_CALLBACK_NODE` 的数组可以在 `_FLT_INSTANCE` 结构中找到;该数组由 IRP “主功能”代码索引,该常量表示回调处理的操作(`IRP_MJ_CREATE`、`IRP_MJ_READ` 等)。
- 此外,由链接到特定卷的实例实现的所有 `_CALLBACK_NODE` 被重新分组到链表中,存储在 `_FLT_VOLUME.Callbacks.OperationLists` 数组中,该数组按 IRP 主功能代码索引。
`EDRSandblast` 浏览这些不同的结构,以检测与 EDR 相关驱动程序关联的过滤器,并枚举包含监控功能的回调节点。为了禁用它们的效果,节点从它们的链表中取消链接,使它们暂时对筛选器管理器不可见。
这样,在指定时间段内,EDR 可能完全不知道任何文件操作。一个基本示例是在磁盘上创建 lsass 内存转储文件,这不会触发 EDR 的任何分析,因此不会基于文件本身进行检测。
### 通过停用 ETW Microsoft-Windows-Threat-Intelligence 提供程序绕过 EDR
`ETW Microsoft-Windows-Threat-Intelligence` 提供程序记录有关某些常用作恶意的 Windows API 的使用数据。这包括由 `nt!NtReadVirtualMemory` 调用的 `nt!MiReadWriteVirtualMemory` API(用于转储 `LSASS` 内存)并由 `nt!EtwTiLogReadWriteVm` 函数监控。
EDR 产品可以通过分别作为 `SERVICE_LAUNCH_PROTECTED_ANTIMALWARE_LIGHT` 或 `PS_PROTECTED_ANTIMALWARE_LIGHT` 运行,并与 `Early Launch Anti Malware (ELAM)` 驱动程序关联的服务或进程来消耗 `ETW TI` 提供程序生成的日志。
如 [`slaeryan` 在 `CNO Development Labs` 的一篇博客文章](https://public.cnotools.studio/bring-your-own-vulnerable-kernel-driver-byovkd/exploits/data-only-attack-neutralizing-etwti-provider) 中所发布,`ETW TI` 提供程序可以通过在内核内存中将其 `ProviderEnableInfo` 属性修补为 `0x0` 来完全禁用。有关该技术的更多信息,请参阅上述优秀博客文章。
类似于内核回调移除,必要的 `ntoskrnl.exe` 偏移量(`nt!EtwThreatIntProvRegHandleOffset`、`_ETW_REG_ENTRY` 的 `GuidEntry` 和 `_ETW_GUID_ENTRY` 的 `ProviderEnableInfo`)在 `NtoskrnlOffsets.csv` 文件中为多个 Windows 内核版本计算。
### 通过绕过用户态钩子绕过 EDR
#### 用户态钩子的工作原理
为了轻松监控进程执行的操作,EDR 产品通常部署一种称为 *用户态钩子 (userland hooking)* 的机制。首先,EDR 产品注册一个内核回调(通常是 *映像加载* 或 *进程创建* 回调,见上文),允许它们在每个进程启动时得到通知。
当 Windows 加载进程时,在进程实际启动之前,EDR 能够将一些包含其监控逻辑的自定义 DLL 注入到进程地址空间中。在加载时,此 DLL 在每个待 EDR 监控的函数的开头注入“*钩子*”。在运行时,当受监控进程调用被监控的函数时,这些钩子将控制流重定向到 EDR 的 DLL 中的某些监控代码,从而允许它检查这些调用的参数和返回值。
大多数情况下,被监控的函数是系统调用(例如 `NtReadVirtualMemory`、`NtOpenProcess` 等),其实现位于 `ntdll.dll` 中。拦截对 `Nt*` 函数的调用使产品能够尽可能接近用户态/内核态边界(同时保持在用户态),但来自一些更高级别 DLL 的函数也可能被监控。