PPL Bypass for LSASS
Protected Process Light prevents OpenProcess from succeeding against lsass.exe — bypassing it via BYOVD kernel driver, nanodump's direct syscall trick, and handle duplication through kernel objects
Windows 8.1 introduced "RunAsPPL" for lsass — when enabled, lsass runs as a Protected Process Light and every attempt to open a handle with PROCESS_VM_READ returns ACCESS_DENIED, even from SYSTEM. You hit this on a hardened Windows 11 machine where the admin enabled PPL via HKLM\SYSTEM\CurrentControlSet\Control\Lsa\RunAsPPL = 1. You need to either strip the PPL protection from the lsass EPROCESS (requires kernel access), escalate to a signer level higher than lsass's PPL level, or find an alternative path that doesn't go through OpenProcess at all.
PPL Architecture — How Protected Process Light Works
Protected Processes were introduced in Vista for media DRM. Protected Process Light (PPL) is a lighter variant added in Windows 8.1. The protection mechanism lives entirely in the kernel: the EPROCESS structure has a Protection field that encodes both the protection level and the signer type. When OpenProcess is called targeting a protected process, PsOpenProcess in ntoskrnl checks the calling process's protection level against the target's level and signer type, and returns ACCESS_DENIED if the caller is less trusted.
PPL Levels and Signer Types
| Protection Level | Value | Signer Type | Examples |
|---|---|---|---|
| Protected Process (PP) | 0x62 | WinTcb (5) | System, smss.exe, csrss.exe |
| PPL WinTcb | 0x61 | WinTcb (5) | lsass.exe (with RunAsPPL), services.exe |
| PPL Windows | 0x41 | Windows (4) | spoolsv.exe, audio services |
| PPL WindowsTcb | 0x51 | WindowsTcb (5, lower than WinTcb) | audiodg.exe |
| PPL Antimalware | 0x31 | Antimalware (3) | MsMpEng.exe (Windows Defender) |
| PPL Authenticode | 0x11 | Authenticode (1) | Signed third-party protected processes |
| None | 0x00 | None (0) | Normal user processes, most malware |
The Protection byte in EPROCESS encodes (ProtectionLevel << 4) | SignerType. For lsass with RunAsPPL: Type=PPL (0x40), Signer=WinTcb (0x01) → combined byte = 0x41. Wait — actually the byte is structured as: bits[3:0] = Signer (PS_PROTECTED_SIGNER), bits[6:4] = Level (PS_PROTECTED_TYPE), bit[7] = Audit. For lsass PPL WinTcb: Signer=WinTcb=6, Level=PPL=1 → 0x61.
EPROCESS Protection Field
BYOVD Kernel Driver Bypass — Stripping the PPL Flag
With a kernel read/write primitive from a BYOVD driver (chapter 42 of Windows Internals book — rtcore64.sys, gdrv.sys, etc.), you can locate lsass's EPROCESS, find the Protection field, and overwrite it with 0x00. After the write, lsass is no longer a protected process and OpenProcess with PROCESS_VM_READ succeeds:
// Conceptual — requires a kernel write primitive from BYOVD driver
// Actual BYOVD driver IOCTL usage is driver-specific (rtcore64, gdrv, etc.)
BOOL StripLsassPPL() {
// Step 1: Get lsass PID
DWORD pid = GetLsassPid();
// Step 2: Get kernel EPROCESS address for lsass
// Method A: NtQuerySystemInformation(SystemProcessInformation) returns
// SYSTEM_PROCESS_INFORMATION structs; each has a UniqueProcessId.
// Does NOT return kernel addresses for unprotected callers.
// Method B: Use NtQuerySystemInformation(SystemHandleInformation) to find
// a handle to lsass held by SYSTEM, then use BYOVD kernel read to follow
// HANDLE_TABLE_ENTRY -> EPROCESS.
// Method C: Scan kernel memory for EPROCESS pool tag "Proc" (0x636f7250)
// via BYOVD kernel read, verify by checking UniqueProcessId field.
ULONG_PTR eprocess = LocateEPROCESS(pid); // via BYOVD
// Step 3: Find Protection field offset
// Offset is build-specific — hardcode for target build or resolve via
// PsLookupProcessByProcessId + subtract KPROCESS base to find offset.
// Reliable approach: use a hardcoded table indexed by OS build number.
DWORD protectionOffset = GetProtectionOffset(); // e.g., 0x87a on Win10 22H2
// Step 4: Write 0x00 to EPROCESS + protectionOffset via BYOVD kernel write
UCHAR zero = 0x00;
KernelWrite(eprocess + protectionOffset, &zero, sizeof(zero));
// Step 5: Now OpenProcess succeeds
HANDLE hLsass = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION,
FALSE, pid);
if (!hLsass) return FALSE;
// Dump as in ch95 ...
// Step 6: Restore protection (operational security — avoid persistent kernel modification)
UCHAR original = 0x61; // WinTcb PPL
KernelWrite(eprocess + protectionOffset, &original, sizeof(original));
return TRUE;
}
The EPROCESS Protection field offset changes with every major Windows build. A robust implementation queries the build number at runtime (RtlGetVersion) and looks up the offset in a table. Tools like PPLdump, PPLKiller, and nanodump maintain these offset tables. Alternatively, use the Windows symbol server (symchk / WinDbg) during development to verify the offset for your target build: dt nt!_EPROCESS Protection in WinDbg gives the exact byte offset.
nanodump's Direct Syscall Approach — No Kernel Driver Needed
nanodump implements a clever trick to dump lsass even when RunAsPPL is enabled, without requiring a kernel driver. The key insight: NtReadVirtualMemory at the kernel level does not do a PPL access check when the calling process is already a PPL of sufficient signer level. The approach is to elevate the attacker's process to PPL status before making the read call.
Since PPL status is enforced by signing (you need a Microsoft-signed binary to run as PPL under normal circumstances), nanodump uses an undocumented trick: on older Windows builds, NtSetInformationProcess with ProcessProtectionInformation could elevate a process to PPL status if called from SYSTEM. This was patched, but the general concept — running a component as PPL by exploiting process creation or token tricks — remains the category of attack.
The practical nanodump approach for PPL-protected lsass uses a different method: fork a helper process that runs inside the PPL trust context by abusing the AddSecurityPackage / Security Support Provider loading mechanism. Here's the conceptual flow:
Handle Duplication via Kernel — No OpenProcess Required
The kernel object manager does not perform PPL access checks when duplicating handles — that check only happens in NtOpenProcess. If you can find a process that already holds a handle to lsass (the kernel has such handles internally for process management), you can duplicate that handle into your own process using NtDuplicateObject called from kernel level. The BYOVD driver provides the kernel context needed to call ZwDuplicateObject on behalf of your process:
// Handle duplication via kernel context (conceptual, requires BYOVD ring-0 execution)
// From kernel context (inside BYOVD driver's IOCTL handler or shellcode):
// Step 1: Find a process that holds a PROCESS_VM_READ handle to lsass
// csrss.exe typically has open handles to all processes in its session
// Use NtQuerySystemInformation(SystemHandleInformation) to enumerate handles
// Step 2: ZwDuplicateObject from kernel
NTSTATUS status = ZwDuplicateObject(
sourceProcess, // handle to the process that owns the lsass handle
sourceHandle, // the handle value (from SystemHandleInformation)
targetProcess, // handle to YOUR process (NtCurrentProcess() or PID lookup)
&duplicatedHandle,
PROCESS_VM_READ | PROCESS_QUERY_INFORMATION,
0,
DUPLICATE_SAME_ACCESS);
// Step 3: Pass duplicated handle back to user-mode implant
// Now the implant has a valid VM_READ handle to lsass
// without ever calling NtOpenProcess against a PPL process
Injecting Code into a PPL Process
An alternative approach that doesn't require dumping at all: if you can inject a shellcode or DLL into lsass itself, the injected code runs with full access to lsass's memory from inside the process. PPL restricts who can open the process — it doesn't restrict code that is already running inside it. The injection path for PPL processes must itself be PPL-compatible:
| Injection Method | Works Against PPL | Requirement |
|---|---|---|
| CreateRemoteThread | No — requires PROCESS_CREATE_THREAD which PPL blocks | N/A |
| NtQueueApcThread | No — requires thread handle with THREAD_SET_CONTEXT | N/A |
| Security Package (SSP) DLL | Yes | Registry key + reboot or lsass restart |
| Authentication Package DLL | Yes | HKLM\SYSTEM\...\Lsa\Authentication Packages + reboot |
| Notification Package DLL | Yes | HKLM\SYSTEM\...\Lsa\Notification Packages + reboot |
| Kernel driver injection via section mapping | Yes (with ring-0) | Requires kernel access |
Detection
| Signal | Source | Notes |
|---|---|---|
| New kernel driver loaded (BYOVD step) | Sysmon EventID 6, Event 7045 | Vulnerable driver name/hash may match known BYOVD blocklist; CodeIntegrity event 3077/3033 if blocklisted |
| EPROCESS.Protection byte change detected | EDR kernel sensor / Volatility | From non-zero to zero for lsass is a critical anomaly |
| New value under HKLM\...\Lsa\Security Packages | Registry monitoring / Sysmon EventID 13 | Unauthorized SSP registration is a strong persistence and injection indicator |
| Unknown DLL loaded into lsass.exe | Sysmon EventID 7 (ImageLoad) in lsass context | Any DLL in lsass not from System32 or WINSXS is anomalous |
| RunAsPPL registry value missing or changed | Registry monitoring | HKLM\SYSTEM\CurrentControlSet\Control\Lsa\RunAsPPL being set to 0 is a clear indicator |
Q&A
Does PPL fully protect lsass on modern Windows 11 with HVCI enabled?
Not fully, but HVCI significantly raises the bar. Without HVCI: PPL can be bypassed by any process with a kernel write primitive (BYOVD or kernel exploit) by overwriting EPROCESS.Protection — a data-only attack that HVCI does not prevent (HVCI protects code pages, not data). With HVCI: BYOVD is constrained — the vulnerable driver can still be loaded (it's a signed binary), but it cannot allocate and execute unsigned kernel code. It can still provide a kernel read/write primitive via its IOCTL interface, which is enough to perform the EPROCESS.Protection overwrite (data-only). So HVCI makes BYOVD harder (no unsigned shellcode) but doesn't prevent the data-only PPL-stripping attack. The remaining mitigation layer is the Microsoft Vulnerable Driver Blocklist, which blocks known BYOVD drivers by hash. Combined with HVCI + Secure Boot + the driver blocklist, the PPL bypass becomes significantly harder — requiring either a 0-day in a newly-discovered vulnerable signed driver, or a kernel vulnerability.
What is the difference between PPL and Credential Guard for lsass protection?
PPL and Credential Guard are independent, complementary protections that defend against different attack vectors: PPL prevents unauthorized processes from opening lsass to read its memory. It is bypassed by kernel access (BYOVD, kernel exploit). Once bypassed, you get full access to lsass memory including all credential structures. Credential Guard moves the actual secrets — NT hashes, Kerberos TGT session keys — into a separate VSM (Virtual Secure Mode) process called LsaIso.exe running in VTL1 (Secure World). Even if you bypass PPL and fully dump lsass memory, the credential blobs you find contain pointers or encrypted handles to secrets that live in VTL1 — inaccessible from VTL0 even with SYSTEM. The dump contains the credential structure shells but the secrets themselves are in a memory space you literally cannot read from Ring 0 in VTL0. Bypassing Credential Guard requires compromising VTL1 itself (the Secure Kernel) or the hypervisor — a fundamentally different and much harder attack. In practice: PPL is your first obstacle (can be bypassed with kernel access), Credential Guard is your second obstacle (cannot be bypassed with VTL0 kernel access alone). The combination makes LSASS credential dumping ineffective even for a sophisticated attacker with kernel execution, unless they also have a hypervisor or Secure Kernel vulnerability.