Der leitende Forscher Tom Court of Context, ein Unternehmen für Informationssicherheit, spricht darüber, wie er es geschafft hat, einen potenziell gefährlichen Fehler im Steam-Clientcode zu erkennen.Sicherheitsbewusste PC-Spieler haben festgestellt, dass Valve kürzlich ein neues Steam-Client-Update veröffentlicht hat.
In diesem Beitrag möchte ich mich
für das Spielen von Spielen bei der Arbeit entschuldigen, um die Geschichte eines verwandten Fehlers
zu erzählen, der mindestens zehn Jahre lang im Steam-Client aufgetreten ist und bis Juli letzten Jahres zur Ausführung von Remotecode führen kann (Remotecode-Ausführung, RCE). Insgesamt 15 Millionen aktive Kunden.
Seit Juli, als Valve (endlich) seinen Code mit aktiviertem modernen Exploit-Schutz kompilierte, konnte dies nur zu einem Clientfehler führen, und RCE war nur in Kombination mit einer separaten Sicherheitslücke in Bezug auf Informationslecks möglich.
Wir haben Valve am 20. Februar 2018 als Sicherheitslücke deklariert und es, wie das Unternehmen zu verdanken hat, weniger als 12 Stunden später in der Beta-Filiale behoben. Der Fix wurde am 22. März 2018 in die stabile Filiale verschoben.
Kurzer Rückblick
Die Grundlage der Sicherheitsanfälligkeit war eine Beschädigung des Heapspeichers in der Steam-Clientbibliothek, die remote aufgerufen werden konnte, in dem Teil des Codes, der an der Wiederherstellung des fragmentierten Datagramms aus mehreren empfangenen UDP-Paketen beteiligt war.
Der Steam-Client tauscht Daten über sein eigenes Protokoll (Steam-Protokoll) aus, das über UDP implementiert ist. Es gibt zwei Bereiche in diesem Protokoll, die aufgrund der Sicherheitsanfälligkeit besonders interessant sind:
- Paketlänge
- Die Gesamtlänge des rekonstruierten Datagramms
Der Fehler wurde durch das Fehlen einer einfachen Überprüfung verursacht. Der Code hat nicht überprüft, ob die Länge des ersten fragmentierten Datagramms kleiner oder gleich der Gesamtlänge des Datagramms ist. Dies scheint ein allgemeines Versehen zu sein, da für alle nachfolgenden Pakete, die Fragmente des Datagramms übertragen, die Überprüfung durchgeführt wird.
Ohne zusätzliche Datenleckfehler ist der Heap-Schaden auf modernen Betriebssystemen sehr schwer zu kontrollieren, sodass die Remote-Codeausführung schwierig zu implementieren ist. In diesem Fall könnte dieser Fehler jedoch dank des Steam-eigenen Speicherzuweisers und der ASLR, die in der Binärdatei steamclient.dll fehlten (bis zum letzten Juli), als Grundlage für einen sehr zuverlässigen Exploit verwendet werden.
Nachfolgend finden Sie eine technische Beschreibung der Sicherheitsanfälligkeit und des damit verbundenen Exploits bis
Implementierungen der Codeausführung.
Sicherheitslücken Details
Informationen, die zum Verständnis notwendig sind
Protokoll
Dritte (z. B.
https://imfreedom.org/wiki/Steam_Friends ) führten basierend auf der Analyse des vom Steam-Client generierten Datenverkehrs ein Reverse Engineering durch und erstellten eine detaillierte Dokumentation des Steam-Protokolls. Das Protokoll wurde ursprünglich 2008 dokumentiert und hat sich seitdem kaum verändert.
Das Protokoll wird als Übertragungsprotokoll mit dem Aufbau einer Verbindung über einen Strom von UDP-Datagrammen implementiert. Pakete haben gemäß der Dokumentation unter dem obigen Link die folgende Struktur:
Wichtige Aspekte:
- Alle Pakete beginnen mit 4 Bytes " VS01 "
- packet_len beschreibt die Länge nützlicher Informationen (bei unfragmentierten Datagrammen entspricht der Wert der Länge der Daten)
- Typ beschreibt den Pakettyp, der die folgenden Werte haben kann:
- 0x2 Anrufauthentifizierung
- 0x4 Verbindung akzeptieren
- 0x5 Verbindung zurücksetzen
- 0x6 Ein Paket ist ein Fragment eines Datagramms
- Das 0x7-Paket ist ein separates Datagramm
- Die Quell- und Zielfelder sind Bezeichner, die zugewiesen sind, um Pakete auf mehreren Verbindungen innerhalb des Steam-Clients korrekt weiterzuleiten
- Falls das Paket ein Fragment eines Datagramms ist:
- split_count gibt die Anzahl der Fragmente an, in die das Datagramm aufgeteilt ist
- data_len gibt die Gesamtlänge des wiederhergestellten Datagramms an
- Die Erstverarbeitung dieser UDP-Pakete erfolgt in der Funktion CUDPConnection :: UDPRecvPkt in steamclient.dll
Verschlüsselung
Nützliche Informationen des Datagrammpakets werden von AES-256 mithilfe eines Schlüssels verschlüsselt, der in jeder Sitzung zwischen Client und Server ausgehandelt wird. Die Schlüsselverhandlung wird wie folgt durchgeführt:
- Der Client generiert einen 32-Byte-AES-Zufallsschlüssel und RSA verschlüsselt ihn mit dem öffentlichen Valve-Schlüssel, bevor er an den Server gesendet wird.
- Der Server mit einem privaten Schlüssel kann diesen Wert entschlüsseln und als AES-256-Schlüssel akzeptieren, der in der Sitzung verwendet wird
- Nachdem der Schlüssel vereinbart wurde, werden alle nützlichen Informationen in der aktuellen Sitzung mit diesem Schlüssel verschlüsselt.
Sicherheitslücke
Die
RecvFragment- Methode der
CUDPConnection- Klasse
weist eine Sicherheitsanfälligkeit auf. In der Release-Version der Steamclient-Bibliothek gibt es keine Symbole. Beim Durchsuchen von Binärzeilen in einer für uns interessanten Funktion wird jedoch ein Link zu "
CUDPConnection :: RecvFragment " gefunden. Die Eingabe dieser Funktion erfolgt, wenn der Client ein UDP-Paket empfängt, das ein Steam-Datagramm vom Typ 0x6 (ein „Fragment eines Datagramms“) enthält.
1. Die Funktion überprüft zunächst den Verbindungsstatus, um sicherzustellen, dass er sich im Status „
Verbunden “ befindet.
2. Anschließend wird das Feld
data_len im Steam-Datagramm überprüft, um sicherzustellen, dass es weniger als
0x20000060 Byte enthält (anscheinend wird dieser Wert willkürlich ausgewählt).
3. Wenn der Test erfolgreich ist, prüft die Funktion, ob die Verbindung Fragmente eines Datagramms sammelt oder ob es sich um das erste Paket des Streams handelt.
4. Wenn dies das erste Paket im Stream ist, wird das Feld
split_count überprüft , um
festzustellen , wie viele Pakete dieser Stream strecken wird
5. Wenn der Stream in mehrere Pakete unterteilt ist, wird das Feld
seq_no_of_first_pkt überprüft , um sicherzustellen, dass es mit der Seriennummer des aktuellen Pakets übereinstimmt. Dies stellt sicher, dass das Paket das erste im Stream ist.
6. Das Feld
data_len wird erneut gegen das Limit von
0x20000060 Bytes
geprüft . Außerdem wird überprüft,
ob split_count weniger als
0x709b- Pakete enthält.
7. Wenn diese Bedingungen erfüllt sind, wird ein Boolescher Wert festgelegt, der angibt, dass jetzt Fragmente gesammelt werden. Es wird auch überprüft, ob noch kein Puffer zum Speichern von Fragmenten zugewiesen ist.
8. Wenn der Zeiger auf den Fragmentauflistungspuffer nicht Null ist, wird der aktuelle Fragmentauflistungspuffer freigegeben und ein neuer Puffer zugewiesen (siehe das gelbe Rechteck in der folgenden Abbildung). Hier tritt der Fehler auf. Es wird erwartet, dass der Fragmentauflistungspuffer in der Größe von
data_len Bytes
zugewiesen wird . Wenn alles erfolgreich war (und der Code nicht überprüft - ein kleiner Fehler), werden die nützlichen Informationen des Datagramms mit
memmove in diesen Puffer kopiert, wobei
darauf vertraut wird, dass die Anzahl der zu kopierenden Bytes in
packet_len angegeben ist .
Das wichtigste Versehen des Entwicklers war, dass die Prüfung " packet_len ist kleiner oder gleich data_len " nicht durchgeführt wird. Dies bedeutet, dass es möglich ist, data_len weniger als packet_len zu übertragen und bis zu 64 KB Daten (da das Feld packet_len 2 Byte breit ist) in einen sehr kleinen Puffer kopiert zu haben, wodurch die Heap-Beschädigung ausgenutzt werden kann.Ausnutzung der Verwundbarkeit
In diesem Abschnitt wird davon ausgegangen, dass es eine Problemumgehung für ASLR gibt. Dies führt dazu, dass vor dem Start des Betriebs die Startadresse von steamclient.dll bekannt ist.
Paket-Spoofing
Damit die angreifenden UDP-Pakete vom Client empfangen werden können, muss er das ausgehende Datagramm (Client -> Server) untersuchen, das gesendet wird, um die Kennungen der Client / Server-Verbindung sowie die Seriennummer herauszufinden. Anschließend muss der Angreifer die IP-Adressen und Quell- / Zielports zusammen mit den Client / Server-IDs fälschen und die gelernte Seriennummer um eins erhöhen.
Speicherverwaltung
Um mehr als 1024 (0x400) Bytes Speicher zuzuweisen, wird ein Standard-Systemzuweiser verwendet. Um Speicher mit weniger als oder gleich 1024 Byte zuzuweisen, verwendet Steam einen eigenen Allokator, der auf allen unterstützten Plattformen gleich funktioniert. In diesem Artikel wird dieser Distributor mit Ausnahme der folgenden Schlüsselaspekte nicht im Detail erläutert:
- Vom Systemzuweiser werden große Speicherblöcke angefordert, die dann zur Verwendung unter Steam-Client-Speicherzuweisungsanforderungen in Fragmente fester Größe unterteilt werden.
- Die Auswahl erfolgt nacheinander, zwischen den verwendeten Fragmenten gibt es keine Metadaten, die sie trennen.
- Jeder große Block speichert seine eigene freie Speicherliste, die als einfach verknüpfte Liste implementiert ist.
- Der obere Rand der Liste des freien Speichers zeigt das erste freie Fragment im Speicher an, und die ersten 4 Bytes dieses Fragments geben das nächste freie Fragment an (falls vorhanden).
Speicherzuordnung
Beim Zuweisen von Speicher wird der erste freie Block vom Anfang der Liste des freien Speichers getrennt, und die ersten 4 Bytes dieses Blocks, die
next_free_block entsprechen, werden in die Mitgliedsvariable
freelist_head innerhalb der
Allokatorklasse kopiert.
Freier Speicher
Wenn ein Block freigegeben wird, wird das Feld
freelist_head in die ersten 4 Bytes des freigegebenen Blocks (
next_free_block )
kopiert , und die Adresse des freigegebenen Blocks wird in die Mitgliedsvariable
freelist_head der Verteilerklasse kopiert.
So erhalten Sie ein Aufnahmeprimitiv
Auf dem Heap tritt ein Pufferüberlauf auf. Abhängig von der Größe der Pakete, die die Beschädigung verursacht haben, kann die Speicherzuweisung entweder durch den Standard-Windows-Allokator (bei Speicherzuweisung von mehr als 0 x 400 Byte) oder durch den Steam-eigenen Zuweiser (bei Zuweisung von Speicher unter 0x400 Byte) gesteuert werden. Aufgrund des Mangels an Sicherheitsmaßnahmen in meinem eigenen Steam-Distributor entschied ich, dass es einfacher war, es für einen Exploit zu verwenden.
Kehren wir zum Abschnitt über die Speicherverwaltung zurück: Es ist bekannt, dass der Anfang der Liste der freien Speicher von Blöcken einer bestimmten Größe als Mitgliedsvariable der Verteilerklasse gespeichert wird und der Zeiger auf den nächsten freien Block in der Liste als die ersten 4 Bytes jedes freien Blocks der Liste gespeichert wird.
Wenn sich neben dem Block, in dem der Überlauf aufgetreten ist, ein freier Block befindet, können wir durch Beschädigung des Heaps den Zeiger
next_free_block überschreiben. Wenn Sie der Meinung sind, dass ein Bündel dafür vorbereitet werden kann, kann der neu
geschriebene Zeiger
next_free_block auf eine Adresse zum Schreiben gesetzt werden, wonach die nachfolgende Speicherzuweisung an diese Stelle geschrieben wird.
Was zu verwenden ist: Datagramme oder Fragmente
Ein Fehler mit Speicherbeschädigung tritt im Code auf, der für die Verarbeitung von Fragmenten von Datagrammen (Paketen vom Typ 6) verantwortlich ist. Nach dem Auftreten eines Schadens befindet sich die
RecvFragment () -Funktion in einem Zustand, in dem weitere Fragmente erwartet werden. Wenn sie jedoch ankommen, wird eine Überprüfung durchgeführt:
fragment_size + num_bytes_already_received < sizeof(collection_buffer)
Dies ist jedoch offensichtlich nicht der Fall, da unser erstes Paket bereits gegen diese Regel verstoßen hat (das Vorhandensein eines Fehlers kann diese Prüfung überspringen) und ein Fehler auftritt. Um dies zu vermeiden, müssen Sie die
CUDPConnection :: RecvFragment () -Methode nach einer Speicherbeschädigung vermeiden.
Glücklicherweise kann
CUDPConnection :: RecvDatagram () weiterhin gesendete Pakete vom Typ 7 (Datagramme) empfangen und verarbeiten, bis
RecvFragment () gültig ist, und dies kann zum Starten des Aufzeichnungsprimitivs verwendet werden.
Verschlüsselungsprobleme
Es wird erwartet, dass die von
RecvDatagram () und
RecvFragment () empfangenen Pakete verschlüsselt werden. Bei
RecvDatagram () erfolgt die Entschlüsselung fast unmittelbar nach dem Empfang. Im Fall von
RecvFragment () tritt es nach dem Empfang des letzten Fragments in der Sitzung auf.
Das Problem der Ausnutzung der Sicherheitsanfälligkeit tritt auf, weil wir den in jeder Sitzung erstellten Verschlüsselungsschlüssel nicht kennen. Dies bedeutet, dass jeder OP-Code / Shell-Code, den wir senden, mit AES256 „entschlüsselt“ wird, wodurch unsere Daten in Müll umgewandelt werden. Daher ist es notwendig, eine Betriebsmethode zu finden, die fast unmittelbar nach dem Empfang des Pakets möglich ist, bevor die Entschlüsselungsprozeduren die im Paketpuffer enthaltenen nützlichen Informationen verarbeiten können.
So erreichen Sie die Codeausführung
Angesichts der oben beschriebenen Entschlüsselungsbeschränkung sollte die Operation vor der Entschlüsselung der eingehenden Daten durchgeführt werden. Dies führt zu zusätzlichen Einschränkungen, aber die Aufgabe ist noch möglich: Sie können den Zeiger so umschreiben, dass er auf das
CWorkThreadPool- Objekt verweist, das an einer vorhersehbaren Stelle im
Datenabschnitt der Binärdatei gespeichert ist. Obwohl die Details und internen Funktionen dieser Klasse unbekannt sind, kann anhand ihres Namens angenommen werden, dass sie einen Thread-Pool unterstützt, den Sie verwenden können, wenn Sie "arbeiten" müssen. Nachdem Sie mehrere Debugging-Zeilen in einer Binärdatei untersucht haben, können Sie verstehen, dass unter solchen Arbeiten Verschlüsselung und Entschlüsselung (
CWorkItemNetFilterEncrypt ,
CWorkItemNetFilterDecrypt ) vorhanden sind. Wenn diese Aufgaben in die Warteschlange gestellt werden, wird die
CWorkThreadPool- Klasse
verwendet . Indem wir diesen Zeiger überschreiben und die gewünschte Stelle darin schreiben, können wir den vtable-Zeiger und die damit verbundene vtable simulieren, wodurch wir beispielsweise Code ausführen können, wenn
CWorkThreadPool :: AddWorkItem () aufgerufen wird, was vor Entschlüsselungsprozessen geschehen muss.
Die folgende Abbildung zeigt die erfolgreiche Ausnutzung der Sicherheitsanfälligkeit bis zur Erlangung der Kontrolle über das EIP-Register.
Von nun an können Sie eine ROP-Kette erstellen, die zur Ausführung von beliebigem Code führt. Das folgende Video zeigt, wie ein Angreifer einen Windows-Rechner in einer vollständig gepatchten Version von Windows 10 remote startet.
Zusammenfassend
Wenn Sie zu diesem Teil des Artikels gelangen, vielen Dank für Ihre Beharrlichkeit! Ich hoffe, Sie verstehen, dass dies ein sehr einfacher Fehler ist, der aufgrund des Mangels an modernen Mitteln zum Schutz vor Exploits recht einfach auszunutzen war. Der anfällige Code war wahrscheinlich sehr alt, aber ansonsten funktionierte er gut, sodass die Entwickler nicht die Notwendigkeit sahen, ihn zu untersuchen oder seine Build-Skripte zu aktualisieren. Die Lehre hier ist, dass es für Entwickler wichtig ist, alten Code regelmäßig zu überprüfen und Systeme zu erstellen, um sicherzustellen, dass sie den modernen Sicherheitsstandards entsprechen, auch wenn die Funktionalität des Codes selbst unverändert bleibt. Es war erstaunlich, 2018 einen so einfachen Fehler mit so schwerwiegenden Folgen auf einer sehr beliebten Softwareplattform zu finden. Dies sollte ein Anreiz sein, für alle Forscher nach solchen Schwachstellen zu suchen!
Schließlich lohnt es sich, über den Prozess der verantwortungsvollen Offenlegung von Informationen zu sprechen. Wir haben Valve diesen Fehler in einem Brief an ihr Sicherheitsteam (
security@valvesoftware.com ) gegen 16 Uhr GMT gemeldet. Nur 8 Stunden später wurde ein Fix erstellt und im Beta-Client Steam gestartet. Dank dessen steht Valve nun an erster Stelle in unserer (imaginären) Tabelle des Wettbewerbs „Wer wird die Sicherheitsanfälligkeit schneller beheben?“ - eine angenehme Ausnahme im Vergleich zur Offenlegung von Fehlern gegenüber anderen Unternehmen, was häufig zu einem langen Genehmigungsprozess führt.
Eine Seite, die die Details aller Client-Updates beschreibt