
Traduzione articolo, sfruttamento della vulnerabilità CVE-2015-0057 su sistemi a 32 e 64 bit. Sfruttamento del bug use-after-free in win32k!xxxEnableWndSBArrows (CVE 2015-0057) su sistemi sia a 32 bit che a 64 bit(Aaron Adams di NCC )
Autore: Aaron Adams
Traduzione: 55-AA
Nota del traduttore: alcune parti sono state tradotte liberamente; per eventuali dubbi si faccia riferimento all'originale.
Terminologia:
All'inizio di quest'anno, mi sono imbattuto in una vulnerabilità interessante in win32k.sys (CVE-2015-0057) e sono riuscito a realizzare uno sfruttamento stabile su sistemi a 32 e 64 bit, con copertura da XP a Windows 8.1 (con alcune eccezioni). Questo articolo descrive in dettaglio come ho realizzato lo sfruttamento su entrambe le piattaforme, includendo alla fine alcune considerazioni aggiuntive. Viene anche descritto come realizzare lo sfruttamento con privilegi di integrità bassi su Windows 8.1 con SMEP abilitato.
L'articolo è lungo; ho cercato di fornire quanti più dettagli possibile per mostrare la complessità dello sfruttamento di questa vulnerabilità, senza nasconderli, anche se ho omesso alcuni particolari. Spero che questi dettagli siano utili a tutti.
Il 10 febbraio 2015, Microsoft ha pubblicato i dettagli di MS15-010. Questo bug è stato scoperto per primo da Udi Yavo di enSilo. Udi ha fornito un'ottima analisi sul blog breaking malware: "one bit rule-bypassing windows 10 protections using single bit". Raccomando una lettura attenta di quell'articolo per comprendere a fondo il bug, anche se in questo articolo fornirò quanti più dettagli possibile, riguardanti gli ostacoli che devono essere superati quando si attiva la vulnerabilità. Lo sfruttamento di questa vulnerabilità è molto interessante; molti dettagli provengono dal blog di Udi, e di seguito la sua dichiarazione:
Divulgazione ragionevole: sebbene questo blog sia tecnico, non riveleremo alcun codice o dettaglio completo, per impedire a qualsiasi esperto tecnico di riprodurre lo sfruttamento della vulnerabilità.
Come ricompensa aggiuntiva per lo sfruttamento di questa vulnerabilità, abbiamo ottenuto un'evoluzione Pokémon: il Pokémon Tecnico. Penso di dover dare a Udi qualche riconoscimento per aver scoperto il bug e per aver fornito nel blog la situazione correlata e i dettagli per sfruttarlo; queste informazioni sono state davvero utili.
In precedenza, non avevo mai sfruttato vulnerabilità di win32k.sys e non conoscevo le callback in modalità utente e molte API correlate, quindi ringrazio anche alcuni rinomati ricercatori di sicurezza per le risorse innovative pubblicate online, come Skywing, Tarjei Mandt, Alex Ionescu e j00ru. Queste persone hanno reso pubbliche così tante informazioni tecniche che meritano tutte lodi. Ho fatto ampio riferimento all'articolo di Tarjei Mandt Win32k.sys exploitation paper.
Mentre scrivevo lo sfruttamento di questa vulnerabilità, un eccellente reverse engineer ha realizzato uno sfruttamento stabile di CVE-2015-1701, il cui codice di esempio sulle callback in modalità utente è stato molto utile; ringrazio l'autore.
È importante notare che la mia analisi seguente è stata condotta su Windows 7, perché sembra essere l'unica versione in cui tutte le strutture in win32k.sys hanno simboli corrispondenti. La maggior parte di questi simboli è utilizzabile per le strutture di altre versioni di win32k.sys. Per qualche motivo sconosciuto, Microsoft ha rimosso questi simboli a partire da Windows 8.
Infine, voglio dire che il mio metodo di sfruttamento è piuttosto complesso. È del tutto possibile che esista un metodo più semplice che non ho scoperto. Sarei molto interessato a sentire se qualcuno ha utilizzato un approccio diverso. In ogni caso, spero che tutto ciò sia utile per lo studio delle vulnerabilità di win32k.sys.
Di seguito vediamo il bug nel disassemblato di win32k!xxxEnableWndSBArrows, un bug piuttosto sottile:
Caso senza patch:
.text:FFFFF97FFF1B157D mov r8d, r13d
.text:FFFFF97FFF1B1580 mov rdx, r14
.text:FFFFF97FFF1B1583 call xxxDrawScrollBar ; attiva callback in usermode
.text:FFFFF97FFF1B1588 jmp short loc_FFFFF97FFF1B1519
[...]
.text:FFFFF97FFF1B1519 mov eax, [rbx] ; riferimento a puntatore tagSBINFO senza controllo
.text:FFFFF97FFF1B151B mov ebp, 0FFFFFFFBh
.text:FFFFF97FFF1B1520 xor eax, esi
Nel codice sopra, Win32K!xxxDrawScrollBar può, in determinate condizioni, effettuare una callback nello spazio utente; nel codice utente, il puntatore a tagSBINFO potrebbe essere rilasciato dall'attaccante. Quando si ritorna al codice sopra, l'istruzione all'indirizzo 0xFFFFF97FFF1B1519 farà riferimento a un puntatore non valido.
Caso con patch:
.text:FFFFF97FFF1D69C3 xor r8d, r8d
.text:FFFFF97FFF1D69C6 mov rdx, rbp
.text:FFFFF97FFF1D69C9 call xxxDrawScrollBar ; attiva callback in usermode
.text:FFFFF97FFF1D69CE cmp rbx, [rdi+0B0h] ; verifica che il puntatore tagSBINFO sia corretto
---.text:FFFFF97FFF1D69D5 jz short loc_FFFFF97FFF1D69E4; se corretto, prosegue con il flusso originale
| .text:FFFFF97FFF1D69D7 mov rcx, rbp
| .text:FFFFF97FFF1D69DA call _ReleaseDC
| .text:FFFFF97FFF1D69DF jmp loc_FFFFF97FFF1D6958 ; salta all'uscita della funzione
|->.text:FFFFF97FFF1D69E4 mov eax, [rbx] ; usa il puntatore tagSBINFO corretto in sicurezza
.text:FFFFF97FFF1D69E6 xor eax, r14d
Nella versione con patch, vediamo che viene effettuato un controllo di nullità sul puntatore tagSBINFO prima di usarlo. Le informazioni sulla struttura correlata verranno fornite in seguito.
Per realizzare lo sfruttamento, abbiamo eseguito più corruzioni. In una di esse abbiamo attivato la vulnerabilità.
La radice tecnica di questo bug è un use-after-free (UAF) nell'heap del desktop. All'inizio questo mi ha confuso, poiché non conoscevo il meccanismo delle callback in modalità utente di win32k.sys e non capivo come funzionasse. Pensavo quindi che si trattasse di una race condition sui lock che portava a UAF. In realtà, i lock di quella struttura erano usati correttamente e il flusso era come previsto. In breve, la vera causa del problema è:
Tutto qui. Ignorando le callback in modalità utente, questa fase è piuttosto chiara.
Ma come eseguiamo la corruzione e perché? Come accennato nel blog di Udi, puoi impostare o cancellare 2 bit in una posizione che il codice di sistema considera come campo WSBflags nella struttura tagSBINFO. Non è l'approccio classico per sfruttare un UAF, ma l'articolo dà un suggerimento su come operare, che spiegherò nei capitoli seguenti. Per prima cosa, capiamo come manipolare e controllare questi bit.
Struttura tagSBINFO (identica su 32 e 64 bit):
kd> dt -b !tagSBINFO
win32k!tagSBINFO
+0x000 WSBflags : Int4B
+0x004 Horz : tagSBDATA
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
+0x014 Vert : tagSBDATA
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
Il bug UAF si trova nella funzione win32k!xxxEnableWndSBArrows(), che serve ad attivare o disattivare le frecce di uno o due controlli barra di scorrimento (orizzontale o verticale). Un controllo barra di scorrimento è una finestra speciale usata per manipolare le barre di scorrimento. Può essere creata con la funzione CreateWindow() usando la classe di finestra incorporata "SCROLLBAR".
Prototipo della funzione win32k!xxxEnableWndSBArrows():
BOOL xxxEnableWndSBArrows(PWND wnd, UINT WSBflags, UINT wArrows);
Il parametro WSBflags ha lo stesso significato definito in WinUser.h, indica su quali barre di scorrimento operare:
#define SB_HORZ 0
#define SB_VERT 1
#define SB_CTL 2
#define SB_BOTH 3
Il parametro wArrows indica se le frecce sono abilitate o disabilitate. Se impostato, le frecce sono disabilitate, altrimenti abilitate. I due bit meno significativi di wArrows rappresentano la barra orizzontale, i due successivi quella verticale; i bit rimanenti non sono rilevanti ai fini di questo sfruttamento.
Il codice seguente, tratto da win32k!xxxEnableWndSBArrows(), imposta o cancella i bit relativi alla barra orizzontale se SB_HORZ o SB_BOTH è impostato:

Il bug esiste quando si impostano i flag per la barra orizzontale e verticale. Dopo aver aggiornato la barra orizzontale, se la finestra corrispondente è visibile sul desktop, win32k!xxxEnableWndSBArrows() chiama win32k!xxxDrawScrollBar(), che come accennato in precedenza può attivare una potenziale callback in modalità utente.
Prima di discutere la callback in modalità utente, continuiamo a esaminare cosa succede dopo la chiamata a win32k!xxxDrawScrollBar(). In realtà ha la stessa logica della barra orizzontale, con solo qualche bit di differenza. Se scegliamo di disabilitare la barra verticale e supponiamo di aver attivato l'UAF, allora verranno scritti 2 bit in qualche punto del blocco heap di tagSBINFO. Quindi, se il valore originale era 0x2, ora diventa 0xe. Come mostrato nella figura seguente.

Questo cambiamento di un bit è sufficiente per portare all'esecuzione di codice. Non ho approfondito come sfruttare la cancellazione dei bit, ma è possibile.
Il punto principale sopra è che, per poter operare sia sulla barra orizzontale che su quella verticale, è necessario creare un controllo barra di scorrimento che abbia entrambi gli elementi. Questo si ottiene chiamando CreateWindow() con i flag WS_HSCROLL e WS_VSCROLL. Il codice è il seguente:
g_hSBCtl = CreateWindowEx(
0, // No extended style
"SCROLLBAR", // class
NULL, // name
SBS_HORZ | WS_HSCROLL | WS_VSCROLL, // verticale + orizzontale
10, // x
10, // y
100, // width
100, // height
g_hSpray[UAFWND], // finestra padre senza modalità
(HMENU)NULL,
NULL, // proprietario finestra
NULL // parametri extra
);
Possiamo garantire che sia visibile con il seguente codice (di solito è implicito, qui lo chiamiamo esplicitamente):
result = ShowWindow(g_hSBCtl, SW_SHOW);
Le barre di scorrimento sono abilitate per default. Quando siamo pronti per attivare il codice della vulnerabilità, possiamo disabilitare le barre di scorrimento per corrompere i bit desiderati:
result = EnableScrollBar(g_hSBCtl, SB_CTL | SB_BOTH, ESB_DISABLE_BOTH);
Sebbene abbiamo descritto sopra i dettagli del bug e come attivare il codice correlato, abbiamo ancora trascurato il passaggio più importante: intercettare la callback in modalità utente avviata da win32k!xxxDrawScrollBar(), in modo da poter modificare il contenuto dell'heap prima che win32k!xxxEnableWndSBArrows() continui l'esecuzione. Dobbiamo davvero attivare il bug, ma senza conoscere nulla di Win32k.sys e delle API correlate, come me all'inizio, è stata un'avventura per me stesso.
L'articolo precedente mostra un buon schema dello stack di chiamate, che approfondisce il processo: la trigger avviene tramite win32k!xxxDrawScrollBar(), poi viene chiamato ClientLoadLibrary() e viene gestito tramite KeUserModeCallback(). Dobbiamo capire esattamente come viene chiamato KeUserModeCallback() per poterlo hookare nel nostro processo.
Ho trovato alcuni buoni articoli che menzionano, più o meno, le callback in modalità utente. Quelli relativi a win32k sono molto utili:
Generalmente, ogni processo ha una tabella di puntatori alle funzioni di callback in modalità utente, puntata da PEB->KernelCallBackTable. Quando il kernel vuole chiamare una funzione in modalità utente, passa l'indice della funzione a KeUserModeCallBack(). Nell'esempio sopra, l'indice punta alla funzione __ClientLoadLibrary() in modalità utente.
KeUserModeCallBack() cerca la funzione corrispondente in PEB->KernelCallBackTable in base all'indice e la esegue, chiamando infine in modalità utente KiUserModeCallbackDispatch().
Per hookare un punto di ingresso specifico, dobbiamo trovare l'indice di __ClientLoadLibrary() in PEB->KernelCallBackTable e sostituirlo con la nostra funzione. È importante notare che questo indice varia in base alla versione del sistema operativo e alla piattaforma hardware.
Se vogliamo vedere PEB->KernelCallBackTable, possiamo trovare l'indirizzo di questa tabella con WinDbg. Confrontando piattaforme a 32 e 64 bit, non si notano grandi differenze.
kd> dt !_PEB @$peb
ntdll!_PEB
+0x000 InheritedAddressSpace : 0 ''
+0x001 ReadImageFileExecOptions : 0 ''
+0x002 BeingDebugged : 0 ''
+0x003 BitField : 0x8 ''
+0x003 ImageUsesLargePages : 0y0
[...]
+0x02c KernelCallbackTable : 0x76daf620 Void
kd> dds 0x76daf620
76daf620 76d96443 user32!__fnCOPYDATA
76daf624 76ddf0e4 user32!__fnCOPYGLOBALDATA
76daf628 76da736b user32!__fnDWORD
76daf62c 76d9d603 user32!__fnNCDESTROY
76daf630 76dc50f9 user32!__fnDWORDOPTINLPMSG
76daf634 76ddf1be user32!__fnINOUTDRAG
76daf638 76dc6cd0 user32!__fnGETTEXTLENGTHS
76daf63c 76ddf412 user32!__fnINCNTOUTSTRING
76daf640 76d9ce49 user32!__fnINCNTOUTSTRINGNULL
[...]
76daf724 76da3962 user32!__ClientLoadLibrary
kd> ?? (0x76daf724-0x76daf620)/4 int 0n65
Nell'esempio sopra, sappiamo che l'indice di __ClientLoadLibrary è 65, che è anche quello che vogliamo hookare. Dopo l'hook, ho scoperto che __ClientLoadLibrary viene chiamato più volte dal codice di win32k! La prima cosa da fare è, prima della chiamata che ci interessa, informare il nostro codice hook in modo da sapere che abbiamo effettivamente hookato nel punto giusto. Pertanto, nel codice hook è stata usata una variabile globale come flag; solo quando questo flag è impostato vengono eseguite le operazioni.
Ora ci sono due ostacoli:
Pertanto, la funzione hook si presenta così:
void ClientLoadLibraryHook(void * p)
{
CHAR Buf[PGSZ];
memset(Buf, 0, sizeof(Buf));
if (g_PwnFlag)
{
dprintf("[+] __ClientLoadLibrary hook called\n");
if (++g_HookCount == 2)
{
g_PwnFlag = 0; // eseguito solo una volta..
ReplaceScrollBarChunk(NULL);
}
}
fpClientLoadLibrary(&Buf); // chiama la funzione originale
}
Una volta che abbiamo stabilito che la chiamata proviene da win32k!xxxDrawScrollBar(), possiamo provare ad attivare il bug. Ora consideriamo solo l'attivazione: basta chiamare DestroyWindow(g_hSBCtl). Questo causerà il rilascio della struttura tagSBINFO della finestra; la finestra stessa non verrà rilasciata immediatamente perché il suo conteggio dei riferimenti è ancora utilizzato dalla chiamata originale, ma tagSBINFO non ha un meccanismo di conteggio dei riferimenti, quindi viene rilasciato immediatamente.
A questo punto, abbiamo attivato il bug. Anche se non abbiamo riallocato il blocco heap contenente tagSBINFO, possiamo comunque scrivere i due bit che indicano la disabilitazione nell'heap già rilasciato. Il passo successivo è sostituire il blocco heap rilasciato con qualcosa che vogliamo, in modo da poter fare cose più interessanti che impostare solo un paio di bit. Per questo, dobbiamo comprendere alcuni concetti di base sull'heap del desktop.
win32k.sys utilizza l'heap del desktop per memorizzare gli oggetti GUI associati a un determinato desktop. Include oggetti finestra e le loro strutture correlate, come elenchi di proprietà, testo della finestra, barre di scorrimento. L'articolo di Tarjei ne parla, ma è importante notare che l'heap del desktop è in realtà solo una versione semplificata di un allocatore di backend in modalità utente, e utilizza anche RtlAllocateHeap() e RtlHeapFree() per le operazioni. L'heap del desktop è gestito da una struttura _HEAP e, poiché non c'è un allocatore front-end, non ci sono LFH, lookaside list o simili.
Ogni volta che viene creato un desktop, viene creato un heap del desktop corrispondente per servirlo. Ciò significa che possiamo allocare un nuovo desktop per ottenere un heap del desktop "pulito", su cui le nostre operazioni sono più prevedibili. Tuttavia, questo non è significativo per i processi con privilegi di integrità bassi, poiché a tali processi non è consentito creare un nuovo desktop.
Ora il problema principale è tracciare il processo di allocazione (maggiori dettagli sui metadati, ecc., saranno trattati in seguito).
Per monitorare le allocazioni e deallocazioni nell'heap del desktop, utilizzo script WinDbg:
Monitoraggio heap a 64 bit
ba e 1 nt!RtlFreeHeap ".printf\"RtlFreeHeap(%p, 0x%x, %p)\", @rcx, @edx, @r8; .echo ; gc";
ba e 1 nt!RtlAllocateHeap "r @$t2 = @r8; r @$t3 = @rcx; gu; .printf \"RtlAllocateHeap(%p, 0x%x):\", @$t3, @$t2; r @rax; gc";
Monitoraggio heap a 32 bit
ba e 1 nt!RtlAllocateHeap "r @$t2 = poi(@esp+c); r @$t3 = poi(@esp+4); gu; .printf \"RtlAllocateHeap(%p, 0x%x):\", @$t3, @$t2; r @eax; gc";
ba e 1 nt!RtlFreeHeap ".printf\"RtlFreeHeap(%p, 0x%x, %p)\", poi(@esp+4), poi(@esp+8), poi(@esp+c); .echo ; gc"
Oltre a questi script di debug, poiché l'heap del desktop è solo una forma semplificata dell'allocatore di backend in modalità utente, possiamo anche usare il comando !heap integrato in WinDbg.
Per sfruttare questo bug, abbiamo bisogno di sostituire l'ultimo blocco heap di tagSBINFO rilasciato, e sappiamo anche come sfruttare questi bug tipici, ovvero corrompere i dati adiacenti. Questo ci impone di preallocare alcuni blocchi heap vicino alla struttura da corrompere. Per prevedere dove verrà allocato un blocco heap, dobbiamo controllare il layout dell'intero heap (o il più possibile). Per soddisfare questo, un metodo praticabile è allocare quanti più blocchi heap possibile per riempire quelli già rilasciati, in modo che i blocchi appena allocati siano contigui. Quando abbiamo bisogno di un buco, possiamo scavarlo in una posizione prevedibile (rilasciando blocchi heap già allocati).
Questa parte è una semplice comprensione dei fattori che influenzano l'allocazione; gli script WinDbg sopra possono aiutarci. Tarjei, nella sua presentazione su win32k, ha menzionato gli oggetti principali allocati nell'heap del desktop, che corrispondono esattamente a ciò che ho visto. Questi sono:
L'heap del desktop è piuttosto interessante: la maggior parte delle allocazioni sono direttamente collegate agli oggetti finestra e gestite tramite la struttura tagWND, il che significa che se vogliamo allocare un blocco heap di dimensione arbitraria (il cosiddetto "piccolo buco da riempire con piccolo blocco"), dobbiamo prima allocare una finestra correlata. Si può pensare che la struttura della finestra sia l'interfaccia per l'allocazione dell'heap. Un'altra cosa interessante è che molti blocchi heap allocati tramite operazioni sulle finestre non possono essere rilasciati immediatamente a meno che la finestra stessa non venga distrutta, il che ovviamente ha un impatto sull'heap. Infine, supponiamo di allocare un blocco heap di dimensione N tramite una finestra, come nell'esempio sopra: dobbiamo allocare molti blocchi heap di dimensione N? È certo che la struttura della finestra allocata, indipendentemente dalla dimensione, non è memorizzata in una lista. Pertanto, ogni finestra può controllare un'allocazione heap di dimensione N. Cioè, se hai bisogno di allocare molti blocchi heap di dimensione N, devi prima creare molte finestre e usare le finestre per coadiuvare l'allocazione dei blocchi.
Ci sono anche tre tipi di dati importanti allocati nell'heap del desktop che possiamo sfruttare indirettamente tramite gli oggetti finestra per controllare i dati sull'heap. Utilizziamo ampiamente questi tipi di dati per realizzare lo sfruttamento e costruire un layout heap. Questi tre tipi sono:
La Figura 2 mostra le relazioni tra questi tipi di dati:

Per inizializzare l'heap, ho creato molte strutture tagWND (creando oggetti finestra). Questo può riempire molti grandi buchi nell'heap e fornisce anche l'interfaccia per allocare altri blocchi heap necessari. Su win8 e win8.1, l'allocazione di una nuova finestra causa l'allocazione automatica di una struttura tagPROPLIST (osservabile tramite gli script WinDbg menzionati). Su win7 e versioni precedenti, allocavamo noi stessi un nuovo tagPROPLIST per riempire piccoli buchi.
Qui, tutti gli oggetti finestra che abbiamo sprayato non hanno una stringa di testo della finestra; tuttavia, se necessario, possiamo comunque usarla per allocare o rilasciare blocchi heap di dimensioni arbitrarie. Una volta creata, non è possibile rimuovere un elenco di proprietà esistente (property list) a meno che la finestra non venga distrutta, ma possiamo controllare la riallocazione di questo elenco per ospitare nuove proprietà; questo meccanismo può essere usato per scavare buchi in posizioni precedenti. Tutto ciò che serve è impostare una nuova proprietà che non esisteva nell'elenco precedente (distinta per atomKey).
È interessante notare che l'heap del desktop è mappato nello spazio utente, sebbene sia di sola lettura. Ciò significa che possiamo verificare il layout heap che abbiamo costruito e assicurarci che funzioni correttamente. Per prima cosa dobbiamo determinare dove l'heap del desktop è mappato in spazio utente. Nell'articolo di Tarjei su win32k viene menzionato. TEB contiene una struttura non documentata chiamata Win32ClientInfo correlata, la cui definizione approssimativa è:
typedef struct _CLIENTINFO {
ULONG_PTR CI_flags;
ULONG_PTR cSpins;
DWORD dwExpWinVer;
DWORD dwCompatFlags;
DWORD dwCompatFlags2;
DWORD dwTIFlags;
PDESKTOPINFO pDeskInfo;
ULONG_PTR ulClientDelta; // incompleto. vedere reactos
} CLIENTINFO, *PCLIENTINFO;
La struttura PDESKTOPINFO è definita come:
typedef struct _DESKTOPINFO {
PVOID pvDesktopBase;
PVOID pvDesktopLimit; // incompleto. vedere reactos
} DESKTOPINFO, *PDESKTOPINFO;
Il primo campo pvDesktopBase punta all'indirizzo in spazio kernel dell'heap del desktop, lo annotiamo. Il campo ulClientDelta di Win32ClientInfo è la differenza tra l'indirizzo kernel e l'indirizzo utente; con queste informazioni possiamo ottenere ciò che vogliamo.
Tuttavia, non vogliamo analizzare da soli la struttura dell'heap; preferiamo avere un handle di user32, come il valore di un HWND, che possa essere convertito in un indirizzo di mapping utente, in modo da poter determinare se è associato ad altre allocazioni heap. Per trovare questo handle, dobbiamo trovare una struttura chiamata gShared, che di solito si trova in uer32.dll e, a partire da win7, è esportata, quindi può essere facilmente trovata.
Su la maggior parte dei sistemi, questa struttura è definita come segue:
kd> dt !tagSHAREDINFO
win32k!tagSHAREDINFO
+0x000 psi : Ptr32 tagSERVERINFO
+0x004 aheList : Ptr32 _HANDLEENTRY
+0x008 HeEntrySize : Uint4B
+0x00c pDispInfo : Ptr32 tagDISPLAYINFO
+0x010 ulSharedDelta : Uint4B
+0x014 awmControl : [31] _WNDMSG
+0x10c DefWindowMsgs : _WNDMSG
+0x114 DefWindowSpecMsgs : _WNDMSG
Nella struttura sopra, aheList punta a un array di _HANDLEENTRY, ogni _HANDLEENTRY contiene un handle che punta a un indirizzo in spazio kernel. Possiamo ottenere un indirizzo utente utilizzabile attraverso "differenza tra indirizzo kernel e indirizzo utente". Sfortunatamente, questo non è possibile su versioni precedenti a win7, perché gSharedInfo non è esportato. L'articolo di Tarjei dice che la funzione non documentata CsrClientConnectToServer può essere utilizzata per ottenere una copia di gSharedInfo, ma non ho trovato un esempio funzionante. Fastidiosamente, questa funzione richiede una struttura la cui lunghezza varia a seconda del sistema, quindi, secondo la mia esperienza, non puoi fidarti completamente di ciò che vedi in ReactOS.
Una volta calcolata la posizione del mapping, possiamo costruire una funzione che ci dice la posizione di un oggetto finestra nell'heap del desktop. Quindi, se vogliamo sapere dove è stato allocato il blocco heap corrispondente all'elenco di proprietà o alla stringa di testo, basta analizzare la struttura in spazio utente.
Ora siamo finalmente vicini allo sfruttamento di questa vulnerabilità. Abbiamo un metodo per controllare i blocchi heap, un metodo per verificare la corretta posizione dei blocchi e possiamo attivare il bug. Quindi ora possiamo finalmente sostituire il blocco heap di tagSBINFO rilasciato con un tagPROPLIST scelto. Nota che tagPROPLIST è solo un'intestazione di un grande elenco, quindi possiamo far sì che la dimensione dell'elenco corrisponda a quella del blocco della barra di scorrimento. La parte successiva di tagPROPLIST è essenzialmente un array di strutture tagPROP, o elenco di proprietà; pertanto, non distinguerò tra array ed elenco. La struttura tagPROPLIST su sistemi a 64 bit è così:
kd> dt -b !tagPROPLIST
win32k!tagPROPLIST
+0x000 cEntries : Uint4B
+0x004 iFirstFree : Uint4B
+0x008 aprop : tagPROP
+0x000 hData : Ptr64
+0x008 atomKey : Uint2B
+0x00a fs : Uint2B
Come accennato in precedenza, un oggetto finestra ha un elenco di proprietà ad esso associato. Questo elenco viene creato tramite la funzione SetProp(). Viene utilizzato per cercare una proprietà esistente in base a atomKey; se la proprietà non esiste, viene creata una nuova voce nell'elenco delle proprietà. Se non esiste alcun elenco di proprietà, ne viene creato uno e collegato alla struttura tagWND.
Se abbiamo già sprayato un tagWND con bug e creato le relative voci tagPROPLIST, il layout finale sarà come mostrato in Figura 3:

Una volta impostato, possiamo allocare il controllo barra di scorrimento che vogliamo sfruttare. Ciò porterà al risultato mostrato in Figura 4:

Quindi, manipolando la barra di scorrimento, attiviamo l'hook nella callback in modalità utente; nella funzione hook, proviamo a rilasciare la struttura tagSBINFO distruggendo la finestra. Questo porta alla situazione in Figura 5:

Su 64 bit, la struttura tagSBINFO è di 0x28 byte; una voce di array tagPROPLIST è di 0x18 byte, di cui 0x10 byte sono il tagPROP predefinito. Quindi, un elenco di proprietà con due voci sarebbe 0x28 byte (0x8 + 0x10 + 0x10), una coincidenza perfetta. Supponiamo di aver già sprayato la memoria in modo da riempire il buco. Abbiamo solo bisogno di una finestra con un elenco di proprietà; subito dopo il rilascio della struttura tagSBINFO (come mostrato nella figura precedente), aggiungiamo una nuova voce all'elenco di proprietà. Il processo consiste prima nel rilasciare il blocco heap tagPROPLIST di 0x18 byte; poiché l'heap è già stato sprayato, non ci sono blocchi heap liberi nelle vicinanze, quindi non si verifica alcuna fusione di blocchi e non c'è spazio sufficiente per il nuovo blocco di 0x28 byte. Pertanto, la posizione appena rilasciata di tagSBINFO (che ha esattamente la dimensione di 0x28 byte) viene utilizzata, come mostrato in Figura 6:

Dopo essere tornati dalla funzione hook di callback, l'UAF viene attivato e alcuni bit vengono scritti nel campo cEntries di tagPROPLIST. Il valore originale di cEntries era 0x2, indicando che avevamo creato due voci nell'elenco di proprietà. Dopo l'overflow, diventa 0xe, con i bit 3 e 4 (contando da 1) impostati a 1.
A questo punto, abbiamo completato l'overflow del nuovo heap e aumentato il numero di voci dell'elenco di proprietà a più di 0xc. Successivamente, eseguiremo l'overflow del blocco heap adiacente, che chiamiamo corruzione di Fase 2.
Nel blog di Udi si spiega solo questo. Fino a questo punto è chiamato "tipico heap overflow"; tuttavia, secondo la mia esperienza, è molto difficile da questo punto realizzare lettura/scrittura arbitraria o esecuzione di codice. Osserviamo ancora la struttura tagPROPLIST su 64 bit:
kd> dt -b !tagPROPLIST
win32k!tagPROPLIST
+0x000 cEntries : Uint4B
+0x004 iFirstFree : Uint4B
+0x008 aprop : tagPROP
+0x000 hData : Ptr64
+0x008 atomKey : Uint2B
+0x00a fs : Uint2B
La corruzione di Fase 1 ci ha fornito un array tagPROPLIST corrotto, permettendoci di aumentare le voci tagPROP. tagPROPLIST ha solo due campi:
Quando viene inserita una nuova voce nell'elenco, una funzione viene chiamata per scorrere ogni voce fino a trovare un valore di iFirstFree appropriato. Se non viene trovato, si controlla se iFirstFree è maggiore di cEntries. Se la voce corrispondente a atomKey non è nell'elenco, si controlla se iFirstFree != cEntries. Se non è uguale, viene inserita una voce nella posizione dell'indice iFirstFree; se è uguale, viene allocato un nuovo elenco di proprietà che possa contenere la proprietà da inserire, e le voci originali vengono copiate e la nuova voce viene inserita.
Il campo atomKey corrisponde a LPCTSTR lpString. Come documentato in MSDN per SetProp(), il chiamante può passare un puntatore a stringa o un valore atom a 16 bit. Quando viene passato un puntatore a stringa, viene automaticamente convertito in un valore atom prima di essere memorizzato nell'elenco di proprietà. Poiché possiamo passare qualsiasi valore atom a SetProp(), abbiamo la capacità di controllare questi due byte, ma con alcune limitazioni. Vale a dire, i dati corrotti di atomKey non devono duplicare un atom già presente, altrimenti impostando una nuova proprietà, sostituirebbe quella esistente con lo stesso valore atom. Inoltre, il campo fs non è controllabile; il suo valore è 0 se atomKey < 0xBFFF, che corrisponde a un valore atom intero. Il valore di fs è 2 se atomKey >= 0xC000.
Un'altra cosa da notare è che tagPROP è solo 0xc byte. Questa struttura su sistemi a 64 bit è allineata a 0x10 byte, quindi quando viene inserita una voce tagPROP, ci sono 4 byte extra che non possono essere corrotti. L'ultimo punto importante è che i primi 8 byte di un blocco heap tagPROPLIST definiscono la dimensione delle voci dell'elenco, il che significa che ogni nuova voce tagPROP inserita sarà sempre scritta a un indirizzo allineato a 8 byte.
Per ogni tagPROP inserito, su sistemi a 64 bit la situazione è:
* Offset 0x0: 8 byte di dati arbitrariamente controllabili (hData)
* Offset 0x8: 2 byte per lo più controllabili (atomKey)
* Offset 0xa: 2 byte non controllabili (fs)
* Offset 0xc: 4 byte non modificabili (padding)
Questa situazione è molto meglio di 2 bit, ma non è ancora perfetta. A meno che non possiamo sovrascrivere qualcosa con i primi 8 byte che provengono dal campo hData completamente controllabile, saremo molto limitati. Se dobbiamo scrivere in campi più profondi di una struttura adiacente, non possiamo evitare di corrompere alcuni valori in modo incontrollabile. Ho speso un po' di tempo a cercare vari oggetti nell'heap del desktop; considerando le limitazioni di corruzione precedenti, l'unico modo che ho trovato per aggirare queste limitazioni e realizzare lettura/scrittura arbitraria è corrompere il campo strName di tagWND, che è una struttura _LARGE_UNICODE_STRING:
kd> dt !_LARGE_UNICODE_STRING
win32k!_LARGE_UNICODE_STRING
+0x000 Length : Uint4B
+0x004 MaximumLength : Pos 0, 31 Bits
+0x004 bAnsi : Pos 31, 1 Bit
+0x008 Buffer : Ptr64 Uint2B
Se riusciamo a corrompere il campo Buffer di questa struttura, possiamo leggere e scrivere fino a MaximumLength byte da un dato indirizzo manipolando il testo della finestra. Questo è quello che volevo fare. Potresti aver notato questa struttura in capitoli precedenti, riguardanti come creare un blocco heap di dimensione e valore arbitrari sull'heap del desktop; quindi la stessa situazione può essere applicata qui.
Ora, sappiamo come usare le voci dell'elenco tagPROPLIST per corrompere i dati, quali parti possiamo controllare e, cosa più importante, le limitazioni che dovremo affrontare, che differiscono tra 32 e 64 bit. Ciò che abbiamo fatto su 64 bit non funziona su 32 bit. Presto passeremo dalla corruzione di Fase 2 (cioè scrivere dati tramite la struttura tagPROP) a un'altra "primitiva di operazione" di corruzione attraverso la quale possiamo scrivere dati completamente controllabili; questa è quella che chiamo corruzione di Fase 3.
L'obiettivo del piano è corrompere il campo strName in un tagWND adiacente. Sappiamo già che è una struttura _LARGE_UNICODE_STRING, ma diamo un'occhiata a più dettagli della struttura tagWND, che appare così:Di seguito è la struttura a 64 bit, possiamo vedere che l'offset della struttura _LARGE_UNICODE_STRING che vogliamo sovrascrivere è 0xd8. Noterai anche un campo importante all'inizio di questa struttura. Inizialmente speravo di poterlo manipolare liberamente, ma in _THRDESKHEAD ci sono molti puntatori che richiedono attenzione e, sfortunatamente, non possiamo controllare dove scriviamo, a causa delle limitazioni discusse in precedenza.
Struttura _THRDESKHEAD:
kd> dt !_THRDESKHEAD
win32k!_THRDESKHEAD
+0x000 h : Ptr64 Void
+0x008 cLockObj : Uint4B
+0x010 pti : Ptr64 tagTHREADINFO
+0x018 rpdesk : Ptr64 tagDESKTOP
+0x020 pSelf : Ptr64 UChar
Il problema con _THRDESKHEAD non solo ci confonde, ma ci costringe a riconsiderare il vincolo di allineamento. La nuova voce della lista tagPROP, indipendentemente dall'offset, la nostra scrittura sovrascriverà direttamente l'inizio di _LARGE_UNICODE_STRING:
win32k!_LARGE_UNICODE_STRING
+0x000 Length <-- hData (completamente controllabile) sovrascrive qui
+0x004 MaximumLength <-- e qui
+0x004 bAnsi <-- e qui
+0x008 Buffer <-- atomKey e fs (parzialmente controllabili) sovrascrivono qui
È chiaro che vogliamo sovrascrivere il puntatore Buffer per accedere a memoria a indirizzi arbitrari; tuttavia, anche se riusciamo ad attaccare in sicurezza gli altri campi di questa struttura, non possiamo controllare il puntatore di cui abbiamo bisogno.
Non possiamo corrompere dati arbitrari. La soluzione a questo problema non è più la corruzione di tagPROPLIST, ma diventa un meccanismo di corruzione completamente diverso.
Nelle versioni di Windows successive a XP, l'intestazione del blocco heap (_HEAP_ENTRY) dell'allocatore backend in modalità utente (come l'heap del desktop del kernel) è memorizzata sull'heap e si trova prima del contenuto effettivo del blocco. L'heap del desktop stesso è gestito dalla struttura _HEAP, che ci dà una certa libertà nello sfruttamento di questo blocco.
La struttura _HEAP_ENTRY è definita come segue:
kd> dt !_HEAP_ENTRY
ntdll!_HEAP_ENTRY
+0x000 PreviousBlockPrivateData : Ptr64 Void
+0x008 Size : Uint2B
+0x00a Flags : UChar
+0x00b SmallTagIndex : UChar
+0x00c PreviousSize : Uint2B
+0x00e SegmentOffset : UChar
+0x00f UnusedBytes : UChar
L'intestazione del blocco heap è di 0x10 byte in totale. I primi 8 byte sono PreviousBlockPrivateData, che quando la dimensione richiesta supera il normale 0x10 (meno di 8 byte allineati a 8 byte) viene utilizzato per contenere i dati effettivi del blocco precedente. Questo è brevemente descritto nell'articolo del blog di Leviathan e anche in articoli precedenti sull'heap in modalità utente. Size e PreviousSize rappresentano la dimensione del blocco corrente e quella del blocco precedente, in unità di 0x10 byte. Flags indica se il blocco è libero, ecc. Se la modalità sicura _HEAP_ENTRY è attivata in _HEAP, SmallTagIndex conterrà un checksum XOR dei dati nel blocco.
Sebbene il vincolo di allineamento sia sfavorevole, esiste. Se chiami tagPROPLIST, è sempre di almeno 0x18 byte, e poi aggiungi 0x10 byte per tagPROP. Un tagPROPLIST di 0x28 byte con due voci sarà posizionato in un blocco heap di 0x20 byte, e i byte extra rappresentati da PreviousBlockPrivateData utilizzano il blocco heap adiacente. Ciò significa che quando aggiungiamo una terza voce, il blocco heap adiacente viene corrotto, e gli 8 byte controllabili di hData sovrascriveranno la parte superiore di _HEAP_ENTRY.
Quello che vogliamo fare è sfruttare questo per poter scrivere dati arbitrari nella posizione superiore di Buffer in qualche modo. Innanzitutto, modifichiamo il layout dell'heap in modo da avvicinarci al blocco heap tagPROPLIST corrotto. Durante il controllo, abbiamo un piccolo blocco heap che contiene una stringa di testo correlata alla finestra, che chiameremo "blocco di copertura". Adiacente a questo "blocco di copertura", posizioniamo un tagWND in modo da corrompere questo tagWND. La figura 7 illustra questo processo. Nota che abbiamo omesso i blocchi heap spruzzati in precedenza per risparmiare spazio, quindi ora dovrebbero essere considerati impliciti.

Successivamente, inseriamo una terza tagPROP nella lista di tagPROPLIST, che sovrascriverà gli ultimi 8 byte di _HEAP_ENTRY e i primi 8 byte del "blocco di copertura". In questo modo possiamo modificare l'_HEAP_ENTRY del "blocco di copertura" in modo che la sua dimensione sia maggiore di quella effettiva, sufficiente a contenere la struttura tagWND adiacente.
Ora rilasciamo il "blocco di copertura" appena corrotto, in modo che il gestore dell'heap lo inserisca nella lista libera corrispondente a blocchi di dimensione (ogni lista libera corrisponde a una dimensione fissa) maggiore della dimensione effettiva del "blocco di copertura". Quindi riutilizziamo questo blocco modificando il testo della finestra (che possiamo controllare completamente). Tuttavia, c'è un piccolo problema da risolvere. Quando il "blocco di copertura" viene rilasciato, il gestore dell'heap cerca il blocco adiacente precedente, a seconda del campo Size corrotto. Il gestore dell'heap controlla se questo blocco adiacente è libero per unirlo. In ogni caso, vogliamo controllarlo e impostare un flag di "in uso". Modificando leggermente il nostro layout dell'heap possiamo raggiungerlo. A questo punto, posizioniamo un falso blocco heap la cui intestazione ha il flag "in uso" e il cui campo PreviousSize è impostato al valore di Size corrotto, in modo da poterlo ottenere semplicemente allocando il testo di un'altra finestra. Il nuovo layout dell'heap è il seguente (Figura 8):

Ora possiamo rilasciare il "blocco di copertura" corrotto aggiornando la stringa di testo della finestra associata in modo che sia più lunga degli originali 0x10 byte. In questo modo, il "blocco di copertura" corrotto viene prima rilasciato e inserito nella lista libera, ma la sua dimensione è corrotta, dichiarata maggiore di quella effettiva. Questa dimensione può essere regolata secondo le nostre necessità. Quindi i nostri dati stringa vengono scritti in questo "blocco di copertura", e poi usiamo questo "blocco di copertura" per corrompere l'tagWND adiacente con dati arbitrari. Come segue (Figura 9):

Questa è la corruzione della fase 3. Ora possiamo sovrascrivere il puntatore strName.Buffer con qualsiasi dato desiderato. Tuttavia, corrompere altri dati di tagWND presenta ancora qualche difficoltà, ma non è un problema perché l'heap del desktop è mappato nello spazio utente! Quindi, prima di corrompere qualsiasi cosa, leggiamo tutto il contenuto di tagWND, modifichiamo il contenuto della struttura strName come desiderato, e inviamo tutti i dati modificando il testo della finestra.
strName ci fornisce non solo la "primitiva" di lettura/scrittura arbitraria, ma possiamo anche modificare ripetutamente strName, cosa consentita dal meccanismo di modifica del testo della finestra. Finché la lunghezza della stringa scritta non supera il valore di MaximumLength, possiamo continuare a utilizzare lo stesso blocco heap. Quindi, ogni volta che vogliamo modificare l'indirizzo di strName per leggere un valore da qualche parte, alleghiamo una nuova stringa e aggiungiamo i nostri dati per aggiornare il "blocco di copertura". Questo riutilizzo è mostrato nella Figura 10. Nota che ho nuovamente ingrandito la granularità della vista a volo d'uccello per mostrare in dettaglio ogni corruzione.

Ciò significa che alla fine dobbiamo corrompere solo due cose aggiuntive (oltre all'elemento della lista tagPROPLIST originale):
tagPROPLIST semplicemente inviando l'atomKey usato per corrompere nella posizione originale!strName, che possiamo modificare facilmente scrivendo il testo della finestra. Durante l'operazione, dobbiamo impostare il valore di ritorno a vuoto.Ora, se vogliamo leggere alcuni byte da un punto della memoria, interroghiamo il testo della finestra tramite la funzione InternalGetWindowText(), dove c'è l'strName corrotto. Possiamo leggere il numero di byte dichiarato dal campo Length. Allo stesso modo, se vogliamo scrivere in una posizione arbitraria della memoria, usiamo la funzione NtUserDefSetText() per aggiornare il testo della finestra corrotto, ma la quantità scritta non deve superare il valore dichiarato da MaximumLength (anche questo impostabile). In questo modo il buffer esistente viene riutilizzato e punta all'indirizzo di memoria desiderato.
Sebbene l'allocatore backend in modalità utente utilizzi la codifica heap a partire da Windows Vista, l'heap del desktop non l'ha mai attivata fino a Windows 8. Quindi, sui sistemi Windows 8 e successivi, si presenta un ostacolo quando eseguiamo la sovrascrittura del "blocco di copertura". Tuttavia, la struttura _HEAP che contiene l'heap include questo cookie e lo usa per codificare l'intera intestazione del blocco heap; quindi possiamo leggere questo cookie dall'heap del desktop mappato nello spazio utente e poi usarlo per codificare l'intestazione del "blocco di copertura". Il metodo di codifica è ottenuto attraverso il reverse engineering del codice dell'allocatore, imitando le sue operazioni, e l'allocatore accetterà questa operazione.
Prima di tutto, notiamo che la struttura tagPROP sui sistemi a 32 bit è di 8 byte, non 0xc byte come sui sistemi a 64 bit, e il campo hData che controlliamo è di soli 4 byte, non 8 byte come sui sistemi a 64 bit. Inoltre non ci sono byte di riempimento extra; sui sistemi a 64 bit ci sono 8 byte di riempimento, quindi l'intera struttura è esattamente di 8 byte. Ciò significa che non possiamo corrompere completamente l'intestazione del blocco heap adiacente se possiamo controllare solo parzialmente i dati. Su alcune versioni di Windows è possibile perché possiamo controllare i campi più importanti, ma su Windows 8 e 8.1 l'intestazione dell'heap è codificata, e alla fine possiamo sovrascrivere in modo insicuro una parte dell'intestazione dell'heap tramite il campo fs. L'intestazione _HEAP_ENTRY a 32 bit sembra simile, ma manca del campo PreviousBlockPrivateData.
Non possiamo comunque corrompere tutte le parti di tagWND a causa dell'inevitabile troncamento dei puntatori. E non ho ancora trovato un oggetto adatto a questo. Dato che _LARGE_UNICODE_STRING funziona bene sui sistemi a 64 bit, voglio usarlo anche sui sistemi a 32 bit.
La mia idea è che se possiamo corrompere il campo iFirstFree della struttura tagPROPLIST (l'indice della prima proprietà liberata nella lista) aumentando il valore dell'indice, possiamo farlo puntare a una posizione più lontana sull'heap. Ad esempio, possiamo farlo puntare all'inizio di tagWND.strName. La Figura 11 mostra questa idea:

Per chiarezza del processo, ora usiamo due strutture tagPROPLIST: "Proprietà A" per UAF e "Proprietà B". Dobbiamo sapere esattamente quali parti della tagPROP inserita in "Proprietà A" sovrascriveranno il campo iFirstFree di "Proprietà B". Dobbiamo anche ricordare che possiamo scrivere solo 8 byte alla volta, quindi dobbiamo inserire almeno una tagPROP extra in "Proprietà A": la prima per corrompere l'intestazione dell'heap adiacente, la seconda per colpire il campo tagPROPLIST di "Proprietà B". Queste potrebbero variare a seconda del sistema operativo e della diversa dimensione dei blocchi heap, e nel mio exploit devo adattarmi a vari layout dell'heap. La Figura 12 mostra come corrompiamo. Nota che nella figura la prima tagPROPLIST non è suddivisa in campi separati, quindi tagPROP[0] è implicito. Tuttavia, nella seconda tagPROPLIST, i suoi membri interni sono separati per mostrare il nostro processo di corruzione. Ecco perché tagPROP[0] è visualizzato:

Prima di tutto notiamo che se scriviamo 8 byte per ogni tagPROP, ciò significa che possiamo controllare solo parzialmente la sovrascrittura di iFirstFree (poiché proviene dai campi atomKey e fs), che è ciò che ci interessa maggiormente. Poiché possiamo controllare completamente almeno due byte chiave tramite il valore di atomKey, quando questo valore è sufficientemente piccolo, il campo fs diventa 0. Quindi usiamo il valore di hData per sovrascrivere cEntries con un valore ragionevole e usiamo atomKey per far puntare iFirstFree a tagWND, dove si trova il puntatore strName.Buffer che vogliamo sovrascrivere. Se non possiamo sovrascrivere direttamente i valori di Length e , possiamo preallocare una stringa alla finestra di destinazione per garantire che la sua lunghezza sia già impostata a un certo valore.
Diamo un'occhiata alla struttura tagWND a 32 bit per vedere cosa possiamo ottenere. Nota che questa volta uso il parametro -b per calcolare facilmente l'offset di Buffer in strName.
kd> dt -b !tagWND
win32k!tagWND
+0x000 head : _THRDESKHEAD
+0x000 h : Ptr32
+0x004 cLockObj : Uint4B
+0x008 pti : Ptr32
+0x00c rpdesk : Ptr32
+0x010 pSelf : Ptr32
+0x014 state : Uint4B
+0x014 bHasMeun : Pos 0, 1 Bit
[SNIPPED FLAGS]
+0x014 bDestroyed : Pos 31, 1 Bit
+0x018 state2 : Uint4B
[SNIPPED FLAGS]
+0x018 bWMCreateMsgProcessed : Pos 31, 1 Bit
+0x01c ExStyle : Uint4B
+0x01c bWS_EX_DLGMODALFRAME : Pos 0, 1 Bit
[SNIPPED FLAGS]
+0x01c bUIStateFocusRectHidden : Pos 31, 1 Bit
+0x020 style : Uint4B
+0x020 bReserved1 : Pos 0, 16 Bits
[SNIPPED FLAGS]
+0x020 bWS_POPUP : Pos 31, 1 Bit
+0x024 hModule : Ptr32
+0x028 hMod16 : Uint2B
+0x02a fnid : Uint2B
+0x02c spwndNext : Ptr32
+0x030 spwndPrev : Ptr32
+0x034 spwndParent : Ptr32
+0x038 spwndChild : Ptr32
+0x03c spwndOwner : Ptr32
+0x040 rcWindow : tagRECT
+0x000 left : Int4B
+0x004 top : Int4B
+0x008 right : Int4B
+0x00c bottom : Int4B
+0x050 rcClient : tagRECT
+0x000 left : Int4B
+0x004 top : Int4B
+0x008 right : Int4B
+0x00c bottom : Int4B
+0x060 lpfnWndProc : Ptr32
+0x064 pcls : Ptr32
+0x068 hrgnUpdate : Ptr32
+0x06c ppropList : Ptr32
+0x070 pSBInfo : Ptr32
+0x074 spmenuSys : Ptr32
+0x078 spmenu : Ptr32
+0x07c hrgnClip : Ptr32
+0x080 hrgnNewFrame : Ptr32
+0x084 strName : _LARGE_UNICODE_STRING
+0x000 Length : Uint4B
+0x004 MaximumLength : Pos 0, 31 Bits
+0x004 bAnsi : Pos 31, 1 Bit
+0x008 Buffer : Ptr32
+0x090 cbwndExtra : Int4B
+0x094 spwndLastActive : Ptr32
+0x098 hImc : Ptr32
+0x09c dwUserData : Uint4B
+0x0a0 pActCtx : Ptr32
+0x0a4 pTransform : Ptr32
+0x0a8 spwndClipboardListenerNext : Ptr32
+0x0ac ExStyle2 : Uint4B
+0x0ac bClipboardListener : Pos 0, 1 Bit
[SNIPPED FLAGS]
+0x0ac bChildNoActivate : Pos 11, 1 Bit
L'offset di strName è 0x84, l'offset di Buffer è 0x8c. Sappiamo di avere l'indice dell'elemento della lista tagPROP e che possiamo scrivere 8 byte. Quindi possiamo facilmente determinare se iFirstFree indicizza all'offset 0x88 della finestra puntato da MaximumLength. Poiché possiamo controllare solo due byte di Buffer, la scrittura non è fattibile, e dato che il nostro obiettivo è usare questa come "primitiva" di lettura/scrittura arbitraria, questo risultato è inaccettabile. Se scriviamo un indice che punta a 0x90, sovrascriveremo cbwndExtra, che non è ciò che cerchiamo.
Rivisitando ciò che possiamo controllare nel feng shui dell'heap precedente, esaminiamo se ci sono offset interessanti in tagWND che possiamo controllare. All'offset 0x70 in tagWND si trova il campo pSBInfo. Questo offset è divisibile per 8, quindi possiamo sovrascrivere questo puntatore con parte dei dati di hData di una falsa tagPROP.
Possiamo sovrascrivere pSBInfo in modo che punti direttamente a strName nella stessa struttura tagWND? Forse possiamo usare le API della barra di scorrimento per corrompere strName ai nostri fini.
pSBInfo punta a una struttura tagSBINFO, menzionata nella fase iniziale UAF.
kd> dt -b !tagSBINFO
win32k!tagSBINFO
+0x000 WSBflags : Int4B
+0x004 Horz : tagSBDATA
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
+0x014 Vert : tagSBDATA
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
Ricordiamo che WSBflags non ci dà molto controllo, ma almeno sappiamo che viene impostato a 1 quando la barra di scorrimento è abilitata e a 0 quando è disabilitata. Questo campo di flag non può essere impostato a valori arbitrari; attraverso il reverse engineering delle funzioni correlate, scopriamo che senza cambiare lo stato della barra di scorrimento, questo campo rimane invariato. I valori nella struttura tagSBDATA sembrano più interessanti. Se leggiamo la documentazione di SetScrollInfo(), possiamo capire bene il significato di questi valori. Sembra che possiamo impostare parametri a SetScrollInfo() tramite la struttura SCROLLINFO. Finché c'è un controllo della barra di scorrimento vicino alla finestra che vogliamo corrompere, possiamo manipolare direttamente il puntatore pSBInfo (invierà un messaggio di finestra speciale al controllo finestra associato). Ovviamente possiamo controllare incondizionatamente i valori di posMin e posMax. I campi page e pos sono un po' problematici perché sono limitati a determinati intervalli; ora cerchiamo di evitarli. Impostiamo il flag nella struttura per dichiarare dove vogliamo impostare i valori minimo e massimo.
Vogliamo sovrascrivere Buffer con dati arbitrari, quindi vogliamo che posMin lo sovrascriva; quindi possiamo sovrascrivere pSBInfo in modo che punti a strName.MaximumLength. Finché non abilitiamo o disabilitiamo la barra di scorrimento, il campo WSBflags non viene sovrascritto, garantendo l'integrità di strName.MaximumLength. Ciò significa che qualsiasi valore impostiamo per posMin (tramite nMin di SCROLLINFO) sovrascriverà Buffer, e posMax verrà scritto su cbwndExtra. Non è un grosso problema; sui sistemi a 64 bit possiamo preleggere questo valore e ripristinarlo in seguito. L'idea generale del traboccamento è mostrata nella Figura 13:

Ora illustriamo il processo di attacco sui sistemi a 32 bit nella Figura 14. Prima di corrompere qualsiasi indirizzo lontano da UAF, facciamo un passo indietro per guardare i blocchi heap e il layout dell'heap correlati nella figura. Ora che conosciamo più dettagli, ciò che dobbiamo fare è ovvio.

Successivamente, inseriamo due voci di proprietà in "Proprietà A", che corromperà i dati vicino a "Proprietà A", grazie alla precedente corruzione UAF, e allo stesso tempo farà puntare iFirstFree di "Proprietà A" a pSBInfo. Nota che questo corromperà anche il valore di pSBInfo nelle vicinanze, ma possiamo preleggerlo per ripristinarlo dopo la corruzione.

Inseriamo una nuova tagPROP in "Proprietà B", il cui identificatore atom è diverso da quelli già presenti nella lista, in modo che questa tagPROP venga inserita nel successivo indice libero. Di conseguenza, pSBInfo viene corrotto per puntare a strName.MaximumLength nello stesso tagWND.

Infine, aggiorniamo la barra di scorrimento per corrompere il campo strName.Buffer (come mostrato nella Figura 17):

Da notare che, a differenza del caso a 64 bit, non possiamo corrompere il valore della lunghezza di strName. Possiamo preallocare una stringa di testo della finestra con una lunghezza adeguata in modo che il suo valore sia già in uso. Successivamente, sia che vogliamo leggere o scrivere dati da/verso un indirizzo del kernel, dobbiamo solo chiamare SetScrollInfo() sulla finestra di destinazione per aggiornare il valore di Buffer, e poi utilizzare le API del testo della finestra per le operazioni.
Ora abbiamo una "primitiva" di lettura/scrittura arbitraria riutilizzabile sui sistemi a 32 bit!
Da questo punto in poi, supponiamo di avere una "primitiva" di lettura/scrittura arbitraria. Pertanto, quando dico trapelare/leggere un valore o sovrascrivere un valore, mi riferisco all'esecuzione di questa "primitiva" stabilita nelle fasi di corruzione precedenti. Questa "primitiva" è sostanzialmente la stessa su entrambe le piattaforme. Non resta che sovrascrivere un puntatore a funzione e puntarlo a un payload di shellcode in qualche punto. L'approccio comune è sovrascrivere la seconda voce di nt!HalDispatchTable, che corrisponde alla funzione HalQuerySystemInformation(). Quindi, in modalità utente, chiamiamo la funzione NtQueryInternalProfile() per attivarla.
Dobbiamo conoscere l'indirizzo di base del modulo del kernel per calcolare l'indirizzo del kernel di nt!HalDispatchTable. A questo scopo, possiamo chiamare NtQuerySystemInformation() in modalità utente per ottenere le informazioni sui moduli, che includono l'indirizzo di base del modulo.
// Il valore enumerato 11 rappresenta SystemModuleInformation, non documentato...
rc = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)11, pModuleInfo, 0x100000, NULL);
Successivamente, carichiamo ntoskrnl.exe in modalità utente per trovare l'offset di nt!HalDispatchTable, in modo da poter ottenere il suo indirizzo nello spazio del kernel. Quindi usiamo la "primitiva di lettura" per leggere l'indirizzo del kernel di HaliQuerySystemInformation() (una funzione non esportata) per modificarlo, e poi usiamo la "primitiva di scrittura" per corrompere il puntatore di tale funzione in modo che punti all'indirizzo del shellcode (può essere nello spazio degli indirizzi del kernel o in quello utente, come descritto in seguito). Il numero di byte letti/scritti è lo stesso sui sistemi a 32 e 64 bit.
Windows 8 e 8.1 hanno introdotto il supporto per SMEP, e alcuni prodotti di sicurezza possono attivarlo anche su Windows 7, quindi assumiamo che sia sempre presente. SMEP impedisce l'esecuzione di codice con privilegi del kernel nello spazio utente, rendendo inutilizzabile la modifica della voce di nt!HalDispatchTable per puntare a un indirizzo dello spazio utente. Pertanto, desideriamo che punti a una posizione controllabile nello spazio del kernel, dove il codice possa modificare il registro cr4 per disabilitare SMEP, in modo da poter saltare allo spazio utente. L'articolo di MWR presenta un interessante trucco a 64 bit: mappando le proprie voci di tabella delle pagine, si ottiene un indirizzo valido nello spazio del kernel per qualsiasi indirizzo virtuale. Quindi, usando la "primitiva di scrittura" si modificano direttamente le voci della tabella delle pagine e si modificano i bit di maschera. Ho portato questo trucco sui sistemi a 32 bit, ma ci sono alcune differenze tra sistemi con PAE abilitata e non.
Il metodo ovvio per ottenere ciò è mappare un indirizzo dello spazio utente nello spazio del kernel, e poi usare la "primitiva di scrittura" per rendere la voce della tabella delle pagine con privilegi di sistema anziché utente. Questo è il primo passo. Quando ho implementato su Windows 8, ho incontrato un problema interessante. A partire da Windows 8, il gestore del desktop (dwm.exe) esegue periodicamente una scansione delle finestre sul desktop e ne interroga i nomi, per motivi che non ho indagato. Questa operazione non invia messaggi alla finestra, ma ha una funzione di gestione della finestra corrispondente che chiama GetInternalWindowText(). Pertanto, il problema è che quando si utilizzano i campi di strName di una finestra per sovrascrivere la voce della tabella delle pagine che contiene il shellcode, questa memoria appartiene alla tabella delle pagine del nostro stesso spazio di processo. Quando dwm.exe recupera il nome della finestra dal kernel, la voce modificata della tabella delle pagine fa sì che il kernel controlli se strName.Buffer è nullo, effettuando un riferimento indiretto a tale indirizzo, e se l'indirizzo non è valido, il sistema va in crash.
Per soddisfare la query di dwm.exe, ho usato un indirizzo del kernel come payload. In questo modo, indipendentemente da come il processo corrente carica qualsiasi cosa, la voce della tabella delle pagine associata a quell'indirizzo è sempre valida. Ho scelto di posizionarlo nell'heap del desktop, perché possiamo calcolarne l'indirizzo del kernel con il metodo descritto in precedenza. Tuttavia, mappiamo ancora la voce della tabella delle pagine da soli, e a questo punto la tabella delle pagine è già contrassegnata come alta autorità, ma non come eseguibile. Quindi dobbiamo solo impostare il bit di esecuzione.
Passaggi:
nt!HalDispatchTable in modo che punti all'indirizzo del kernel trovato nella fase 1.NtQueryInternalProfile() per saltare al payload.cr4 e saltare al payload nello spazio utente della fase 2.cr4 per prevenire il rilevamento di PatchGuard e pulire prima di ritornare.Su Windows 8.1 c'è un altro problema: NtQuerySystemInformation() controlla il valore SID a bassa integrità, il che significa che solo processi a integrità media o superiore possono ottenere l'indirizzo base del kernel. Questo può essere facilmente aggirato usando il noto trucco di sidt. Salviamo l'indirizzo dell'IDT in modalità utente (non richiede controlli di autorizzazione), poi usiamo la "primitiva di lettura" per leggere l'indice IDT desiderato, che di solito punta allo spazio degli indirizzi del kernel, quindi possiamo trapelare l'indirizzo del kernel del gestore di interrupt, e quindi cercare l'offset nel file PE del modulo del kernel corrispondente.
Una volta ottenuto l'indirizzo base di caricamento del kernel, possiamo calcolare l'indirizzo di nt!HalDispatchTable.
Il metodo usuale è caricare il file ntoskrnl.exe e interpretare l'offset dei suoi simboli, aggiungendo l'indirizzo base di caricamento trapelato. Tuttavia, questo non funziona in una sandbox con diritti elevati a causa delle limitazioni del file system: non è possibile leggere C:\windows\system32\ntoskrnl.exe. Per bypassare questa limitazione, usiamo la nostra "primitiva di trapelamento" per analizzare l'immagine PE del kernel in memoria e risolvere l'indirizzo del simbolo desiderato.
Questo è tutto il materiale. Grazie per la lettura. Utilizzando le tecniche presentate in questo articolo, sono riuscito a realizzare exploit stabili su sistemi a 32 e 64 bit: XP, Vista, 7, 8, 8.1 e Server 2012. Su Windows 2003 e 2008 non funziona per impostazione predefinita, poiché non è possibile hookare i callback in modalità utente, quindi non è possibile attaccare questi sistemi a meno che non siano soddisfatte le condizioni richieste. Il processo di exploit è piuttosto complesso, con molti ostacoli da superare, ma questo ha fornito anche molto divertimento e cose da imparare; molti dei metodi e dei risultati di ricerca utilizzati in questo articolo sono già stati menzionati in articoli di altri ricercatori. Per quanto ne so, l'unica mitigazione che può impedire lo sfruttamento di win32k.sys è quella utilizzata da Google Chrome sandbox, che blocca efficacemente le chiamate di sistema del kernel win32k in esecuzione. Spero in qualsiasi miglioramento o feedback; se ci sono carenze nelle tecniche che ho presentato, fatemelo sapere e aggiornerò questo documento. Puoi contattarmi su Twitter @fidgetingbits o via email [email protected].
MaximumLengthSIF_RANGESCROLLINFO