Eintauchen in den Fahrer: das allgemeine Prinzip des Rückwärtsfahrens am Beispiel der Aufgabe NeoQUEST-2019


Wie alle Programmierer lieben Sie Code. Du und er sind beste Freunde. Aber früher oder später im Leben wird es einen solchen Moment geben, in dem kein Code bei Ihnen ist. Ja, es ist schwer zu glauben, aber es wird eine große Lücke zwischen Ihnen geben: Sie sind draußen und er ist tief drinnen. Aus Hoffnungslosigkeit müssen Sie, wie jeder andere auch, auf die andere Seite gehen. Auf der Seite des Reverse Engineering.

Am Beispiel der Aufgabe Nr. 2 aus der Online-Phase von NeoQUEST-2019 werden wir das allgemeine Prinzip des Reverse-Treiber-Windows analysieren. Natürlich ist das Beispiel ziemlich vereinfacht, aber das Wesentliche des Prozesses ändert sich nicht davon - die einzige Frage ist die Menge an Code, die angezeigt werden muss. Mit Erfahrung und Glück können wir loslegen!

Gegeben


Der Legende nach erhielten wir zwei Dateien: einen Verkehrsdump und eine Binärdatei, die denselben Verkehr erzeugten. Schauen Sie sich zunächst die Müllkippe mit Wireshark an:


Der Speicherauszug enthält einen Strom von UDP-Paketen, von denen jedes 6 Byte Daten enthält. Diese Daten sind auf den ersten Blick eine zufällige Menge von Bytes - es ist nicht möglich, etwas aus dem Verkehr herauszuholen. Deshalb wenden wir uns dem Binar zu, das Ihnen sagen soll, wie Sie alles entschlüsseln können.
Öffnen Sie es in der IDA:


Es scheint, dass wir vor einer Art Fahrer stehen. Funktionen mit dem WSK-Präfix beziehen sich auf Winsock Kernel, die Netzwerkprogrammierschnittstelle im Windows-Kernelmodus. Auf MSDN sehen Sie eine Beschreibung der in WSK verwendeten Strukturen und Funktionen.

Zur Vereinfachung können Sie das Windows Driver Kit 8 (Kernelmodus) - wdk8_km (oder eine neuere) Bibliothek in die IDA laden, um die dort definierten Typen zu verwenden:


Achtung, umgekehrt!


Beginnen Sie wie immer am Einstiegspunkt:


Lass uns in Ordnung gehen. Zunächst wird Wsk initialisiert, ein Socket erstellt und gebunden - wir werden diese Funktionen nicht im Detail beschreiben, sie enthalten keine Informationen, die für uns nützlich sind.

Die Funktion sub_140001608 setzt 4 globale Variablen. Nennen wir es InitVars. In einem von ihnen wird ein Wert an die Adresse 0xFFFFF78000000320 geschrieben. Wenn wir diese Adresse ein wenig googeln, können wir davon ausgehen, dass sie die Anzahl der Ticks des System-Timers ab dem Zeitpunkt des Systemstarts aufzeichnet. Nennen wir zunächst die Variable TickCount.


EntryPoint richtet dann Funktionen zur Verarbeitung von IRP-Paketen ein (E / A-Anforderungspaket). Sie können mehr darüber auf MSDN lesen . Für alle Arten von Anforderungen wird eine Funktion definiert, die das Paket einfach an den nächsten Treiber im Stapel weiterleitet.


Für den Typ IRP_MJ_READ (3) ist jedoch eine separate Funktion definiert; Nennen wir es IrpRead.



Darin ist wiederum CompletionRoutine installiert.


CompletionRoutine füllt die unbekannte Struktur mit Daten, die vom IRP empfangen wurden, und fügt sie in die Liste ein. Bisher wissen wir nicht, was sich im Paket befindet - wir werden später auf diese Funktion zurückkommen.
Wir schauen weiter in EntryPoint. Nach dem Definieren der IRP-Handler wird die Funktion sub_1400012F8 aufgerufen. Lassen Sie uns nach innen schauen und sofort feststellen, dass ein Gerät (IoCreateDevice) darin erstellt wurde.


Rufen Sie die Funktion AddDevice auf. Wenn die Typen korrekt sind, sehen wir, dass der Gerätename "\\ Gerät \\ KeyboardClass0" lautet. Unser Treiber interagiert also mit der Tastatur. Wenn Sie im Kontext der Tastatur über IRP_MJ_READ googeln, können Sie feststellen , dass die KEYBOARD_INPUT_DATA-Struktur in Paketen übertragen wird. Kehren wir zu CompletionRoutine zurück und sehen, welche Art von Daten übergeben werden.


Die IDA hier analysiert die Struktur nicht gut, aber durch Offsets und weitere Aufrufe können Sie verstehen, dass sie aus ListEntry, KeyData (der Scan-Code des Schlüssels wird hier gespeichert) und KeyFlags besteht.
Nach AddDevice wird in EntryPoint die Funktion sub_140001274 aufgerufen. Sie erstellt einen neuen Stream.


Mal sehen, was in ThreadFunc passiert.


Sie erhält den Wert aus der Liste und verarbeitet sie. Achten Sie sofort auf die Funktion sub_140001A18.


Es übergibt die verarbeiteten Daten zusammen mit einem Zeiger auf WskSocket und der Nummer 0x89E0FEA928230002 an den Eingang der Funktion sub_140001A68. Nachdem wir die Parameternummer in Bytes analysiert haben (0x89 = 137, 0xE0 = 224, 0xFE = 243, 0xA9 = 169, 0x2328 = 9000), erhalten wir genau dieselbe Adresse und denselben Port aus dem Verkehrsdump: 169.243.224.137:9000. Es ist logisch anzunehmen, dass diese Funktion ein Netzwerkpaket an die angegebene Adresse und den angegebenen Port sendet - wir werden dies nicht im Detail betrachten.
Mal sehen, wie die Daten vor dem Senden verarbeitet werden.

Für die ersten beiden Elemente wird ein Äquivalent mit dem generierten Wert durchgeführt. Da die Anzahl der Ticks zur Berechnung verwendet wird, kann davon ausgegangen werden, dass wir mit der Erzeugung einer Pseudozufallszahl konfrontiert sind.



Nach dem Generieren der Zahl wird der Wert der Variablen überschrieben, die wir zuvor TickCount genannt haben. Variablen für die Formel werden in InitVars festgelegt. Wenn wir zum Aufruf dieser Funktion zurückkehren, werden wir die Werte für diese Variablen herausfinden und als Ergebnis die folgende Formel erhalten:

(54773 + 7141 * prev_value)% 259200

Dies ist ein linearer kongruenter Pseudozufallszahlengenerator . Es wird in InitVars mit TickCount initialisiert. Für jede nachfolgende Nummer fungiert die vorherige als Anfangswert (der Generator gibt einen Doppelbyte-Wert zurück und derselbe wird für die nachfolgende Generierung verwendet).


Nach dem Äquivalent mit einer Zufallszahl von zwei von der Tastatur übertragenen Werten wird eine Funktion aufgerufen, die die verbleibenden zwei Bytes der Nachricht bildet. Es wird einfach xor von zwei bereits verschlüsselten Parametern und einem konstanten Wert erzeugt. Es ist unwahrscheinlich, dass dies die Daten irgendwie entschlüsselt, sodass die letzten zwei Bytes der Nachricht für uns keine nützlichen Informationen enthalten und nicht berücksichtigt werden können. Aber was tun mit verschlüsselten Daten?
Schauen wir uns genauer an, was genau verschlüsselt ist. KeyData ist ein Scan-Code, der einen ziemlich großen Wertebereich annehmen kann, was nicht einfach ist. Aber KeyFlags ist ein kleines Feld:


Wenn Sie sich die Tabelle der Scan-Codes ansehen, werden Sie feststellen, dass das Flag meistens entweder 0 (die Taste ist gedrückt) oder 1 (die Taste ist angehoben) ist. KEY_E0 wird ziemlich selten ausgesetzt sein, aber es kann vorkommen, aber um KEY_E1 zu treffen, sind die Chancen sehr gering. Daher können Sie Folgendes versuchen: Wir gehen die Daten aus dem Speicherauszug durch, wählen einen Wert aus, der mit KeyFlags verschlüsselt ist, machen ein Äquivalent mit 0 und generieren zwei aufeinanderfolgende PSCs. Erstens ist KeyData ein einzelnes Byte, und wir können die Richtigkeit des generierten MSS durch ein hohes Byte überprüfen. Und zweitens nehmen die nächsten verschlüsselten KeyFlags, wenn sie ein Äquivalent mit dem richtigen PSC ausführen, dieselben Bitwerte an. Wenn sich herausstellt, dass dies falsch ist, nehmen wir an, dass die KeyFlags, die wir ursprünglich betrachtet haben, 1 usw. waren.
Versuchen wir, unseren Algorithmus zu implementieren. Wir werden dafür Python verwenden:

Implementierung des Algorithmus
#  -   keymap = […] # ,   Wireshark traffic_dump = […] #  def bxnor(a, b): return ((~a & 0xffff) | b) & (a | (~b & 0xffff)) #   def brgen(a): return ((7141 * a + 54773) % 259200) & 0xffff def decode(): #     for i in range(0, len(traffic_dump) - 1): #   KeyFlags probe = traffic_dump[i][1] #   - scancode = traffic_dump[i+1][0] #    KeyFlags tester = traffic_dump[i+1][1] fail = True #     (  KEY_E1) for flag in range(4): rnd_flag = bxnor(flag, probe) rnd_sc = brgen(rnd_flag) next_flag = bxnor(tester, brgen(rnd_sc)) #   KeyFlags if next_flag in range(4): sc = bxnor(rnd_sc, scancode) if sc < len(keymap): sym = keymap[sc] if next_flag % 2 == 0: print(sym, end='') fail = False break #   -      KeyFlags   if fail: print('Something went wrong on {} pair'.format(i)) return print() if __name__ == "__main__": decode() 


Führen Sie unser Skript für die vom Speicherauszug empfangenen Daten aus:


Und im entschlüsselten Verkehr finden wir unsere begehrteste Leitung!

NQ2019DABE17518674F97DBA393415E9727982FC52C202549E6C1740BC0933C694B3DE


Bald wird es Artikel mit Analyse der verbleibenden Aufgaben geben, nicht verpassen!

PS Und wir erinnern Sie daran, dass jeder, der mindestens eine Aufgabe bei NeoQUEST-2019 vollständig erledigt hat, Anspruch auf einen Preis hat! Überprüfen Sie Ihre E-Mails auf einen Brief. Wenn er nicht bei Ihnen eingegangen ist , schreiben Sie an support@neoquest.ru !

Source: https://habr.com/ru/post/de446462/


All Articles