wavestone-cdt/EDRSandblast

GitHub: wavestone-cdt/EDRSandblast

利用漏洞驱动实现内核读写原语,从内核与用户态双重层面绕过EDR监控并转储LSASS内存的Windows安全测试工具。

Stars: 1788 | Forks: 315

# EDRSandBlast `EDRSandBlast` 是一个用 `C` 语言编写的工具,它利用存在漏洞的签名驱动程序来绕过 EDR 检测(通知例程回调、对象回调和 `ETW TI` 提供程序)以及 `LSASS` 保护。该工具还实现了多种用户态脱钩技术,以规避用户态监控。 在发布时,结合用户态(`--usermode`)和内核态(`--kernelmode`)技术,已成功在 EDR 的监控下转储 `LSASS` 内存,且未被阻止,也未在产品(云端)控制台中生成“操作系统凭据转储”相关事件。测试在 3 款不同的 EDR 产品上进行,均获得了成功。 ## 描述 ### 通过移除内核通知例程绕过 EDR EDR 产品在 Windows 上使用内核“通知例程”回调,以便内核通知其系统活动,例如进程和线程的创建以及镜像(`exe` / `DLL`)的加载。 这些内核回调是在内核态定义的,通常由实现回调的驱动程序使用一系列文档化的 API(`nt!PsSetCreateProcessNotifyRoutine`、`nt!PsSetCreateThreadNotifyRoutine` 等)进行定义。这些 API 将驱动程序提供的回调例程添加到内核空间中未文档化的例程数组中: - `PspCreateProcessNotifyRoutine` 用于进程创建 - `PspCreateThreadNotifyRoutine` 用于线程创建 - `PspLoadImageNotifyRoutine` 用于镜像加载 `EDRSandBlast` 枚举这些数组中定义的例程,并移除任何与预定义 EDR 驱动程序列表(支持超过 1000 个安全产品驱动程序,参见 [EDR driver detection section](#edr-drivers-and-processes-detection))相关联的回调例程。 枚举和移除操作是通过利用漏洞驱动程序提供的任意内核内存读/写原语实现的(参见 [Vulnerable drivers section](#vulnerable-drivers-detection))。 上述数组的偏移量通过多种技术恢复,请参阅 [Offsets section](#ntoskrnl-and-wdigest-offsets)。 ### 通过移除对象回调绕过 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; // 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; The `OB_CALLBACK` structure mentionned above is also undocumented, and is defined by the following: ```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; In order to disable EDR-registered object callbacks, three techniques are implemented in `EDRSandblast`; however only one is enabled for the moment. #### 使用 `OB_CALLBACK_ENTRY` 的 `Enabled` 字段 This is the default technique enabled in `EDRSandblast`. In order to detect and disable EDR-related object callbacks, the `CallbackList` list located in the `_OBJECT_TYPE` objects tied to the *Process* and *Thread* types is browsed. Both `_OBJECT_TYPE`s are pointed by public global symbols in the kernel, `PsProcessType` and `PsThreadType`. Each item of the list is assumed to fit the `OB_CALLBACK_ENTRY` structure described above (assumption that seems to hold at least in all Windows 10 builds at the time of writing). Functions defined in `PreOperation` and `PostOperation` fields are located to checks if they belong to an EDR driver, and if so, callbacks are simply disabled toggling the `Enabled` flag. While being a pretty safe technique, it has the inconvenient of relying on an undocumented structure; to reduce the risk of unsafe manipulation of this structure, basic checks are performed to validate that some fields have the expected values : * `Enabled` is either `TRUE` or `FALSE` (*don't laugh, a `BOOL` is an `int`, so it could be anything other than `1` or `0`*); * `Operations` is `OB_OPERATION_HANDLE_CREATE`, `OB_OPERATION_HANDLE_DUPLICATE` or both; * `ObjectType` points on `PsProcessType` or `PsThreadType`. #### 解除线程和进程的 `CallbackList` 链接 Another strategy that do not rely on an undocumented structure (and is thus theoretically more robust against NT kernel changes) is the unlinking of the whole `CallbackList` for both processes and threads. The `_OBJECT_TYPE` object is the following: ```C struct _OBJECT_TYPE { LIST_ENTRY TypeList; UNICODE_STRING Name; [...] _OBJECT_TYPE_INITIALIZER TypeInfo; [...] LIST_ENTRY CallbackList; #### } Making the `Flink` and `Blink` pointers of the `CallbackList` `LIST_ENTRY` point to the `LIST_ENTRY` itself effectively make the list empty. Since the `_OBJECT_TYPE` structure is published in the kernel' symbols, the technique does not rely on hardcoded offsets/structures. However, it has some drawbacks. The first being not able to only disable callbacks from EDR; indeed, the technique affects all object callbacks that could have been registered by "legitimate" software. It should nevertheless be noted that object callbacks are not used by any pre-installed component on Windows 10 (at the time of writing) so disabling them should not affect the machine stability (even more so if the disabling is only temporary). The second drawback is that process or thread handle operation are really frequent (nearly continuous) in the normal functioning of the OS. As such, if the kernel write primitive used cannot perform a `QWORD` write "atomically", there is a good chance that the `_OBJECT_TYPE.CallbackList.Flink` pointer will be accessed by the kernel in the middle of its overwriting. For instance, the MSI vulnerable driver `RTCore64.sys` can only perform a `DWORD` write at a time, so 2 distinct IOCTLs will be needed to overwrite the pointer, between which the kernel has a high probability of using it (resulting in a crash). On the other hand, the vulnerable DELL driver `DBUtil_2_3.sys` can perform writes of arbitrary sizes in one IOCTL, so using this method with it does not risk causing a crash. #### 完全禁用对象回调 One last technique we found was to disable entirely the object callbacks support for thread and processes. Inside the `_OBJECT_TYPE` structure corresponding to the process and thread types resides a `TypeInfo` field, following the documented `_OBJECT_TYPE_INITIALIZER` structure. The latter contains a `ObjectTypeFlags` bit field, whose `SupportsObjectCallbacks` flag determines if the described object type (Process, Thread, Desktop, Token, File, etc.) supports object callback registering or not. As previously stated, only Process, Thread and Desktop object types supports these callbacks on a Windows installation at the time of writing. Since the `SupportsObjectCallbacks` bit is checked by `ObpCreateHandle` or `ObDuplicateObject` before even reading the `CallbackList` (and before executing callbacks, of course), flipping the bit at kernel runtime effectively disable all object callbacks execution. The main drawback of the method is simply that *KPP* ("*PatchGuard*") monitors the integrity of some (all ?) `_OBJECT_TYPE` structures, and triggers a [`0x109 Bug Check`](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/bug-check-0x109---critical-structure-corruption) with parameter 4 being equal to `0x8`, meaning an object type structure has been altered. However, performing the disabling / re-enabling (and "malicious" action in-between) quickly enough should be enough to "race" *PatchGuard* (unless you are unlucky and a periodic check is performed just at the wrong moment). ### 通过解链微过滤器回调绕过 EDR The Windows Filter Manager system allows an EDR to load a "minifilter" driver and register callbacks in order to be notified of I/O operations, such as file opening, reading, writing, etc. Here is a quick sum-up of different internal structures used by the filter manager: - The Filter Manager establishes a "frame" (`_FLTP_FRAME`) as its root structure; - A "volume" structure (`_FLT_VOLUME`) is instanciated for each "disk" managed by the Filter Manager (can be partitions, shadow copies, or special ones corresponding to named pipes or remote file systems); - To each registered minifilter driver corresponds a "filter" structure (`_FLT_FILTER`), describing various properties such as its supported operations; - These minifilters are not all attached to each volume; an "instance" (`_FLT_INSTANCE`) structure is created to mark each of the filter<->volume associations; - Minifilters register callback functions that are to be executed before and/or after specific operations (file open, write, read, etc.). These callbacks are described in `_CALLBACK_NODE` structures, and can be accessed by different ways: - An array of all `_CALLBACK_NODE`s implemented by an instance of a minifilter can be found in the `_FLT_INSTANCE` structure; the array is indexed by the IRP "major function" code, a constant representing the operations handled by the callbacks (`IRP_MJ_CREATE`, `IRP_MJ_READ`, etc.). - Also, all `_CALLBACK_NODE`s implemented by instances linked to a specific volume are regrouped in linked lists, stored in the `_FLT_VOLUME.Callbacks.OperationLists` array indexed by IRP major function codes. These different structures are browsed by `EDRSandblast` to detect filters that are associated with EDR-related drivers, and the callback nodes containing monitoring functions are enumerated. To disable their effect, the nodes are unlinked from their lists, making them temporarily invisible from the filter manager. This way, during a specified period, the EDR can be completely unaware of any file operations. A basic example would be the creation of an lsass memory dump file on disk, that would not trigger any analysis from the EDR, and thus no detection based on the file itself. ### 通过停用 ETW Microsoft-Windows-Threat-Intelligence 提供程序绕过 EDR The `ETW Microsoft-Windows-Threat-Intelligence` provider logs data about the usages of some Windows API commonly used maliciously. This include the `nt!MiReadWriteVirtualMemory` API, called by `nt!NtReadVirtualMemory` (which is used to dump `LSASS` memory) and monitored by the `nt!EtwTiLogReadWriteVm` function. EDR products can consume the logs produced by the `ETW TI` provider through services or processes running as, respectively, `SERVICE_LAUNCH_PROTECTED_ANTIMALWARE_LIGHT` or `PS_PROTECTED_ANTIMALWARE_LIGHT`, and associated with an `Early Launch Anti Malware (ELAM)` driver. As published by [`slaeryan` in a `CNO Development Labs` blog post](https://public.cnotools.studio/bring-your-own-vulnerable-kernel-driver-byovkd/exploits/data-only-attack-neutralizing-etwti-provider), the `ETW TI` provider can be disabled altogether by patching, in kernel memory, its `ProviderEnableInfo` attribute to `0x0`. Refer to the great aforementioned blog post for more information on the technique. Similarly to the Kernel callbacks removal, the necessary `ntoskrnl.exe` offsets (`nt!EtwThreatIntProvRegHandleOffset`, `_ETW_REG_ENTRY`'s `GuidEntry`, and `_ETW_GUID_ENTRY`'s `ProviderEnableInfo`) are computed in the `NtoskrnlOffsets.csv` file for a number of the Windows Kernel versions. ### 通过绕过用户态挂钩绕过 EDR #### 用户态挂钩的工作原理 In order to easily monitor actions that are performed by processes, EDR products often deploy a mechanism called *userland hooking*. First, EDR products register a kernel callback (usually *image loading* or *process creation* callbacks, see above) that allows them to be notified upon each process start. When a process is loaded by Windows, and before it actually starts, the EDR is able to inject some custom DLL into the process address space, which contains its monitoring logic. While loading, this DLL injects "*hooks*" at the start of every function that is to be monitored by the EDR. At runtime, when the monitored functions are called by the process under surveillance, these hooks redirect the control flow to some supervision code present in the EDR's DLL, which allows it to inspect arguments and return values of these calls. Most of the time, monitored functions are system calls (such as `NtReadVirtualMemory`, `NtOpenProcess`, etc.), whose implementations reside in `ntdll.dll`. Intercepting calls to `Nt*` functions allows products to be as close as possible to the userland / kernel-land boundary (while remaining in userland), but functions from some higher-level DLLs may also be monitored as well. Bellow are examples of the same function, before and after beeing hooked by the EDR product: ```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 #### 挂钩检测 Userland hooks have the "weakness" to be located in userland memory, which means they are directly observable and modifiable by the process under scrutiny. To automatically detect hooks in the process address space, the main idea is to compare the differences between the original DLL on disk and the library residing in memory, that has been potentially altered by an EDR. To perform this comparison, the following steps are followed by EDRSandblast: * The list of all loaded DLLs is enumerated thanks to the `InLoadOrderModuleList` located int the `PEB` (to avoid calling any API that could be monitored and suspicious) * For each loaded DLL, its content on disk is read and its headers parsed. The corresponding library, residing in memory, is also parsed to identify sections, exports, etc. * Relocations of the DLL are parsed and applied, by taking the base address of the corresponding loaded library into account. This allows the content of both the in-memory library and DLL originating from disk to have the exact same content (on sections where relocations are applied), and thus making the comparison reliable. * Exported functions are enumerated and the first bytes of the "in-memory" and "on-disk" versions are compared. Any difference indicates an alteration that has been made after the DLL was loaded, and thus is very probably an EDR hook. Note: The process can be generalized to find differences anywhere in non-writable sections and not only at the start of exported functions, for example if EDR products start to apply hooks in the middle of function :) Thus not used by the tool, this has been implemented in `findDiffsInNonWritableSections`. In order to bypass the monitoring performed by these hooks, multiples techniques are possible, and each has benefits and drawbacks. #### 使用...取消挂钩进行挂钩绕过 The most intuitive method to bypass the hook-based monitoring is to remove the hooks. Since the hooks are present in memory that is reachable by the process itself, to remove a hook, the process can simply: * Change the permissions on the page where the hook is located (RX -> RWX or RW) * Write the original bytes that are known thanks to the on-disk DLL content * Change back the permissions to RX This approach is fairly simple, and can be used to remove every detected hook all at once. Performed by an offensive tool at its beginning, this allows the rest of the code to be completely unaware of the hooking mechnanism and perform normally without being monitored. However, it has two main drawbacks. The EDR is probably monitoring the use of `NtProtectVirtualMemory`, so using it to change the permissions of the page where the hooks have been installed is (at least conceptually) a bad idea. Also, if a thread is executed by the EDR and periodically check the integrity of the hooks, this could also trigger some detection. For implementation details, check the `unhook()` function's code path when `unhook_method` is `UNHOOK_WITH_NTPROTECTVIRTUALMEMORY`. **Important note: for simplicity, this technique is implemented in EDRSandblast as the base technique used to *showcase* the other bypass techniques; each of them demonstrates how to obtain an unmonitored version of `NtProtectVirtualMemory`, but performs the same operation afterward (unhooking a specific hook).** #### 使用自定义跳板进行挂钩绕过 To bypass a specific hook, it is possible to simply "jump over" and execute the rest of the function as is. First, the original bytes of the monitored function, that have been overwritten by the EDR to install the hook, must be recovered from the DLL file. In our previous code example, this would be the bytes corresponding to the following instructions: ```assembly mov r10, rcx #### mov eax, 50h Identifying these bytes is a simple task since we are able to perform a clean *diff* of both the memory and disk versions of the library, as previously described. Then, we assemble a jump instruction that is built to redirect the control flow to the code following immediately the hook, at address `NtProtectVirtualMemory + sizeof(overwritten_instructions)` ```assembly #### jmp NtProtectVirtualMemory+8 Finally, we concatenate these opcodes, store them in (newly) executable memory and keep a pointer to them. This object is called a "*trampoline*" and can then be used as a function pointer, strictly equivalent to the original `NtProtectVirtualMemory` function. The main benefit of this technique as for every techniques bellow, is that the hook is never erased, so any integrity check performed on the hooks by the EDR should pass. However, it requires to allocate writable then executable memory, which is typical of a shellcode allocation, thus attracting the EDR's scrutiny. For implementation details, check the `unhook()` function's code path when `unhook_method` is `UNHOOK_WITH_INHOUSE_NTPROTECTVIRTUALMEMORY_TRAMPOLINE`. Please remember the technique is only showcased in our implementation and is, in the end, used to **remove** hooks from memory, as every technique bellow. #### 使用 EDR 自身的跳板进行挂钩绕过 The EDR product, in order for its hook to work, must save somewhere in memory the opcodes that it has removed. Worst (*or "better", from the attacker point of view*), to effectively use the original instructions the EDR has probably allocated itself a *trampoline* somewhere to execute the original function after having intercepted the call. This trampoline can be searched for and used as a replacement for the hooked function, without the need to allocate executable memory, or call any API except `VirtualQuery`, which is most likely not monitored being an innocuous function. To find the trampoline in memory, we browse the whole address space using `VirtualQuery` looking for commited and executable memory. For each such region of memory, we scan it to look for a jump instruction that targets the address following the overwritten instructions (`NtProtectVirtualMemory+8` in our previous example). The trampoline can then be used to call the hooked function without triggering the hook. This technique works surprisingly well as it recovers nearly all trampolines on tested EDR. For implementation details, check the `unhook()` function's code path when `unhook_method` is `UNHOOK_WITH_EDR_NTPROTECTVIRTUALMEMORY_TRAMPOLINE`. #### 使用重复 DLL 进行挂钩绕过 Another simple method to get access to an unmonitored version of `NtProtectVirtualMemory` function is to load a duplicate version of the `ntdll.dll` library into the process address space. Since two identical DLLs can be loaded in the same process, provided they have different names, we can simply copy the legitimate `ntdll.dll` file into another location, load it using `LoadLibrary` (or reimplement the loading process), and access the function using `GetProcAddress` for example. This technique is very simple to understand and implement, and have a decent chance of success, since most of EDR products does not re-install hooks on newly loaded DLLs once the process is running. However, the major drawback is that copying Microsoft signed binaries under a different name is often considered as suspicious by EDR products as itself. This technique is nevertheless implemented in `EDRSandblast`. For implementation details, check the `unhook()` function's code path when `unhook_method` is `UNHOOK_WITH_DUPLICATE_NTPROTECTVIRTUALMEMORY`. #### 使用直接系统调用进行挂钩绕过 In order to use system calls related functions, one program can reimplement syscalls (in assembly) in order to call the corresponding OS features without actually touching the code in `ntdll.dll`, which might be monitored by the EDR. This completely bypasses any userland hooking done on syscall functions in `ntdll.dll`. This nevertheless has some drawbacks. First, this implies being able to know the list of syscall numbers of functions the program needs, which changes for each version of Windows. This is nevertheless mitigated by implementing multiple heuristics that are known to work in all the past versions of Windows NT (sorting `ntdll`'s' `Zw*` exports, searching for `mov rax, #syscall_number` instruction in the associated `ntdll` function, etc.), and checking they all return the same result (see `Syscalls.c` for more details). Also, functions that are not technically syscalls (e.g. `LoadLibraryX`/`LdrLoadDLL`) could be monitored as well, and cannot simply be reimplemented using a syscall. The direct syscalls technique is implemented in EDRSandblast. As previously stated, it is only used to execute `NtProtectVirtualMemory` safely, and remove all detected hooks. For implementation details, check the `unhook()` function's code path when `unhook_method` is `UNHOOK_WITH_DIRECT_SYSCALL`. ### 易受攻击驱动程序的利用 As previously stated, every action that needs a kernel memory read or write relies on a vulnerable driver to give this primitive. In EDRSanblast, adding the support for a new driver providing the read/write primitive can be "easily" done, only three functions need to be implemented: * A `ReadMemoryPrimitive_DRIVERNAME(SIZE_T Size, DWORD64 Address, PVOID Buffer)` function, that copies `Size` bytes from kernel address `Address` to userland buffer `Buffer`; * A `WriteMemoryPrimitive_DRIVERNAME(SIZE_T Size, DWORD64 Address, PVOID Buffer)` function, that copies `Size` bytes from userland buffer `Buffer` to kernel address `Address`; * A `CloseDriverHandle_DRIVERNAME()` that ensures all handles to the driver are closed (needed before uninstall operation which is driver-agnostic, for the moment). As an example, two drivers are currently supported by EDRSandblast, `RTCore64.sys` (SHA256: `01AA278B07B58DC46C84BD0B1B5C8E9EE4E62EA0BF7A695862444AF32E87F1FD`) and `DBUtils_2_3.sys` (SHA256: `0296e2ce999e67c76352613a718e11516fe1b0efc3ffdb8918fc999dd76a73a5`). The following code in `KernelMemoryPrimitives.h` is to be updated if the used vulnerable driver needs to be changed, or if a new one implemented. ```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 驱动程序和进程检测 目前使用多种技术来确定特定驱动程序或进程是否属于 EDR 产品。 首先,驱动程序的名称可直接用于此目的。实际上,Microsoft 为所有需要在内核中插入回调的驱动程序分配称为“高度”的特定编号。这允许根据驱动程序的用途而非注册顺序,确定回调执行的顺序。可以在 [on MSDN](https://docs.microsoft.com/en-us/windows-hardware/drivers/ifs/allocated-altitudes) 中找到已保留特定*高度*的驱动程序(供应商)列表。 因此,Microsoft 提供了一份几乎全面的安全驱动程序名称列表,主要包含在“FSFilter Anti-Virus”和“FSFilter Activity Monitor”列表中。这些驱动程序名称列表已嵌入 EDRSandblast 中,同时也包含了额外的贡献内容。 此外,EDR 可执行文件和 DLL 通常使用供应商的签名证书进行数字签名。因此,检查与进程关联的可执行文件或 DLL 的签名者可以快速识别 EDR 产品。 另外,驱动程序必须由 Microsoft 直接签名才能被允许加载到内核空间。虽然驱动程序的供应商并非驱动程序本身的直接签名者,但供应商名称似乎仍包含在签名的属性中;不过该检测技术尚待调查和实现。 最后,当面对 EDRSandblast 未知的 EDR 时,最佳方法是以“审计”模式运行该工具,并检查已注册内核回调的驱动程序列表;然后将该驱动程序名称添加到列表中,重新编译并运行该工具。 ### RunAsPPL 绕过 `本地安全机构 (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` 进程内存,因为它“支配”运行于 `RunAsPPL` 机制下的 `LSASS` 进程的保护级别 `PsProtectedSignerLsa-Light`。 `EDRSandBlast` 按如下方式实现自我保护: - 打开当前进程的句柄 - 使用 `NtQuerySystemInformation` 泄露所有系统句柄,以找到当前进程的已打开句柄,以及当前进程在内核内存中的 `EPROCESS` 结构地址。 - 利用漏洞驱动程序的任意读/写漏洞,覆盖内核内存中当前进程的 `_PS_PROTECTION` 字段。`_PS_PROTECTION` 字段相对于 `EPROCESS` 结构的偏移量(由所使用的 `ntoskrnl` 版本定义)在 `NtoskrnlOffsets.csv` 文件中计算。 ### Credential Guard 绕过 Microsoft `Credential Guard` 是一项基于虚拟化的隔离技术,在 Microsoft 的 `Windows 10(企业版)` 中引入,用于防止直接访问存储在 `LSASS` 进程中的凭据。 当激活 `Credentials Guard` 时,会在 `虚拟安全模式` 下创建一个 `LSAIso`(*LSA 隔离*)进程,该功能利用 CPU 的虚拟化扩展为内存中的数据提供额外的安全性。即使以 `NT AUTHORITY\SYSTEM` 安全上下文访问,对 `LSAIso` 进程的访问也受到限制。处理哈希时,`LSA` 进程向 `LSAIso` 进程执行 `RPC` 调用,并等待 `LSAIso` 的结果以继续。因此,`LSASS` 进程不会包含任何机密,而是存储 `LSA Isolated Data`。 正如 `N4kedTurtle` 进行的研究所述:“可以通过修补内存中 `g_fParameter_useLogonCredential` 和 `g_IsCredGuardEnabled` 的值,在启用了 Credential Guard 的系统上启用 `Wdigest`”。 激活 `Wdigest` 将导致对于任何新的交互式登录,明文凭据将被存储在 `LSASS` 内存中(无需重启系统)。有关此技术的更多详细信息,请参阅 [original research blog post](https://teamhydra.blog/2020/08/25/bypassing-credential-guard/)。 `EDRSandBlast` 只是让原始 PoC 在行动安全方面更加友好,并提供了对多种 `wdigest.dll` 版本的支持(通过计算 `g_fParameter_useLogonCredential` 和 `g_IsCredGuardEnabled` 的偏移量)。 ### 偏移量检索 为了可靠地执行内核监控绕过操作,EDRSandblast 需要确切知道在何处读写内核内存。这是通过目标镜像(ntoskrnl.exe、wdigest.dll)内部全局变量的偏移量,以及 Microsoft 在符号文件中发布的结构中特定字段的偏移量来实现的。这些偏移量特定于目标镜像的每个构建版本,必须针对特定的平台版本至少收集一次。 选择使用“硬编码”偏移量而非特征码搜索来定位 EDRSandblast 使用的结构和变量,是因为负责内核回调添加/移除的未文档化 API 可能会发生变化,且任何在错误地址读取或写入内核内存的尝试都可能(且经常会)导致 `Bug Check`(`蓝屏死机`)。在红队行动和常规渗透测试场景中,机器崩溃是不可接受的,因为崩溃的机器对防御者来说非常显眼,并且会丢失攻击时刻内存中的所有凭据。 为了检索每个特定 Windows 版本的偏移量,实现了两种方法。 #### 手动偏移量检索 所需的 `ntoskrnl.exe` 和 `wdigest.dll` 偏移量可以使用提供的 `ExtractOffsets.py` Python 脚本提取,该脚本依赖 `radare2` 和 `r2pipe` 从 PDB 文件下载和解析符号,并从中提取所需的偏移量。偏移量随后存储在 CSV 文件中,供 EDRSandblast 稍后使用。 为了开箱即支持广泛的 Windows 构建版本,[Winbindex](https://winbindex.m417z.com/) 引用了许多版本的 `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 `)。应下载漏洞驱动程序的副本并提供给 EDRSandblast,以使其内核操作正常工作。 经过测试的驱动程序哈希值在实现 EDRSanblast 所用内核内存读写原语的每个 `Driver.c` 文件开头都有提及。利用这些哈希值,可以很容易在互联网上找到驱动程序样本,特别是在 `https://www.loldrivers.io` 上。 以下是支持的漏洞驱动程序列表及其下载链接: | 支持的驱动程序 | 下载链接 | SHA256 | |------------------|----------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------| | `GDRV.sys` | [LOLDrivers link](https://github.com/magicsword-io/LOLDrivers/raw/main/drivers/9ab9f3b75a2eb87fafb1b7361be9dfb3.bin) | 31f4cfb4c71da44120752721103a16512444c13c2ac2d857a7e6f13cb679b427 | | `RTCore64.sys` | [LOLDrivers link](https://github.com/magicsword-io/LOLDrivers/raw/main/drivers/2d8e4f38b36c334d0a32a7324832501d.bin) | 01aa278b07b58dc46c84bd0b1b5c8e9ee4e62ea0bf7a695862444af32e87f1fd | | `DBUtil_2_3.sys` | [LOLDrivers link](https://github.com/magicsword-io/LOLDrivers/raw/main/drivers/c996d7971c49252c582171d9380360f2.bin) | 0296e2ce999e67c76352613a718e11516fe1b0efc3ffdb8918fc999dd76a73a5 | ### 快速使用 ``` Usage: EDRSandblast.exe [-h | --help] [-v | --verbose] [--usermode] [--unhook-method ] [--direct-syscalls] [--add-dll ]* [--kernelmode] [--dont-unload-driver] [--no-restore] [--nt-offsets ] [--fltmgr-offsets ] [--wdigest-offsets ] [--ci-offsets ] [--internet] [--vuln-driver ] [--vuln-service ] [--unsigned-driver ] [--unsigned-service ] [--no-kdp] #### [-o | --dump-output ] ### 选项 ``` -h | --help 显示此帮助信息并退出。 -v | --verbose 启用更详细的输出。 操作模式: ``` 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 '' in the current directory or at the specified file using -o | --output . 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 执行用户态操作(DLL 脱钩)。 --kernelmode 执行内核态操作(移除内核回调和禁用 ETW TI)。 脱钩相关选项: --add-dll 在开始任何操作之前,将任意库加载到进程的地址空间中。这对于审计默认未由此程序加载的 DLL 的用户态挂钩非常有用。多次使用此选项可一次性加载多个 DLL。 值得关注的 DLL 示例:user32.dll、ole32.dll、crypt32.dll、samcli.dll、winhttp.dll、urlmon.dll、secur32.dll、shell32.dll... --unhook-method 从以下选项中选择用户态脱钩技术: ``` 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 使用直接系统调用储选定的进程内存,而不解除用户态挂钩。 BYOVD 选项: --dont-unload-driver 将漏洞驱动程序保留在主机上安装 默认为自动卸载驱动程序。 --no-restore 不恢复已移除的 EDR 驱动程序的内核回调。 默认为恢复回调。 --vuln-driver 漏洞驱动程序文件的路径。 默认为当前目录下的 'gdrv.sys'。 --vuln-service 要安装/启动的漏洞服务名称。 驱动程序侧载选项: --unsigned-driver 未签名驱动程序文件的路径。 默认为当前目录下的 'evil.sys'。 --unsigned-service 要安装/启动的未签名驱动程序服务名称。 --no-kdp 切换到 g_CiOptions 修补方法以禁用 DSE(默认为回调交换)。 偏移量相关选项: --nt-offsets 包含所需 ntoskrnl.exe 偏移量的 CSV 文件路径。 默认为当前目录下的 'NtoskrnlOffsets.csv'。 --fltmgr-offsets 包含所需 fltmgr.sys 偏移量的 CSV 文件路径 默认为当前目录下的 'FltmgrOffsets.csv'。 --wdigest-offsets 包含所需 wdigest.dll 偏移量的 CSV 文件路径 (仅用于 'credguard' 模式)。 默认为当前目录下的 'WdigestOffsets.csv'。 --ci-offsets 包含所需 ci.dll 偏移量的 CSV 文件路径 (仅用于 'load_unsigned_driver' 模式)。 默认为当前目录下的 'WdigestOffsets.csv'。 -i | --internet 启用从 Microsoft 符号服务器自动下载符号 如果存在相应的 *Offsets.csv 文件,则将下载的偏移量追加到该文件中以备后用 OpSec 警告:下载并在磁盘上释放相应镜像的 PDB 文件 转储选项: -o | --dump-output 'dump' 模式生成的转储文件的输出路径。 默认为当前目录下的 'process_name'。 #### --process-name 要转储的进程文件名(默认为 'lsass.exe') ### 构建 `EDRSandBlast`(仅限 x64)是在 Visual Studio 2019 上构建的(Windows SDK 版本:`10.0.19041.0`,平台工具集:`Visual Studio 2019 (v142)`)。 ### ExtractOffsets.py 用法 #### 注意 `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 供应商、Microsoft、查看 EDR 遥测数据的 SOC 分析师等)的角度来看,可以使用多个指标来检测或阻止此类技术。 ### 驱动程序白名单 由于该工具在内核模式内存中执行的每个操作都依赖于漏洞驱动程序来读/写任意内容,因此驱动程序加载事件应受到 EDR 产品(或 SOC 分析师)的严格审查,并在任何非正常驱动程序加载时发出警报,甚至阻止已知的漏洞驱动程序。后一种方法甚至已被 [recommended by Microsoft themselves](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 驱动程序可以定期检查其内核回调是否仍处于注册状态,方法是通过直接检查内核内存(如本工具所做),或者简单地通过触发事件(进程创建、线程创建、镜像加载等)并检查回调函数是否确实被执行内核调用。 作为补充说明,此类数据结构可以通过最近的 [Kernel Data Protection (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` -> syscall)触发的,还是从异常路径(例如 `program.exe` -> 直接系统调用)触发的。 ## 作者 [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/
标签:0day挖掘, Awesome列表, Conpot, EDR绕过, ETW绕过, GPU加速, Object Callbacks, Ruby on Rails, Web报告查看器, Windows安全, 代码生成, 内存转储, 内核安全, 反取证, 口令猜测, 哈希破解, 嗅探欺骗, 安全测试, 安全评估, 客户端加密, 客户端加密, 密码恢复, 攻击性安全, 攻击模拟, 权限维持, 渗透测试工具, 用户态脱钩, 网络钓鱼, 逆向工具, 邮件钓鱼, 钓鱼框架, 驱动签名利用, 高交互蜜罐