Contents

Kernel Drivers, Process Protection, and ...Bears?

Acknowledgments: Thanks to the various people that proofread my ramblings and offered valuable feedback. Thanks to @_RastaMouse (and ZeroPointSecurity) for creating courses that have inspired me to learn more about security every day.
 

I want to start this blog by stating basically none of this research is “new”. A lot of the information surrounding process protection in Windows, and the various methods of exploitation have been explored in extreme depth (such as the work done by Benjamin Delpy and the mimikatz driver). The goal here is to share my personal journey through understanding process protection in Windows, and document a few “gotchas” I encountered on my way to disabling PPL for a process like lsass.exe.

What is a “Protected Process” in Windows?

Protection Levels, such as Process Protection Light (PPL), play an integral role in the security infrastructure of the Windows operating system, ensuring that processes operate within delineated privilege boundaries to maintain system stability and security. These protection levels are hierarchical, dictating which processes can interact with others based on their assigned levels. For instance, with the introduction of PPL, Windows has added a finer granularity in the protection scheme, allowing certain critical system processes to run at elevated protection levels, while others might be relegated to a “light” level, like Process Protection Light.

 

Processes running at a higher protection level are shielded from interference by those at lower levels. This hierarchy ensures that critical system functions are isolated from potential threats, both malicious and unintentional, by restricting access and interactions based on the designated Protection Level. The overarching goal is to maintain system integrity by preventing less trusted processes from compromising or interrupting more trusted ones.

Not all PPL are created equally

For direct interaction with a Protected Process Light (PPL), another process typically also needs to be at the PPL level or at an even higher protection level. But here’s where the intricacy comes in: there are various protection levels within the PPL designation itself, and they are organized hierarchically. A process with a higher PPL level can interact with or inspect a process at a lower PPL level, but not vice versa.

Local Security Authority Subsystem Service

The Local Security Authority Subsystem Service (lsass.exe) runs at a high process protection level known as “Protected Process Light” (PPL). In the case of lsass.exe, this is particularly significant since it manages user logins and stores authentication credentials, making it a frequent target for cyber-attacks.

Microsoft Protections

This KB article holds a lot of important information on process protection relating to LSA and lsass.exe. I’m going to quote the first paragraph because it gives a good summary of what these protections offer, and what versions of Windows support them.

 

“The LSA, which includes the Local Security Authority Server Service (LSASS) process, validates users for local and remote sign-ins and enforces local security policies. The Windows 8.1 operating system and later provides additional protection for the LSA to prevent reading memory and code injection by non-protected processes. This feature provides added security for the credentials that LSA stores and manages. The protected process setting for LSA can be configured in Windows 8.1 and later. When this setting is used with UEFI lock and Secure Boot, additional protection is achieved because disabling the HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa registry key has no effect.”

Notes on the LSASS Process

By default, the Local Security Authority (LSA) does not launch with any type of protection (in certain Windows versions). This means that, unless explicitly configured, we can access lsass.exe memory (and dump it) without anything stopping us. An important distinction that I want to call out is that, if you’ve ever tried dumping lsass.exe (like say by just right clicking the process in Task manager) and gotten an error like this one:

 

/images/driverdev/lsass_deny.png

 

This is because of the kernel directly preventing access to the process by means of process protection, and NOT the result of a security product intervening. Although, some EDR will prevent attempts to dump LSASS regardless of whether or not the kernel would allow it. While Defender may freak out at you dumping lsass.exe, with elevated privilege you can allow the .dmp file write through the AV with no problem by just saying “Allow” in the security center notification.

 

OPSEC Note: obviously dumping LSA is a huge red-flag to most security tools. I am only using lsass.exe as an example process because it is so often a target for attackers.

 

In Windows 10 and Windows 11, this protection must be enabled in a variety of ways. Prior to a recent update, Local Security Authority protection was a setting that could be enabled within the Core Isolation security center menu. However, that menu no longer exists, at least on my version of Windows (probably a good thing).

If you want to follow along with our eventual goal of disabling process protection on lsass.exe, you’ll want to make sure it’s enabled. This can be accomplished in one of these various ways:

LSA as PPL via Registry

If you want to modify security settings so LSA runs with PPL, you can modify the following registry key: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa and add a DWORD RunAsPPL and set the value to 1 or 2. To know which you’d want to use, consult the MS Documentation - the TLDR is one involves using UEFI and the other doesn’t.

LSA as PPL via Group Policy

The other option is to enable the protection via GPO. This can be accessed by launching gpedit.msc and then following Administrative Templated -> System -> Local Security Authority (or just use a filter and search for LSASS).

/images/driverdev/gpo1.png

/images/driverdev/gpo2.png

Once you’ve completed these steps, you need to restart Windows for the change to take place.

Important Windows Internals

In order to start developing code that can modify and interact with the process protection of a process, we first need to collect some important Windows internals information that can guide us.

Protection Levels

For starters, we should be aware of the various types of process protection levels and their implications. This page shows all potential ProtectionLevel a process could potentially have:

Value Meaning
PROTECTION_LEVEL_WINTCB_LIGHT For internal use only.
PROTECTION_LEVEL_WINDOWS For internal use only.
PROTECTION_LEVEL_WINDOWS_LIGHT For internal use only.
PROTECTION_LEVEL_ANTIMALWARE_LIGHT For internal use only.
PROTECTION_LEVEL_LSA_LIGHT For internal use only.
PROTECTION_LEVEL_WINTCB Not implemented.
PROTECTION_LEVEL_CODEGEN_LIGHT Not implemented.
PROTECTION_LEVEL_AUTHENTICODE Not implemented.
PROTECTION_LEVEL_PPL_APP The process is a third party app that is using process protection.
PROTECTION_LEVEL_NONE The process is not protected.

Viewing Process Protection Levels

Using something like ProcessExplorer or Process Hacker, we can view what protection levels a given process is running with:

/images/driverdev/procexp1.png

The value for a certain process is stored in a struct, _PS_PROTECTION which is defined as such (credit to Vergilius Project):

//0x1 bytes (sizeof)
struct _PS_PROTECTION
{
    union
    {
        UCHAR Level;                                                        
        struct
        {
            UCHAR Type:3;                                                   
            UCHAR Audit:1;                                                  
            UCHAR Signer:4;                                                 
        };
    };
}; 

Disabling process protection is as “simple” as modifying this struct and setting all values to 0.

Accessing needed structs for a process

The _PS_PROTECTION struct is, itself, stored within another struct - EPROCESS (https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/eprocess). Every process in Windows has an associated EPROCESS struct stored in kernel memory. This structure contains process attributes and points to other data structures, like _PS_PROTECTION which we are interested in. In order to read a process’s protection level, we first need to get a pointer to the EPROCESS struct so we can access the _PS_PROTECTION struct within (struct-ception).

 

The documentation for this struct (EPROCESS) states that it is associated with the following headers: Wdm.h and the following Includes: Wdm.h, Ntddk.h, Ntifs.h. If we look at the documentation for NTifs , and do a search for “EPROCESS” we can find the function PsLookupProcessByProcessId, which will help us accomplish the goal of getting to the underlying struct.

 

/images/driverdev/pslookup.png

What IS an opaque structure?

As the Microsoft documentation states here, the EPROCESS struct is opaque. This means that we cannot directly access member data. If you want to do a deep dive into what opaque structures are, here is an awesome article by Nick Miller. It’s kind of out of scope for this post, but still really good info. So we know the member data can’t be directly accessed, so are we just dead in the water? Nope. Because it’s a pointer, and the structure is generally well-defined, we can access the portion of the struct we need by using a memory-address offset. “But how do we get the offset?” I’m so glad you asked! We can use a kernel-debugger to do that.

 

This blog is a deeper dive into EPROCESS, for use in memory forensics and I highly recommend reading it as it gives some really solid background into it. For this post, we’ll use WinDBG. I won’t go through the setup of the tool, and how to attach the debugger to a machine. Instead I’ll just show an example output of dumping (dt) the Windows NT (nt!) structure EPROCESS - full command: dt nt!_EPROCESS.

/images/driverdev/wdbg1.png

Eventually, we can find the _PS_PROTECTION memory address we need:

/images/driverdev/wdbg2.png

Note: An important callout here is that these memory offset addresses will vary from Windows version. We need to take note of the memory address that is the starting address for the struct, which in this case is 0x878.

Kernel Driver

We now have all the information needed to write a kernel-mode driver capable of accessing and modifying the protection level of a process.

  1. A method to access the EPROCESS struct
  2. Figuring out the _PS_PROTECTION offset
  3. Knowing what to change in order to de facto disable process protection

I am not going to dump driver code here that you can copy-paste, and then compile into a functioning piece of malware. Instead, I will break down the steps to creating a driver and explain what each piece does. (If you ARE interested in learning how to create a driver, take ZeroPointSecurity’s Offensive Driver Development course - not only is the course well made, it can be purchased for the cost of a weeks worth of Starbucks lattes).

I am also not going to explain every aspect of coding the driver (like writing about IOCTL/IRP, etc), but rather explain how we solve each step above.

Retrieving a Process PID

We know from the PsLookupProcessByProcessId documentation that we need to provide a process ID (PID) to the function in order to return the pointer to the EPROCESS struct for that PID. We will create a struct that the driver can use to read the PID. If you imagine how we might interact with the driver (via client application), we’d need to pass in a PID somehow. PIDs are integers and therefore a definition such as this one is all we need:

Driver Code

struct ProcPid
{
	int PId;
};

Client Code

Assuming we’ll run our client application as a console app and something like client.exe <PID>, we’d need to read in the arg and store it as member data of our struct

    ProcPid p; // Declares ProcPid struct var
    p.PId = atoi(argv[1]); // sets PId member data to argv[1] from our CLI

Sending PID to Driver

We now need to send the PID to the driver, which can be done via a defined IOCTL (you need to make this on your own):

Client Code

BOOL success = DeviceIoControl(
	hDriver, // handle to the driver
	SOME_EDGY_IOCTL_NAME,
	&p,              // pointer to the PID struct
	sizeof(p),       // the size of the struct
	nullptr,
	NULL,
	nullptr,
	nullptr);

Driver Code

We passed in the pointer to p (address where our process ID struct is) to the DeviceIoControl of our driver (via some IOCTL).

// Declare pointer of type ProcPid
// in this context, stack is an instance of PIO_STACK_LOCATION
// and points to IoGetCurrentIrpStackLocation(Irp)
ProcPid* p = (ProcPid*)stack->Parameters.DeviceIoControl.Type3InputBuffer;

At this point, p is a pointer to the location on the Irp stack storing our ProcPid struct which was passed in as an argument via the client application.

Retrieve EPROCESS Struct

If you recall, PsLookupProcessByProcessId will return a pointer to a PID’s EPROCESS.

Driver Code

// We initialize a new PEPROCESS variable
PEPROCESS eProcess = NULL;
// Get handle to P to read PId
// pass eProcess by reference so function can store value in our variable
status = PsLookupProcessByProcessId((HANDLE)p->PId, &eProcess);

Per the PsLookupProcessByProcessId documentation, the function has a return type of NTSTATUS but also will also return out a pointer to a PEPROCESS object (which itself is an ObjectType representing a process). At this point, status will store the NTSTATUS return value (which is useful for debugging), and eProcess will hold the memory location of the EPROCESS struct for that PID.

Retrieving PROCESS_PROTECTION_INFO from EPROCESS

Driver Code

We first need to declare two structs in our driver code (because one will use the other…)

  1. Declare the _PS_PROTECTION struct, which if you recall we can copy from Vergilius
typedef struct _PS_PROTECTION
{
	UCHAR Type : 3;
	UCHAR Audit : 1;
	UCHAR Signer : 4;
} PS_PROTECTION, * PPS_PROTECTION;
  1. Declare the _PROCESS_PROTECTION_INFO struct which we can gather from a bigger overview of the EPROCESS struct (Vergilius link)
typedef struct _PROCESS_PROTECTION_INFO
{
	UCHAR SignatureLevel;
	UCHAR SectionSignatureLevel;
	PS_PROTECTION Protection; // previously defined struct
} PROCESS_PROTECTION_INFO, * PPROCESS_PROTECTION_INFO;
  1. Write function that creates pointer to PROCESS_PROTECTION_INFO struct, and adds the memory address of our EPROCESS variable, eProcess to the offset (0x878) we found with the kernel debugger (which I am quite literally just now seeing is already documented in Vergilius)
// Add the offset we found by using kernel debugger

PROCESS_PROTECTION_INFO* psProtection = (PROCESS_PROTECTION_INFO*)(((ULONG_PTR)eProcess) + 0x878);

Removing Process Protection

At this point, psProtection is storing the memory address of the PROCESS_PROTECTION_INFO struct, which allows us to directly access the Protection member like you normally would in C++ like so:

psProtection->SignatureLevel = 0;
psProtection->SectionSignatureLevel = 0;
psProtection->Protection.Type = 0; // accessing Type from the PS_PROTECTION struct
psProtection->Protection.Signer = 0; // same thing with Signer
And now we’re done. Process Protection is disabled for whatever PID we passed in.

Gotchas

Per the **PsLookupProcessByProcessId** documentation:

“If the call to PsLookupProcessByProcessId is successful, PsLookupProcessByProcessID increases the reference count on the object returned in the Process parameter. Consequently, when a driver has completed using the Process parameter, the driver must call ObDereferenceObject to dereference the Process parameter received from the PsLookupProcessByProcessID routine.”

In this case, the Process parameter was the process object (EPROCESS/PEPROCESS) we passed into the function, and can be dereferenced via ObDereferenceObject(eProcess);.

Demonstration

/images/driverdev/example1.png

/images/driverdev/example2.png

Thanks for Reading

Thank you for taking the time to read. Please connect with me on LinkedIn or Twitter. I’m always looking to learn something new, and open to any and all feedback.