Je profite des vacances pour jeter un oeil sur la faille trouvée par Tavis Ormandy dans le système de gestion du mode virtuel de Windows. Faille présente depuis Windows NT 3.1 (1993).
Pour pouvoir utiliser des exécutables 16bits(mode réel) dans un environnement 32bits(mode protégé) il faut passer par le mode virtuel. Pour cela, Windows a crée un sous-système appelé “NTVDM” qui va interagir avec le binaire 16bits, offrant à celui-ci la possibilité d’effectuer des instructions telles que CLI, STI, PUSHF, POPF, IRET
… Le passage en V86 se fait par le biais de la fonction NtVdmControl()
.
Il existe déjà un article sur le V86 sous Windows ici. Passons à ce qui nous intéresse, l’exploitation de la faille se fait en 3 étapes.
1] Utiliser NtVdmControl requière le SeTcbPrivilege.
La solution est basique, il suffit d’invoquer un exécutable 16bits. Alors ntvdm.exe va être lancé à son tour pour servir d’interface entre le binaire 16bit et le système d’exploitation. On injecte une dll dans ntvdm.exe et le code qu’elle exécutera bénéficiera du SeTcbPrivilege
car les contrôles pour ce token se font au niveau du processus. EPROCESS->Flags.VdmAllowed
.
Avant d’appeler la fonction NtVdmControl
il faut donner au champ Vdm du TEB
l’adresse d’une structure de type VDM_TIB
:
Ensuite, pour initialiser la VDM, il faut au préalable mapper le premier MB de mémoire virtuelle dans le processus ntvdm.exe. On peut maintenant appeler la fonction VdmpInitialize
(via NtVdmControl(3)) Le binaire 16bit pourra donc adresser les 1MB de RAM grâce à la segmentation (cs << 4) + (eip & 0xffff)
.
Maintenant qu’on a initialisé la VDM, on va pouvoir jouer avec. Pour cela, on va s’intéresser à nt!VdmpStartExecution
. Cette fonction est invoquée via NtVdmControl(0, NULL). nt!NtVdmControl
va d’abord appeler nt!VdmpGetVdmTib
pour vérifier que notre VdmTib (TEB->Vdm) a une taille conforme. Tavis a utilisé une boucle qui incrémente la taille du champ TEB->Vdm->Size
afin de trouver la bonne taille. (Et pour que l’exploit passe sur la plupart des versions de Windows). Sur Xp la taille est hardcoder.
nt!VdmpGetVdmTib+0x50:
805b7aad cmp dword ptr [eax],674h
Pourquoi cette valeur? C’est la taille de la structure VdmTib (Elle change suivant les versions de Windows). Avant de lancer l’exécution de la VDM je mets le champ VdmTib.Size = 0x674;
.
2] Modification du registre CS en ring3
En mode protégé, le Cpl (Current Privilege Level) d’un thread est indiqué par les deux bits de poids faible de ces segments CS, DS, ES, FS, GS et SS
. C’est une manière simple de savoir s’il doit tourner en ring0(kernel) ou ring3(user). Mais en mode réel cela n’existe pas, l’adressage se fait de la façon suivante: (cs<<4) + (eip&0xffff) (segment+offset)
. Idem pour le mode virtuel qui nous laisse modifier le registre CS
.
3] Une “trap frame” ne peut être forgée en ring3
La fonction nt!VdmpStartExecution
va faire passer notre thread en mode virtuel pour cela elle va faire appel à la fonction nt!VdmSwapContexts
. Celle-ci va mettre à jour la trap frame crée par KiFastCallEntry
avec notre VdmTib.VdmContext
.
La trap frame contient l’état de tous les registres du thread avant le passage en ring0(kernel). Voici ce qui va se passer:
Donc on contrôle notre trap frame. A la fin de la fonction nt!VdmpStartExecution
l’instruction IRET
fait basculer notre thread vers le ring3(user) et restaure ses registres avec ceux de la trap frame.
Le registre qui nous intéresse le plus est l’EFLAG
qui contient TF=1
et VM=1
. Le VM=1
va créer une task en mode virtuel, le TF=1
quant à lui sort de nulle part:
En effet le handler de #DB
est la fonction KiTrap01
mais c’est la KiTrap0D
qui est appelée. On a donc affaire à une exception de type #GP
! D’après Rob Collins, le comportement de l’instruction IRET
avec TF=1
provoque un #GP
mais les pentiums les plus récents ne provoquent plus de #GP
.
Bref, une exception de type #GP
est levée. On est encore en ring0 donc il n’y a pas de changement de privilège, la pile n’est pas changée et notre trap frame reste la même. On passe donc par le handler KiTrap0D. Celui-ci va effectuer des tests dont:
nt!KiTrap0D+0x22f:
804e116e mov eax,offset nt!Ki386BiosCallReturnAddress (805093fd)
804e1173 cmp eax,dword ptr [edx] ; edx = KTRAP_FRAME.HardwareEsp (VdmContext->eip)
...
804e1177 mov eax,dword ptr [edx+4] ; edx+4 = KTRAP_FRAME.HardwareSegSs (VdmContext->SegCs)
804e117a cmp ax,0Bh
Donc, en faisant pointer VdmContext->eip
sur l’adresse de Ki386BiosCallReturnAddress
(qui a déclenché kitrap0d), et VdmContext->SegCs
sur 0xB on est dirigé vers la fonction Ki386BiosCallReturnAddress
. Dans le cas contraire KiTrap0D nous renvoie un “Access violation”.
// Pour simplifier au max la fonction Ki386BiosCallReturnAddress va faire:
80509415 mov esp,dword ptr [ebp+58h] ; esp = &KernelStack; (VdmContext.Esi notre fausse pile)
80509418 add esp,4 ; esp += 4
...
80509421 mov dword ptr [ecx+18h],edi ; (KTHREAD)CurrentThread->InitialStack = &KernelStack;+0x230
Donc, on contrôle ESP
et ces 8 premiers DWORD (Le reste est écrasé). C’est assez pour écraser le saved EIP
et rediriger le flux d’exécution vers une fonction de notre dll. Par contre on peut voir aussi qu’avant d’arriver dans notre dll, Ki386BiosCallReturnAddress
fout le bordel dans notre KTHREAD
. Donc dès qu’on retourne dans notre dll on profite d’être en ring0 (cpl à 0 pour le registre cs) pour bloquer les interruptions et réparer notre KTHREAD
. Tavis scanne les 512 premiers DWORDs de cette structure, sûrement car cette structure change en fonction des versions de windows. En tout cas, il y a plus simple (je peux me tromper). la structure KTHREAD
est modifiée au niveau du champ InitialStack
.
Offset du champ InitialStack
dans la structure KTHREAD
:
+0x18 pour Xp sp3
+0x28 pour Vista et Win Seven
Cela fait 2 valeurs à scanner, enfin une en utilisant GetVersionEx()
. Ensuite on lui remet sa valeur par défaut. Dans le cas contraire au moment du changement de thread (nt!SwapContext
) on se mange un joli BSOD.
Le PoC de tavis est là, le code est super clean. Cela vaut le coup d’oeil.
EDIT: Merci à ivanlef0u pour la relecture/correction :]
http://seclists.org/fulldisclosure/2010/Jan/341
http://cert.lexsi.com/weblog/index.php/2010/01/20/359-windows-de-nouveau-impacte-par-une-0-day-vdm
Des sites qui m’ont fait gagner du temps:
http://www.reactos.org/fr/index.html
http://msdn.msuiche.net/