
Traduction d'article : exploitation de la vulnérabilité CVE-2015-0057 sur les systèmes 32 bits et 64 bits. Exploitation du bug use-after-free win32k!xxxEnableWndSBArrows (CVE-2015-0057) sur les systèmes 32 bits et 64 bits (Aaron Adams de NCC )
Auteur : Aaron Adams
Traduction : 55-AA
Note du traducteur : certaines parties de cet article sont traduites librement ; en cas de doute, veuillez vous référer à l'original.
Termes :
Plus tôt cette année, j'ai eu l'occasion de travailler sur une vulnérabilité intéressante dans win32k.sys (CVE-2015-0057) et j'ai réussi à en faire une exploitation stable sur systèmes 32 et 64 bits, avec une portée allant de XP à Windows 8.1 (avec quelques exceptions). Cet article décrit en détail comment j'ai procédé sur ces deux plates-formes, et inclut également quelques éléments supplémentaires à la fin. Il décrit aussi comment réaliser l'exploitation avec des privilèges de faible intégrité sur Windows 8.1 avec SMEP activé.
Cet article est long ; j'ai essayé de fournir autant de détails que possible pour montrer la complexité de l'exploitation de cette vulnérabilité, sans les cacher, même si j'ai bien sûr omis certains points. J'espère que ces détails seront utiles à tous.
Le 10 février 2015, Microsoft a publié les détails de MS15-010. Ce bug a été découvert en premier par Udi Yavo d'enSilo. Udi a fourni une excellente analyse sur le blog breaking malware : "one bit rule-bypassing windows 10 protections using single bit". Je recommande vivement la lecture de cet article pour bien comprendre le bug, bien que je donne ici le maximum de détails sur les obstacles à surmonter lors du déclenchement de la vulnérabilité. L'exploitation de cette vulnérabilité est très intéressante, et de nombreux détails proviennent du blog d'Udi. Voici sa déclaration :
Divulgation raisonnable : bien que ce blog soit technique, nous ne divulguerons aucun code ni détail complet, afin d'empêcher tout expert technique de reproduire cette exploitation.
En récompense supplémentaire pour l'exploitation de cette vulnérabilité, nous avons obtenu une évolution de Pokémon : le Pokémon technique. Je pense devoir rendre hommage à Udi pour avoir découvert ce bug, avoir fourni des informations sur le blog et les détails de son exploitation, qui ont été très utiles.
Avant cela, je n'avais jamais exploité de vulnérabilité dans win32k.sys, je n'étais pas familier avec les rappels en mode utilisateur ni avec de nombreuses API associées. Je remercie donc également les chercheurs en sécurité renommés qui ont mis en ligne des ressources précieuses, comme Skywing, Tarjei Mandt, Alex Ionescu et j00ru. Tous ces gens méritent d'être loués pour avoir rendu publiques tant d'informations techniques. Je me suis largement appuyé sur un article de Tarjei Mandt : Win32k.sys exploitation paper.
Au moment où j'écrivais cette exploitation, un excellent ingénieur en rétro-ingénierie a réalisé une exploitation stable de CVE-2015-1701. Les exemples de code pour les rappels en mode utilisateur ont été très utiles ; merci à cet auteur.
Il est à noter que mon analyse ci-dessous a été réalisée sur Windows 7, car c'est apparemment la seule version où toutes les structures de win32k.sys ont des symboles correspondants. La plupart de ces symboles sont utilisables pour les structures des autres versions de Win32k.sys. Pour une raison inconnue, Microsoft a supprimé ces symboles à partir de Windows 8.
Enfin, je précise que ma méthode d'exploitation est assez complexe. Il est tout à fait possible qu'il existe une méthode plus simple que je n'ai pas trouvée. J'aimerais entendre parler de méthodes différentes. Quoi qu'il en soit, j'espère que tout cela sera utile pour l'étude des vulnérabilités de win32k.sys.
Voyons maintenant le bug dans le désassemblage de win32k!xxxEnableWndSBArrows, un bug assez subtil :
Avant correctif :
.text:FFFFF97FFF1B157D mov r8d, r13d
.text:FFFFF97FFF1B1580 mov rdx, r14
.text:FFFFF97FFF1B1583 call xxxDrawScrollBar ; déclenche un rappel mode utilisateur
.text:FFFFF97FFF1B1588 jmp short loc_FFFFF97FFF1B1519
[...]
.text:FFFFF97FFF1B1519 mov eax, [rbx] ; référence le pointeur tagSBINFO sans vérification
.text:FFFFF97FFF1B151B mov ebp, 0FFFFFFFBh
.text:FFFFF97FFF1B1520 xor eax, esi
Dans le code ci-dessus, Win32K!xxxdrawscrollbar peut effectuer un rappel vers l'espace utilisateur dans certaines conditions. Dans le code utilisateur, le pointeur tagSBINFO peut être libéré par l'attaquant. De retour en mode noyau, le code à l'adresse 0xFFFFF97FFF1B1519 utilisera un pointeur invalide.
Après correctif :
.text:FFFFF97FFF1D69C3 xor r8d, r8d
.text:FFFFF97FFF1D69C6 mov rdx, rbp
.text:FFFFF97FFF1D69C9 call xxxDrawScrollBar ; déclenche un rappel mode utilisateur
.text:FFFFF97FFF1D69CE cmp rbx, [rdi+0B0h] ; vérifie que le pointeur tagSBINFO est correct
---.text:FFFFF97FFF1D69D5 jz short loc_FFFFF97FFF1D69E4; si correct, continue le flux original
| .text:FFFFF97FFF1D69D7 mov rcx, rbp
| .text:FFFFF97FFF1D69DA call _ReleaseDC
| .text:FFFFF97FFF1D69DF jmp loc_FFFFF97FFF1D6958 ; saute vers la sortie de la fonction
|->.text:FFFFF97FFF1D69E4 mov eax, [rbx] ; utilise le pointeur tagSBINFO correct en toute sécurité
.text:FFFFF97FFF1D69E6 xor eax, r14d
Dans la version corrigée ci-dessus, on voit que le pointeur tagSBINFO est vérifié avant utilisation. Les informations sur la structure associée seront données plus loin.
Pour réaliser cette exploitation, nous avons effectué plusieurs corruptions. L'une d'elles déclenche la vulnérabilité.
La cause technique de ce bug est un UAF (use-after-free) dans le tas du bureau (Desktop Heap). Au début, cela m'a beaucoup dérouté, car je ne connaissais pas le mécanisme de rappel en mode utilisateur de win32k.sys ni son fonctionnement. J'ai donc pensé qu'il s'agissait d'une condition de concurrence sur un verrou menant à un UAF. En fait, le verrou de cette structure est utilisé correctement et le flux est conforme aux attentes. En bref, le vrai problème est le suivant :
Voilà, si on ignore le rappel en mode utilisateur, cette phase est assez simple.
Mais comment effectuons-nous la corruption et pourquoi ? Comme mentionné dans le blog d'Udi, vous pouvez définir ou effacer 2 bits à un emplacement que le code système considère comme le champ WSBflags de la structure tagSBINFO. Ce n'est pas une approche classique d'exploitation de UAF, mais l'article donne une indication sur comment procéder, que j'expliquerai dans les sections suivantes. Commençons par comprendre comment manipuler ces bits.
Structure tagSBINFO (identique sur 32 et 64 bits) :
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
Ce bug de UAF se trouve dans la fonction win32k!xxxEnableWndSBArrows(), qui sert à activer ou désactiver les flèches d'un ou deux contrôles de barre de défilement (horizontal ou vertical). Un contrôle de barre de défilement est une fenêtre spéciale utilisée pour manipuler la barre. On peut le créer via la fonction CreateWindow() avec la classe de fenêtre intégrée "SCROLLBAR".
Prototype de win32k!xxxEnableWndSBArrows() :
BOOL xxxEnableWndSBArrows(PWND wnd, UINT WSBflags, UINT wArrows);
Le paramètre WSBflags a la même signification que dans WinUser.h, indiquant quelles barres de défilement sont concernées :
#define SB_HORZ 0
#define SB_VERT 1
#define SB_CTL 2
#define SB_BOTH 3
Le paramètre wArrows indique si les flèches sont activées ou désactivées. Un bit à 1 signifie flèche désactivée, 0 signifie activée. Les deux bits de poids faible de wArrows correspondent à la barre horizontale, les deux suivants à la verticale, les autres bits ne sont pas pertinents pour notre exploitation.
Le code suivant, extrait de la fonction win32k!xxxEnableWndSBArrows, active ou désactive les bits correspondant à la barre horizontale si SB_HORZ ou SB_BOTH est défini :

Le bug réside dans la définition des drapeaux pour les barres horizontale et verticale. Après avoir rafraîchi la barre horizontale, si la fenêtre associée à la barre est visible sur le bureau, la fonction win32k!xxxEnableWndSBArrows appelle win32k!xxxDrawScrollBar(), qui, comme mentionné précédemment, peut déclencher un rappel potentiel en mode utilisateur.
Avant de discuter du rappel en mode utilisateur, continuons sur ce qui se passe après l'appel à win32k!xxxDrawScrollBar(). Cela suit en fait la même logique que pour la barre horizontale, avec seulement quelques différences de bits. Si nous choisissons de désactiver la barre verticale et supposons que nous déclenchons l'UAF, cela écrira 2 bits à un endroit du bloc tagSBINFO. Ainsi, si la valeur originale est 0x2, elle devient 0xe. Comme illustré ci-dessous :

Ce changement de bit est suffisant pour conduire à l'exécution de code. Je n'ai pas approfondi la manière d'exploiter en effaçant des bits, mais c'est possible.
L'essentiel ci-dessus est que, pour pouvoir manipuler les barres horizontale et verticale simultanément, il faut créer un contrôle de barre de défilement possédant ces deux éléments. Cela se fait en appelant CreateWindow() avec les drapeaux WS_HSCROLL et WS_VSCROLL. Code correspondant :
g_hSBCtl = CreateWindowEx(
0, // Pas de style étendu
"SCROLLBAR", // classe
NULL, // nom
SBS_HORZ | WS_HSCROLL | WS_VSCROLL, // vertical + horizontal
10, // x
10, // y
100, // largeur
100, // hauteur
g_hSpray[UAFWND], // fenêtre parente sans mode
(HMENU)NULL,
NULL, // propriétaire de la fenêtre
NULL // paramètres supplémentaires
);
On peut s'assurer qu'elle est visible (c'est généralement le cas par défaut, mais on l'appelle explicitement ici) :
result = ShowWindow(g_hSBCtl, SW_SHOW);
La barre de défilement est activée par défaut. Lorsque nous sommes prêts à déclencher le code de vulnérabilité, nous pouvons désactiver la barre pour corrompre les bits nécessaires :
result = EnableScrollBar(g_hSBCtl, SB_CTL | SB_BOTH, ESB_DISABLE_BOTH);
Bien que nous ayons décrit les détails du bug et comment déclencher le code associé, nous avons omis l'étape cruciale : intercepter le rappel en mode utilisateur initié par win32k!xxxDrawScrollBar() afin de modifier le contenu du tas avant que win32k!xxxEnableWndSBArrows() ne continue. Nous devons réellement déclencher le bug, mais sans connaissance de Win32k.sys ni des API associées, comme j'ai commencé, c'est une aventure en soi.
Les articles précédents donnaient un bon diagramme de la pile d'appels, montrant le processus en profondeur : via win32k!xxxDrawScrollBar(), puis ClientLoadLibrary() est appelé, et dispatché via KeUserModeCallback(). Nous devons comprendre précisément le déroulement de KeUserModeCallback() pour pouvoir le hooker dans notre processus.
J'ai trouvé quelques bons articles mentionnant plus ou moins les rappels en mode utilisateur. Ceux concernant win32k sont très utiles :
En général, chaque processus possède une table de pointeurs vers les fonctions de rappel en mode utilisateur, pointée par PEB->KernelCallBackTable. Lorsque le noyau veut appeler une fonction en mode utilisateur, il passe l'index de la fonction à KeUserModeCallBack(). Dans l'exemple ci-dessus, l'index pointe vers la fonction utilisateur __ClientLoadLibrary().
KeUserModeCallBack() recherche la fonction correspondante dans PEB->KernelCallBackTable et l'exécute, aboutissant finalement à l'appel de KiUserModeCallbackDispatch() en mode utilisateur.
Pour hooker un point d'entrée spécifique, il faut trouver l'index de __ClientLoadLibrary() dans PEB->KernelCallBackTable et le remplacer par notre propre fonction. Il est à noter que cet index varie selon la version du système d'exploitation et la plate-forme matérielle.
Pour examiner PEB->KernelCallBackTable, on peut trouver l'adresse de cette table avec WinDbg. En comparant les plates-formes 32 et 64 bits, les différences ne sont pas très importantes.
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
Dans l'exemple ci-dessus, l'index de __ClientLoadLibrary est 65, c'est celui que nous devons hooker. Après avoir hooké, j'ai découvert que __ClientLoadLibrary était appelé plusieurs fois par le code de win32k ! La première chose à faire est d'informer notre code de hook que nous avons atteint l'appel qui nous intéresse, afin de savoir quand agir. Pour cela, nous utilisons une variable globale qui, lorsqu'elle est définie, déclenche l'action.
Il y a maintenant deux obstacles :
Ainsi, la fonction de hook ressemble à ceci :
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; // exécute une seule fois..
ReplaceScrollBarChunk(NULL);
}
}
fpClientLoadLibrary(&Buf); // appel de la fonction originale
}
Une fois que nous avons déterminé que l'appel provient de win32k!xxxDrawScrollBar(), nous pouvons tenter de déclencher le bug. Pour cela, il suffit d'appeler DestroyWindow(g_hSBCtl). Cela libère la structure tagSBINFO de la fenêtre. La fenêtre elle-même n'est pas libérée immédiatement car son compteur de références est toujours utilisé par l'appel original, mais tagSBINFO n'a pas un tel mécanisme, donc il est libéré immédiatement.
À ce stade, le bug est déclenché. Même si nous n'avons pas réalloué le bloc contenant tagSBINFO, nous pouvons écrire les deux bits représentant « désactiver » sur le bloc libéré. L'étape suivante consiste à remplacer ce bloc libéré par ce que nous voulons, afin de faire quelque chose de plus intéressant que de simplement modifier deux bits. Pour cela, nous devons comprendre le contexte du tas du bureau.
win32k.sys utilise le tas du bureau pour stocker les objets GUI associés à un bureau donné. Cela inclut les objets fenêtre et leurs structures associées, comme les listes de propriétés, le texte des fenêtres, les barres de défilement. L'article de Tarjei en parle, mais il faut noter que le tas du bureau n'est en fait qu'une version simplifiée de l'allocateur arrière-plan en mode utilisateur, et qu'il utilise également RtlAllocateHeap() et RtlFreeHeap(). Le tas du bureau est géré par une structure _HEAP, et comme il n'y a pas d'allocateur avant (front-end), il n'y a pas de tas à faible fragmentation (LFH) ni de listes de voisinage.
Chaque bureau créé dispose d'un tas correspondant. Cela signifie que nous pouvons allouer un nouveau bureau pour obtenir un tas « propre », sur lequel nos opérations seront plus prévisibles. Cependant, cela n'a pas de sens pour un processus de faible intégrité, car il n'est pas autorisé à créer un nouveau bureau.
Le principal problème est maintenant de suivre les allocations (nous aborderons plus tard les détails sur les métadonnées, etc.).
Pour suivre les allocations et libérations du tas du bureau, j'utilise des scripts WinDbg :
Surveillance du tas 64 bits
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";
Surveillance du tas 32 bits
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"
En plus de ces scripts de débogage, comme le tas du bureau n'est qu'une forme simplifiée de l'allocateur arrière-plan en mode utilisateur, on peut aussi utiliser la commande !heap de WinDbg.
Pour exploiter ce bug, nous devons remplacer le bloc tagSBINFO récemment libéré par un autre. Nous savons aussi comment utiliser ces bugs typiques : corrompre les données voisines. Cela implique de pré-allouer des blocs près de la structure à corrompre. Pour prédire où un bloc sera alloué, nous devons contrôler la disposition du tas (autant que possible). Pour y parvenir, il est possible d'allouer autant de blocs que possible pour combler les blocs déjà libérés, de sorte que les nouveaux blocs soient contigus. Lorsque nous avons besoin d'un trou, nous pouvons en créer un à un endroit prévisible en libérant un bloc déjà alloué.
Cette partie est une simple compréhension des facteurs influençant l'allocation ; les scripts WinDbg ci-dessus nous aident. Tarjei mentionne dans sa présentation sur win32k les principaux objets alloués sur le tas du bureau, ce qui correspond à ce que j'ai observé. Ce sont :
Le tas du bureau est assez intéressant : la plupart des allocations sont directement liées aux objets fenêtre et gérées via la structure tagWND. Cela signifie que pour allouer un bloc de taille arbitraire (par exemple un petit bloc pour combler un petit trou), nous devons d'abord allouer une fenêtre associée. On peut considérer que la structure de la fenêtre est l'interface d'allocation du tas. Un autre point intéressant est que de nombreux blocs alloués via les opérations sur les fenêtres ne peuvent pas être libérés immédiatement, à moins que la fenêtre elle-même ne soit détruite, ce qui a évidemment un impact sur le tas. Enfin, supposons que nous voulions allouer un bloc de taille N via une fenêtre, comme dans l'exemple ci-dessus. Devons-nous allouer beaucoup de blocs de taille N ? On peut déterminer que la structure de fenêtre allouée, quelle que soit sa taille, n'est pas stockée dans une liste chaînée. Par conséquent, chaque fenêtre peut contrôler une allocation de bloc de taille N. Autrement dit, si vous devez allouer un grand nombre de blocs de taille N, vous devez d'abord créer un grand nombre de fenêtres, en utilisant les fenêtres comme intermédiaires pour l'allocation.
Sur le tas du bureau, trois autres types de données importants sont alloués, que nous pouvons utiliser indirectement via les objets fenêtre pour contrôler les données sur le tas. Nous utilisons abondamment ces types de données pour réaliser l'exploitation et construire un « tas Feng Shui ». Ces trois types sont :
La figure 2 montre les relations entre ces types de données :

Pour initialiser le tas, j'ai créé beaucoup de structures tagWND (en créant des objets fenêtre). Cela permet de combler de nombreux grands trous dans le tas, et fournit également une interface pour allouer d'autres blocs nécessaires. Sur Windows 8 et 8.1, l'allocation d'une nouvelle fenêtre entraîne l'allocation automatique d'une structure tagPROPLIST (observable via les scripts WinDbg mentionnés précédemment). Sur Windows 7 et versions antérieures, nous allouons nous-mêmes un nouveau tagPROPLIST pour combler les petits trous.
Ici, tous les objets fenêtre que nous avons pulvérisés n'ont pas de texte de fenêtre ; cependant, si nécessaire, nous pouvons utiliser ce mécanisme pour allouer ou libérer des blocs de taille arbitraire. Une fois créé, vous ne pouvez pas supprimer une liste de propriétés existante sans détruire la fenêtre, mais nous pouvons contrôler la réallocation de cette liste pour accueillir de nouvelles propriétés, ce qui peut être utilisé pour créer un trou à un emplacement précédent. Il suffit de définir une nouvelle propriété qui n'existe pas dans la liste actuelle (distinguée par atomKey).
Il est intéressant de noter que le tas du bureau est mappé dans l'espace utilisateur, bien qu'en lecture seule. Cela signifie que nous pouvons vérifier la disposition que nous avons construite et nous assurer qu'elle fonctionne correctement. Commençons par déterminer où le tas du bureau est mappé dans l'espace utilisateur. L'article de Tarjei sur win32k mentionne cela. Il existe une structure non documentée Win32ClientInfo dans le TEB qui s'y rapporte, dont la définition approximative est :
typedef struct _CLIENTINFO {
ULONG_PTR CI_flags;
ULONG_PTR cSpins;
DWORD dwExpWinVer;
DWORD dwCompatFlags;
DWORD dwCompatFlags2;
DWORD dwTIFlags;
PDESKTOPINFO pDeskInfo;
ULONG_PTR ulClientDelta; // incomplet. Voir reactos
} CLIENTINFO, *PCLIENTINFO;
La structure PDESKTOPINFO est définie comme suit :
typedef struct _DESKTOPINFO {
PVOID pvDesktopBase;
PVOID pvDesktopLimit; // incomplet. Voir reactos
} DESKTOPINFO, *PDESKTOPINFO;
Le premier champ, pvDesktopBase, pointe vers l'adresse en mode noyau du tas du bureau, que nous notons. Le champ ulClientDelta de Win32ClientInfo est la différence entre l'adresse noyau et l'adresse utilisateur ; avec ces informations, nous pouvons obtenir ce que nous voulons.
Cependant, au lieu d'analyser nous-mêmes la structure du tas, nous préférons avoir un handle user32, comme la valeur HWND, que nous pouvons convertir en adresse de mappage utilisateur, afin de déterminer s'il est associé à d'autres allocations de tas. Pour trouver ce handle, nous avons besoin d'une structure appelée gShared, généralement située dans uer32.dll. Sous Windows 7 et versions ultérieures, elle est exportée, donc facile à trouver.
Sur la plupart des systèmes, cette structure est définie comme suit :
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
Dans la structure ci-dessus, aheList pointe vers un tableau de _HANDLEENTRY, chaque _HANDLEENTRY contient un handle pointant vers une adresse noyau. Nous pouvons obtenir une adresse utilisateur utilisable via la « différence entre adresse noyau et adresse utilisateur ». Malheureusement, cela n'est pas possible sur les versions antérieures à Windows 7, car gSharedInfo n'est pas exporté. L'article de Tarjei mentionne que la fonction non documentée CsrClientConnectToServer peut être utilisée pour obtenir une copie de gSharedInfo, mais je n'ai pas trouvé d'exemple fonctionnel. Ennuis : la structure nécessaire à cette fonction a une longueur variable selon le système, donc d'après mon expérience, on ne peut pas se fier entièrement à ce qu'on voit dans ReactOS.
Une fois que nous avons calculé l'emplacement du mappage, nous pouvons construire une fonction qui nous indique la position d'un objet fenêtre sur le tas du bureau. Ensuite, si nous voulons connaître l'emplacement du bloc correspondant à la liste de propriétés ou au texte, il suffit d'analyser la structure utilisateur.
Maintenant, nous approchons enfin de l'exploitation de la vulnérabilité. Nous avons une méthode pour contrôler les blocs du tas, une méthode pour vérifier leur emplacement, et nous pouvons déclencher le bug. Nous pouvons donc maintenant remplacer le bloc tagSBINFO libéré par une liste tagPROPLIST choisie. Notez que tagPROPLIST n'est que l'en-tête d'une grande liste, donc nous pouvons faire correspondre la taille de la liste à celle du bloc de barre de défilement. La partie qui suit est essentiellement un tableau de structures tagPROP, ou liste de propriétés ; par conséquent, je ne distinguerai pas les termes tableau et liste. La structure tagPROPLIST sur système 64 bits est :
kd> dt -b !tagPROPLIST
win32k!tagPROPLIST
+0x000 cEntries : Uint4B
+0x004 iFirstFree : Uint4B
+0x008 aprop : tagPROP
+0x000 hData : Ptr64
+0x008 atomKey : Uint2B
+0x00a fs : Uint2B
Comme mentionné précédemment, un objet fenêtre a une liste de propriétés associée. Cette liste est créée via la fonction SetProp(). Elle recherche une propriété existante par atomKey ; si elle n'existe pas, elle crée une nouvelle entrée dans la liste. S'il n'y a pas de liste du tout, une liste est créée et rattachée à la structure tagWND.
Si nous avons déjà pulvérisé un tagWND avec bug et créé les entrées tagPROPLIST associées, la disposition finale est comme dans la figure 3 :

Une fois cela configuré, nous pouvons allouer le contrôle de barre de défilement que nous voulons exploiter. Cela donne le résultat de la figure 4 :

Ensuite, nous manipulons la barre de défilement pour déclencher le hook du rappel en mode utilisateur. Dans le hook, nous tentons de détruire la fenêtre pour libérer la structure tagSBINFO. Cela mène à la situation de la figure 5 :

Sur 64 bits, la structure tagSBINFO fait 0x28 octets, et une entrée de tableau tagPROPLIST fait 0x18 octets, avec un tagPROP par défaut de 0x10 octets. Ainsi, une liste de propriétés avec deux entrées fait 0x28 octets (0x8 + 0x10 + 0x10), ce qui est parfait. En supposant que nous avons pulvérisé la mémoire pour combler le trou, nous avons simplement besoin d'une fenêtre avec une liste de propriétés. Immédiatement après avoir libéré la structure tagSBINFO (comme montré précédemment), nous ajoutons une nouvelle entrée à la liste de propriétés. Ce processus libère d'abord le bloc tagPROPLIST de 0x18 octets précédent, mais comme le tas a été pulvérisé, il n'y a pas de blocs libres à proximité, donc pas de fusion de blocs, et donc pas d'espace suffisant pour le nouveau bloc de 0x28 octets. Ainsi, l'emplacement de tagSBINFO récemment libéré (exactement de taille 0x28) est utilisé, comme le montre la figure 6 :

Après le retour de notre hook de rappel, l'UAF est déclenché, et quelques bits sont écrits sur le champ cEntries de tagPROPLIST. La valeur originale de cEntries était 0x2, indiquant que nous avions créé deux entrées dans la liste. Après corruption, elle devient 0xe, les 3e et 4e bits (en partant de 1) sont mis à 1.
À ce stade, nous avons effectué un débordement de tas et augmenté le nombre d'entrées de la liste à plus de 0xc. Ensuite, nous allons corrompre le bloc voisin, ce que j'appelle la corruption de phase 2.
Le blog d'Udi explique jusqu'à ce point. Avant cela, on parlait de « débordement de tas typique », mais d'après mon expérience, il est difficile d'arriver à une lecture/écriture arbitraire ou à une exécution de code à partir de là. Regardons à nouveau la structure tagPROPLIST sur 64 bits :
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 corruption de phase 1 nous a donné un tableau tagPROPLIST corrompu, nous permettant d'ajouter des entrées tagPROP. tagPROPLIST n'a que deux champs :
Lorsqu'une nouvelle entrée est insérée dans la liste, une fonction est appelée pour parcourir chaque entrée jusqu'à trouver l'index iFirstFree approprié. Si elle n'est pas trouvée, on vérifie si iFirstFree est supérieur à cEntries. Si l'atomKey correspondant n'est pas dans la liste, on vérifie si iFirstFree != cEntries. Si différent, on insère une entrée à l'index iFirstFree. Si égal, on alloue une nouvelle liste de propriétés pouvant contenir l'entrée insérée, on copie les anciennes entrées, et on insère la nouvelle.
Le champ atomKey correspond à LPCTSTR lpString. Comme le document MSDN sur SetProp(), l'appelant peut passer un pointeur de chaîne ou une valeur atom 16 bits. Lorsqu'un pointeur de chaîne est passé, il est automatiquement converti en atom avant d'être stocké dans la liste de propriétés. Comme nous pouvons passer n'importe quelle valeur atom à SetProp(), nous avons la capacité de contrôler ces deux octets, mais avec quelques contraintes. À savoir, les données corrompues de atomKey ne doivent pas être en double, sinon lors de la définition d'une nouvelle propriété, elle remplacera une propriété existante avec la même valeur atom. De plus, le champ fs n'est pas contrôlable ; sa valeur est 0 si atomKey < 0xBFFF (correspondant aux atomes entiers), et 2 si atomKey >= 0xC000.
Une autre chose à noter est que tagPROP ne fait que 0xc octets. Cette structure est alignée sur 0x10 octets sur 64 bits, donc lors de l'insertion d'une entrée tagPROP, il y a 4 octets supplémentaires qui ne peuvent pas être corrompus. Enfin, les 8 premiers octets d'un bloc tagPROPLIST définissent la taille de la liste, ce qui signifie que chaque nouvelle insertion tagPROP s'écrit toujours à une adresse alignée sur 8 octets.
Pour chaque tagPROP inséré, sur système 64 bits, la situation est la suivante :
* Offset 0x0 : 8 octets de données entièrement contrôlables (hData)
* Offset 0x8 : 2 octets de données majoritairement contrôlables (atomKey)
* Offset 0xa : 2 octets de données non contrôlables (fs)
* Offset 0xc : 4 octets de données non modifiables (padding)
Cette situation est bien meilleure que 2 bits, mais encore imparfaite. À moins que nous puissions recouvrir quelque chose avec les 8 premiers octets, qui proviennent du champ hData entièrement contrôlable, nous sommes limités. Si nous devons écrire sur des champs plus profonds d'une structure voisine, nous ne pouvons pas éviter la corruption incontrôlable de certaines valeurs. J'ai passé du temps à chercher divers objets sur le tas du bureau, et compte tenu des limitations de corruption précédentes, pour contourner ces limitations et réaliser une lecture/écriture arbitraire, la seule solution que j'ai trouvée est de corrompre le champ strName de tagWND, qui est une structure _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
Si nous pouvons corrompre le champ Buffer de cette structure, nous pouvons lire ou écrire MaximumLength octets à partir d'une adresse donnée en manipulant le texte de la fenêtre. C'est ce que je vais faire. Vous avez peut-être remarqué cette structure dans les sections précédentes, concernant la création d'un bloc de taille et de valeur arbitraires sur le tas du bureau ; la même chose s'applique ici.
Maintenant, nous savons comment utiliser les entrées tagPROPLIST pour corrompre des données, quelles parties nous pouvons contrôler, et surtout les limitations auxquelles nous sommes confrontés, qui diffèrent entre 32 et 64 bits. Ce que nous avons fait sur 64 bits ne fonctionne pas sur 32 bits. La prochaine étape consiste à passer de la corruption de phase 2 (écriture via la structure tagPROP) à une autre « primitive d'opération » de corruption, par laquelle nous pouvons écrire des données entièrement contrôlables, ce que j'appelle la corruption de phase 3.
Le plan est de corrompre le champ strName d'un tagWND voisin. Nous savons déjà qu'il s'agit d'une structure _LARGE_UNICODE_STRING, mais regardons plus de détails sur la structure tagWND, qui ressemble à :kd> dt !tagWND win32k!tagWND +0x000 head : THRDESKHEAD +0x028 state : Uint4B +0x028 bHasMeun : Pos 0, 1 Bit +0x028 bHasVerticalScrollbar : Pos 1, 1 Bit +0x028 bHasHorizontalScrollbar : Pos 2, 1 Bit [DRAPEAUX SUPPRIMÉS] +0x028 bDestroyed : Pos 31, 1 Bit +0x02c state2 : Uint4B [DRAPEAUX SUPPRIMÉS] +0x02c bWMCreateMsgProcessed : Pos 31, 1 Bit +0x030 ExStyle : Uint4B +0x030 bWS_EX_DLGMODALFRAME : Pos 0, 1 Bit +0x030 bUnused1 : Pos 1, 1 Bit +0x030 bWS_EX_NOPARENTNOTIFY : Pos 2, 1 Bit [DRAPEAUX SUPPRIMÉS] +0x030 bUIStateFocusRectHidden : Pos 31, 1 Bit +0x034 style : Uint4B +0x034 bReserved1 : Pos 0, 16 Bits [DRAPEAUX SUPPRIMÉS] +0x034 bWS_POPUP : Pos 31, 1 Bit +0x038 hModule : Ptr64 Void +0x040 hMod16 : Uint2B +0x042 fnid : Uint2B +0x048 spwndNext : Ptr64 tagWND +0x050 spwndPrev : Ptr64 tagWND +0x058 spwndParent : Ptr64 tagWND +0x060 spwndChild : Ptr64 tagWND +0x068 spwndOwner : Ptr64 tagWND +0x070 rcWindow : tagRECT +0x080 rcClient : tagRECT +0x090 lpfnWndProc : Ptr64 int64 +0x098 pcls : Ptr64 tagCLS +0x0a0 hrgnUpdate : Ptr64 HRGN_ +0x0a8 ppropList : Ptr64 tagPROPLIST +0x0b0 pSBInfo : Ptr64 tagSBINFO +0x0b8 spmenuSys : Ptr64 tagMENU +0x0c0 spmenu : Ptr64 tagMENU +0x0c8 hrgnClip : Ptr64 HRGN__ +0x0d0 hrgnNewFrame : Ptr64 HRGN__ +0x0d8 strName : LARGE_UNICODE_STRING +0x0e8 cbwndExtra : Int4B +0x0f0 spwndLastActive : Ptr64 tagWND +0x0f8 hImc : Ptr64 HIMC_ +0x100 dwUserData : Uint8B +0x108 pActCtx : Ptr64 _ACTIVATION_CONTEXT +0x110 pTransform : Ptr64 _D3DMATRIX +0x118 spwndClipboardListenerNext : Ptr64 tagWND +0x120 ExStyle2 : Uint4B +0x120 bClipboardListener : Pos 0, 1 Bit [DRAPEAUX SUPPRIMÉS] +0x120 bChildNoActivate : Pos 11, 1 Bit
Ce qui précède est la structure sous 64 bits. On voit que le décalage de la structure _LARGE_UNICODE_STRING que nous voulons corrompre est 0xd8. Vous remarquerez également un champ important au début de cette structure. J'aurais aimé pouvoir la malmener à loisir, mais il y a de nombreux pointeurs dans _THRDESKHEAD qui nous obligent à rester lucides, et malheureusement, nous ne pouvons pas contrôler l'endroit où nous écrivons, pour les raisons discutées précédemment.
Définition de la structure _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
Le problème avec _THRDESKHEAD ne nous rend pas seulement perplexes, il nous oblige également à réexaminer la contrainte d'alignement. Quel que soit le décalage du nouvel élément de la liste tagPROP, notre opération d'écriture écrasera directement le début de _LARGE_UNICODE_STRING :
win32k!_LARGE_UNICODE_STRING
+0x000 Length <-- hData (totalement contrôlable) écrase ici
+0x004 MaximumLength <-- et ici
+0x004 bAnsi <-- et ici
+0x008 Buffer <-- atomKey et fs (partiellement contrôlables) écrase ici
Il est clair que nous voulons écraser le pointeur Buffer pour accéder à la mémoire à une adresse arbitraire. Cependant, même si nous pouvons corrompre les autres champs de cette structure en toute sécurité, nous ne pouvons pas contrôler le pointeur dont nous avons besoin.
Nous ne pouvons pas corrompre des données arbitraires. La solution à ce problème n'est plus une corruption de tagPROPLIST, mais un mécanisme de corruption complètement différent.
Sous Windows XP et versions ultérieures, l'en-tête de bloc de tas (c'est-à-dire _HEAP_ENTRY) de l'allocateur backend en mode utilisateur (comme le tas de bureau noyau) est stocké sur le tas, juste avant le contenu réel du bloc. Le tas de bureau est lui-même géré par la structure _HEAP, ce qui nous donne une certaine liberté lors de l'exploitation de ce bloc de tas.
Définition de la structure _HEAP_ENTRY :
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'en-tête de bloc fait 0x10 octets au total. Les 8 premiers octets sont PreviousBlockPrivateData, qui sert à contenir les données réelles du bloc précédent lorsque la taille demandée dépasse 0x10 (moins de 8 octets sont alignés sur 8 octets). Ceci est brièvement décrit dans l'article du blog Leviathan, ainsi que dans des articles antérieurs sur le tas en mode utilisateur. Size et PreviousSize représentent la taille du bloc actuel et la taille du bloc précédent, en unités de 0x10 octets. Flags indique si le bloc est libre, etc. Si le mode sécurisé de _HEAP_ENTRY est activé dans _HEAP, SmallTagIndex contient une somme de contrôle XOR des données du bloc.
Bien que la contrainte d'alignement joue contre nous, elle existe bel et bien. Si vous appelez tagPROPLIST, il fait toujours au moins 0x18 octets, puis ajoute 0x10 octets pour tagPROP. Pour un tagPROPLIST avec deux éléments, de taille 0x28 octets, il sera placé dans un bloc de tas de 0x20 octets, et les octets supplémentaires représentés par PreviousBlockPrivateData utilisent le bloc de tas adjacent. Cela signifie que lorsque nous ajoutons un troisième élément de liste, le bloc de tas adjacent est corrompu, et les 8 octets contrôlables de hData écrasent le dessus de _HEAP_ENTRY.
Ce que nous voulons faire, c'est utiliser cela pour pouvoir écrire des données arbitraires à l'endroit du Buffer. Tout d'abord, nous modifions la disposition du tas pour nous rapprocher du bloc tagPROPLIST corrompu. Pendant le contrôle, nous avons un petit bloc de tas contenant une chaîne de texte liée à une fenêtre ; nous l'appelons « bloc de couverture ». À côté de ce « bloc de couverture », nous plaçons un tagWND de manière à corrompre ce tagWND. La figure 7 ci-dessous illustre ce processus. Notez que nous avons omis les blocs de tas préalablement pulvérisés pour économiser de l'espace, ils doivent donc maintenant être considérés comme implicites.

Ensuite, nous insérons un troisième tagPROP dans la liste tagPROPLIST, qui écrasera les 8 derniers octets de _HEAP_ENTRY et les 8 premiers octets du « bloc de couverture ». Ainsi, nous pouvons modifier le _HEAP_ENTRY du « bloc de couverture » pour lui donner une taille supérieure à sa taille réelle, suffisante pour inclure la structure tagWND adjacente.
Maintenant, nous libérons le « bloc de couverture » corrompu afin que le gestionnaire de tas le place dans la liste libre correspondant à une taille (chaque liste libre correspond à une taille fixe) supérieure à la taille réelle du « bloc de couverture ». Ensuite, nous réutilisons ce bloc en modifiant le texte de la fenêtre (que nous contrôlons entièrement). Cependant, cela pose un petit problème à résoudre. Lorsque le « bloc de couverture » est libéré, le gestionnaire de tas cherche le bloc adjacent précédent, en fonction du champ Size corrompu. Il vérifie si ce bloc adjacent est libre pour le fusionner. Quoi qu'il en soit, nous voulons le contrôler et définir un indicateur d'utilisation active. En modifiant légèrement notre disposition du tas, nous y parvenons. Nous plaçons un faux bloc de tas dont l'en-tête a le drapeau « en cours d'utilisation » et dont la valeur de PreviousSize est définie sur la valeur corrompue de Size, ce que nous faisons simplement en allouant le texte de fenêtre d'une autre fenêtre. La nouvelle disposition du tas est la suivante (figure 8) :

Nous pouvons maintenant libérer le « bloc de couverture » corrompu en mettant à jour la chaîne de texte de la fenêtre associée pour qu'elle soit plus longue que les 0x10 octets d'origine. Ainsi, le « bloc de couverture » corrompu est d'abord libéré et placé dans la liste libre, mais sa taille est corrompue : elle est déclarée plus grande que la taille réelle. Cette taille peut être ajustée selon nos besoins réels. Nos données de chaîne sont alors écrites dans ce « bloc de couverture », et nous utilisons ce bloc pour écraser le tagWND adjacent avec des données arbitraires. Ceci est illustré ci-dessous (figure 9) :

C'est la corruption de la phase 3. Nous pouvons maintenant écraser le pointeur strName.Buffer avec toutes les données souhaitées. Cependant, corrompre d'autres données de tagWND reste un peu gênant, mais ce n'est pas un problème car le tas de bureau est mappé dans l'espace utilisateur ! Par conséquent, avant de corrompre quoi que ce soit, nous lisons tout le contenu de tagWND, modifions le contenu de la structure strName comme nous le souhaitons, et envoyons toutes les données en modifiant le texte de la fenêtre.
strName nous donne non seulement une « primitive » de lecture/écriture arbitraire, mais permet également de modifier strName de manière répétée, ce qui est possible grâce au mécanisme de modification du texte de la fenêtre. Tant que la longueur de la chaîne écrite n'est pas supérieure à la valeur de MaximumLength, le même bloc de tas peut être réutilisé. Par conséquent, chaque fois que nous voulons modifier l'adresse de strName pour lire une valeur à un certain endroit, nous attachons une nouvelle chaîne et ajoutons nos données pour mettre à jour le « bloc de couverture ». Cette réutilisation est illustrée dans la figure 10. Notez que j'ai à nouveau augmenté la granularité de la vue d'ensemble pour montrer chaque corruption en détail.

Cela signifie que nous n'avons finalement besoin de corrompre que deux éléments supplémentaires (en plus de l'élément de liste tagPROPLIST original) :
Maintenant, si nous voulons lire quelques octets d'un emplacement mémoire, nous interrogeons le texte de la fenêtre via la fonction InternalGetWindowText(), dont l'entrée strName a été corrompue. Nous pouvons lire le nombre d'octets déclaré par le champ Length. De même, si nous voulons écrire à un emplacement arbitraire de la mémoire, nous utilisons la fonction NtUserDefSetText() pour mettre à jour le texte de la fenêtre corrompu, mais la quantité écrite ne doit pas dépasser la valeur déclarée dans MaximumLength (que nous pouvons également définir). Ainsi, le tampon existant est réutilisé et pointe vers l'adresse mémoire souhaitée.
Bien que depuis Windows Vista, l'allocateur backend en mode utilisateur utilise un encodage de tas (heap encoding), le tas de bureau ne l'a jamais activé avant Windows 8. Par conséquent, sous Windows 8 et versions ultérieures, cela crée un obstacle lors de l'écrasement du « bloc de couverture ». Cependant, en pratique, la structure _HEAP qui contient le tas inclut ce cookie et l'utilise pour encoder tout l'en-tête du tas. Par conséquent, nous pouvons lire ce cookie à partir du tas de bureau mappé dans l'espace utilisateur, puis l'utiliser pour encoder l'en-tête du « bloc de couverture », en imitant l'opération de l'allocateur par rétro-ingénierie du code de l'allocateur, ce que l'allocateur accepte.
Tout d'abord, notez que la structure tagPROP sur un système 32 bits fait 8 octets, au lieu de 0xc octets sur un système 64 bits, et que le champ hData que nous contrôlons ne fait que 4 octets, au lieu de 8 octets sur un système 64 bits. Il n'y a pas non plus d'octets de remplissage supplémentaires – sur un système 64 bits, il y a 8 octets de remplissage, donc la structure entière fait exactement 8 octets. Cela signifie que nous ne pouvons pas corrompre complètement l'en-tête du bloc de tas adjacent si nous ne contrôlons que partiellement les données. Sur certaines versions de Windows, c'est possible car nous contrôlons le champ le plus important, mais sur Windows 8 et 8.1, l'en-tête de tas est encodé, et nous finissons par écraser une partie de l'en-tête de tas de manière non sécurisée via le champ fs. L'en-tête _HEAP_ENTRY pour les systèmes 32 bits est similaire, mais il manque le champ PreviousBlockPrivateData.
Nous ne pouvons toujours pas corrompre toutes les parties de tagWND car il est impossible d'éviter les pointeurs tronqués. Et je n'ai toujours pas trouvé d'objet qui réponde à cela ; étant donné que _LARGE_UNICODE_STRING fonctionne bien sur 64 bits, je pense l'utiliser également sur 32 bits.
Mon idée est que si nous pouvons corrompre le champ iFirstFree de la structure tagPROPLIST (l'index du premier élément libéré dans la liste de propriétés) en augmentant sa valeur d'index, nous pouvons alors le faire pointer vers un emplacement plus loin sur le tas. Par exemple, nous pourrions le faire pointer vers le début de tagWND.strName. La figure 11 illustre cette idée :

Pour que le processus soit clair, nous utilisons maintenant deux structures tagPROPLIST : « PropListA » pour l'UAF, et « PropListB ». Nous avons besoin de savoir exactement quelles parties du tagPROP inséré dans « PropListA » écraseront le champ iFirstFree de « PropListB ». Nous devons également garder à l'esprit que nous ne pouvons écrire que 8 octets à la fois, donc nous devons insérer au moins un tagPROP supplémentaire dans « PropListA » : la première corruption écrase l'en-tête de tas adjacent, la seconde touche les champs tagPROPLIST de « PropListB ». Cela peut varier selon le système d'exploitation et la taille du bloc de tas, et dans mon exploitation, je dois m'adapter à diverses dispositions de tas. La figure 12 montre comment nous corrompons. Notez que le premier tagPROPLIST n'est pas divisé en champs individuels, donc tagPROP[0] est implicite. Cependant, dans le second tagPROPLIST, les membres internes sont montrés afin d'illustrer notre corruption. C'est pourquoi tagPROP[0] est affiché :

Tout d'abord, remarquons que si nous écrivons 8 octets pour chaque tagPROP, cela signifie que nous ne pouvons contrôler que partiellement l'écrasement de iFirstFree (puisqu'il provient des champs atomKey et fs), ce qui nous intéresse le plus. Comme nous pouvons contrôler entièrement au moins deux octets clés via la valeur atomKey, lorsque cette valeur est suffisamment petite, le champ fs devient 0. Par conséquent, nous utilisons la valeur hData pour écraser cEntries avec une valeur raisonnable, et utilisons atomKey pour faire pointer iFirstFree vers tagWND, où se trouve le pointeur strName.Buffer que nous voulons écraser. Si nous ne pouvons pas écraser directement les valeurs de Length et MaximumLength, nous pouvons pré-allouer une chaîne à la fenêtre cible pour garantir que sa longueur est déjà définie à une certaine valeur.
Examinons la structure tagWND sur 32 bits pour voir ce que nous obtenons. Notez que j'utilise le paramètre -b cette fois pour calculer facilement le décalage de Nuffer dans 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
[DRAPEAUX SUPPRIMÉS]
+0x014 bDestroyed : Pos 31, 1 Bit
+0x018 state2 : Uint4B
[DRAPEAUX SUPPRIMÉS]
+0x018 bWMCreateMsgProcessed : Pos 31, 1 Bit
+0x01c ExStyle : Uint4B
+0x01c bWS_EX_DLGMODALFRAME : Pos 0, 1 Bit
[DRAPEAUX SUPPRIMÉS]
+0x01c bUIStateFocusRectHidden : Pos 31, 1 Bit
+0x020 style : Uint4B
+0x020 bReserved1 : Pos 0, 16 Bits
[DRAPEAUX SUPPRIMÉS]
+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
[DRAPEAUX SUPPRIMÉS]
+0x0ac bChildNoActivate : Pos 11, 1 Bit
Le décalage de strName est 0x84, celui de Buffer est 0x8c. Sachant que nous avons l'index des éléments de la liste tagPROP et que nous pouvons écrire 8 octets, nous pouvons facilement savoir si iFirstFree indexe l'offset 0x88 de la fenêtre pointé par MaximumLength. Comme nous ne pouvons contrôler que deux octets de Buffer, l'écriture n'est pas réalisable ; étant donné que notre objectif est d'utiliser cela comme notre « primitive » de lecture/écriture arbitraire, ce résultat est inacceptable. Si nous écrivons un index qui pointe vers 0x90, nous écraserons cbwndExtra, ce qui n'est pas ce que nous recherchons.
Revenons à ce que nous pouvons contrôler lors du « tas windé » (heap spray) précédent, et voyons s'il existe un décalage intéressant dans tagWND que nous pouvons contrôler. À l'offset 0x70 dans tagWND se trouve le champ pSBInfo. Cet offset est divisible par 8, donc nous pouvons écraser ce pointeur avec une partie des données de hData d'un faux tagPROP.
Pouvons-nous faire pointer pSBInfo directement vers strName dans la même structure tagWND ? Peut-être pouvons-nous utiliser l'API de la barre de défilement pour corrompre strName afin d'atteindre notre objectif.
pSBInfo pointe vers une structure tagSBINFO, mentionnée lors de l'UAF initial.
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
Nous nous souvenons que WSBflags ne nous donne pas beaucoup de contrôle, mais nous savons au moins qu'il est mis à 1 lorsque la barre de défilement est activée, et à 0 lorsqu'elle est désactivée. Ce champ d'indicateur ne peut pas être défini à une valeur arbitraire ; en rétro-ingéniérie des fonctions pertinentes, nous constatons que si l'état de la barre de défilement ne change pas, ce champ reste inchangé. Les valeurs dans tagSBDATA semblent plus intéressantes. Si nous lisons la documentation de SetScrollInfo(), nous comprenons bien la signification de ces valeurs. Il semble que nous puissions définir les paramètres de SetScrollInfo() via la structure SCROLLINFO. Pourvu qu'il y ait un contrôle de barre de défilement près de la fenêtre que nous voulons corrompre, nous pouvons manipuler directement le pointeur pSBInfo (il envoie un message spécial à la fenêtre associée). Évidemment, nous pouvons contrôler posMin et posMax de manière inconditionnelle. Les champs page et pop sont un peu plus délicats, car ils sont limités à une certaine plage ; nous essayons actuellement de les éviter. Nous définissons le drapeau SIF_RANGE dans la structure SCROLLINFO pour déclarer où nous voulons définir les valeurs minimale et maximale.
Nous voulons écraser Buffer avec des données arbitraires, ce qui signifie que nous voulons que posMin écrase Buffer. Nous pouvons donc écraser pSBInfo pour qu'il pointe vers strName.MaximumLength. Tant que nous n'activons ni ne désactivons la barre de défilement, le champ WSBflags ne sera pas modifié, ce qui préserve l'intégrité de strName.MaximumLength. Ainsi, quelle que soit la façon dont nous définissons posMin (via nMin de SCROLLINFO), cela écrasera Buffer, et posMax sera écrit dans cbwndExtra. Ce n'est pas un gros problème ; sur un système 64 bits, nous pouvons pré-lire cette valeur et la restaurer plus tard. Le schéma général de l'overflow est illustré dans la figure 13 :

Maintenant, nous décrivons le processus d'attaque sur un système 32 bits avec la figure 14. Avant de corrompre quoi que ce soit loin de l'UAF, nous reculons d'abord pour voir la disposition relative des blocs de tas et du tas dans le diagramme. Maintenant que nous connaissons plus de détails, ce qu'il faut faire devient évident.

Ensuite, nous insérons deux éléments de propriété dans « PropListA », ce qui corrompt les données autour de « PropListA » grâce à la corruption précédente de l'UAF, et fait pointer iFirstFree de « PropListA » vers pSBInfo. Notez que cela corrompt également la valeur pSBInfo voisine, mais nous pouvons la pré-lire pour la restaurer après la corruption.

Nous insérons un nouveau tagPROP dans « PropListB », dont l'identifiant atom est différent de ceux déjà présents dans la liste, de sorte que ce tagPROP soit inséré à l'index libre suivant. Cela fait pointer pSBInfo vers strName.MaximumLength dans le même tagWND.

Enfin, nous actualisons la barre de défilement pour corrompre le champ strName.Buffer (comme le montre la figure 17) :

Notez que, contrairement au cas 64 bits, nous ne pouvons pas corrompre la valeur de longueur de strName. Nous pouvons pré-allouer une chaîne de texte de fenêtre de longueur appropriée afin que sa valeur soit déjà en utilisation. Par la suite, que nous voulions lire ou écrire des données à partir d'une adresse noyau, il suffit d'appeler SetScrollInfo() sur la fenêtre cible pour mettre à jour la valeur de Buffer, puis d'utiliser l'API de texte de fenêtre pour effectuer l'opération.
Nous avons maintenant une « primitive » de lecture/écriture arbitraire réutilisable sur les systèmes 32 bits !
À partir de maintenant, supposons que nous ayons une « primitive » de lecture/écriture arbitraire (primitive). Ainsi, lorsque je dis fuite/lecture d'une valeur ou réécriture d'une valeur, cela signifie l'exécution de cette « primitive » établie lors de la phase de corruption précédente. Cette « primitive » est sensiblement la même sur les deux plates-formes. Il ne reste plus qu'à écraser un pointeur de fonction et à le faire pointer vers une charge utile (shellcode) quelque part. La méthode courante consiste à écraser la deuxième entrée de nt!HalDispatchTable, qui correspond à la fonction HalQuerySystemInformation(). Ensuite, en mode utilisateur, nous appelons la fonction NtQueryInternalProfile() pour la déclencher.
Nous avons besoin de connaître l'adresse de base du chargement du module noyau pour calculer l'adresse noyau de nt!HalDispatchTable. Pour cela, nous pouvons appeler NtQuerySystemInformation() en mode utilisateur pour obtenir les informations du module, qui contiennent l'adresse de base du module.
// La valeur d'énumération 11 représente SystemModuleInformation, ce qui n'est pas documenté...
rc = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)11, pModuleInfo, 0x100000, NULL);
Ensuite, nous chargeons ntoskrnl.exe en mode utilisateur pour trouver le décalage de nt!HalDispatchTable, ce qui nous donne son adresse dans l'espace noyau. Ensuite, nous utilisons la « primitive de lecture » pour lire l'adresse noyau de HaliQuerySystemInformation() (cette fonction n'est pas exportée) afin de la modifier, puis nous utilisons la « primitive d'écriture » pour écraser le pointeur de cette fonction pour qu'il pointe vers l'adresse du shellcode (peut-être dans l'espace noyau ou utilisateur, comme détaillé plus tard). Le nombre d'octets lus/écrits est le même sur les systèmes 32 bits et 64 bits.
Windows 8 et 8.1 ont introduit le support de SMEP, et certains produits de sécurité peuvent également l'activer sous Windows 7, donc nous supposons qu'il est présent. SMEP empêche l'exécution de code en espace utilisateur avec des privilèges noyau, ce qui rend inutilisable le fait de faire pointer l'entrée de nt!HalDispatchTable vers une adresse en espace utilisateur. Par conséquent, nous voulons qu'elle pointe vers un emplacement contrôlable dans l'espace noyau, où le code peut modifier la valeur du registre cr4 pour désactiver SMEP, afin que nous puissions sauter vers l'espace utilisateur. L'article de MWR présente une astuce intéressante sous 64 bits : en mappant ses propres entrées de table de pages, on obtient une adresse noyau valide pour n'importe quelle adresse virtuelle. Ensuite, via la « primitive d'écriture », on modifie directement l'entrée de la table de pages et on change le masque de bits. J'ai porté cette astuce aux systèmes 32 bits, mais il existe des différences selon que PAE est activé ou non.
Pour y parvenir, la méthode évidente consiste à mapper une adresse en espace utilisateur vers l'espace noyau, puis à utiliser la « primitive d'écriture » pour que l'entrée de la table de pages ait des privilèges système plutôt qu'utilisateur. C'est la première chose que nous faisons. J'ai rencontré un problème intéressant lors de l'implémentation sous Windows 8. À partir de Windows 8, le gestionnaire de bureau (dwm.exe) scanne périodiquement les fenêtres du bureau et interroge leurs noms. Je n'ai pas enquêté sur la raison exacte. Cette opération n'envoie pas non plus de message à la fenêtre, mais il y a une fonction de gestion de fenêtre correspondante qui appelle GetInternalWindowText(). Par conséquent, le problème est que lorsque nous utilisons le champ strName de la fenêtre pour écraser l'entrée de la table de pages de la mémoire contenant le shellcode, cette mémoire appartient à la table de pages de notre propre espace processus. Lorsque dwm.exe récupère le nom de la fenêtre depuis le noyau, l'entrée de la table de pages modifiée fait que le noyau vérifie si strName.Buffer est non nul, ce qui déréférence indirectement cette adresse ; si l'adresse est invalide, cela provoque un crash du système.
Pour satisfaire la requête de dwm.exe, j'ai utilisé une adresse noyau comme charge utile. Ainsi, quel que soit le processus chargé, l'entrée de la table de pages associée à cette adresse sera toujours valide. J'ai choisi de la placer sur le tas de bureau, car nous pouvons calculer son adresse noyau via la méthode précédente. Nous utilisons toujours la technique de mappage de sa propre entrée de table de pages ; à ce stade, la table de pages est déjà marquée comme privilégiée mais pas comme exécutable. Il nous suffit donc de définir le bit d'exécution.
Étapes :
Sous Windows 8.1, il y a un autre problème : NtQuerySystemInformation() vérifie le SID de faible intégrité, ce qui signifie que seules les intégrités moyennes ou supérieures peuvent obtenir l'adresse de base du noyau. Cela peut être contourné facilement avec la technique bien connue de sidt. Nous sauvegardons l'adresse de l'IDT dans l'espace utilisateur (cela ne nécessite pas de vérification de privilège), puis utilisons la « primitive de lecture » pour lire l'index IDT souhaité, qui pointe généralement vers l'espace d'adressage noyau, ce qui nous permet de divulguer l'adresse noyau de la routine d'interruption, puis nous recherchons le décalage dans le fichier PE du module noyau correspondant.
Une fois que nous avons l'adresse de base du noyau, nous pouvons calculer l'adresse de nt!HalDispatchTable.
La méthode habituelle consiste à charger le fichier ntoskrnl.exe et à interpréter ses décalages de symboles, puis à ajouter l'adresse de base divulguée. Cependant, cela ne fonctionne pas sous un sandbox en mode amélioré, car il y a des restrictions du système de fichiers : vous ne pouvez pas lire C:\windows\system32\ntoskrnl.exe. Pour contourner cette restriction, nous utilisons notre « primitive de fuite » pour analyser l'image PE du noyau en mémoire afin de trouver l'adresse du symbole dont nous avons besoin.
C'est tout le matériel. Merci de votre lecture. En utilisant les techniques présentées dans cet article, j'ai pu réaliser une exploitation stable sur les systèmes 32 bits et 64 bits : XP, Vista, 7, 8, 8.1 et Server 2012. Sous Windows 2003 et 2008, ce n'est pas possible par défaut car il n'est pas possible de hooker les rappels en mode utilisateur, donc ces systèmes ne peuvent pas être attaqués à moins que les conditions requises ne soient remplies. Le processus d'exploitation est assez complexe, avec de nombreux obstacles à surmonter, mais cela m'a procuré beaucoup de plaisir et de choses à apprendre. De nombreuses méthodes et résultats de recherche utilisés dans cet article ont déjà été mentionnés dans des articles d'autres chercheurs. À ma connaissance, la seule mesure d'atténuation qui peut empêcher l'exploitation de win32k.sys est celle utilisée par le sandbox de Google Chrome, qui bloque efficacement les appels système du noyau win32k lors de l'exécution. J'attends avec impatience toute amélioration ou retour. Si vous avez des critiques sur certaines de mes techniques, faites-le moi savoir, je mettrai à jour ce document. Vous pouvez me contacter via Twitter @fidgetingbits ou par email [email protected].