Einführung ins Reverse Engineering mit Ghidra

Beim Betrachten des Assemblercodes einer unbekannten Binärdatei wird man schnell mit vielen Informationen konfrontiert. Selbst eine kleine Binärdatei von einigen Kilobytes enthält bereits viele Befehle und greift wahrscheinlich auch auf externe Informationen (DLLs, Assets, Aufrufe des Betriebssystems usw.). Die offensichtliche Frage ist: Wo anfangen?

Wo anfangen

Für Reverse Engineering verlasse ich mich hauptsächlich auf Ghidra. Es bietet eine gute Auswahl an Funktionen, und seine Benutzeroberfläche ist selbst für Einsteiger gut zu verstehen. In meinem speziellen Beispiel wollte ich das Format der Datei SPELL.BIN aus Spellbinder: The Nexus Conflict Demo verstehen. Diese Datei enthält die Zaubersprüche, Effekte und Runen, die von der Spiellogik verwendet werden. Die Datei selbst enthält nur die Assets, und eine DLL oder EXE muss sie für das Spiel laden. Fangen wir also damit an, unser Ziel zu identifizieren: Wo befindet sich der Code wahrscheinlich?

Lokalisierung Ihrer Binärdatei

Im Fall der Spellbinder-Demo haben wir 2 relevante Dateien:

  1. spell.exe
  2. game.dll

Die erste ist nur etwa 90 KB groß, die zweite etwa 1,7 MB. Da die Demo bereits einen Auto-Updater verwendet, nahm ich an, dass die gesamte Spiellogik in game.dll sein muss, damit sie durch die Updates ersetzt werden kann. Außerdem verweist spell.exe nicht über den statischen Linker auf game.dll, sodass sie Änderungen an der game.dll ausführen könnte.

Importieren Ihrer Binärdatei

Der erste wichtige Schritt besteht darin, Ihre Binärdatei ordnungsgemäß zu importieren. Versuchen Sie, alle von der Ziel-DLL referenzierten DLLs in den Bibliothekspfaden verfügbar zu machen. Ein Beispiel für game.dll ist im folgenden Bildschirm zu sehen:

Bibliothekspfade

Damit erhalten Sie schnell Einblicke in die Drittanbieterkomponenten, von denen Ihre Ziel-DLL abhängt. Im hier gezeigten Beispiel sehen Sie, dass es von DDRAW.DLL abhängt, was darauf hinweist, dass es wahrscheinlich Direct Draw für die Darstellung verwendet. Wenn Sie also am Rendering-Stack interessiert sind, wissen Sie, wonach Sie suchen müssen.

Referenzierte DLLs

Analyse der Binärdatei

Beim ersten Öffnen des CodeBrowsers für Ihr Ziel wird Ghidra Sie auffordern, eine Analyse durchzuführen. Tun Sie das! Wählen Sie die gewünschten Analysewerkzeuge aus. Normalerweise wähle ich alle aus, außer “Agressive Instruction Finder” und “MS DIA-SDK”.

Begrenzung Ihrer Analyse

Jetzt sind Sie am Punkt der manuellen Arbeit angekommen. Das Wichtigste ist, den Umfang so gering wie möglich zu halten. Sie werden wahrscheinlich niemals alles verstehen, was in der Binärdatei passiert, aber das Gute ist: Sie müssen es nicht. Ich bin daran interessiert, spell.bin zu lesen, also kommt wahrscheinlich der Dateiname im Code vor. Bei der Suche danach fanden wir es als Zeichenkonstante:

Vorkommen von spell.bin

Die Verweise führen alle zu FUN_0045ede0, was anscheinend die Methode zum Laden von Spellbinders BIN-Dateien ist. Nennen wir diese Methode LoadSpellBinFile. Alle Vorkommen erhalten dieselbe konstante Zeichenkette übergeben, daher wird sie nur zum Lesen der spell.bin-Datei verwendet, selbst wenn sie mehr tun könnte. Interessanterweise fand ich eine zusätzliche Methode FUN_004571d0, die eine spell.dat übergeben bekommt, die scheinbar den gleichen Inhalt in anderem Format anbietet.

Identifizierung von Datenstrukturen

In unserer Methode LoadSpellBinFile sehen wir viele lokale Daten, wie im folgenden Screenshot zu sehen ist:

Datenstrukturen

Ghidra kann Datenstrukturen automatisch generieren, aber vorerst machen wir es manuell. Die erste verwendete Datenstruktur befindet sich bei puVar2, was dasselbe ist wie ppuVar11, was dasselbe ist wie ppuVar6. Und woher kommt das?

Identifizierung von Datenstrukturen

Wir sehen eine Methode, der 344 übergeben wird und die einen Zeiger zurückgibt, wahrscheinlich eine Allokation von 344 Bytes. Das ist großartig: Wenn wir uns spell.bin ansehen, sehen wir ein schönes Muster von 344 Bytes Größe. Was passiert mit diesen allokierten Bytes? Wenn die Allokation erfolgreich war, wird der Zeiger PTR_LAB_0056f458 zugewiesen. Was soll das sein? Wahrscheinlich eine virtuelle Funktionstabelle. Es könnte sich also um objektorientierten Code handeln, der entweder (noch) nicht als C++ erkannt wird oder auf C basierend ein anderes objektorientiertes Framework verwendet.

Vtables - Virtuelle Funktionstabellen

Wir haben also herausgefunden, dass PTR_LAB_0056f458 eine Vtable ist. Jetzt müssen wir ihr einen geeigneten Typ zuweisen:

344 Vtable

Sobald dies eingerichtet ist (Sie müssen die erforderlichen Funktionsdefinitionen erstellen), sieht der Speicher bei 0056f458 folgendermaßen aus:

344 statische Vtable

Mit Ihrer neu erstellten Vtbl können Sie auch eine passende Struktur erstellen, um sie zu halten:

344 Vtable Verwendung

Leider zeigt der Code von LoadSpellBinFile nicht, was die einzelnen Felder unserer neu erstellten Struktur bedeuten. Wir können sehen, dass es wahrscheinlich Blöcke von 4 Bytes sind, aber das ist leider der einzige Anhaltspunkt.

Einen Schritt zurück anstelle von zu tief einzusteigen

Erinnern Sie sich an FUN_004571d0, das in einem ähnlichen Bereich arbeitet und wahrscheinlich die Zaubersprüche in einem anderen Format liest? Sie wird nur einmal aufgerufen, also habe ich sie in LoadSpellDatFile umbenannt. Wenn Sie diese Methode öffnen, sehen Sie, dass sie mit denselben Strukturen wie LoadSpellBinFile arbeitet. Und sie verwendet viele Zeichenkonstanten:

  • “Loading Spell #%d\n”
  • “Error Loading ’type’ from section ‘%s’ file: %s\n”

Das gibt uns sofort mehr Einblicke. Sobald wir Ghidra mitteilen, dass piVar5 ein Zeiger auf die 344 Bytes große Struktur ist, zeigt es uns, dass Feld 0x1c Müdigkeit ist. Das Format von spell.dat scheint also ein auf .ini basierendes Format zu sein.

344 Felder

Nun erfolgt die eigentliche Arbeit, jeden Feldnamen zu benennen und dieselben Schritte wie oben für andere Datenstrukturen zu wiederholen.

Weitere Hinweise

Wenn Ihr Interesse darin besteht, Dateien zu lesen, suchen Sie nach spezifischen Methoden dafür. Um Dateien zu lesen, müssen Sie sie zuerst öffnen. Bei WIN32-basierten Anwendungen würden Sie normalerweise OpenFile verwenden. Durchsuchen Sie also die Importe, um das Folgende zu finden:

OpenFile

Unter Verwendung der Verweise auf die Funktion finden Sie die Stellen, an denen eine Datei geöffnet wird.

In diesem speziellen Beispiel gibt es zwei Vorkommen, beide in FUN_00516b60. Nach dem Umbenennen der lokalen Variablen, um ein besseres Verständnis des Codes zu erhalten, sieht der Code folgendermaßen aus:

FUN_00516b60

Es wird versucht, mindestens 768 Bytes zu lesen, und falls dies nicht der Fall ist, wird abgebrochen. Wir haben also herausgefunden, dass _read verwendet wird, um Daten zu laden. Wir haben auch gelernt, dass FUN_00535f80 Speicher allokiert (oder Zugriff auf bereits allokierten Speicher bereitstellt). Da der Speicher in der Methode nicht freigegeben wird, behält die Methode FUN_00535f80 wahrscheinlich den Überblick darüber.

Es bleibt auch zu beachten, dass es andere Methoden zum Lesen von Dateien wie ReadFile gibt.

GetProcAddress

Eine weitere Sache, die Sie im Kopf behalten sollten, ist, dass nicht alles statisch oder dynamisch verlinkt ist. Manchmal werden DLLs im Programmcode geladen. Überprüfen Sie immer die Verweise auf GetProcAddress. Dies gibt Einblicke in die tatsächlichen Methoden, die hinter Zeigern versteckt sind. Im Beispiel von game.dll können alle mit Sockets verbundenen Methoden von zwei verschiedenen DLLs bereitgestellt werden, und der Code lädt sie nach Belieben. Daher ist der gesamte netzwerkbezogene Code schwer zu finden, bis Sie einen geeigneten Datentyp und einen geeigneten Namen für die Funktionszeiger zuweisen. Im folgenden Beispiel wurde closesocket ordnungsgemäß zugewiesen, während es WSAAsyncSelect noch nicht wurde.

closesocket

Copyright © christophbrill.de, 2002-2025.