
Utiliser CVE-2016-3308 pour corrompre le tas de bureau win32k
auteur : @55-AA, 18 septembre 2016
##Introduction
Le tas du bureau (Desktop Heap) est un pool noyau utilisé par win32k ; il peut être exploité par une application en mode utilisateur. Je vais décrire en détail comment implémenter une exploitation fiable afin de lire/écrire une adresse arbitraire dans le noyau. Cet article et l'analyse associée ont été réalisés sur une installation win7_sp1_x86 (build 17842).
Le 9 août 2016, Microsoft a publié MS16-098. Le code vulnérable se trouve dans la fonction win32k!xxxInsertMenuItem, dont le prototype est :
BOOL xxxInsertMenuItem(
PMENU pMenu,
UINT wIndex,
BOOL fByPosition,
LPMENUITEMINFOW lpmii,
PUNICODE_STRING pstrItem
);
Commençons par examiner le pseudo-code du bug dans xxxInsertMenuItem :
if (pMenu->cItems >= pMenu->cAlloced) {
if (pMenu->rgItems) {
pNewItems = (PITEM)DesktopAlloc(
pMenu->head.rpdesk,
(pMenu->cAlloced + CMENUITEMALLOC) * sizeof(ITEM),
DTAG_MENUITEM);
......
pMenu->cAlloced += CMENUITEMALLOC;
pMenu->rgItems = pNewItems;
if (wIndex != MFMWFP_NOITEM)
pItem = MNLookUpItem(pMenu, wIndex, fByPosition, &pMenuItemIsOn);
......
pMenu->cItems++;
if (pItem != NULL) {
RtlMoveMemory(pItem + 1, pItem, (pMenu->cItems - 1) *
sizeof(ITEM) - ((char *)pItem - (char *)pMenu->rgItems));
} else {
Dans le code ci-dessus, lorsque le 9e (à partir du 1er) élément a été ajouté au pMenu, DesktopAlloc() a été appelé pour réallouer un nouveau pMenu->rgItems. Ensuite, MNLookUpItem() a été appelé pour obtenir l'emplacement de l'élément dans pMenu->rgItems. Mais le pItem retourné par MNLookUpItem() est un rgItems d'un autre pSubMenu, et non du pMenu. Ainsi, lorsque RtlMoveMemory() est appelé, le pItem du pSubMenu et les octets suivants sont écrasés en raison de la taille de déplacement erronée.
Voici le code de désassemblage du bug, qui déclenche un écrasement du tas ; il peut être exploité pour construire un faux bloc :
0: kd> u win32k!xxxInsertMenuItem+0x1f5 l8
win32k!xxxInsertMenuItem+0x1f5:
95d295af 6bc06c imul eax,eax,6Ch
95d295b2 2bc3 sub eax,ebx
95d295b4 034634 add eax,dword ptr [esi+34h]
95d295b7 50 push eax
95d295b8 8d436c lea eax,[ebx+6Ch]
95d295bb 53 push ebx
95d295bc 50 push eax
95d295bd e85ea40100 call win32k!memmove (95d43a20)
Pour suivre le bug, j'utilise ces points d'arrêt dans WinDbg :
ba e1 win32k!xxxInsertMenuItem
ba e1 win32k!xxxInsertMenuItem+0xf3
95d294e3 e843e70200 call win32k!DesktopAlloc (836d7bf5)
ba e1 win32k!xxxInsertMenuItem+0x129
95d294e3 e80de70200 call win32k!DesktopAlloc (836d7bf5)
ba e1 win32k!xxxInsertMenuItem+0x1f5
95d295af 6bc06c imul eax,eax,6Ch
Pour déclencher le bug, les étapes suivantes doivent être effectuées :
Le tas du bureau est un pool global utilisé par tous les processus GUI. Tous les objets GUI, comme les fenêtres et les menus, sont stockés dans le tas du bureau et gérés par l'allocateur de tas du noyau. L'allocateur de tas du noyau utilise des fonctions familières telles que RtlAllocateHeap et RtlFreeHeap. Contrairement au tas en mode utilisateur, le tas du bureau n'utilise aucun allocateur frontal : pas de Low Fragmentation Heap, pas de liste Lookaside, etc. Il n'y a pas non plus de Heap Encoding avant Windows 8 et ultérieur. Voici la structure d'un bloc sur win7_sp1_x86 :
typedef struct _HEAP_ENTRY {
USHORT Size;
UCHAR Flags;
UCHAR SegmentIndex;
USHORT PreviousSize;
UCHAR SegmentOffset;
UCHAR UnusedBytes;
} HEAP_ENTRY, *PHEAP_ENTRY;
Les champs Size et PreviousSize représentent la taille des blocs, décalée vers la droite de HEAP_GRANULARITY_SHIFT (défini comme 3 sur les systèmes 32 bits). Le champ Size spécifie le bloc courant, et PreviousSize spécifie le bloc précédent. Le bit de poids faible de Flags est généralement défini à HEAP_ENTRY_BUSY (0x01), indiquant que le bloc est en cours d'utilisation ; sinon, il est à 0x00.
La figure suivante illustre la relation entre ces champs et les blocs. Le deuxième WORD souligné en vert (0x000f) indique que la taille du bloc courant est de 0x78 octets, le deuxième WORD souligné en noir (0x0003) indique que la taille du bloc précédent est de 0x18 octets, et les WORDs soulignés en rouge (0x0001) indiquent que les blocs courants sont en cours d'utilisation. Ici, la taille du bloc inclut la taille de l'en-tête, dont la structure HEAP_ENTRY est définie ci-dessus.

C'est la caractéristique la plus importante pour la corruption du tas : l'allocateur de tas récupère toujours le bloc libéré le plus récemment. Cela signifie que nous pouvons en réalité allouer un bloc de n'importe quelle taille et à un emplacement précis de notre choix.
En exploitant le bug, je peux écraser certains octets dans le tas du bureau. J'obtiens ainsi un faux bloc qui remplace un bloc normal ; puis je libère le bloc remplacé, de sorte que le faux bloc est poussé en tête de la liste des blocs libres. Ensuite, le faux bloc est réutilisé et je peux y écrire n'importe quels octets. La région inscriptible chevauche plusieurs blocs normaux, mais elle ne peut pas couvrir tout l'espace noyau. Je dois donc construire une autre primitive de lecture/écriture dans la région chevauchée, en tirant parti de tagWND.strName pour écrire à une adresse arbitraire. Le pointeur de strName.Buffer peut nous mener n'importe où, aussi bien dans l'espace noyau que dans l'espace utilisateur. Bien entendu, notre cible est uniquement nt!HalDispatchTable.
La figure suivante montre le déroulement des modifications du tas :

Comme le montrent les démonstrations, je corromps le tas du bureau étape par étape et mets en œuvre l'exploitation selon les étapes suivantes :