原始项目地址 https://github.com/wavestone-cdt/EDRSandblast
EDRSandBlast
EDRSandBlast
是一个用C
编写的工具,利用一个存在漏洞的签名驱动程序进行武器化,以绕过EDR(威胁检测和响应)的检测(通知例程回调、对象回调和ETW TI
提供程序)以及LSASS
的保护机制。还实现了多种用户空间解钩技术,以逃避用户空间监控。
在这个版本中,使用了用户空间(--usermode
)和内核空间(--kernelmode
)技术来在EDR的监视下转储LSASS
内存,而不被阻止,也不会在产品(云)控制台中生成与"OS凭据转储"相关的事件。这些测试在3款不同的EDR产品上进行,并且在每种情况下都成功。
详细介绍
通过移除内核通知例程绕过EDR
EDR产品在Windows上使用内核的“通知例程”回调来通知内核系统活动,例如进程和线程的创建以及图像的加载(exe / DLL)。
这些内核回调通常是在内核层定义的,通常是从实现回调的驱动程序中,使用一些文档化的API(如nt!PsSetCreateProcessNotifyRoutine
,nt!PsSetCreateThreadNotifyRoutine
等)。
这些API将驱动程序提供的回调例程添加到内核空间中未记录的例程数组中:
PspCreateProcessNotifyRoutine
用于进程创建
PspCreateThreadNotifyRoutine
用于线程创建
PspLoadImageNotifyRoutine
用于图像加载
EDRSandBlast
枚举了这些数组中定义的例程,并移除与预定义的EDR驱动程序列表(支持1000多个安全产品的驱动程序,请参见EDR驱动程序和进程检测)相关联的任何回调例程。
通过利用受漏洞驱动程序的利用提供的任意内核内存读写原语,可以实现枚举和移除(请参见受漏洞驱动程序检测)。
所述数组的偏移量是使用多种技术恢复的,请参阅偏移量部分。
通过删除对象回调绕过EDR
EDR(甚至EPP)产品通常通过使用nt!ObRegisterCallbacks
内核API注册"对象回调"。这些回调允许安全产品在特定对象类型(进程、线程和桌面相关的对象回调现在由Windows支持)的每个句柄生成时得到通知。句柄生成可能发生在对象打开时(调用OpenProcess
、OpenThread
等)以及句柄复制时(调用DuplicateHandle
等)。
通过在每个操作上得到内核的通知,安全产品可以分析句柄创建的合法性(例如,未知进程尝试打开LSASS),并在检测到威胁时阻止操作。
在使用ObRegisterCallbacks
进行每个回调注册时,将在描述受回调影响的对象类型(进程、线程或桌面)的_OBJECT_TYPE
对象中的CallbackList
双链表中添加一个新条目。不幸的是,这些条目由一种未经Microsoft文档化或在符号文件中发布的结构描述。然而,从各种ntoskrnl.exe
版本研究来看,这个结构在至少Windows 10版本10240和22000之间(从2015年到2022年)没有发生改变。
所提及的表示对象回调注册的结构如下:
typedef struct OB_CALLBACK_ENTRY_t {
LIST_ENTRY CallbackList; // 与_OBJECT_TYPE.CallbackList相关联的链接元素
OB_OPERATION Operations; // 位字段:1表示创建,2表示复制
BOOL Enabled; // 自我解释
OB_CALLBACK* Entry; // 指向包含它的结构
POBJECT_TYPE ObjectType; // 指向受回调影响的对象类型
POB_PRE_OPERATION_CALLBACK PreOperation; // 在每个句柄操作之前调用的回调函数
POB_POST_OPERATION_CALLBACK PostOperation; // 在每个句柄操作之后调用的回调函数
KSPIN_LOCK Lock; // 用于同步的锁对象
} OB_CALLBACK_ENTRY;
上述提及的OB_CALLBACK
结构也是未经文档化的,并被定义如下:
typedef struct OB_CALLBACK_t {
USHORT Version; // 通常为0x100
USHORT OperationRegistrationCount; // 注册的回调数量
PVOID RegistrationContext; // 注册时传递的任意数据
UNICODE_STRING AltitudeString; // 用于确定回调顺序
struct OB_CALLBACK_ENTRY_t EntryItems[1]; // 长度为OperationRegistrationCount的项目数组
WCHAR AltitudeBuffer[1]; // AltitudeString.Buffer所指向的字节长度等于AltitudeString.MaximumLength
} 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
对象如下所示:
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
写入,因此需要两个不同的IOCTL来覆盖指针之间,而在其中内核很有可能使用它(导致崩溃)。另一方面,易受攻击的DELL驱动程序DBUtil_2_3.sys
可以一次执行任意大小的写入,因此使用该方法不会导致崩溃的风险。
完全禁用对象回调
我们发现的最后一种技术是完全禁用线程和进程的对象回调支持。在对应于进程和线程类型的_OBJECT_TYPE
结构中,有一个TypeInfo
字段,跟随着文档化的_OBJECT_TYPE_INITIALIZER
结构。后者包含一个ObjectTypeFlags
位字段,其中的SupportsObjectCallbacks
标志决定了描述的对象类型(进程、线程、桌面、令牌、文件等)是否支持对象回调注册。正如之前所述,在撰写本文时,Windows安装只支持进程、线程和桌面对象类型上的这些回调。
由于内核在读取CallbackList
(在执行回调之前)之前,会先检查SupportsObjectCallbacks
位,在内核运行时改变该位,从而有效地禁用了所有对象回调的执行。
该方法的主要缺点是,KPP("PatchGuard")监视着一些(全部?)_OBJECT_TYPE
结构的完整性,一旦一个对象类型结构被修改,就会触发一个0x109 Bug Check
,其中参数4等于0x8
,意味着某个对象类型结构已被篡改。
然而,快速执行禁用/重新启用操作(以及其中的 "恶意 "操作)应该足够 "赛过 " PatchGuard(除非你不幸地在错误的时机进行了周期性检查)。
通过关闭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
运行的服务或进程消耗ETW TI
提供程序生成的日志,并与Early Launch Anti Malware (ELAM)
驱动程序相关联。
如slaeryan (CNO Development Labs)
在博客文章中发布的信息,可以通过在内核内存中对ProviderEnableInfo
属性进行修补将ETW TI
提供程序完全禁用。有关该技术的更多信息,请参阅上述博客文章。
与内核回调的删除类似,必要的ntoskrnl.exe
偏移量(nt!EtwThreatIntProvRegHandleOffset
,_ETW_REG_ENTRY
的GuidEntry
和_ETW_GUID_ENTRY
的ProviderEnableInfo
)在NtoskrnlOffsets.csv
文件中针对一些Windows内核版本进行了计算。
通过用户态Hook绕过EDR
用户态Hook的工作原理
为了方便监控进程执行的操作,EDR产品通常会使用一种称为用户态Hook的机制。首先,EDR产品会注册一个内核回调(通常是映像加载或进程创建回调,详见上文),以便在每个进程启动时被通知。
当Windows加载进程,并在实际启动之前,EDR能够将一些自定义的DLL注入到进程的地址空间中,该DLL包含监控逻辑。在加载过程中,该DLL在每个需要被EDR监视的函数开头注入hook。在运行时,当受监视的函数被监控进程调用时,这些hook将控制流重定向到EDR的DLL中的一些监控代码,使其能够检查这些调用的参数和返回值。
大部分情况下,受监视的函数是系统调用(例如NtReadVirtualMemory
,NtOpenProcess
等),其实现位于ntdll.dll
中。拦截对Nt*
函数的调用可以使产品尽可能接近用户态/内核态边界(同时保持在用户态),但一些更高级的DLL函数也可能被监视。
下面是一个被EDR产品Hook之前和之后的相同函数的示例:
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
NtProtectVirtualMemory proc near
jmp sub_7FFC74490298 ; --> "hook",跳转到EDR分析函数
int 3 ; 被覆盖的指令
int 3 ; 被覆盖的指令
int 3 ; 被覆盖的指令
test byte_7FFE0308, 1 ; <-- 继续执行的位置,用于分析之后
jnz short loc_7FFCB44AD1E5
syscall
retn
loc_7FFCB44AD1E5:
int 2Eh
retn
NtProtectVirtualMemory endp
钩子检测
用户空间钩子的"弱点"在于它们位于用户空间内存中,这意味着进程可以直接观察和修改它们。为了自动检测进程地址空间中的钩子,主要思路是比较磁盘上的原始DLL和存储在内存中的库之间的差异,后者可能已经被EDR(终端检测与响应)修改过。EDRSandblast执行以下步骤来进行比较:
- 枚举所有加载的DLL列表,通过
PEB
中的InLoadOrderModuleList
来实现(以避免调用可能会被监控和视为可疑的API)。
- 对于每个已加载的DLL,读取其磁盘上的内容并解析其头部。同时解析存储在内存中的相应库,以识别节、导出等信息。
- 解析并应用DLL的重定位,考虑到相应加载库的基地址。这样,内存中的库和来自磁盘的DLL的内容在重定位已应用的节上完全相同,从而使比较结果可靠。
- 枚举导出函数,并比较“内存中”和“磁盘上”版本的前几个字节。任何差异都表明DLL加载后进行了更改,因此很可能是EDR钩子。
注意:该过程可以泛化为在非可写节的任何位置找到差异,而不仅仅是在导出函数的开头,例如,如果EDR产品开始在函数中部应用钩子 :) 尽管该工具没有使用,但已在findDiffsInNonWritableSections
中实现了这一点。
为了躲避这些钩子的监控,有多种技术可供选择,每种技术都有优缺点。
使用...技术绕过Hook
绕过基于Hook的监控最直观的方法就是移除Hook。由于Hook存在于进程本身可以访问的内存中,因此要移除一个Hook,进程可以简单地执行以下步骤:
- 更改Hook所在页面的权限(RX -> RWX或RW)
- 写入已知的原始字节,这得益于磁盘上的DLL内容
- 将权限改回为RX
这种方法相当简单,可以一次性移除所有检测到的Hook。在攻击工具的开始阶段执行此操作,可以使其余的代码完全不知道Hook机制而正常执行,而无法被监视。
然而,它有两个主要缺点。EDR可能正在监视NtProtectVirtualMemory
的使用情况,因此在使用它来更改安装Hook的页面的权限时(至少在概念上)是一个坏主意。此外,如果EDR执行了一个线程,并定期检查Hook的完整性,这也可能触发某些检测。
有关实现细节,请查看unhook()
函数的代码路径,当unhook_method
为UNHOOK_WITH_NTPROTECTVIRTUALMEMORY
时。
重要说明:为了简单起见,EDRSandblast实现了这种技术作为展示其他绕过技术的基本技术;每种技术演示如何获得未监视版本的NtProtectVirtualMemory
,但之后执行相同的操作(解除特定Hook)。
使用自定义跳板绕过Hook
要绕过特定的Hook,可以简单地“跳过”并执行剩余的函数。首先,需要从DLL文件中恢复被EDR覆盖以安装Hook的受监视函数的原始字节。在我们之前的代码示例中,这将是与以下指令对应的字节:
mov r10, rcx
mov eax, 50h
识别这些字节是一个简单的任务,因为我们可以对内存和磁盘版本的库进行干净的diff,如前面所述。然后,我们组装一个跳转指令,该指令用于将控制流重新定向到紧接在Hook后面的代码,即地址NtProtectVirtualMemory + sizeof(overwritten_instructions)
jmp NtProtectVirtualMemory+8
最后,我们将这些操作码连接起来,存储在(新)可执行内存中,并保留对它们的指针。这个对象被称为 “跳板”,可以被用作函数指针,与原始的 NtProtectVirtualMemory
函数完全相同。
与下面的每种技术一样,这种技术的主要优点是Hook永远不会被删除,因此EDR对Hook执行的任何完整性检查都应该通过。然而,它需要分配可写和可执行的内存,这是典型的shellcode分配,因此会引起EDR的注意。
有关实现细节,请查看 unhook()
函数在 unhook_method
为 UNHOOK_WITH_INHOUSE_NTPROTECTVIRTUALMEMORY_TRAMPOLINE
时的代码路径。请记住,该技术仅在我们的实现中展示,并最终用于从内存中移除Hook,就像下面的每种技术一样。
使用自己的EDR的弹射来绕过Hook
为了使EDR产品的Hook起作用,它必须将已删除的操作码保存在内存的某个位置。最糟糕的是(或者从攻击者的角度来看,这更好),为了有效地使用原始指令,EDR可能在拦截调用之后分配了一个弹射(trampoline)来执行原始函数。
可以在内存中搜索并使用这个弹射来替换被Hook的函数,而无需分配可执行内存,也不需要调用除了VirtualQuery
之外的任何API,因为VirtualQuery
很可能不会被监视,因为它是一个无害的函数。
为了在内存中找到弹射,我们使用VirtualQuery
遍历整个地址空间,寻找已经提交和可执行的内存。对于每个这样的内存区域,我们扫描它以查找跳转指令,该指令跳转到被覆盖指令后面的地址(在之前的示例中是NtProtectVirtualMemory+8
)。然后可以使用这个弹射来调用被Hook的函数,而不触发Hook。
这种技术表现出色,在测试过的EDR中几乎恢复了所有的弹射。有关实现细节,请查看unhook()
函数在unhook_method
为UNHOOK_WITH_EDR_NTPROTECTVIRTUALMEMORY_TRAMPOLINE
时的代码路径。
通过复制DLL来绕过Hook
另一种获取对未监视版本的NtProtectVirtualMemory
函数的访问权限的简单方法是将ntdll.dll
库的副本加载到进程地址空间中。由于可以在同一进程中加载两个相同的DLL,只要它们具有不同的名称,我们可以简单地将合法的ntdll.dll
文件复制到另一个位置,使用LoadLibrary
加载它(或重新实现加载过程),然后使用GetProcAddress
等方式访问该函数。
这种技术非常简单易懂和实现,并且具有相当大的成功机会,因为大多数EDR产品一旦进程运行起来后就不会重新安装在新加载的DLL上的钩子。然而,主要的缺点是将Microsoft签名的二进制文件复制到不同的名称下通常被EDR产品视为可疑行为。
尽管如此,EDRSandblast
仍然实现了这种技术。有关实现细节,请查看unhook()
函数的代码路径,当unhook_method
为UNHOOK_WITH_DUPLICATE_NTPROTECTVIRTUALMEMORY
时。
使用直接系统调用绕过Hook
为了使用与系统调用相关的函数,一个程序可以重新实现系统调用(用汇编语言),以便在不实际触及ntdll.dll
中的代码的情况下调用相应的操作系统功能,从而绕过EDR对系统调用函数的用户态Hook。
然而,这种方法有一些缺点。首先,这意味着需要知道程序所需的函数的系统调用号列表,而不同版本的Windows中这些号码是会变化的。然而,通过实现多个已知在Windows NT的所有过去版本中都起效的启发式方法(如对ntdll
的Zw*
导出进行排序,搜索相关ntdll
函数中的mov rax,#syscall_number
指令等),可以缓解这个问题,并检查它们是否都返回相同的结果(有关更多详细信息,请参见Syscalls.c
)。
此外,那些在技术上不是系统调用的函数(例如LoadLibraryX
/LdrLoadDLL
)也可能受到监控,而不能简单地使用系统调用重新实现。
直接系统调用技术在EDRSandblast中实现。如前所述,它仅用于安全地执行NtProtectVirtualMemory
函数,并移除所有检测到的Hook。
有关实现详细信息,请查看unhook()
函数的代码路径,当unhook_method
为UNHOOK_WITH_DIRECT_SYSCALL
时。
可利用的驱动程序漏洞利用
正如之前所述,需要进行内核内存读写的每个操作都依赖于一个可利用的驱动程序来提供这个基本功能。在EDRSanblast中,添加对一个新驱动程序的支持以提供读写基本功能可以相对"容易"地完成,只需要实现三个函数:
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
中的以下代码:
#define RTCore 0
#define DBUtil 1
// 选择要使用的驱动程序,使用以下 #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驱动程序和进程检测
目前有多种技术用于确定特定驱动程序或进程是否属于EDR产品。
首先,可以使用驱动程序的名称来判断。事实上,Microsoft为所有需要在内核中插入回调的驱动程序分配了称为“Altitudes”的特定数字。这样可以确保回调的执行顺序是确定的,独立于注册顺序,只基于驱动程序的使用情况。在MSDN上可以找到一份(供应商的)驱动程序的特定altitude列表。因此,Microsoft提供了一个几乎全面的与安全产品相关的驱动程序名称列表,主要在 “FSFilter Anti-Virus”
和“FSFilter Activity Monitor”
列表中。这些驱动程序名称列表包含在EDRSandblast中,以及其他贡献者的贡献。
此外,EDR可执行文件和DLL往往都使用供应商签名证书进行数字签名。因此,检查与进程关联的可执行文件或DLL的签名者可能会快速识别出EDR产品。
此外,驱动程序需要直接由Microsoft签名才能加载到内核空间。虽然驱动程序的供应商并不直接是驱动程序本身的签名者,但似乎供应商名称仍然包含在签名的属性中;这种检测技术尚待进一步调查和实施。
最后,当面对一个EDRSandblast中未知的EDR产品时,最好的方法是在“审核”模式下运行该工具,并检查已注册内核回调的驱动程序列表;然后可以将驱动程序名称添加到列表中,重新编译并重新运行该工具。
RunAsPPL 绕过
本地安全领域 (Local Security Authority - LSA) 保护 机制首次引入于Windows 8.1和Windows Server 2012 R2,并利用 受保护的进程轻量级 (Protected Process Light - PPL) 技术限制对 LSASS
进程的访问。 PPL
保护规定和限制操作,如内存注入或对受保护进程的内存转储,即使在持有 SeDebugPrivilege
特权的进程中也是如此。在进程保护模型下,只有以较高保护级别运行的进程才能对受保护进程执行操作。
Windows内核 使用的 _EPROCESS
结构在内核内存中表示进程,并通过其 Type
("_PS_PROTECTED_TYPE")
和 Signer
("_PS_PROTECTED_SIGNER")
属性定义进程的保护级别。
通过写入内核内存,EDRSandblast进程能够将自身的保护级别升级到 PsProtectedSignerWinTcb-Light
。这个级别足以转储 LSASS
进程的内存,因为它相对于 RunAsPPL
机制下运行的 LSASS
进程的保护级别 PsProtectedSignerLsa-Light
是 dominates
的。
EDRSandBlast 实现了自我保护的过程如下:
- 打开一个对当前进程的句柄
- 使用
NtQuerySystemInformation
泄露所有系统句柄,以找到在当前进程上打开的句柄和当前进程的 EPROCESS
结构在内核内存中的地址。
- 使用
Micro-Star MSI Afterburner
驱动的任意读/写漏洞来覆写内核内存中当前进程的 _PS_PROTECTION
字段。相对于 EPROCESS
结构的 _PS_PROTECTION
字段的偏移量(由使用的 ntoskrnl
版本定义)在 NtoskrnlOffsets.csv
文件中计算。
Credential Guard 绕过
Microsoft的Credential Guard
是一种基于虚拟化的隔离技术,最初引入于微软的Windows 10(企业版)
中,它防止直接访问存储在LSASS
进程中的凭据。
当激活Credentials Guard
时,会创建一个LSAIso
(LSA隔离)进程,该进程位于Virtual Secure Mode
中,该模式利用CPU的虚拟化扩展,提供了内存数据的额外安全性。即使以NT AUTHORITY\SYSTEM
安全上下文的访问,也无法访问LSAIso
进程。在处理哈希时,LSA
进程会向LSAIso
进程发出RPC
调用,并等待LSAIso
的结果后继续执行。因此,LSASS
进程不会包含任何密码,而是存储了LSA隔离数据
。
根据N4kedTurtle
的原始研究,“通过在内存中修补g_fParameter_useLogonCredential
和g_IsCredGuardEnabled
的值,可以在启用Credential Guard的系统上启用Wdigest
”。激活Wdigest
会导致明文凭据存储在LSASS
内存中,用于任何新的交互式登录(无需重新启动系统)。请参阅原始研究博文了解更多关于此技术的详细信息。
EDRSandBlast
只是使原始的PoC更加隐匿,并支持多个wdigest.dll
版本(通过计算g_fParameter_useLogonCredential
和g_IsCredGuardEnabled
的偏移量)。
获取偏移量
为了能够可靠地执行内核监控绕过操作,EDRSandblast需要准确知道在哪里读写内核内存。这是通过获取目标映像(ntoskrnl.exe、wdigest.dll)中全局变量的偏移量,以及在Microsoft的符号文件中公开的结构体中特定字段的偏移量来实现的。这些偏移量对于每个目标映像的构建是特定的,必须至少针对特定平台版本进行一次收集。
选择使用“硬编码”的偏移量来定位EDRSandblast使用的结构体和变量,而不是进行模式搜索,是有道理的。这是由于对于负责添加/删除内核回调的未记录API可能会发生变化,而且任何试图在错误的地址读写内核内存的尝试都可能(通常会)导致 "Bug Check"(蓝屏死机)。在红队测试和正常渗透测试场景中,机器崩溃是不可接受的,因为崩溃的机器非常容易被防御者察觉,并且将丢失攻击时仍在内存中的任何凭证。
为了获取每个特定版本Windows的偏移量,实施了两种方法。
手动偏移检索
可以使用提供的 ExtractOffsets.py
Python脚本提取所需的ntoskrnl.exe
和 wdigest.dll
偏移量。该脚本依赖于 radare2
和 r2pipe
来下载和解析PDB文件中的符号,并从中提取所需的偏移量。偏移量然后被存储在CSV文件中,以供EDRSandblast后续使用。
为了支持各种各样的Windows版本,许多版本的 ntoskrnl.exe
和 wdigest.dll
二进制文件被 Winbindex 引用,并且可以通过 ExtractOffsets.py
自动下载(并提取其偏移量)。这样可以从几乎所有曾经发布在Windows更新包中的文件中提取偏移量(目前提供了450+ ntoskrnl.exe
和30+ wdigest.dll
版本的预计算值)。
自动获取和更新偏移量
在 EDRSandBlast
中实现了一个额外的选项,使程序能够从Microsoft符号服务器自动下载所需的.pdb
文件,提取所需的偏移量,甚至在存在的情况下更新对应的.csv
文件。
使用 --internet
选项可以使工具的执行变得更简单,但引入了额外的OpSec风险,因为在此过程中会下载并将一个 .pdb
文件放在磁盘上。这是由于用于解析符号数据库的 dbghelp.dll
函数所需;但是,将来可能会实现完整的内存中PDB解析,以消除对此要求并减少工具的占用空间。
用法
可以在以下位置获取有漏洞的 RTCore64.sys
驱动程序:
http://download-eu2.guru3d.com/afterburner/%5BGuru3D.com%5D-MSIAfterburnerSetup462Beta2.zip
快速用法
用法:EDRSandblast.exe [-h | --help] [-v | --verbose] <audit | dump | cmd | credguard> [--usermode [--unhook-method <N>]] [--kernelmode] [--dont-unload-driver] [--dont-restore-callbacks] [--driver <RTCore64.sys>] [--service <SERVICE_NAME>] [--nt-offsets <NtoskrnlOffsets.csv>] [--wdigest-offsets <WdigestOffsets.csv>] [--add-dll <dll name or path>]* [-o | --dump-output <DUMP_FILE>]
选项
-h | --help 显示帮助消息并退出。
-v | --verbose 启用更详细的输出。
操作模式:
audit 显示用户态挂钩和/或内核回调,而不采取措施。
dump 转储LSASS进程,默认为当前目录中的 'lsass' 或在指定文件中使用 -o | --output <DUMP_FILE>。
cmd 打开一个 cmd.exe 提示符。
credguard 对LSASS进程的内存进行修补,以在主机上启用 Wdigest 明文密码缓存,即使启用了凭据保护。无需内核态操作。
--usermode 执行用户态操作(DLL 解钩)。
--kernelmode 执行内核态操作(内核回调删除和 ETW TI 禁用)。
--unhook-method <N>
选择用户态解挂钩技术,如下所示:
1(默认值) 使用(可能受监视)ntdll中的NtProtectVirtualMemory函数来删除所有已存在的用户态挂钩。
2 构建一个“解挂钩”(即未被监视的)版本的NtProtectVirtualMemory,通过分配一个跳过挂钩的可执行跳板,并删除所有已存在的用户态挂钩。
3 搜索EDR自身分配的现有跳板,以获取“解挂钩”(即未被监视的)版本的NtProtectVirtualMemory,并删除所有已存在的用户态挂钩。
4 将nolibrary库的另一个版本加载到内存中,并使用该库中的(希望未被监视的)NtProtectVirtualMemory版本删除所有已存在的用户态挂钩。
5 分配一个使用直接系统调用调用NtProtectVirtualMemory的shellcode,并使用它来删除所有检测到的挂钩
其他选项:
--dont-unload-driver 保留主机上的有漏洞的驱动程序安装
默认情况下,自动卸载驱动程序。
--dont-restore-callbacks 不恢复已删除的EDR驱动程序内核回调。
默认情况下恢复回调。
--driver <RTCore64.sys> 有漏洞驱动文件的路径。
默认为当前目录中的 'RTCore64.sys'。
--service <SERVICE_NAME> 要安装/启动的有漏洞服务的名称。
--nt-offsets <NtoskrnlOffsets.csv> 包含所需的ntoskrnl.exe偏移量的CSV文件的路径。
默认为当前目录中的 'NtoskrnlOffsets.csv'。
--wdigest-offsets <WdigestOffsets.csv> 包含所需的wdigest.dll偏移量的CSV文件的路径(仅对于“credguard”模式)。
默认为当前目录中的 'WdigestOffsets.csv'。
--add-dll <dll name or path> 在启动之前将任意库加载到进程的地址空间中。这对于审计默认情况下此程序不加载的 DLL 的用户态勾取非常有用。多次使用此选项以一次性加载多个 DLL。
感兴趣的 DLL 示例:user32.dll,ole32.dll,crypt32.dll,samcli.dll,winhttp.dll,urlmon.dll,secur32.dll,shell32.dll...
-o | --output <DUMP_FILE> “dump”模式生成的转储文件的输出路径。
默认为当前目录中的 'lsass'。
-i | --internet 启用从Microsoft Symbol Server自动下载符号
如果存在相应的 *Offsets.csv 文件,则将下载的偏移量追加到文件中以供以后使用
OpSec警告:从磁盘下载并存储ntoskrnl.exe和/或wdigest.dll的PDB文件
构建
EDRSandBlast
(仅限x64)是在Visual Studio 2019(Windows SDK 版本:10.0.19041.0
和平台工具集:Visual Studio 2019 (v142)
)上构建的。
注意,ExtractOffsets.py
仅在Windows上进行了测试。
# 安装Python依赖
pip.exe install -m .\requirements.txt
# 脚本使用
ExtractOffsets.py [-h] -i INPUT [-o OUTPUT] [-d] mode
位置参数:
mode ntoskrnl或wdigest。用于下载和提取ntoskrnl或wdigest的偏移量的模式
可选参数:
-h, --help 显示帮助信息并退出
-i INPUT, --input INPUT
单个文件或包含ntoskrnl.exe / wdigest.dll的目录以从中提取偏移量。如果处于下载模式,从MS符号服务器下载的PE将放置在此文件夹中。
-o OUTPUT, --output OUTPUT
用于写入偏移量的CSV文件。如果指定的文件已存在,将仅下载/分析新的ntoskrnl版本。
默认为当前文件夹中的NtoskrnlOffsets.csv / WdigestOffsets.csv。
-d, --download 使用winbindex.m417z.com的版本列表从Microsoft服务器下载PE的标志。
检测
从防御方(EDR供应商、微软、SOC分析师等)的角度来看,可以使用多个指标来检测或预防这种技术。
驱动程序白名单
由于工具在内核模式内存中执行的每个操作都依赖于一个有漏洞的驱动程序来读取/写入任意内容,EDR产品(或SOC分析师)应该严格审查驱动程序加载事件,并在任何不常见的驱动程序加载时发出警报,甚至阻止已知的有漏洞的驱动程序。这后一种方法甚至被微软本人推荐:启用HVCI(Hypervisor-protected code integrity,Hypervisor保护代码完整性)的Windows设备嵌入了一个驱动程序黑名单,并且这将逐渐成为Windows的默认行为(在Windows 11上已经是如此)。
内核内存完整性检查
由于攻击者仍然可以使用未知的易受攻击的驱动程序在内存中执行相同的操作,EDR驱动程序可以定期检查其内核回调是否仍然注册,直接通过检查内核内存(就像这个工具做的那样),或者通过触发事件(进程创建,线程创建,图像加载等)并检查执行内核是否确实调用了回调函数。
附带一提,可以通过最新的Kernel Data Protection (KDP)机制来保护这种类型的数据结构,该机制基于虚拟基于安全性,以便在没有调用正确的API的情况下使内核回调数组为不可写。
相同的逻辑也可以应用于敏感的ETW变量,例如ProviderEnableInfo
,此工具滥用该变量以禁用ETW Threat Intelligence事件生成。
用户模式检测
确定进程是否正在积极尝试规避用户空间的hook的第一个指标是对每个已加载模块对应的DLL文件进行文件访问;在正常执行中,用户模式进程很少需要在除LoadLibrary
调用之外读取DLL文件,尤其是ntdll.dll
。
为了保护API hooking不被绕过,EDR产品可以定期检查钩子在内存中是否被修改,其中包括每个受监视的进程。
最后,为了检测hook绕过(滥用跳板,使用直接的syscall等)而不需要将hook移除,EDR产品可以潜在地依赖于与被滥用syscalls关联的内核回调(例如,对于NtCreateProcess
syscall,可以使用PsCreateProcessNotifyRoutine
,对于NtOpenProcess
syscall,可以使用ObRegisterCallbacks
等),并执行用户模式调用堆栈分析,以确定syscall是否是从正常路径(kernel32.dll
-> ntdll.dll
-> syscall)还是异常路径(例如program.exe
直接syscall)触发的。
致谢
作者
Thomas DIOT (Qazeer)
Maxime MEIGNAN (themaks)
许可证
CC BY 4.0 许可证 - https://creativecommons.org/licenses/by/4.0/