Mit Ghidra einen einfachen „Riss“ brechen - Teil 1

Viele Leute wissen wahrscheinlich bereits aus erster Hand, was für ein Biest das ist - Ghidra ("Hydra") und was es aus erster Hand mit dem Programm zu tun hat, obwohl dieses Tool erst kürzlich öffentlich zugänglich gemacht wurde - im März dieses Jahres. Ich werde die Leser nicht mit einer Beschreibung von Hydra, seiner Funktionalität usw. belästigen. Ich bin sicher, diejenigen, die sich mit dem Thema befassen, haben dies alles bereits selbst studiert, und diejenigen, die sich noch nicht mit dem Thema befassen, können dies jederzeit tun, da es jetzt einfach ist, detaillierte Informationen im Internet zu finden. Übrigens, einer der Aspekte von Hydra (die Entwicklung von Plugins dafür) wurde bereits auf Habré behandelt (ausgezeichneter Artikel!). Ich werde nur die Hauptlinks geben:


Hydra ist also ein kostenloser plattformübergreifender interaktiver Disassembler und Decompiler mit modularem Aufbau, der Unterstützung für fast alle Haupt-CPU-Architekturen und eine flexible grafische Oberfläche für die Arbeit mit disassembliertem Code, Speicher, wiederhergestelltem (dekompiliertem) Code, Debugging-Symbolen und vielem mehr bietet .

Versuchen wir etwas mit dieser Hydra zu brechen!

Schritt 1. Finden und studieren Sie den Riss


Als "Opfer" finden wir ein einfaches "Crackme" -Programm. Ich bin gerade zu crackmes.one gegangen und habe bei der Suche den Schwierigkeitsgrad = 2-3 ("einfach" und "mittel"), die Ausgangssprache des Programms = "C / C ++" und die Plattform = "Multiplattform" angegeben, wie im folgenden Screenshot gezeigt:



Die Suche ergab 2 Ergebnisse (unten grün). Der erste Riss stellte sich als 16-Bit heraus und startete nicht auf meinem Win10 64-Bit, aber der zweite ( Level_2 von Seveb ) kam auf. Sie können es von diesem Link herunterladen.

Laden Sie den Riss herunter und entpacken Sie ihn. Das auf der Website angegebene Passwort für das Archiv lautet crackmes.de . Im Archiv finden wir zwei Verzeichnisse, die Linux und Windows entsprechen. Auf meinem Computer gehe ich in das Windows-Verzeichnis und treffe darin die einzige "ausführbare Datei" - level_2.exe . Lass uns rennen und sehen, was sie will:



Es scheint wie ein Mist! Beim Start zeigt das Programm nichts an. Wir versuchen es erneut auszuführen und übergeben ihm eine beliebige Zeichenfolge als Parameter (wartet plötzlich auf einen Schlüssel?) - und wieder nichts ... Aber verzweifeln Sie nicht. Nehmen wir an, wir müssen auch die Startparameter als Aufgabe herausfinden! Es ist Zeit, unser "Schweizer Messer" - Hydra - aufzudecken.

Schritt 2. Erstellen eines Projekts in Hydra und vorläufige Analyse


Angenommen, Sie haben Hydra bereits installiert. Wenn noch nicht, dann ist alles einfach.

Installieren Sie Ghidra
1) Installieren Sie JDK Version 11 oder höher (ich habe 12 )

2) Laden Sie Hydra herunter (zum Beispiel von hier ) und installieren Sie es (zum Zeitpunkt des Schreibens ist die neueste Version von Hydra 9.0.2, ich habe 9.0.1).

Wir starten Hydra und erstellen im geöffneten Projektmanager sofort ein neues Projekt. Ich gab ihm den Namen crackme3 (d. H. Crackme- und crackme2-Projekte wurden bereits für mich erstellt). Ein Projekt ist in der Tat ein Verzeichnis von Dateien. Sie können ihm beliebige Dateien zum Studieren hinzufügen (exe, dll usw.). Wir werden sofort unsere level_2.exe hinzufügen ( Datei | Importieren oder nur die I- Taste):



Wir sehen, dass Hydra vor dem Import unseren experimentellen Quacksalber als 32-Bit-PE (tragbare ausführbare Datei) für das Win32-Betriebssystem und die x86-Plattform identifiziert hat. Nach dem Import warten wir auf noch mehr Informationen:



Hier könnte uns zusätzlich zu der oben erwähnten Bittiefe noch die Endianness-Reihenfolge interessiert sein , die in unserem Fall Little (von niedrigem zu hohem Byte) ist, was für die Intel 86. Plattform zu erwarten war.

Mit einer vorläufigen Analyse sind wir fertig.

Schritt 3. Führen Sie eine automatische Analyse durch


Zeit für eine vollautomatische Analyse des Programms in Hydra. Dies erfolgt durch Doppelklick auf die entsprechende Datei (level_2.exe). Hydra ist modular aufgebaut und bietet alle grundlegenden Funktionen mit einem Plug-In-System, das unabhängig hinzugefügt / deaktiviert oder entwickelt werden kann. Das gleiche gilt für die Analyse - jedes Plugin ist für die Art der Analyse verantwortlich. Daher sehen wir uns zunächst mit diesem Fenster konfrontiert, in dem Sie die gewünschten Analysetypen auswählen können:

Fenster "Analyseeinstellungen"

Für unsere Zwecke ist es sinnvoll, die Standardeinstellungen beizubehalten und die Analyse auszuführen. Die Analyse selbst wird ziemlich schnell durchgeführt (ich habe ungefähr 7 Sekunden gebraucht), obwohl Benutzer in den Foren sich darüber beschweren, dass Hydra bei großen Projekten an Geschwindigkeit gegenüber IDA Pro verliert. Dies mag zutreffen, aber für kleine Dateien ist dieser Unterschied nicht signifikant.

Damit ist die Analyse abgeschlossen. Die Ergebnisse werden im Code-Browser-Fenster angezeigt:



Dieses Fenster ist das Hauptfenster für die Arbeit in Hydra, daher sollten Sie es genauer studieren.

Übersicht über die Code-Browser-Oberfläche
Die Standardeinstellungen der Benutzeroberfläche teilen das Fenster in drei Teile.

Im zentralen Teil befindet sich das Hauptfenster - eine Auflistung des Disassemblers, die seinen "Brüdern" in IDA, OllyDbg usw. mehr oder weniger ähnlich ist. Standardmäßig lauten die Spalten in dieser Liste (von links nach rechts): Speicheradresse, Opcode des Befehls, ASM-Befehl, Parameter des ASM-Befehls, Querverweis (falls zutreffend). Natürlich kann die Anzeige durch Klicken auf die Schaltfläche in Form einer Mauer in der Symbolleiste dieses Fensters geändert werden. Um ehrlich zu sein, habe ich noch nie eine so flexible Konfiguration der Ausgabe des Disassemblers gesehen, sie ist äußerst praktisch.

Im linken Teil des 3-Panels:

  1. Abschnitte des Programms (klicken Sie mit der Maus, um durch Abschnitte zu navigieren)
  2. Zeichenbaum (Importe, Exporte, Funktionen, Überschriften usw.)
  3. Geben Sie den Baum der verwendeten Variablen ein

Für uns ist das nützlichste Fenster hier ein Symbolbaum, mit dem Sie beispielsweise eine Funktion anhand ihres Namens schnell finden und zur entsprechenden Adresse wechseln können.

Auf der rechten Seite finden Sie eine Auflistung des dekompilierten Codes (in unserem Fall in C).

Zusätzlich zu den Standardfenstern können Sie im Menü Fenster Dutzende anderer Fenster und Anzeigen an einer beliebigen Stelle im Browser auswählen und platzieren. Der Einfachheit halber habe ich in der Mitte ein Byte-Fenster und ein Fenster mit einem Funktionsdiagramm sowie rechts Zeichenfolgenvariablen (Zeichenfolgen) und eine Funktionstabelle (Funktionen) hinzugefügt. Diese Fenster sind jetzt in separaten Registerkarten verfügbar. Außerdem können alle Fenster abgenommen und "schwebend" gemacht werden, wobei sie nach eigenem Ermessen platziert und in der Größe geändert werden - dies ist meiner Meinung nach auch eine sehr durchdachte Lösung.

Schritt 4. Erlernen des Programmalgorithmus - main () -Funktion


Kommen wir zu einer direkten Analyse unserer Crack-Programme. In den meisten Fällen sollten Sie zunächst nach dem Einstiegspunkt des Programms suchen, d. H. Die Hauptfunktion, die beim Start aufgerufen wird. Da wir wissen, dass unser Crack in C / C ++ geschrieben wurde, vermuten wir, dass der Name der Hauptfunktion main () oder so ähnlich sein wird :) Es wird gesagt und getan. Geben Sie "main" in den Filter des Symbolbaums ( im linken Bereich) ein und sehen Sie sich die Funktion _main () im Abschnitt " Funktionen " an. Gehen Sie mit einem Mausklick dorthin.

Übersicht über die main () - Funktion und Umbenennen von obskuren Funktionen


In der Disassembler-Liste wird sofort der entsprechende Codeabschnitt angezeigt, und rechts sehen wir den dekompilierten C-Code dieser Funktion. Ein weiteres praktisches Merkmal von Hydra ist die Synchronisation der Auswahl: Wenn eine Maus einen Bereich von ASM-Befehlen auswählt, wird der entsprechende Codeabschnitt im Dekompiler hervorgehoben und umgekehrt. Wenn das Fenster zur Speicheranzeige geöffnet ist, wird die Zuordnung außerdem mit dem Speicher synchronisiert. Wie sie sagen, ist alles Geniale einfach!

Ich stelle sofort ein wichtiges Merkmal der Arbeit in Hydra fest (im Gegensatz beispielsweise zur Arbeit in IDA). Die Arbeit in Hydra konzentriert sich hauptsächlich auf die Analyse von dekompiliertem Code . Aus diesem Grund haben die Entwickler von Hydra (wir erinnern uns - wir sprechen über Spione der NSA :)) der Qualität der Dekompilierung und der Bequemlichkeit der Arbeit mit Code große Aufmerksamkeit geschenkt. Insbesondere können Sie einfach Funktionen, Variablen und Speicherbereiche definieren, indem Sie einfach in den Code doppelklicken. Außerdem kann jede Variable und Funktion sofort umbenannt werden, was sehr praktisch ist, da die Standardnamen keine Bedeutung haben und verwirrend sein können. Wie Sie später sehen werden, werden wir diesen Mechanismus häufig verwenden.

Hier ist also die main () -Funktion, die Hydra wie folgt „seziert“ hat:

Listing main ()
int __cdecl _main(int _Argc,char **_Argv,char **_Env) { bool bVar1; int iVar2; char *_Dest; size_t sVar3; FILE *_File; char **ppcVar4; int local_18; ___main(); if (_Argc == 3) { bVar1 = false; _Dest = (char *)_text(0x100,1); local_18 = 0; while (local_18 < 3) { if (bVar1) { _text(_Dest,0,0x100); _text(_Dest,_Argv[local_18],0x100); break; } sVar3 = _text(_Argv[local_18]); if (((sVar3 == 2) && (((int)*_Argv[local_18] & 0x7fffffffU) == 0x2d)) && (((int)_Argv[local_18][1] & 0x7fffffffU) == 0x66)) { bVar1 = true; } local_18 = local_18 + 1; } if ((bVar1) && (*_Dest != 0)) { _File = _text(_Dest,"rb"); if (_File == (FILE *)0x0) { _text("Failed to open file"); return 1; } ppcVar4 = _construct_key(_File); if (ppcVar4 == (char **)0x0) { _text("Nope."); _free_key((void **)0x0); } else { _text("%s%s%s%s\n",*ppcVar4 + 0x10d,*ppcVar4 + 0x219,*ppcVar4 + 0x325,*ppcVar4 + 0x431); _free_key(ppcVar4); } _text(_File); } _text(_Dest); iVar2 = 0; } else { iVar2 = 1; } return iVar2; } 


Es scheint, dass alles normal erscheint - Definitionen von Variablen, Standard-C-Typen, Bedingungen, Schleifen, Funktionsaufrufe. Bei näherer Betrachtung des Codes stellen wir jedoch fest, dass die Namen einiger Funktionen aus irgendeinem Grund nicht definiert und durch die Pseudofunktion _text () (im Dekompilerfenster - .text () ) ersetzt wurden. Beginnen wir mit der Definition dieser Funktionen.

Doppelklicken Sie auf den Hauptteil des ersten Anrufs

  _Dest = (char *)_text(0x100,1); 

Wir sehen, dass dies nur eine Wrapper-Funktion um die Standardfunktion calloc () ist, mit der Speicher für Daten zugewiesen wird. Benennen wir diese Funktion einfach in calloc2 () um . Setzen Sie den Cursor auf den Funktionsheader, rufen Sie das Kontextmenü auf, wählen Sie Funktion umbenennen (Hotkey - L ) und geben Sie einen neuen Namen in das folgende Feld ein:



Wir sehen, dass die Funktion sofort umbenannt wurde. Wir kehren zum Hauptteil () zurück (die Schaltfläche Zurück in der Symbolleiste oder Alt + <- ) und sehen, dass hier anstelle des mysteriösen _text () calloc2 () bereits steht. Großartig!

Wir machen dasselbe mit allen anderen Wrapper-Funktionen: Wir gehen nacheinander auf ihre Definitionen ein, schauen uns an, was sie tun, benennen sie um (ich habe Index 2 zu den Standardnamen von C-Funktionen hinzugefügt) und kehren zur Hauptfunktion zurück.

Wir verstehen den Funktionscode main ()


Okay, wir haben einige seltsame Funktionen herausgefunden. Wir beginnen den Code der Hauptfunktion zu studieren. Wenn wir Variablendeklarationen überspringen, sehen wir, dass die Funktion den Wert der Variablen iVar2, der Null ist (ein Zeichen für den Erfolg der Funktion), nur zurückgibt, wenn die durch die Zeichenfolge angegebene Bedingung erfüllt ist

 if (_Argc == 3) { ... } 

_Argc ist die Anzahl der Befehlszeilenparameter (Argumente), die an main () übergeben werden . Das heißt, unser Programm "frisst" 2 Argumente (das erste Argument, an das wir uns erinnern, ist immer der Pfad zur ausführbaren Datei).

OK, lass uns weitermachen. Hier erstellen wir einen C-String ( char- Array) mit 256 Zeichen:

 char *_Dest; _Dest = (char *)calloc2(0x100,1); //  new char[256]  C++ 

Als nächstes haben wir eine Schleife von 3 Iterationen. Darin prüfen wir zunächst, ob das Flag bVar1 gesetzt ist, und kopieren in diesem Fall das folgende Befehlszeilenargument (Zeichenfolge) nach _Dest :

 while (i < 3) { /*    .  */ if (bVar1) { /*   */ memset2(_Dest,0,0x100); /*    _Dest    */ strncpy2(_Dest,_Argv[i],0x100); break; } ... } 

Dieses Flag wird gesetzt, wenn das folgende Argument analysiert wird:

 n_strlen = strlen2(_Argv[i]); if (((n_strlen == 2) && (((int)*_Argv[i] & 0x7fffffffU) == 0x2d)) && (((int)_Argv[i][1] & 0x7fffffffU) == 0x66)) { bVar1 = true; } 

Die erste Zeile berechnet die Länge dieses Arguments. Ferner prüft die Bedingung, ob die Länge des Arguments 2, das vorletzte Zeichen == "-" und das letzte Zeichen == "f" sein muss. Beachten Sie, wie der Dekompiler die Extraktion von Zeichen aus der Zeichenfolge mithilfe einer Bytemaske "übersetzt" hat.
Dezimalwerte von Zahlen und gleichzeitig die entsprechenden ASCII-Zeichen können ausspioniert werden, indem der Cursor über das entsprechende hexadezimale Literal gehalten wird. Die ASCII-Zuordnung funktioniert nicht immer (?). Ich empfehle daher, die ASCII-Tabelle im Internet zu betrachten. Sie können Skalare auch direkt in Hydra von einem beliebigen Zahlensystem in ein anderes konvertieren (über das Kontextmenü -> Konvertieren ). In diesem Fall wird diese Nummer überall im ausgewählten Zahlensystem angezeigt (im Disassembler und im Dekompiler). aber ich persönlich ziehe es vor, hexes im code zu lassen, um die arbeit zu harmonisieren, weil Speicheradressen, Offsets usw. Hexen sind überall gesetzt.
Nach der Schleife kommt dieser Code:

 if ((bVar1) && (*_Dest != 0)) { /*    1) "-f"  2)  -         */ _File = fopen2(_Dest,"rb"); if (_File == (FILE *)0x0) { /*  1    */ perror2("Failed to open file"); return 1; } ... } 

Hier habe ich sofort Kommentare hinzugefügt. Wir überprüfen die Gültigkeit der Argumente ("-f path_to_file") und öffnen die entsprechende Datei (das 2. übergebene Argument, das wir nach _Dest kopiert haben). Die Datei wird im Binärformat gelesen, wie durch den Parameter "rb" der Funktion fopen () angegeben . Wenn der Lesevorgang fehlschlägt (z. B. ist die Datei nicht verfügbar), wird im stderror-Stream eine Fehlermeldung angezeigt und das Programm wird mit Code 1 beendet.

Weiter ist das interessanteste:

  /* !!!     !!! */ ppcVar3 = _construct_key(_File); if (ppcVar3 == (char **)0x0) { /*    ,  "Nope" */ puts2("Nope."); _free_key((void **)0x0); } else { /*    -      */ printf2("%s%s%s%s\n",*ppcVar3 + 0x10d,*ppcVar3 + 0x219,*ppcVar3 + 0x325,*ppcVar3 + 0x431); _free_key(ppcVar3); } fclose2(_File); 

Der offene Dateideskriptor ( _File ) wird an die Funktion _construct_key () übergeben , die offensichtlich die Überprüfung des gesuchten Schlüssels durchführt. Diese Funktion gibt ein zweidimensionales Byte-Array ( char ** ) zurück, das in der Variablen ppcVar3 gespeichert ist. Wenn das Array leer ist, wird das prägnante "Nope" auf der Konsole angezeigt (dh unserer Meinung nach "Nope!") Und der Speicher wird freigegeben. Andernfalls (wenn das Array nicht leer ist) wird der scheinbar korrekte Schlüssel angezeigt und der Speicher wird ebenfalls freigegeben. Am Ende der Funktion wird der Dateideskriptor geschlossen, der Speicher freigegeben und der Wert von iVar2 zurückgegeben .

Jetzt haben wir erkannt, dass wir Folgendes brauchen:

1) Erstellen Sie eine Binärdatei mit dem richtigen Schlüssel.
2) Übergeben Sie den Pfad im Riss nach dem Argument "-f".

Im zweiten Teil des Artikels werden wir die Funktion _construct_key () analysieren, die, wie wir herausgefunden haben, für die Überprüfung des gewünschten Schlüssels in der Datei verantwortlich ist.

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


All Articles