Im vergangenen Monat hat Microsoft eine Sicherheitslücke im Microsoft Kernel Streaming Server geschlossen, einer Windows-Kernelkomponente, die bei der Virtualisierung und gemeinsamen Nutzung von Kamerageräten zum Einsatz kommt. Die Schwachstelle CVE-2023-36802 ermöglicht es einem lokalen Angreifer, seine Berechtigungen auf SYSTEM zu erweitern.
Dieser Blogbeitrag beschreibt meinen Prozess der Erforschung einer neuen Angriffsfläche im Windows-Kernel, der Entdeckung einer 0-Day-Sicherheitslücke, der Untersuchung einer interessanten Fehlerklasse und der Entwicklung eines stabilen Exploits. Für diesen Beitrag sind keine speziellen Kenntnisse des Windows-Kernels erforderlich, allerdings sind grundlegende Kenntnisse über Speicherbeschädigungen und Betriebssystemkonzepte hilfreich. Ich werde auch die Grundlagen der ersten Analyse eines unbekannten Kernel-Treibers behandeln und den Prozess der Untersuchung eines neuen Ziels vereinfachen.
Der Microsoft Kernel Streaming Server (mskssrv.sys) ist eine Komponente des Windows Multimedia Framework-Dienstes Frame Server. Der Dienst virtualisiert die Kamera und ermöglicht die gemeinsame Nutzung des Geräts durch mehrere Anwendungen.
Ich habe begonnen, diese Angriffsfläche zu erkunden, nachdem ich auf CVE-2023-29360 aufmerksam wurde, das ursprünglich als TPM-Treiber-Schwachstelle aufgeführt war. Der Bug liegt tatsächlich im Microsoft Kernel Streaming Server. Obwohl ich zu diesem Zeitpunkt noch nicht mit MS KS Server vertraut war, weckte der Name dieses Treibers mein Interesse. Obwohl ich nichts über den Zweck oder die Funktionsweise wusste, dachte ich, ein Streaming-Server im Kernel könnte ein nützlicher Ort sein, um nach Sicherheitslücken zu suchen. Ohne weitere Vorkenntnisse versuchte ich, die folgenden Fragen zu beantworten:
Um die erste Frage zu beantworten, habe ich zunächst die Binärdatei in einem Disassembler analysiert. Ich habe die oben erwähnte Sicherheitslücke schnell identifiziert, ein einfacher und eleganter Logikfehler. Das Problem schien leicht auszulösen und vollständig auszunutzen. Daher versuchte ich, einen schnellen Machbarkeitsnachweis zu entwickeln, um das Innenleben des mskssrv.sys-Treibers besser zu verstehen.
Branchen-Newsletter
Bleiben Sie mit dem Think-Newsletter über die wichtigsten – und faszinierendsten – Branchentrends in den Bereichen KI, Automatisierung, Daten und mehr auf dem Laufenden. Weitere Informationen finden Sie in der IBM Datenschutzerklärung.
Ihr Abonnement wird auf Englisch geliefert. In jedem Newsletter finden Sie einen Abmeldelink. Hier können Sie Ihre Abonnements verwalten oder sich abmelden. Weitere Informationen finden Sie in unserer IBM Datenschutzerklärung.
Zunächst müssen wir in der Lage sein, den Treiber von einer Anwendung im Benutzerbereich aus zu erreichen. Die anfällige Funktion ist über die Routine DispatchDeviceControl des Treibers erreichbar, d. h. sie kann durch Ausführen eines IOCTL-Befehls an den Treiber aufgerufen werden. Dazu muss ein Handle auf das Gerät des Treibers über einen Aufruf von CreateFile mithilfe des Geräts erhalten werden. In der Regel lässt sich der Gerätename/-pfad leicht identifizieren: Suchen Sie im Treiber nach einem Aufruf von IoCreateDevice und überprüfen Sie den dritten Parameter, der den Gerätenamen enthält.
Funktion in mskssrv.sys, die IoCreateDevice mit einem NULL-Hinweis für den Gerätenamen aufruft
In diesem Fall ist der Parameter für den Gerätenamen NULL. Der Name der aufrufenden Funktion deutet darauf hin, dass mskssrv einPnP-Treiber ist, und der Aufruf von IoAttachDeviceToDeviceStack zeigt an, dass das erstellte Geräteobjekt Teil eines Gerätestapels ist. Das bedeutet, dass mehrere Treiber aufgerufen werden, wenn eine E/A-Anforderung an ein Gerät gesendet wird. Bei PnP-Geräten wird der Geräteschnittstellenpfad benötigt, um auf das Gerät zugreifen zu können.
Mit dem WinDbg Kernel-Debugger können wir sehen, welche Geräte zum mskssrv-Treiber und zum Device-Stack gehören:
Ausgabe von !drvobj- und !devobj-Befehlen, die obere und untere Geräte anzeigen
Oben sehen wir, dass das Gerät von mskssrv an das untere Geräteobjekt angehängt ist, das zum swenum.sys Treiber gehört. Es wurde ein oberes Gerät angeschlossen, das zu ksthunk.sys gehört.
Im Geräte-Manager finden wir die Geräteinstanz-ID des Zielgeräts:
Geräte-Manager zeigt Geräteinstanz-ID und Schnittstellen-GUID
Wir haben jetzt genug Informationen, um den Pfad der Geräteschnittstelle mit Konfigurationsmanager oder SetupApi-Funktionen zu erhalten. Mit dem abgerufenen Pfad der Geräteschnittstelle können wir ein Handle für das Gerät öffnen.
Schließlich können wir nun die Codeausführung innerhalb mskssrv.sys starten. Wenn das Gerät erstellt wird, wird die Funktion PnP dispatch create des Treibers aufgerufen. Um die Ausführung von zusätzlichem Code auszulösen, können wir IOCTLs senden, um mit dem Gerät zu kommunizieren, das in der Dispatch-Gerätesteuerungsfunktion des Treibers ausgeführt wird.
Bei der Durchführung von Binäranalysen ist es am besten, eine Kombination aus statischen (Disassembler, Decompiler) und dynamischen (Debugger) Tools zu verwenden. WinDbg kann verwendet werden, um den Treiber im Kernel zu debuggen. Durch das Festlegen von Breakpoints an Stellen, an denen die Codeausführung zu erwarten ist (Dispatch Create, Dispatch Device Control).
Am Anfang hatte ich ein paar Schwierigkeiten – keiner der Breakpoints, die ich im Treiber gesetzt hatte, wurde getroffen. Ich bekam Zweifel, ob ich das richtige Gerät geöffnet oder etwas anderes falsch gemacht hatte. Später wurde mir klar, dass meine Breakpoints nicht festgelegt wurden, weil der Treiber entladen wurde. Ich habe im Internet nach Antworten gesucht, allerdings gibt es nicht viele Ergebnisse bei der Suche nach mskssrv, obwohl es unter Windows standardmäßig geladen und zugänglich ist. Unter den wenigen Ergebnissen, die ich fand, war ein Thread auf OSR, wo jemand ein ähnliches Problem hatte.
Wie sich herausstellt, können PnP-Filtertreiber, wenn sie eine Weile nicht benutzt wurden, entladen und bei Bedarf wieder geladen werden.
Ich habe die Probleme gelöst, indem ich nach dem Öffnen eines Handles zum Gerät, aber vor dem Aufruf von DeviceIoControl, Breakpoints gesetzt habe, um sicherzustellen, dass der Treiber kürzlich geladen wurde.
Der mskssrv-Treiber ist nur ein 72KB-großer Binärwert und unterstützt Geräte-IO-Steuercodes, die in folgende Funktionen aufrufen:
Aus diesen Symbolnamen können wir auf einige Funktionen des Treibers schließen, die mit dem Senden und Empfangen von Streams zu tun haben. An diesem Punkt habe ich mich eingehender mit der beabsichtigten Funktionalität des Treibers befasst. Ich habe diese Präsentation von Michael Maltsev über das Multimedia-Framework von Windows gefunden, aus der ich erfuhr, dass der Treiber Teil eines prozessübergreifenden Mechanismus zum Teilen von Kamerastreams ist.
Da der Treiber nicht sehr groß ist und es nicht viele IOCTLs gibt, konnte ich mir jede Funktion ansehen, um mir ein Bild von den Interna des Treibers zu machen. Jede IOCTL-Funktion arbeitet entweder mit einem Kontextregistrierungsobjekt oder einem Streamregistrierungsobjekt, das über die entsprechenden „Initialize”-IOCTLs zugewiesen und initialisiert wird. Der Zeiger auf das Objekt wird unter Irp->CurrentStackLocation->FileObject->FsContext2 gespeichert. FileObject zeigt auf das für jede geöffnete Datei erstellte Gerätedateiobjekt, und FsContext2 ist ein Feld, das zur Speicherung von Metadaten pro Dateiobjekt vorgesehen ist.
Ich entdeckte diesen Fehler, als ich zu verstehen versuchte, wie man direkt mit dem Treiber kommunizieren kann. Dabei verzichtete ich zunächst auf die Analyse der Usermode-Komponenten, fsclient.dll und frameserver.dll. Ich hätte den Fehler fast übersehen, weil ich annahm, dass die Entwickler eine einfache Prüfung eingebaut haben, die übersehen wurde. Werfen wir einen Blick auf die PublishRx IOCTL-Funktion:
FSRendezvousServer::PublishRx Dekompilierungsausschnitt
Nachdem das Stream-Objekt aus FsContext2 abgerufen wurde, wird die Funktion FSRendezvousServer::FindObject aufgerufen, um zu überprüfen, ob der Zeiger mit einem Objekt übereinstimmt, das in zwei vom globalen FSRendezvousServer gespeicherten Listen gefunden wurde. Zunächst ging ich davon aus, dass diese Funktion eine Möglichkeit bieten würde, den angeforderten Objekttyp zu überprüfen. Die Funktion gibt jedoch TRUE zurück, wenn der Zeiger entweder in der Liste der Kontext-Objekte oder in der Liste der Stream-Objekte gefunden wird. Beachten Sie, dass keine Informationen darüber, welcher Typ das Objekt sein soll, an FindObject übergeben werden. Das bedeutet, dass ein Kontextobjekt als Stream-Objekt übergeben werden kann. Dies ist eine Sicherheitslücke aufgrund einer Verwechslung des Objekttyps! Sie tritt in jeder IOCTL-Funktion auf, die mit Stream-Objekten arbeitet. Um die Sicherheitslücke zu schließen, hat Microsoft FSRendezvousServer::FindObject durch FSRendezvousServer::FindStreamObject ersetzt, das zunächst durch Überprüfen eines Typfeldes sicherstellt, dass es sich bei dem Objekt um ein Stream-Objekt handelt.
Da Kontextregistrierungsobjekte kleiner sind als Streamregistrierungsobjekte (0x1D8 Bytes, 0x78 Bytes), können Streamobjektoperationen auch außerhalb des zulässigen Speicherbereichs durchgeführt werden:
Illustration der Schwachstelle durch Objekttypverwechslung
Um die Schwachstelle ausnutzen zu können, müssen wir in der Lage sein, den Speicherbereich außerhalb der Grenzen zu kontrollieren, auf den zugegriffen wird. Dies kann erreicht werden, indem die Zuweisung vieler Objekte im gleichen Speicherbereich des anfälligen Objekts ausgelöst wird. Diese Technik wird als Heap- oder Pool-Spraying bezeichnet. Das anfällige Objekt wird in einem nicht ausgelagerten Heap-Pool mit geringer Fragmentierung zugewiesen. Wir können die klassische Technik von Alex Ionescu verwenden, um Puffer zu sprühen, die die vollständige Kontrolle über den Speicherinhalt unter einem 0x30 Byte DATA_QUEUE_ENTRY Header bieten. Durch das Sprühen mit dieser Technik erhalten wir das in der Abbildung dargestellte Speicherlayout:
Mit der gewählten Methode des Pool-Sprayings können Felder in Objekt-Offsets innerhalb der Bereiche 0xC0-0x10F und 0x150-0x19F kontrolliert werden. Ich habe mir noch einmal die IOCTL-Funktionen für Streamobjekte angesehen, um nach Primitiven zum Ausnutzen zu suchen. Ich habe nach Stellen gesucht, an denen auf die steuerbaren Objektfelder zugegriffen wird und diese manipuliert werden.
Ich habe in der PublishRx IOCTL ein gutes, konstantes Write-Where-Primitiv gefunden. Mit dieser primitiven Funktion kann ein konstanter Wert an einer beliebigen Speicheradresse gespeichert werden. Werfen wir einen Blick auf einen Ausschnitt der Funktion FSStreamReg::PublishRx:
FSStreamReg::PublishRx Dekompilierungsausschnitt
Das Streamobjekt enthält eine Listenüberschrift im Offset 0x188, die eine Liste von FSFrameMdl-Objekten beschreibt. Im obigen Dekompilationsausschnitt wird diese Liste iteriert, und wenn der Tag-Wert im FSFrameMdl-Objekt mit dem Tag im von der Anwendung übermittelten System-Buffer übereinstimmt, wird die Funktion FSFrameMdl::UnmapPages aufgerufen.
Mithilfe des zuvor erwähnten ausnutzenden Primitivs kann die FSFrameMdlList und damit das von pFrameMdl referenzierte FsFrameMdl-Objekt vollständig kontrolliert werden. Schauen wir uns nun UnmapPages an:
FSFrameMdl:UnmapPages-Dekompilierung
In der letzten Zeile der oben dekompilierten Funktion wird der konstante Wert 2 in einen Offset-Wert dieses (FSFrameMdl Objekts) geschrieben, der steuerbar ist. Dieses konstante Schreiben kann in Verbindung mit der I/O-Ring-Technik verwendet werden, um beliebige Kernel-Lese- und Schreibzugriffe zu erhalten und die Privilegien zu erweitern. Mehr über diese Technik erfahren Sie hier und hier.
Obwohl ich mich für die Verwendung der Konstanten-Schreibprimitive entschieden habe, gibt es in dieser Funktion noch eine weitere nützliche Exploit-Primitive. Sowohl das Argument BaseAddress als auch das Argument MemoryDescriptorList für den Aufruf von MmUnmapLockedPages sind steuerbar. Dies könnte verwendet werden, um eine Zuordnung an einer beliebigen virtuellen Adresse aufzuheben und eine Primitive vom Typ use-after-free zu erstellen.
Zum jetzigen Zeitpunkt wurden mehrere geeignete Primitives zum Ausnutzen identifiziert, die beliebiges Lesen und Schreiben des Kernels ermöglichen. Möglicherweise ist Ihnen aufgefallen, dass mehrere Prüfungen des Inhalts des Streamobjekts durchgeführt werden müssen, um den gewünschten Codepfad auszulösen. Der gewünschte Zustand des Objekts kann größtenteils durch Pool-Spraying erreicht werden. Ich bin jedoch auf ein Problem gestoßen, das einige Schwierigkeiten verursacht hat. Nachfolgend ein Codeausschnitt von FSStreamReg::PublishRx, nachdem die Schleife über die FSFrameMdlList abgeschlossen ist:
FSStreamReg::PublishRx Dekompilierungsausschnitt
In der obigen Dekompilierung ist bPagesUnmapped eine boolesche Variable, die gesetzt wird, wenn FSFrameMdl::UnmapPages aufgerufen wird. Ist dies der Fall, wird der Offset 0x1a8 des Stream-Objekts abgerufen und, wenn er nicht null ist, wird KeSetEvent aufgerufen.
Dieser Offset entspricht einem Speicherbereich außerhalb der Grenzen und zeigt innerhalb eines POOL_HEADER, der Datenstruktur, die die Pufferzuweisungen im Pool trennt. Insbesondere verweist er auf das Feld ProcessBilled, das dazu dient, einen Zeiger auf das Objekt _EPROCESS für den Prozess zu speichern, der mit der Zuweisung „belastet” ist. Dies dient dazu, festzulegen, wie viele Poolzuweisungen ein bestimmter Prozess haben kann. Nicht alle Poolzuweisungen werden einem Prozess „belastet”, und diejenigen, die nicht das Feld ProcessBilled auf NULL im POOL_HEADER gesetzt haben. Darüber hinaus wird der in ProcessBilled gespeicherte EPROCESS-Zeiger tatsächlich mit einem zufälligen Cookie XOR-verknüpft, sodass ProcessBilled keinen gültigen Zeiger enthält.
Dies stellt ein Problem dar, da NpFr Buffer dem aufrufenden Prozess zugeschrieben werden und somit ProcessBilled festgelegt wird. Wenn Sie das benötigte ausnutzende Primitiv auslösen, wird bPagesUnmapped auf TRUE gesetzt. Wird ein ungültiger Hinweis an KeSetEvent übergeben, stürzt das System ab. Daher muss sichergestellt werden, dass der POOL_HEADER für eine nicht abgerechnete Zuteilung bestimmt ist. An diesem Punkt bemerkte ich, dass das Kontextregistrierungsobjekt (Creg) selbst nicht zugewiesen ist. Dieses Objekt erlaubt jedoch keine Kontrolle über den Speicherinhalt beim FSFrameMdl-Offset. Es müssen also sowohl NpFr- als auch Creg-Objekte gesprayt werden und dazu in der richtigen Reihenfolge angeordnet sein.
Anders als bei großen Pool-Zuweisungen können Sie die Adressen von LFH-Pool-Zuweisungen nicht über NtQuerySystemInformation herausfinden. Außerdem ist die Reihenfolge der Zuweisungen zufällig. Daher weiß man nie, ob sich die an das verwundbare Objekt angrenzenden Buffer in der richtigen Reihenfolge befinden, um sowohl das ausnutzende Primitiv auszulösen, als auch einen Absturz des Systems zu vermeiden. Glücklicherweise kann die Schwachstelle genutzt werden, um ein Pool-Leak in den angrenzenden Buffern auszulösen. Werfen wir einen Blick auf die IOCTL-Funktion für ConsumeTx:
FSRendezvousServer::ConsumeTx Dekompilierungsausschnitt
Oben wird die Funktion FSStreamReg::GetStats aufgerufen:
FSStreamReg::GetStats Dekompilierung
Hierbei werden die außerhalb des zulässigen Speicherbereichs liegenden Inhalte des anfälligen Streamobjekts in den SystemBuffer kopiert, der an die aufrufende Benutzeranwendung zurückgegeben wird. Dieses primitive Pool-Informationsleck kann verwendet werden, um eine Signaturprüfung auf Buffern durchzuführen, die an das verwundbare Objekt angrenzen. Es kann ein Scan mehrerer anfälliger Objekte durchgeführt werden, bis das Objekt innerhalb des gewünschten Speicherlayouts gefunden ist. Sobald das gewünschte Objekt gefunden wurde, sieht das Speicherlayout wie folgt aus:
CVE-2023-36802: Heap-Pool-Grooming mit geringer Fragmentierung
Nachdem das angreifbare Zielobjekt an der richtigen Stelle im Speicher lokalisiert wurde, kann die zuvor erwähnte ausnutzende Funktion auf dem Zielobjekt ausgelöst werden, ohne dass das System abstürzt.
Nachdem er das Problem an MSRC gemeldet hatte, wurde die Ausbeutung der Sicherheitslücke in der Realität entdeckt.
Die in diesem Blogbeitrag vorgestellten Ausbeutungsmethoden sind nur einige von vielen möglichen Ansätzen. Derzeit gibt es keine öffentlichen Informationen darüber, wie Angreifer diese Schwachstelle in der Praxis ausgenutzt haben. Der Ausbeutungs-Code ist hier zu finden.
Eine rückwirkende Patch-Analyse ergab, dass ein großer Teil des neuen Codes zu mskssrv.sys in der Version 1809 von Windows 10 hinzugefügt wurde. Die Überwachung auf neue Codezusätze ist oft hilfreich, um Sicherheitslücken zu finden.
Eine weitere abgedroschene, aber klassische Erkenntnis, die Sie aus dieser Analyse ziehen können: Gehen Sie nicht automatisch von durchgeführten Kontrollen aus. Ein Freund und Kollege schlug vor, dass die Typverwirrung bei der Verwendung von FsContext2 eine „häufige, aber wenig erforschte Fehlerklasse“ sein könnte. Ich glaube, dass für diese Fehlerklasse mehr Variantenanalysen gerechtfertigt sind, insbesondere bei Treibern, die mit der prozessübergreifenden Kommunikation zu tun haben.
Die Schwachstelle wurde entdeckt, als wir versuchten, mit einer unbekannten Angriffsfläche zu arbeiten. „Nahezu keine Kenntnisse” über ein System zu haben, kann auch bedeuten, dass man eine neue Denkweise hat, um es zu knacken.