
CVE-2020-15368, alias „Wie man einen verwundbaren Treiber ausnutzt“
Exploit und Proof of Concept (PoC) für CVE-2020-15368. Asrock hat den rweverything-Treiber für ihr RGB-Controller-Konfigurationstool neu verpackt und signiert. Sie „schützen“ ihn, indem sie ihre IOCTLs verschlüsseln ... lol. Wir haben diese CVE letzten Sommer zufällig gefunden, und soweit ich weiß, ist der Treiber immer noch nicht gepatcht. Der Impact ist natürlich beliebige Codeausführung im Kernel usw. Also genießt diesen „0day“ lol.
Wenn du mit mir darüber diskutieren willst, ob das eine ECHTE BONA FIDE CVE ist, kannst du mich gerne auf Twitter kontaktieren – wir können einen großen Streit in den sozialen Medien austragen, und es wird für alle Beteiligten wirklich aufregend sein! Ich kaufe sogar eine Domain für diesen Bug, wenn du dazu geneigt bist. Es geht doch nur um Marketing!!!!
Wie auch immer, dieser Bug ist ziemlich beschissen, also werde ich ihn als Tutorial dafür nutzen, wie man einen typischen verwundbaren Treiber pwned. Dieser Beitrag richtet sich also an Anfänger. Du lernst, wie man einen verwundbaren Treiber ausnutzt. Es gibt tonnenweise andere beschissene Treiber da draußen wie diesen. Die Welt liegt dir zu Füßen. Viel Spaß
HAFTUNGSAUSSCHLUSS: Diese Veröffentlichung dient nur zu Bildungszwecken. Es liegt in der Verantwortung des Lesers, alle geltenden lokalen, landes- und bundesrechtlichen Gesetze zu befolgen. Der/die Autor(en) dieser Veröffentlichung übernehmen keine Haftung und sind nicht verantwortlich für jeglichen Missbrauch oder Schäden, die durch die in dieser Veröffentlichung enthaltene Software verursacht werden.
Im Quarantäne-Lockdown hingen meine Mitbewohner (Pear0, Codetector) und ich zusammen herum und spielten mit Pear0s neuem Asrock-Motherboard herum. Die hellen roten LEDs waren extrem nervig und es war nicht möglich, sie unter Linux zu konfigurieren. Unser Plan war es also, den Windows-Treiber zu reversen, der sie steuerte, und die I/O-Operationen unter Linux nachzubilden.
Kurz gesagt, es dauerte nicht lange, bis wir erkannten, dass der Treiber buchstäblich nur ein generischer Treiber ist, der beliebigen Lese-/Schreibzugriff auf alles gewährt. Das umfasst Kontrollregister wie CR3, CR4, physischen Speicher usw. Treiber wie dieser sind für die Verwendung als Debugging-Werkzeug gedacht und die Website des Anbieters sagt das deutlich.

Wir fanden das äußerst witzig. Es ist ziemlich aufregend, wenn man zum ersten Mal von User Mode aus einen Triple Fault auslöst und den Computer hart neu starten lässt. (Beim zwanzigsten Mal vielleicht weniger aufregend.) Wie auch immer, wir meldeten den Bug und vergaßen ihn dann ein Jahr lang.
Als Kernel-Noob fragte ich mich, wie ich den Treiber tatsächlich laden und mit ihm interagieren kann. Wie sich herausstellt, ist das extrem einfach.
Du kannst einfach einen Dienst für den Treiber in Process Hacker erstellen (offensichtlich sind Administratorrechte erforderlich, um Treiber zu laden). Dann kannst du einfach mit der rechten Maustaste klicken und ihn starten. Ja, es ist wirklich so einfach.

Wir können unser Device-Objekt in WinObjEx64 anzeigen.

Wir können sogar in FileTest mit dem Gerät spielen.


Alle diese 3 Werkzeuge sind erstaunlich, besonders PH und FileTest. Sie sind wie ein Schweizer Taschenmesser und sollten in der Toolbox jedes Windows-Reversers stecken. Zum Beispiel hat Jonas L meines Wissens nach unzählige Windows-LPE-Schwachstellen gefunden, nur indem er in FileTest herumgespielt hat. Windows hat also wirklich einige großartige Werkzeuge zum Herumspielen. Ich wünschte, es gäbe diesen Scheiß unter Linux.
Rweverything hat einen IOCTL, der einen IOCTL als Parameter entgegennimmt, der steuert, welche Operation ausgeführt werden soll (Speicher lesen, Speicher schreiben, MSR lesen usw.), sowie eine Union aus operationsspezifischen Parametern wie Quelladresse, Zieladresse usw. Wenn wir den Code der beiden Treiber vergleichen:

🤔🤔🤔🤔🤔🤔🤔🤔🤔
Nichtsdestotrotz unternimmt der Treiber einen stümperhaften Versuch von Sicherheit durch Obskurität, indem er verlangt, dass alle IOCTL-Aufrufe mit einem hartcodierten AES-Schlüssel entsprechend verschlüsselt werden. Der Code (nach einiger Bereinigung) sieht so aus:
if ( IoControlCode == 0x22EC00 )
{
char enc_key[32];
memset(enc_key, 0, sizeof(enc_key));
memmove(enc_key, "C110DD4FE9434147B92A5A1E3FDBF29A", 32ui64);
memcpy(enc_key + 13, ioctl_args->key, 16);
size_t cb_decrypted = 0;
my_decrypted_cmd* decryptedCmd = NULL;
DWORD iv_size = ioctl_args->iv_size;
DWORD input_size = *(DWORD*)(ioctl_args + bufferLen - 6);
// really just calls BCrypt API to get an AES implementation
if ( (unsigned int)decrypt_ioctl_params(enc_key, 32, ioctl_args->iv, iv_size, ioctl_args + bufferLen - input_size - 6, input_size, &decryptedCmd, &cb_decrypted) )
{
// Decryption failed
if ( decryptedCmd )
ExFreePoolWithTag(decryptedCmd, 0);
irp->IoStatus.Status = 0xC000000D;
goto Fail_Out;
}
IoControlCode = decryptedCmd->opcode;
Rweverything_Args = &decryptedCmd->args;
}
else // not 0x22EC00
{
if ( IoControlCode != 0x22E858 &&
IoControlCode != 0x22E860 &&
IoControlCode != 0x22E800 &&
IoControlCode != 0x22E804 )// whitelisted control codes
IoControlCode = 0; // block everything else
}
Der Treiber erlaubt einige langweilige Operationen, die etwas PMIO machen, aber alle „lustigen“ Steuercodes sind hinter dieser Entschlüsselungsroutine versteckt. Trotz dieser expliziten Allow-Liste enthält er immer noch die gesamte gefährliche Rweverything-Funktionalität. Statt diese gefährlichen Funktionen zu verstecken, hätten sie wahrscheinlich einfach ganz entfernt werden sollen.
Interessanterweise erlaubt der Treiber dem Benutzer auch, einen Teil des Schlüssels anzugeben (???), aus welchem Grund auch immer – ich habe keine Ahnung. Der Code ist einfach sehr schlecht geschrieben.
Wie auch immer, es ist relativ einfach, den Client-Code zu schreiben, um diese seltsame verschlüsselte API zu nutzen und ihr beliebige IOCTL-Aufrufe zu übergeben, die wir wollen. Ich werde dich nicht mit den Details langweilen.
Wir öffnen ein Handle auf den Treiber und verwenden DeviceIoControl, um den IOCTL aufzurufen – ganz Standard.
HANDLE hDevice = CreateFileA("\\\\.\\GlobalRoot\\Device\\AsrDrv104", GENERIC_READ | GENERIC_WRITE | SYNCHRONIZE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
// ... set up the encrypted ioctl data
BOOL result = DeviceIoControl(hDevice, 0x22EC00, ioctl_data, in_buf_size, out_buf, sizeof(out_buf), &bytes_returned, NULL);
Jetzt, wo wir mit dem versteckten Rweverything-Teil des Treibers kommunizieren können, wollte ich als Erstes einen Absturz auslösen, um zu wissen, dass mein Treiber-Client funktioniert.
Der einfachste Weg, dies zu erreichen, ist, CR3 mit Müll zu überschreiben. Ich weiß, einige von euch, die das lesen, sind Noobs, und das ist in Ordnung, also werde ich es im Detail erklären. Ich bin auch dumm, also hilft euch das vielleicht beim Lernen. Wenn ihr wisst, was ihr tut, könnt ihr das überspringen.
Unter x86 zeigt CR3, wenn Paging aktiviert ist (so gut wie immer in jedem modernen Betriebssystem), auf die physische Basisadresse des obersten Page-Table-Verzeichnisses. Wenn du nicht weißt, was das bedeutet, lies den Wikipedia-Artikel über virtuellen Speicher.
Wenn wir CR3 mit Müll überschreiben, sagen wir 0x0000000000000000, wird der TLB geleert, und beim Versuch, die nächste Instruktion auszuführen, versucht der Prozessor (genauer gesagt die MMU), den Instruktionszeiger in eine physische Adresse zu übersetzen. Die Adressübersetzung kann man sich im Wesentlichen als eine Reihe von Page-Table-Walks vorstellen, die bei CR3 beginnen. CR3 zeigt nun auf den physischen Speicher bei 0, der tatsächlich existiert und zugänglich ist; es ist jedoch äußerst unwahrscheinlich, dass dies eine gültige Page Table ist. (Page-Table-Einträge, kurz PTEs, müssen eine bestimmte Struktur aufweisen.)
Wenn das passiert, erhalten wir einen Page Fault bei der Adressübersetzung. Normalerweise würde uns die CPU zur Adresse des Page-Fault-Handlers bringen. Aber woher weiß sie, wo sich die Page-Fault-Handler-Funktion befindet? Das ist in einer Speicherdatenstruktur gespeichert, die als Interrupt Descriptor Table (IDT) bekannt ist. Der Prozessor hat ein Register (gelesen/geschrieben durch die sidt- und lidt-Instruktionen), das die virtuelle Adresse der IDT enthält. Erkennst du das Problem jetzt? Um den Page Fault zu behandeln, müssen wir zuerst einen weiteren Zugriff auf den virtuellen Speicher durchführen und damit eine weitere Adressübersetzung.
Natürlich wird auch unsere zweite Adressübersetzung einen Fehler auslösen. Jetzt haben wir einen Double Fault: ein Fehler, der auftritt, während der erste Page Fault behandelt wird. Das ist ziemlich ernst, aber immer noch behebbar---der Prozessor gibt uns eine letzte Chance zur Wiederherstellung. Natürlich wird auch dieser Versuch mit einem dritten und letzten Page Fault, einem Triple Fault, brutal abgebrochen. An diesem Punkt gibt die CPU einfach auf und setzt die Maschine hart zurück. Wenn du diese Prozedur auf einem physischen Rechner durchgeführt hast, würdest du wahrscheinlich gerade jetzt den BIOS-Splash-Screen sehen.
Wenn du jetzt irgendwelche Fragen hast, gebe ich dir dieselbe Antwort, die mir immer gegeben wurde. Nämlich: Lies das Intel Manual Volume 3A. (auch bekannt als die Bibel).
OK, wie nutzen wir den Treiber nun tatsächlich aus? Wenn wir uns umsehen, sehen wir eine freie Primitive zum beliebigen Lesen/Schreiben von physischem Speicher. Sie bildet im Grunde jede gewünschte physische Adresse mit MmMapIoSpace ab, kopiert deinen Puffer dorthin (oder umgekehrt) und hebt die Abbildung wieder auf.
Kleiner Hinweis: Wenn du versuchst,
MmMapIoSpacemit irgendwelchen dummen Argumenten wie wir aufzurufen, während ein Kernel-Debugger angeschlossen ist, löst du einen Bugcheck aus. Du kannst das umgehen, indem du ein Magic Byte in WinDbg schreibst. Schau nach dem Kommentar, derMiShowBadMapperin exploit.cpp referenziert, um mehr zu erfahren. Ich weiß nicht wirklich, was das soll, und ich will es auch gar nicht herausfinden
Wie können wir diese Primitive nutzen, um Codeausführung im Kernel zu erlangen? Das Hauptproblem dieser Primitive ist, dass sie auf physischem Speicher arbeitet. Als Usermode-Programm haben wir so gut wie keine Ahnung, wie das Layout des physischen Speichers aussieht---das Betriebssystem erledigt das alles für uns. Selbst wenn wir virtuelle Adressen einiger Kernel-Datenstrukturen oder Kernel-Funktionszeiger erhalten, haben wir keine Ahnung, wo sie sich im physischen Adressraum befinden.
Eine Idee ist, CR3 zu lesen, die Page Tables zu lesen und die virtuelle Adressübersetzung selbst durchzuführen. Das ist eine großartige Idee. Sie funktioniert nicht. Das liegt daran, dass Windows es nicht mehr erlaubt, Page Tables mit MmMapIoSpace abzubilden. Also müssen wir schlauer werden.
Ich habe die Technik von xeroxz aus VDM verwendet. Sie ist ziemlich einfach, aber die Technik ist ziemlich raffiniert. Obwohl wir das Layout des physischen Speichers nicht kennen, können wir den gesamten physischen Speicher durchsuchen, bis wir finden, wonach wir suchen. Eine Sache, die wir ausnutzen können, ist, dass die Inhalte einer Seite sowohl physisch als auch virtuell immer gleich sind: Alle Offsets relativ zu Seitengrenzen bleiben immer erhalten. Wenn ich zum Beispiel die Seite 0x7fff000000000XXX auf den physischen Frame 0x0000000123456XXX abgebildet habe, ist das XXX aller Adressen sowohl in der physischen als auch in der virtuellen Adresse gleich. Die gesamte Intra-Seiten-Struktur bleibt erhalten; wir können also nach einer interessanten Seite suchen, die wir überschreiben möchten.
Das einfachste, was wir überschreiben können, ist wahrscheinlich ein leicht erreichbarer Syscall- oder IOCTL-Handler. Unter Windows gibt es eine Standardfunktion Beep(), die deinen Computer piepen lässt. Ob du es glaubst oder nicht, diese ist in einem Treiber implementiert, Beep.sys, der das Beep-Gerät bereitstellt. (Du kannst es tatsächlich im WinObjEx64-Screenshot von vorhin sehen.) Jeder kann das Beep-Gerät verwenden, und es wird selten aufgerufen. Also überschreiben wir den Beep-IOCTL-Handler.
Wir können Beep.sys in IDA laden und uns den DeviceIoControl-Handler ansehen.

Bei Seitenoffset 0x270 haben wir diesen Code mit den Bytes 40 53 48 .... Keines dieser Bytes ist reloziert, daher ist das Scannen nach dieser Funktion sehr einfach. Wenn es relozierte Bytes gäbe, müssten wir sie mit Wildcards maskieren. Es ist dieselbe Idee wie Signatur-Scanning, wenn man einen Game-Hack schreibt.
Nachdem wir also den physischen Speicher durchsucht haben, um diesen Code zu lokalisieren, können wir ihn einfach mit unserem eigenen Shellcode überschreiben. Du musst auch vorsichtig sein, da es möglicherweise mehrere Kopien dieser Seite im physischen Speicher gibt (!) – also such alle Kopien.
An diesem Punkt können wir ziemlich einfach Privilegien ausweiten, indem wir das Sicherheits-Token unseres Prozesses mit dem eines Systemprozesses austauschen, um nt authority\system-Berechtigungen zu erhalten. Leider erfordert der Asrock-Treiber ohnehin Administratorrechte zum Öffnen, daher ist das nicht sehr interessant.
Für uns schreiben wir einen grundlegenden Shellcode, der ein Stage-2-Payload alloziert und kopiert und dann einen neuen Kernel-Thread erzeugt. Wir können nicht alles in unserem überschriebenen Beep-Handler erledigen, denn 1) wir sind auf 1 Seite begrenzt und 2) wir werden das System zum Absturz bringen, wenn wir versuchen, unser Handle auf das Beep-Gerät zu schließen, da wir auch den Rest des Codes im Beep-Gerät beschädigt haben. Was das Beschaffen von Kernel-Zeigern betrifft, ist das eigentlich einfach, denn NtQuerySystemInformation gibt sie uns gratis, wenn wir nett fragen.
__int64 __declspec(dllexport) __fastcall MyIRPHandler(struct _DEVICE_OBJECT* a1, IRP* irp)
{
MyIrpStruct* user_data = (MyIrpStruct*)irp->AssociatedIrp.SystemBuffer;
void* my_rwx = user_data->nt_ExAllocatePoolWithTag(NonPagedPoolExecute, user_data->payload_size, 'lmao');
user_data->nt_memcpy(my_rwx, user_data->payload, user_data->payload_size);
HANDLE hThread;
user_data->nt_PsCreateSystemThread(&hThread, THREAD_ALL_ACCESS, NULL, NULL, NULL, (PKSTART_ROUTINE)my_rwx, NULL);
user_data->nt_IofCompleteRequest(irp, 0);
return 0;
}
Also patchen wir Beep schnell, rufen den überschriebenen IOCTL-Handler auf und machen den Patch wieder rückgängig. Jetzt haben wir sicher einen Kernel-Thread erstellt, der unseren Code ausführt, ohne irgendetwas anderes auf dem System zu beschädigen. An diesem Punkt können wir unsere eigenen Treiber laden oder was auch immer.
Ich bin ein schlechter Sicherheitsforscher und ich finde nur zufällig wertlose Bugs. Danke an alle fürs Lesen. Bitte abonniert meinen OnlyFans