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 的函数也可能被监控。
下面是同一函数在被 EDR 产品钩住之前和之后的示例:```assembly
NtProtectVirtualMemory proc near
mov r10, rcx
mov eax, 50h
test byte ptr ds:7FFE0308h, 1
jnz short loc_18009D1E5
syscall
retn
loc_18009D1E5:
int 2Eh
retn
NtProtectVirtualMemory endp
.```assembly NtProtectVirtualMemory proc near jmp sub_7FFC74490298 ; --> "hook", jump to EDR analysis function int 3 ; overwritten instructions int 3 ; overwritten instructions int 3 ; overwritten instructions test byte_7FFE0308, 1 ; <-- execution resumes here after analysis jnz short loc_7FFCB44AD1E5 syscall retn loc_7FFCB44AD1E5: int 2Eh retn NtProtectVirtualMemory endp
#### 钩子检测
用户态钩子的“弱点”在于它们位于用户态内存中,这意味着它们可以被受检查的进程直接观察和修改。为了自动检测进程地址空间中的钩子,主要思路是比较磁盘上原始DLL与内存中可能已被EDR修改的库之间的差异。为了执行此比较,EDRSandblast遵循以下步骤:
* 通过位于`PEB`中的`InLoadOrderModuleList`枚举所有已加载DLL的列表(以避免调用任何可能被监控且可疑的API)。
* 对于每个已加载的DLL,读取其在磁盘上的内容并解析其头部。同时解析内存中对应的库,以识别节区、导出等。
* 解析并应用DLL的重定位,并考虑对应已加载库的基址。这使得内存中的库与磁盘上的DLL在应用了重定位的节区上具有完全相同的内容,从而使比较可靠。
* 枚举导出函数,并比较“内存中”与“磁盘上”版本的前几个字节。任何差异都表明DLL加载后发生了修改,因此很可能是EDR钩子。
注意:该过程可以推广,以查找任何不可写节区中的差异,而不仅仅是导出函数的起始位置。例如如果EDR产品开始在函数中间应用钩子:)该工具未使用此方法,但已在`findDiffsInNonWritableSections`中实现。
为了绕过这些钩子执行的监控,有多种技术可行,每种技术都有其优缺点。
#### 使用……解除钩子来绕过钩子
绕过基于钩子的监控最直观的方法是移除钩子。由于钩子存在于进程自身可访问的内存中,要移除钩子,进程只需:
* 更改钩子所在页面的权限(RX -> RWX 或 RW)
* 写入已知的原始字节(通过磁盘上的DLL内容获得)
* 将权限改回RX
这种方法相当简单,可用于一次性移除所有检测到的钩子。由攻击性工具在开始时执行,这样后续代码可以完全忽略钩子机制,正常执行且不被监控。
然而,它有两个主要缺点。EDR可能正在监控`NtProtectVirtualMemory`的使用,因此使用它来更改已安装钩子页面的权限(至少在概念上)不是一个好主意。此外,如果EDR运行了一个线程并定期检查钩子的完整性,这也可能触发检测。
关于实现细节,请检查`unhook()`函数在`unhook_method`为`UNHOOK_WITH_NTPROTECTVIRTUALMEMORY`时的代码路径。
**重要说明:为简单起见,此技术在EDRSandblast中作为基础技术实现,用于*展示*其他绕过技术;每种技术都演示了如何获取不受监控的`NtProtectVirtualMemory`版本,但之后执行相同的操作(解除特定钩子)。**
#### 使用自定义跳板绕过钩子
要绕过特定钩子,可以简单地“跳过”它并正常执行函数的其余部分。首先,必须从DLL文件中恢复被EDR覆盖的受监控函数的原始字节。在我们之前的代码示例中,这些字节对应于以下指令的字节:```assembly
mov r10, rcx
mov eax, 50h
识别这些字节是一项简单的任务,因为如前所述,我们能够对库的内存版本和磁盘版本进行干净的差异对比。然后,我们组装一条跳转指令,该指令用于将控制流重定向到紧跟钩子之后的代码,地址为 `NtProtectVirtualMemory + sizeof(overwritten_instructions)````assembly jmp NtProtectVirtualMemory+8
最后,我们将这些操作码连接起来,存入(新分配的)可执行内存,并保留一个指向它们的指针。这个对象被称为“*trampoline*”(蹦床),随后可作为函数指针使用,与原始的 `NtProtectVirtualMemory` 函数完全等价。
该技术的主要优势(与以下所有技术相同)在于,钩子从未被擦除,因此EDR对钩子进行的任何完整性检查都应通过。然而,它需要先分配可写内存,再转为可执行内存,这正是shellcode分配的典型特征,从而引起EDR的关注。
有关实现细节,请查看 `unhook()` 函数中 `unhook_method` 为 `UNHOOK_WITH_INHOUSE_NTPROTECTVIRTUALMEMORY_TRAMPOLINE` 时的代码路径。请记住,该技术仅在我们的实现中展示,并且最终用于**移除**内存中的钩子,与以下所有技术相同。
#### 利用EDR自身的trampoline绕过钩子
EDR产品为了使其钩子工作,必须在内存中某处保存它已移除的操作码。更糟糕的是(或者说从攻击者角度看“更好”),为了有效使用原始指令,EDR可能已在某处为自己分配了一个*trampoline*,以在拦截调用后执行原始函数。
这个trampoline可以被搜索到,并用作被钩函数的替代品,无需分配可执行内存,也无需调用除 `VirtualQuery` 之外的任何API,而 `VirtualQuery` 很可能不受监控,因为它是一个无害的函数。
为了在内存中找到trampoline,我们使用 `VirtualQuery` 浏览整个地址空间,寻找已提交且可执行的内存。对于每个这样的内存区域,我们扫描它,寻找一条跳转指令,其目标地址是覆盖指令之后的位置(在我们之前的例子中为 `NtProtectVirtualMemory+8`)。然后,该trampoline可用于调用被钩函数而不触发钩子。
该技术效果出奇地好,因为它几乎能恢复所有在测试过的EDR上的trampoline。有关实现细节,请查看 `unhook()` 函数中 `unhook_method` 为 `UNHOOK_WITH_EDR_NTPROTECTVIRTUALMEMORY_TRAMPOLINE` 时的代码路径。
#### 使用重复DLL绕过钩子
另一种获取不受监控版本的 `NtProtectVirtualMemory` 函数的简单方法是,将 `ntdll.dll` 库的一个副本加载到进程地址空间中。由于两个相同的DLL可以加载到同一个进程中,只要它们具有不同的名称,我们可以简单地将合法的 `ntdll.dll` 文件复制到另一个位置,使用 `LoadLibrary`(或重新实现加载过程)加载它,并使用 `GetProcAddress` 等访问该函数。
该技术非常易于理解和实现,并且有相当大的成功机会,因为大多数EDR产品在进程运行后不会在新加载的DLL上重新安装钩子。然而,主要缺点是,将Microsoft签名的二进制文件以不同名称复制,本身通常会被EDR产品视为可疑行为。
该技术已在 `EDRSandblast` 中实现。有关实现细节,请查看 `unhook()` 函数中 `unhook_method` 为 `UNHOOK_WITH_DUPLICATE_NTPROTECTVIRTUALMEMORY` 时的代码路径。
#### 使用直接系统调用绕过钩子
为了使用与系统调用相关的函数,程序可以重新实现系统调用(用汇编语言),以便在不实际触及 `ntdll.dll` 中的代码(可能被EDR监控)的情况下调用相应的操作系统功能。这完全绕过了对 `ntdll.dll` 中系统调用函数进行的任何用户态钩子。
然而,这也有一些缺点。首先,这意味着需要知道程序所需函数的系统调用编号列表,而该列表随每个Windows版本而变化。不过,通过实现多种启发式方法(这些方法已知在所有过去的Windows NT版本中有效,例如对 `ntdll` 的 `Zw*` 导出进行排序,在相关的 `ntdll` 函数中搜索 `mov rax, #syscall_number` 指令等),并检查它们是否返回相同的结果(详见 `Syscalls.c`),可以缓解此问题。
此外,技术上不属于系统调用的函数(例如 `LoadLibraryX`/`LdrLoadDLL`)也可能被监控,并且不能简单地通过系统调用来重新实现。
直接系统调用技术在EDRSandblast中已实现。如前所述,它仅用于安全地执行 `NtProtectVirtualMemory`,并移除所有检测到的钩子。
有关实现细节,请查看 `unhook()` 函数中 `unhook_method` 为 `UNHOOK_WITH_DIRECT_SYSCALL` 时的代码路径。
### 利用脆弱驱动程序
如前所述,每个需要内核内存读写操作的行为都依赖于一个脆弱的驱动程序来提供这一原语。在EDRSandblast中,添加对提供读写原语的新驱动程序的支持可以“轻松”完成,只需实现三个函数:
* 一个 `ReadMemoryPrimitive_DRIVERNAME(SIZE_T Size, DWORD64 Address, PVOID Buffer)` 函数,将 `Size` 字节从内核地址 `Address` 复制到用户态缓冲区 `Buffer`;
* 一个 `WriteMemoryPrimitive_DRIVERNAME(SIZE_T Size, DWORD64 Address, PVOID Buffer)` 函数,将 `Size` 字节从用户态缓冲区 `Buffer` 复制到内核地址 `Address`;
* 一个 `CloseDriverHandle_DRIVERNAME()` 函数,确保所有指向该驱动程序的句柄都被关闭(在卸载操作前需要,该操作目前与驱动程序无关)。
例如,EDRSandblast目前支持两个驱动程序:`RTCore64.sys`(SHA256: `01AA278B07B58DC46C84BD0B1B5C8E9EE4E62EA0BF7A695862444AF32E87F1FD`)和 `DBUtils_2_3.sys`(SHA256: `0296e2ce999e67c76352613a718e11516fe1b0efc3ffdb8918fc999dd76a73a5`)。如果使用的脆弱驱动程序需要更改,或者实现了新的驱动程序,则需要更新 `KernelMemoryPrimitives.h` 中的以下代码。```C
#define RTCore 0
#define DBUtil 1
// Select the driver to use with the following #define
#define VULN_DRIVER RTCore
#if VULN_DRIVER == RTCore
#define DEFAULT_DRIVER_FILE TEXT("RTCore64.sys")
#define CloseDriverHandle CloseDriverHandle_RTCore
#define ReadMemoryPrimitive ReadMemoryPrimitive_RTCore
#define WriteMemoryPrimitive WriteMemoryPrimitive_RTCore
#elif VULN_DRIVER == DBUtil
#define DEFAULT_DRIVER_FILE TEXT("DBUtil_2_3.sys")
#define CloseDriverHandle CloseDriverHandle_DBUtil
#define ReadMemoryPrimitive ReadMemoryPrimitive_DBUtil
#define WriteMemoryPrimitive WriteMemoryPrimitive_DBUtil
#endif
当前使用多种技术来确定特定驱动或进程是否属于某个 EDR 产品。
首先,驱动的名称可直接用于此目的。事实上,微软为所有需要在内核中插入回调的驱动分配了称为“Altitudes”的特定编号。这保证了回调执行的确定性顺序,独立于注册顺序,仅基于驱动的用途。已预留特定 altitude 的驱动(供应商)列表可在 MSDN 找到。因此,微软提供了一份近乎全面的安全驱动名称列表,这些驱动与安全产品相关,主要包含在“FSFilter 防病毒”和“FSFilter 活动监视器”列表中。这些驱动名称列表已嵌入 EDRSandblast,此外还有额外贡献。
此外,EDR 可执行文件和 DLL 通常会使用供应商的签名证书进行数字签名。因此,检查与进程关联的可执行文件或 DLL 的签名者,可以快速识别 EDR 产品。
另外,驱动必须直接由微软签名才能被允许加载到内核空间。虽然驱动供应商并非驱动本身的直接签名者,但供应商名称似乎仍包含在签名的某个属性中;不过这种检测技术尚待研究和实现。
最后,当遇到 EDRSandblast 未知的 EDR 时,最佳方法是运行工具的“审计”模式,检查已注册内核回调的驱动列表;然后将驱动名称添加到列表中,重新编译工具并再次运行。
本地安全机构 (LSA) 保护 机制,首次在 Windows 8.1 和 Windows Server 2012 R2 中引入,利用 受保护进程轻量级 (PPL) 技术限制对 LSASS 进程的访问。PPL 保护规范并限制对受保护进程的操作,例如内存注入或内存转储,即使从拥有 SeDebugPrivilege 权限的进程执行也是如此。在进程保护模型下,只有运行在更高保护级别的进程才能对受保护进程执行操作。
Windows 内核用于表示进程的 _EPROCESS 结构包含一个 _PS_PROTECTION 字段,该字段通过其 Type(_PS_PROTECTED_TYPE)和 Signer(_PS_PROTECTED_SIGNER)属性定义进程的保护级别。
通过在内存中写入内核数据,EDRSandblast 进程可以将其自身保护级别提升至 PsProtectedSignerWinTcb-Light。该级别足以转储 LSASS 进程的内存,因为它“支配”了 PsProtectedSignerLsa-Light——即运行 RunAsPPL 机制的 LSASS 进程的保护级别。
EDRSandBlast 实现自保护的过程如下:
NtQuerySystemInformation 泄露所有系统句柄,以找到当前进程上打开的句柄,以及当前进程的内核内存中 EPROCESS 结构的地址。_PS_PROTECTION 字段。_PS_PROTECTION 字段相对于 EPROCESS 结构(由使用的 ntoskrnl 版本定义)的偏移量在 NtoskrnlOffsets.csv 文件中计算得出。微软的 Credential Guard 是一种基于虚拟化的隔离技术,在微软的 Windows 10(企业版) 中引入,该技术阻止直接访问存储在 LSASS 进程中的凭据。
当 Credential Guard 激活时,会在 虚拟安全模式 中创建一个 LSAIso(LSA 隔离)进程,该模式利用 CPU 的虚拟化扩展来提供内存数据的额外安全性。即使对于拥有 NT AUTHORITY\SYSTEM 安全上下文的访问,对 LSAIso 进程的访问也受到限制。在处理哈希时,LSA 进程会向 LSAIso 进程发起 RPC 调用,并等待 LSAIso 的结果以继续执行。因此,LSASS 进程不会包含任何机密信息,而会存储 LSA 隔离数据。
正如 N4kedTurtle 的原始研究所指出的:“可以通过修补内存中的 g_fParameter_useLogonCredential 和 g_IsCredGuardEnabled 值,在启用了 Credential Guard 的系统上启用 Wdigest”。启用 Wdigest 将导致任何新的交互式登录(无需重启系统)的明文凭据被存储在 LSASS 内存中。有关此技术的更多详细信息,请参阅原始研究博客文章。
EDRSandBlast 仅将原始 PoC 做得更符合操作安全(OpSec)要求,并为多个版本的 wdigest.dll 提供支持(通过计算 g_fParameter_useLogonCredential 和 g_IsCredGuardEnabled 的偏移量)。
为了可靠地执行内核监控绕过操作,EDRSandblast 需要确切知道读写内核内存的位置。这是通过使用目标镜像(ntoskrnl.exe、wdigest.dll)中全局变量的偏移量,以及微软在符号文件中发布的结构定义中特定字段的偏移量来实现的。这些偏移量特定于每个目标镜像的构建版本,并且必须针对特定的平台版本至少收集一次。
选择使用“硬编码”偏移量而非模式搜索来定位 EDRSandblast 使用的结构和变量,其理由在于负责内核回调添加/移除的未公开 API 可能会发生变化,并且任何对内核内存的读写操作如果使用了错误地址,都可能导致(而且通常会)Bug Check(蓝屏死机)。机器崩溃在红队和常规渗透测试场景中都是不可接受的,因为崩溃的机器会被防御者高度察觉,并且会丢失攻击时内存中仍然存在的任何凭据。
为了获取每个特定 Windows 版本的偏移量,实现了两种方法。
所需的 ntoskrnl.exe 和 wdigest.dll 偏移量可以使用提供的 ExtractOffsets.py Python 脚本提取,该脚本依赖 radare2 和 r2pipe 从 PDB 文件下载并解析符号,并从中提取所需偏移量。偏移量随后存储在 CSV 文件中,供 EDRSandblast 后续使用。
为了开箱即用支持广泛的 Windows 构建版本,Winbindex 引用了许多版本的 ntoskrnl.exe 和 wdigest.dll 二进制文件,并且可以通过 ExtractOffsets.py 自动下载(并提取其偏移量)。这允许从 Windows 更新包中曾经发布过的几乎所有文件中提取偏移量(截至目前,已有 450 多个 ntoskrnl.exe 版本和 30 多个 wdigest.dll 版本可用并预先计算)。
在 EDRSandBlast 中实现了一个附加选项,允许程序自行从 Microsoft 符号服务器下载所需的 .pdb 文件,提取所需的偏移量,如果存在相应的 .csv 文件,甚至可以更新它们。
使用 --internet 选项可以使工具的执行更加简单,但同时引入了额外的操作安全(OpSec)风险,因为在此过程中会下载 .pdb 文件并将其丢弃到磁盘上。这是解析符号数据库所需的 dbghelp.dll 函数所要求的;然而,未来可能会实现完全内存中的 PDB 解析,消除此要求并减少工具的痕迹。
EDRSandblast 公开支持至少 3 个易受攻击的驱动:gdrv.sys(默认)、RTCore64.sys 和 DBUtil_2_3.sys。实际使用的驱动在编译工具之前决定(参见 includes/KernelMemoryPrimitive.h 中的 #define VULN_DRIVER <driver name>)。应下载易受攻击驱动的副本并将其提供给 EDRSandblast,以便其内核操作正常工作。
已测试驱动的哈希值在每个实现 EDRSandblast 使用的内核内存读写原语的 Driver<name>.c 文件开头提及。利用这些哈希值,可以轻松在互联网上找到驱动样本,尤其是在 https://www.loldrivers.io 上。
以下是支持的易受攻击驱动及其下载链接列表:
Usage: EDRSandblast.exe [-h | --help] [-v | --verbose] <audit | dump | cmd | credguard | firewall | load_unsigned_driver> [--usermode] [--unhook-method ] [--direct-syscalls] [--add-dll ]* [--kernelmode] [--dont-unload-driver] [--no-restore] [--nt-offsets <NtoskrnlOffsets.csv>] [--fltmgr-offsets <FltmgrOffsets.csv>] [--wdigest-offsets <WdigestOffsets.csv>] [--ci-offsets <CiOffsets.csv>] [--internet] [--vuln-driver <RTCore64.sys>] [--vuln-service <SERVICE_NAME>] [--unsigned-driver <evil.sys>] [--unsigned-service <SERVICE_NAME>] [--no-kdp] [-o | --dump-output <DUMP_FILE>]
### 选项```
-h | --help Show this help message and exit.
-v | --verbose Enable a more verbose output.
Actions mode:
audit Display the user-land hooks and / or Kernel callbacks without taking actions.
dump Dump the process specified by --process-name (LSASS process by default), as '<process_name>' in the current directory or at the
specified file using -o | --output <DUMP_FILE>.
cmd Open a cmd.exe prompt.
credguard Patch the LSASS process' memory to enable Wdigest cleartext passwords caching even if
Credential Guard is enabled on the host. No kernel-land actions required.
firewall Add Windows firewall rules to block network access for the EDR processes / services.
load_unsigned_driver Load the specified unsigned driver, bypassing Driver Signature Enforcement (DSE).
WARNING: currently an experimental feature, only works if KDP is not present and enabled.
--usermode Perform user-land operations (DLL unhooking).
--kernelmode Perform kernel-land operations (Kernel callbacks removal and ETW TI disabling).
Hooking-related options:
--add-dll <dll name or path> Loads arbitrary libraries into the process' address space, before starting
anything.This can be useful to audit userland hooking for DLL that are not
loaded by default by this program. Use this option multiple times to load
multiple DLLs all at once.
Example of interesting DLLs to look at: user32.dll, ole32.dll, crypt32.dll,
samcli.dll, winhttp.dll, urlmon.dll, secur32.dll, shell32.dll...
--unhook-method <N> Choose the userland un-hooking technique, from the following:
0 Do not perform any unhooking (used for direct syscalls operations).
1 (Default) Uses the (probably monitored) NtProtectVirtualMemory function in ntdll to remove all
present userland hooks.
2 Constructs a 'unhooked' (i.e. unmonitored) version of NtProtectVirtualMemory, by allocating an executable trampoline jumping over the hook, and remove all present
userland hooks.
3 Searches for an existing trampoline allocated by the EDR itself, to get an 'unhooked'
(i.e. unmonitored) version of NtProtectVirtualMemory, and remove all present userland
hooks.
4 Loads an additional version of ntdll library into memory, and use the (hopefully unmonitored) version of NtProtectVirtualMemory present in this library to remove all
present userland hooks.
5 Allocates a shellcode that uses a direct syscall to call NtProtectVirtualMemory, and uses it to remove all detected hooks
--direct-syscalls Use direct syscalls to dump the selected process memory without unhooking unserland hooks.
BYOVD options:
--dont-unload-driver Keep the vulnerable driver installed on the host
Default to automatically unsinstall the driver.
--no-restore Do not restore the EDR drivers' Kernel Callbacks that were removed.
Default to restore the callbacks.
--vuln-driver <gdrv.sys> Path to the vulnerable driver file.
Default to 'gdrv.sys' in the current directory.
--vuln-service <SERVICE_NAME> Name of the vulnerable service to intall / start.
Driver sideloading options:
--unsigned-driver <evil.sys> Path to the unsigned driver file.
Default to 'evil.sys' in the current directory.
--unsigned-service <SERVICE_NAME> Name of the unsigned driver's service to intall / start.
--no-kdp Switch to g_CiOptions patching method for disabling DSE (default is callback swapping).
Offset-related options:
--nt-offsets <NtoskrnlOffsets.csv> Path to the CSV file containing the required ntoskrnl.exe's offsets.
Default to 'NtoskrnlOffsets.csv' in the current directory.
--fltmgr-offsets <FltmgrOffsets.csv> Path to the CSV file containing the required fltmgr.sys's offsets
Default to 'FltmgrOffsets.csv' in the current directory.
--wdigest-offsets <WdigestOffsets.csv> Path to the CSV file containing the required wdigest.dll's offsets
(only for the 'credguard' mode).
Default to 'WdigestOffsets.csv' in the current directory.
--ci-offsets <CiOffsets.csv> Path to the CSV file containing the required ci.dll's offsets
(only for the 'load_unsigned_driver' mode).
Default to 'WdigestOffsets.csv' in the current directory.
-i | --internet Enables automatic symbols download from Microsoft Symbol Server
If a corresponding *Offsets.csv file exists, appends the downloaded offsets to the file for later use
OpSec warning: downloads and drops on disk a PDB file for the corresponding image
Dump options:
-o | --dump-output <DUMP_FILE> Output path to the dump file that will be generated by the 'dump' mode.
Default to 'process_name' in the current directory.
--process-name <NAME> File name of the process to dump (defaults to 'lsass.exe')
EDRSandBlast(仅 x64)在 Visual Studio 2019 上构建(Windows SDK
版本:10.0.19041.0,平台工具集:Visual Studio 2019 (v142))。
注意,ExtractOffsets.py 仅在 Windows 上测试过。```
pip.exe install -m .\requirements.txt
ExtractOffsets.py [-h] -i INPUT [-o OUTPUT] [-d] mode
positional arguments: mode ntoskrnl or wdigest. Mode to download and extract offsets for either ntoskrnl or wdigest
optional arguments: -h, --help show this help message and exit -i INPUT, --input INPUT Single file or directory containing ntoskrnl.exe / wdigest.dll to extract offsets from. If in download mode, the PE downloaded from MS symbols servers will be placed in this folder. -o OUTPUT, --output OUTPUT CSV file to write offsets to. If the specified file already exists, only new ntoskrnl versions will be downloaded / analyzed. Defaults to NtoskrnlOffsets.csv / WdigestOffsets.csv in the current folder. -d, --download Flag to download the PE from Microsoft servers using list of versions from winbindex.m417z.com.
## 检测
从防御方(EDR供应商、Microsoft、SOC分析师查看EDR遥测数据等)的角度来看,可以使用多种指标来检测或防止这类技术。
### 驱动程序白名单
由于该工具在内核模式内存中执行的每项操作都依赖于一个存在漏洞的驱动程序来读写任意内容,因此EDR产品(或SOC分析师)应严格审查驱动程序加载事件,并对任何不常见的驱动程序加载发出警报,甚至直接阻止已知的易受攻击驱动程序。后一种做法甚至受到[微软自身的推荐](https://docs.microsoft.com/en-us/windows/security/threat-protection/windows-defender-application-control/microsoft-recommended-driver-block-rules):任何启用了HVCI(*基于虚拟机监控程序的代码完整性*)的Windows设备都内嵌了驱动程序黑名单,这将成为Windows的默认行为(在Windows 11上已是如此)。
### 内核内存完整性检查
即使攻击者仍可能使用未知的易受攻击驱动程序在内存中执行相同操作,EDR驱动程序也可以定期检查其内核回调是否仍然注册——可以直接通过检查内核内存(就像本工具所做的那样),或者简单地通过触发事件(进程创建、线程创建、镜像加载等)并检查这些回调函数是否确实被执行内核调用。
顺便提一下,这类数据结构可以通过最近的[内核数据保护(KDP)](https://www.microsoft.com/security/blog/2020/07/08/introducing-kernel-data-protection-a-new-platform-security-technology-for-preventing-data-corruption/)机制来保护,该机制基于虚拟化安全,使得内核回调数组在不调用正确API的情况下不可写。
同样的逻辑也适用于敏感的ETW变量,例如本工具滥用的 `ProviderEnableInfo`,用于禁用ETW威胁智能事件生成。
### 用户态检测
进程试图主动规避用户态挂钩的第一个迹象是对应已加载模块的每个DLL的文件访问;在正常执行中,用户态进程很少需要在 `LoadLibrary` 调用之外读取DLL文件,尤其是 `ntdll.dll`。
为了保护API挂钩不被绕过,EDR产品可以定期检查每个被监控进程的内存中挂钩是否未被篡改。
最后,为了检测不涉及删除挂钩的挂钩绕过(滥用跳板、直接系统调用等),EDR产品可以依赖于与被滥用系统调用关联的内核回调(例如 `NtCreateProcess` 系统调用的 `PsCreateProcessNotifyRoutine`,`NtOpenProcess` 系统调用的 `ObRegisterCallbacks` 等),并执行用户态调用栈分析,以确定系统调用是从正常路径(`kernel32.dll` -> `ntdll.dll` -> 系统调用)触发,还是从异常路径(例如 `program.exe` -> 直接系统调用)触发。
## 致谢
- 内核回调枚举与移除:
https://github.com/br-sn/CheekyBlinder
- 通过易受攻击的`Micro-Star MSI Afterburner`驱动程序实现内核内存读/写原语:
https://github.com/Barakat/CVE-2019-16098/
- 禁用ETW威胁智能提供程序:
https://public.cnotools.studio/bring-your-own-vulnerable-kernel-driver-byovkd/exploits/data-only-attack-neutralizing-etwti-provider
- 驱动程序安装/卸载:https://github.com/gentilkiwi/mimikatz
- EDR驱动程序名称的初始列表:
https://github.com/SadProcessor/SomeStuff/blob/master/Invoke-EDRCheck.ps1
- 通过LSASS内存修补重新启用Wdigest绕过Credential Guard:
https://teamhydra.blog/2020/08/25/bypassing-credential-guard/
## 作者
[Thomas DIOT (Qazeer)](https://github.com/Qazeer/)
[Maxime MEIGNAN (themaks)](https://github.com/themaks)
## 感谢贡献者
- [v1k1ngfr](https://github.com/v1k1ngfr):用于驱动程序签名强制绕过(通过 `g_CiOptions` 修补)和GDRV.sys驱动程序支持
- [Windy Bug](https://github.com/0mWindyBug):用于兼容KDP的驱动程序签名强制绕过(通过回调交换)以及对微过滤绕过功能的重大贡献
## 许可证
CC BY 4.0 许可证 - https://creativecommons.org/licenses/by/4.0/
| 支持的驱动 | 下载链接 | SHA256 |
|---|
GDRV.sys | LOLDrivers 链接 | 31f4cfb4c71da44120752721103a16512444c13c2ac2d857a7e6f13cb679b427 |
RTCore64.sys | LOLDrivers 链接 | 01aa278b07b58dc46c84bd0b1b5c8e9ee4e62ea0bf7a695862444af32e87f1fd |
DBUtil_2_3.sys | LOLDrivers 链接 | 0296e2ce999e67c76352613a718e11516fe1b0efc3ffdb8918fc999dd76a73a5 |