Eintauchen in den Fahrer: Das allgemeine Prinzip des Rückwärtsfahrens am Beispiel des NeoQUEST-2019-Jobs


    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 Sie keinen Code haben. 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 alle anderen, auf die andere Seite gehen. Auf der Seite des Reverse Engineering.

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

    Gegeben


    Der Legende nach erhielten wir zwei Dateien: einen Verkehrsdump und eine Binärdatei, die denselben Verkehr erzeugten. Schauen Sie sich zuerst den Dump mit Wireshark an:


    Der Speicherauszug enthält einen Strom von UDP-Paketen, von denen jedes 6 Datenbytes 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. Aus diesem Grund wenden wir uns dem Binar zu, der Ihnen zeigen soll, wie Sie alles entschlüsseln können.
    Öffne 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. In MSDN wird eine Beschreibung der in WSK verwendeten Strukturen und Funktionen angezeigt.

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


    Achtung, rückwärts!


    Beginnen Sie wie immer am Einstiegspunkt:


    Gehen wir in Ordnung. Zunächst wird Wsk initialisiert, ein Socket erstellt und in Gruppen zusammengefasst. 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 Sie diese Adresse ein wenig googeln, können Sie davon ausgehen, dass sie die Anzahl der Ticks des Systemzeitgebers ab dem Zeitpunkt des Systemstarts aufzeichnet. Nennen wir zunächst die Variable TickCount.


    EntryPoint richtet dann Funktionen zur Verarbeitung von IRP-Paketen (I / O Request Packets) ein. Sie können mehr über sie auf MSDN lesen . Für alle Arten von Anforderungen ist eine Funktion definiert, die das Paket einfach an den nächsten Treiber im Stapel weitergibt.


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



    Darin wird wiederum CompletionRoutine installiert.


    CompletionRoutine füllt die unbekannte Struktur mit den vom IRP empfangenen Daten und fügt sie in die Liste ein. Bisher wissen wir nicht, was in dem Paket enthalten ist - 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. Schauen wir hinein und stellen sofort fest, dass ein Gerät (IoCreateDevice) darin erstellt wird.


    Rufen Sie die Funktion AddDevice auf. Wenn die Typen korrekt sind, sehen wir, dass der Gerätename "\\ Device \\ KeyboardClass0" ist. So interagiert unser Fahrer mit der Tastatur. Wenn Sie über IRP_MJ_READ im Kontext der Tastatur 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 ist 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. Beachten Sie sofort 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 byteweise analysiert haben (0x89 = 137, 0xE0 = 224, 0xFE = 243, 0xA9 = 169, 0x2328 = 9000), erhalten wir genau die gleiche Adresse und den gleichen 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.
    Lassen Sie uns sehen, wie die Daten vor dem Senden verarbeitet werden.

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



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

    (54773 + 7141 * prev_value)% 259200

    Dies ist ein linearer kongruenter Pseudozufallszahlengenerator . Sie wird in InitVars mit TickCount initialisiert. Für jede nachfolgende Zahl 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 erzeugt einfach x oder zwei bereits verschlüsselte Parameter und einen konstanten Wert. Es ist unwahrscheinlich, dass die Daten auf irgendeine Weise entschlüsselt werden. Daher enthalten die letzten beiden Bytes der Nachricht für uns keine nützlichen Informationen und können nicht berücksichtigt werden. 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 wahrscheinlich nicht einfach ist. Aber KeyFlags ist ein Bitfeld:


    Wenn Sie auf den Tisch schauenWenn Sie die Codes scannen, können Sie feststellen, dass das Flag meistens entweder 0 (die Taste ist gedrückt) oder 1 (die Taste ist gedrückt) ist. KEY_E0 wird ziemlich selten offen gelegt, aber es mag vorkommen, dass die Chancen, KEY_E1 zu treffen, sehr gering sind. Daher können Sie versuchen, Folgendes zu tun: Wir gehen die Daten aus dem Dump durch, wählen einen Wert aus, der mit KeyFlags verschlüsselt ist, stellen ein Äquivalent mit 0 her und generieren zwei aufeinanderfolgende PSCs. Erstens ist KeyData ein einzelnes Byte, und wir können die Richtigkeit der generierten MSS durch High-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 nicht der Fall ist, akzeptieren wir, dass die ursprünglich betrachteten KeyFlags 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 auf NeoQUEST-2019 vollständig erledigt hat, Anspruch auf einen Preis hat! Suchen Sie in Ihrer E-Mail nach einem Brief, und schreiben Sie an support@neoquest.ru , falls Sie diesen nicht erhalten haben .

    Jetzt auch beliebt: