
Schritt-für-Schritt-Anleitung zur Verwendung des lokalen KI-Modells Gemma 4 E4B von Google, um einen Windows-Crackme mit Ghidra zu reverse-engineeren, einschließlich Einrichtung für lokale Inferenz und automatischer Umbenennung von Funktionen/Variablen.
Ich habe mit dem neuen Gemma E4B Open-Weights-Lokalmodell herumgespielt, das Google veröffentlicht hat, und zu meiner Überraschung hatte ich großen Erfolg bei der Verwendung für lokale Offline-Reverse-Engineering-Szenarien. Ich wollte dieses Tutorial schreiben, um die Botschaft zu verbreiten, dass lokale KI jetzt für viele grundlegende Reversing-Aufgaben gut genug ist und dass sich die Dinge ab hier wahrscheinlich schnell verbessern werden.
Einer der mühsamsten Teile beim Reversing einer neuen Binärdatei ist ganz am Anfang, wenn man keine Einblicke in die wichtigen Funktionen und Variablen hat. Es gibt viele Tricks, die Reverser verwenden, um einzusteigen, wie das Betrachten von String-Referenzen, Binary Diffing oder Abgleich ähnlicher Funktionen.
KI ist hier sehr hilfreich, und ich persönlich hatte großen Erfolg mit der OpenAI-API, um eine Binärdatei zu markieren oder die Ausgabe des Decompilers zu bereinigen. Es gibt jedoch mehrere Nachteile bei der Verwendung dieser APIs:
Kosten - Dekompilierung und Disassemblierung erzeugen Unmengen von Tokens. Die APIs berechnen $ pro Token, sodass die Analyse größerer Binärdateien eine ganze Menge Geld kosten kann. Wenn man es mit einem großen Ziel mit vielen Binärdateien zu tun hat, die jede Woche aktualisiert werden, können sich diese Kosten schnell summieren und sind für Hobby-Reverse-Engineers unerschwinglich.
Privatsphäre - Wenn man eine Remote-API verwendet, hat der Host der KI Einblick in das, was man tut. Das ist in einigen professionellen Szenarien ein No-Go.
Kontrolle - Wenn man sich auf eine Remote-API verlässt, hat man keine Kontrolle darüber, welche Modelle einem bereitgestellt werden oder wie die Qualität der Modelle ist. Wenn man sich für kritische Aufgaben auf sie verlässt, kann dies ein Problem sein, wenn sie langsam werden oder ausfallen, wenn man sie braucht, oder wenn ihre Ausgabequalität so weit abnimmt, dass sie nutzlos sind.
Das Ausführen eigener lokaler KI-Modelle behebt einige dieser Probleme:
Kosten - Es kann viel billiger sein, ein lokales Modell auszuführen, als sich auf einen gehosteten Dienst zu verlassen. Auch wenn Ihr lokales Modell wahrscheinlich kleiner und langsamer ist als das eines guten Anbieters, kann es sinnvoll sein, Geld zu sparen, indem Sie Ihr eigenes Modell ausführen, wenn es gut genug ist und in einer angemessenen Zeit läuft, insbesondere wenn Sie große Datenmengen für einfache Aufgaben verarbeiten.
Privatsphäre - Wenn Sie das Modell lokal ausführen, finden keine Netzwerkaufrufe statt, und Sie haben die vollständige Kontrolle über Ihre Privatsphäre. Niemand kann sehen, wofür Sie das Modell auf Ihrem eigenen Rechner verwenden.
Kontrolle - Das Schöne an einem Open-Weights-Modell ist, dass es Ihnen niemand wegnehmen kann. OpenAI oder Anthropic könnten eines Tages ihre SotA-Modelle nicht mehr verfügbar machen, entweder durch Preiserhöhungen oder durch explizite Entfernung ihrer APIs. Aber mit einem Open-Weights-Modell haben Sie die Kontrolle über Ihr Schicksal, im Guten wie im Schlechten.
Lokale KI-Modelle haben jedoch auch ihre Nachteile:
Größe - Je größer Ihr Modell, desto intelligenter ist es. Allerdings passen die meisten großen Modelle nicht auf Consumer-Hardware. Aus diesem Grund laufen Sie, wenn Sie ein lokales Modell ausführen, wahrscheinlich ein Modell, das 10x-100x kleiner ist als ein SotA-Modell (State-of-the-Art). Diese Verringerung der Größe führt direkt zu einer Verringerung der Intelligenz des Modells, wodurch es für viele Aufgaben ungeeignet wird, die in SotA-Modellen wie ChatGPT/Codex oder Claude als selbstverständlich angesehen werden.
Geschwindigkeit - Lokale Modelle werden auf Ihrem Rechner wahrscheinlich langsamer laufen als bei Verwendung einer KI-API. Auch dies ist auf die Einschränkungen der Consumer-Hardware und einige Tricks zurückzuführen, die API-Anbieter anwenden können, die Ihnen im Allgemeinen nicht zur Verfügung stehen.
Konfiguration - Das Ausführen lokaler Modelle ist vergleichbar mit dem Versuch, Linux auf einem refurbished Laptop zum Laufen zu bringen, im Gegensatz zum Betreten des Apple Stores und dem Kauf eines sauberen neuen Macbook Air. Das Codex- und Claude-Code-Erlebnis ist das Apple-Store-Erlebnis der KI. Das lokale KI-Modell-Erlebnis ist der Typ mit einem Fedora in seiner Garage, der an einem hakeligen Computer herumhackt, um ihn zum Laufen zu bringen. Mindestens müssen Sie sich um folgende Dinge kümmern:
Es ist keine leichte Reise, und viele geben auf und nehmen an, dass lokale KI-Modelle der Aufgabe nicht gewachsen sind, weil sie nie die richtige Kombination aus Hardware/Modell/Einstellungen/Prompting/Harness gefunden haben, um sie für ihre Aufgabe zum Laufen zu bringen. Obwohl sie in vielen Fällen recht haben, hoffe ich, dass dieses Tutorial zumindest zeigt, wie weit lokale Modelle gekommen sind, wie sie beim Reverse Engineering helfen können, und Menschen dazu inspiriert, lokaler KI eine Chance zu geben.
(Archiv-Passwort ist crackmes.one). Ich habe einen alternativen Link hier im Repo bereitgestellt, falls der ursprüngliche Link nicht mehr funktioniert.
Ghidra 12.04 wird für Disassemblierung und Dekompilierung verwendet. Sie müssen OpenJDK 21 installieren, um es zu nutzen: https://github.com/nationalsecurityagency/ghidra
Während der Arbeit an diesem Tutorial habe ich mit Claude Code ein Ghidra-Plugin vibe-coded, um Funktionen und Variablen mit KI umzubenennen. Sie können das Plugin hier herunterladen: https://github.com/markoglasgow/Ghidra_FastAIRenamer_Plugin
Um es zu installieren, verschieben Sie einfach die Zip-Datei ghidra_12.0.4_PUBLIC_20260427_FastAIRenamerPlugin.zip nach ${GHIDRA_HOME}\Extensions\Ghidra, führen Sie dann Ghidra aus, indem Sie ${GHIDRA_HOME}\ghidraRun.bat ausführen. Um das Plugin zu aktivieren, wählen Sie im anfänglichen Ghidra-Bildschirm im oberen Menü File -> Install Extensions, aktivieren Sie dann im Plugin-Browser das Kontrollkästchen neben FastAIRenamerPlugin und klicken Sie auf Ok. Ghidra wird Sie auffordern, es neu zu starten, also tun Sie das sofort.
Um das Plugin zu konfigurieren, gehen Sie beim nächsten Start von Ghidra im oberen Menü zu Tools -> Run Tool -> CodeBrowser. Ghidra wird sagen: "New Extensions detected. Would you like to configure them?". Klicken Sie auf Ja, aktivieren Sie dann erneut das Kontrollkästchen neben FastAIRenamerPlugin und klicken Sie auf Ok. Wenn der CodeBrowser geöffnet wird, klicken Sie im oberen Menü auf Window -> Fast AI Renamer und dann auf die Schaltfläche Config. Hier können Sie Ihr KI-Modell konfigurieren. Schließen Sie nach Abschluss das Plugin-Fenster und das leere CodeBrowser-Fenster.
Hinweis: Wenn Sie Probleme beim Laden des Plugins haben, müssen Sie möglicherweise den Entwicklermodus in Ghidra aktivieren (File -> Configure -> Kontrollkästchen neben Developer)
Hinweis: Sie können jederzeit überprüfen, ob das Plugin geladen ist, indem Sie zum CodeBrowser gehen, auf File -> Configure -> Ghidra Core klicken, dann auf die blaue Schaltfläche Configure klicken, nach "FastAIRenamer" filtern und sicherstellen, dass das Kontrollkästchen neben dem Namen aktiviert ist.
Hinweis: Um das Plugin zu deinstallieren, öffnen Sie zuerst den CodeBrowser, File -> Configure -> Ghidra Core -> klicken Sie auf die blaue Schaltfläche Configure -> filtern Sie nach "FastAIRenamer" -> deaktivieren Sie das Häkchen -> ok. Schließen Sie den CodeBrowser, dann im anfänglichen Ghidra-Fenster File -> Install Extensions -> deaktivieren Sie "FastAIRenamer". Schließen Sie Ghidra schließlich und löschen Sie ghidra_12.0.4_PUBLIC_20260427_FastAIRenamerPlugin.zip aus ${GHIDRA_HOME}\Extensions\Ghidra. Um sicherzustellen, dass die Erweiterung gelöscht ist, gehen Sie beim nächsten Start von Ghidra im anfänglichen Fenster zu Help -> Runtime Information -> Extension Points -> filtern Sie nach "FastAIRenamer" und stellen Sie sicher, dass nichts angezeigt wird. Puh.
Stellen Sie schließlich sicher, dass Sie Visual Studio oder eine andere C++-Entwicklungsumgebung eingerichtet haben, damit Sie bei Bedarf eine Lösung für das Crackme vibe-coden können.
Die lokalen KI-Setups der Leute werden je nach verfügbarer Hardware und Geld stark variieren. Für mich:
Ich persönlich verwende eine Nvidia GTX 3080 mit installiertem Cuda 13.2. Sie können Ihre Cuda-Version überprüfen, indem Sie nvcc --version in Ihrem Windows-Terminal ausführen.
Ich verwende die Quantisierungen von https://huggingface.co/bartowski/google_gemma-4-E4B-it-GGUF/blob/main/google_gemma-4-E4B-it-Q8_0.gguf von bartowski. Es mag Aberglaube meinerseits sein, aber bei analytischen Aufgaben versuche ich, mit der geringstmöglichen Quantisierung zu arbeiten und sie ganz zu vermeiden, wenn ich kann.
Ich verwende llama.cpp als meinen Inferenzserver in dieser spezifischen Version: llama-b8893-bin-win-cuda-13.1-x64.
Ich führe llama.cpp mit den folgenden CLI-Einstellungen:```
..\llama-b8893-bin-win-cuda-13.1-x64\llama-server.exe ^
--port 8090 ^
--threads 12 ^
--n-gpu-layers 256 ^
--no-mmap ^
--model "google_gemma-4-E4B-it-Q8_0.gguf" ^
--ctx-size 32768 ^
--temp 1.0 ^
--top-k 64 ^
--top-p 0.95 ^
--offline
Dies ergibt `75 tokens/sec`, was für lokale KI ziemlich flott ist.
## Keine Hardware? Kein Problem
Falls Sie nicht über die nötige Hardware verfügen oder Probleme mit der Konfiguration haben, aber dennoch diesem Tutorial folgen möchten, können Sie sich bei OpenRouter anmelden und eine der kostenlosen APIs nutzen:
https://openrouter.ai/models/?q=free
Google bietet derzeit (nur für begrenzte Zeit) `Gemma 4 31B` und `Gemma 4 26B-A4B` kostenlos an:
https://openrouter.ai/google/gemma-4-31b-it:free
https://openrouter.ai/google/gemma-4-26b-a4b-it:free
... beachten Sie jedoch, dass diese kostenlosen APIs langsam und unzuverlässig sind und alle Daten, die Sie an sie senden, wahrscheinlich in ihrer internen Analyse protokolliert und in ihrem nächsten Trainingsdurchlauf verwendet werden. Dennoch sollten diese APIs für die Zwecke dieses Tutorials ausreichen, um Ihnen das Mitverfolgen zu ermöglichen.
Um das Ghidra-Plugin für die Verwendung Ihres OpenRouter-Logins zu konfigurieren, öffnen Sie die Plugin-Konfiguration und geben Sie Folgendes ein:```
Base URL: https://openrouter.ai/api/
API Key: <your OpenRouter API Key>
Model Name: qwen/qwen3-235b-a22b-2507
Dieses Beispiel führt das Plugin gegen das qwen3-Modell hier aus.
Entpacken Sie und führen Sie crackmepls.exe aus. Es sollte ein Standard-Anmeldebildschirm erscheinen. Geben Sie ein beliebiges Passwort ein, und Sie erhalten eine 'Zugriff verweigert'-Meldung:```
User: marko
Pass: 123
Access denied
Öffnen Sie Ghidra, indem Sie `ghidraRun.bat` ausführen. Wählen Sie in der Symbolleiste `File -> New Project`, lassen Sie `Non-Shared Project` ausgewählt und klicken Sie auf `Next >>`, wählen Sie dann ein leeres Projektverzeichnis und geben Sie dem Projekt einen Namen. Klicken Sie dann auf `Finish`.
Klicken Sie als Nächstes auf `File -> Import File` und wählen Sie `crackmepls.exe` aus, um es zum Projekt hinzuzufügen. Ghidra zeigt einige Details zur Datei an, die besagen, dass es sich um eine `Portable Executable (PE)`-Datei für `x86:LE:64:default:windows` handelt. Klicken Sie einfach auf `OK`, um dies ohne Änderungen zu akzeptieren. Nach einer kurzen Verzögerung erscheinen weitere Details zur Datei, auch hier klicken Sie einfach auf `OK`. Doppelklicken Sie abschließend im Projekt auf `crackmepls.exe`, um den Code Browser zu öffnen und mit der Disassemblierung zu beginnen.
Ganz zu Anfang zeigt Ghidra eine Meldung an: `crackmepls.exe has not been analyzed. Would you like to analyze it now?`. Klicken Sie auf `Yes` und dann im nächsten Fenster auf die Schaltfläche `Analyze`, und warten Sie, bis Ghidra alle Funktionen in der Binärdatei lokalisiert, disassembliert und dekompiliert hat. Möglicherweise erhalten Sie während der Analyse einen oder zwei Fehler bezüglich nicht gefundener PDB-Dateien – klicken Sie einfach auf `Ok` und ignorieren Sie diese.
Sobald Ghidra mit der Analyse der Datei fertig ist, sollten Sie in etwa Folgendes sehen:

Dies ist der übliche MSVC-Einstiegspunkt. Doppelklicken Sie auf `FUN_14000200c` und scrollen Sie nach unten.
Sie sollten im Dekompilierer-Fenster Verweise auf `__p___argv`, `__p___argc` und dann einen Funktionsaufruf `FUN_140001290` sehen, der diese als Parameter akzeptiert. Dies ist wahrscheinlich die `main()`-Funktion des Crackme. Doppelklicken Sie darauf.

Sobald Sie sich in der Funktion `FUN_140001290` befinden, scrollen Sie im Dekompilierer-Fenster ein wenig nach unten. Sie werden String-Referenzen auf die Zeichenketten `User:` und `Pass:` sehen, zusammen mit Verweisen auf `basic_istream` (Eingabestream) und `basic_ostream` (Ausgabestream).

Diese Zeichenketten stimmen mit den Ausgabe- und Eingabeanweisungen überein, die wir beim ersten Ausführen des Crackme gesehen haben. Wir wissen also, dass wir uns in der `main()`-Funktion des Crackme befinden.
An diesem Punkt würde normalerweise die mühsame Arbeit beginnen, bei der wir uns hinsetzen und jede Variable und jeden Funktionsaufruf in etwas Sinnvolles umbenennen müssten, während wir analysieren, was die Binärdatei tut, um das Crackme zu lösen. Reverse Engineers mussten dies früher von Hand in der Disassembly erledigen, aber zum Glück haben uns Technologie und Steuergelder diesen praktischen Dekompilierer beschert, der mit KI skriptbar ist.
Statt also echte Arbeit zu verrichten, lehnen wir uns zurück, schalten unser Gehirn aus und lassen unsere lokale KI die ganze Arbeit für uns erledigen.
Während der Einrichtung für dieses Tutorial hätten Sie ein Ghidra-Plugin installiert haben sollen, das bei der Umbenennung von Funktionen und Variablen hilft. Jetzt ist es an der Zeit, es zu verwenden. Klicken Sie in Ghidra in der oberen Symbolleiste auf `Window` und dann auf `Fast AI Renamer`, um das Plugin zu öffnen.

Sie sollten die Plugin-Oberfläche sehen, die aus einer Reihe von Schaltflächen und einem Textbereich besteht. Klicken Sie zunächst auf die Schaltfläche "Config" und stellen Sie sicher, dass alles richtig konfiguriert ist, um mit Ihrer lokalen KI zu kommunizieren. Hier sehen Sie, wie es auf meinem Rechner aussieht:

Sie können das Konfigurationsfenster schließen, indem Sie auf "Save" klicken, und dann auf die Schaltfläche `Rename Variables` klicken. Ein Fortschrittsfenster mit einem Drachen sollte erscheinen, und Sie hören möglicherweise, wie Ihr Rechner zu arbeiten beginnt, während das lokale KI-Modell alle Variablen im Dekompilierer-Fenster umbenennt:

Sobald die KI fertig ist (dauert auf meinem Rechner etwa 10–20 Sekunden), sollten Sie im Textbereich des Plugins eine Beschreibung der umbenannten Variablen sehen, und die Variablen selbst sollten im Dekompilierer-Fenster umbenannt sein:

Alle KI-Modelle, insbesondere kleine lokale, sind grundsätzlich unzuverlässig, daher können bei diesem letzten Schritt einige Fehler auftreten. Darauf werden wir später noch eingehen. In der Zwischenzeit können Sie die KI jederzeit erneut ausführen, indem Sie erneut auf die Schaltfläche `Rename Variables` klicken, bis Sie Ergebnisse ohne Fehler erhalten, die für Sie zufriedenstellend sind.
## Lösen des Crackme
An diesem Punkt haben wir eine schön annotierte Dekompilierer-Ausgabe, bei der alle Variablennamen in etwas Sinnvolles umbenannt wurden. Traditionell könnte ein Reverse Engineer diesen Code nun lesen und anfangen, eine Lösung für das Crackme zu entwerfen. Allerdings dachte ich, es wäre interessant zu versuchen, die lokale KI dazu zu bringen, das Crackme für uns zu lösen. Denken Sie daran – wir werden heute unser Gehirn nicht benutzen.
Zuerst habe ich die KI damit beauftragt, die gesamte Binärdatei für uns zu markieren, indem ich auf die Schaltfläche `Rename ALL Functions and Variables` geklickt habe. Dadurch werden allen Funktionsaufrufen in dieser Crackme-Funktion Namen zugewiesen, wodurch die Dekompilierer-Ausgabe noch sauberer wird. Auf meinem Rechner dauert das etwa 15 Minuten.
Wenn Sie `llama.cpp` mit den von mir unter `Local AI Setup` aufgeführten CLI-Parametern verwenden, können Sie eine Chat-Oberfläche für Ihr lokales Modell öffnen, indem Sie im Browser zu [http://localhost:8090/](http://localhost:8090/) navigieren.
Dort habe ich die Dekompilierer-Ausgabe der Funktion aus dem Crackme eingegeben (Sie können sie erhalten, indem Sie im Plugin-UI auf die Schaltfläche "Decompile Function" klicken oder sie einfach aus dem Dekompilierer-Fenster auf der rechten Seite kopieren) und die KI gebeten, eine Lösung für uns zu programmieren:
======================================================================
======================================================================
======================================================================
Ich möchte, dass Sie mir helfen, eine Lösung für ein Crackme zu schreiben. Unten sehen Sie die Dekompilierer-Liste einer Funktion eines Crackme, bei dem der Benutzer einen Benutzernamen und ein Passwort eingibt und dann Zugriff gewährt wird, wenn das richtige Passwort angegeben wurde. Das Passwort wird in der Funktion berechnet.```
/* WARNING: Function: __security_check_cookie replaced with injection: security_check_cookie */
/* **Reasoning:**
The function takes user input (a username/input and a password) via standard input. It processes
the user input by calculating a complex, custom checksum/hash. It then compares the provided
password input against a target buffer (likely a stored hash or secret). Finally, it determines
and outputs whether "Access granted" or "Access denied," indicating the function serves as an
authentication routine. */
undefined8 authenticate_user(undefined8 param_1,undefined8 param_2,undefined8 max_len)
{
uint user_checksum;
int iVar1;
undefined8 ****temp_free_ptr;
ulonglong user_input_byte_index;
char *result_message;
undefined1 *status_buffer;
undefined8 ****data_buffer_ptr;
ulonglong pass_input_length_1;
bool access_granted;
undefined8 uStack_d0;
undefined1 status_buffer_small [8];
undefined1 status_buffer_large [32];
undefined8 ***allocated_ptrs [2];
size_t compare_length;
ulonglong ptr_metadata_size;
undefined8 ***user_input_buffer;
undefined8 uStack_78;
ulonglong user_input_length;
ulonglong user_input_length_param;
undefined8 ***pass_input_buffer;
undefined8 uStack_58;
size_t pass_input_length;
ulonglong pass_input_length_param;
ulonglong checksum_seed;
code *code_pointer;
undefined8 ***pass_input_buffer_ptr;
undefined8 ***user_input_buffer_ptr;
ulonglong user_input_length_1;
status_buffer = status_buffer_small;
checksum_seed = MAGIC_VALUE_1 ^ (ulonglong)status_buffer_small;
user_input_byte_index = 0;
uStack_78 = 0;
user_input_length = 0;
user_input_length_param = 0xf;
user_input_buffer = (undefined8 ****)0x0;
uStack_58 = 0;
pass_input_length = 0;
pass_input_length_param = 0xf;
pass_input_buffer = (undefined8 ****)0x0;
formatted_string_output((basic_ostream<char,struct_std::char_traits<char>_> *)cout_exref,"User: ");
extract_token_from_stream((basic_istream<char,struct_std::char_traits<char>_> *)cin_exref,
(longlong *)&user_input_buffer,max_len);
formatted_string_output((basic_ostream<char,struct_std::char_traits<char>_> *)cout_exref,"Pass: ");
extract_token_from_stream((basic_istream<char,struct_std::char_traits<char>_> *)cin_exref,
(longlong *)&pass_input_buffer,max_len);
user_input_length_1 = user_input_length_param;
user_input_buffer_ptr = user_input_buffer;
user_checksum = 0;
pass_input_length_1 = user_input_byte_index;
if (user_input_length != 0) {
do {
temp_free_ptr = &user_input_buffer;
if (0xf < user_input_length_param) {
temp_free_ptr = (undefined8 ****)user_input_buffer;
}
user_checksum =
((int)user_input_byte_index + 1) *
(int)*(char *)((longlong)temp_free_ptr + user_input_byte_index) +
(int)pass_input_length_1;
user_checksum = user_checksum * 8 ^ user_checksum;
user_input_byte_index = user_input_byte_index + 1;
pass_input_length_1 = (ulonglong)user_checksum;
} while (user_input_byte_index < user_input_length);
}
int_to_string_dynamic(allocated_ptrs,user_checksum * 0x539 ^ 0x5a5a);
pass_input_length_1 = pass_input_length_param;
pass_input_buffer_ptr = pass_input_buffer;
temp_free_ptr = &pass_input_buffer;
if (0xf < pass_input_length_param) {
temp_free_ptr = (undefined8 ****)pass_input_buffer;
}
data_buffer_ptr = allocated_ptrs;
if (0xf < ptr_metadata_size) {
data_buffer_ptr = (undefined8 ****)allocated_ptrs[0];
}
if (compare_length == pass_input_length) {
if (compare_length == 0) {
access_granted = true;
}
else {
iVar1 = memcmp(data_buffer_ptr,temp_free_ptr,compare_length);
access_granted = iVar1 == 0;
}
}
else {
access_granted = false;
}
if (0xf < ptr_metadata_size) {
temp_free_ptr = (undefined8 ****)allocated_ptrs[0];
status_buffer = status_buffer_small;
if (0xfff < ptr_metadata_size + 1) {
temp_free_ptr = (undefined8 ****)allocated_ptrs[0][-1];
data_buffer_ptr =
(undefined8 ****)((longlong)allocated_ptrs[0] + (-8 - (longlong)temp_free_ptr));
status_buffer = status_buffer_small;
if ((undefined8 ****)0x1f < data_buffer_ptr) {
code_pointer = (code *)swi(0x29);
(*code_pointer)(5);
temp_free_ptr = data_buffer_ptr;
status_buffer = status_buffer_large;
}
}
*(undefined8 *)(status_buffer + -8) = 0x140001424;
free(temp_free_ptr);
}
result_message = "Access granted\n";
if (!access_granted) {
result_message = "Access denied\n";
}
*(undefined8 *)(status_buffer + -8) = 0x140001443;
formatted_string_output((basic_ostream<char,struct_std::char_traits<char>_> *)cout_exref,result_message);
if (0xf < pass_input_length_1) {
temp_free_ptr = (undefined8 ****)pass_input_buffer_ptr;
if (0xfff < pass_input_length_1 + 1) {
temp_free_ptr = (undefined8 ****)pass_input_buffer_ptr[-1];
data_buffer_ptr =
(undefined8 ****)((longlong)pass_input_buffer_ptr + (-8 - (longlong)temp_free_ptr));
if ((undefined8 ****)0x1f < data_buffer_ptr) {
code_pointer = (code *)swi(0x29);
(*code_pointer)(5);
status_buffer = status_buffer + 8;
temp_free_ptr = data_buffer_ptr;
}
}
*(undefined8 *)(status_buffer + -8) = 0x14000147d;
free(temp_free_ptr);
}
if (0xf < user_input_length_1) {
temp_free_ptr = (undefined8 ****)user_input_buffer_ptr;
if (0xfff < user_input_length_1 + 1) {
temp_free_ptr = (undefined8 ****)user_input_buffer_ptr[-1];
data_buffer_ptr =
(undefined8 ****)((longlong)user_input_buffer_ptr + (-8 - (longlong)temp_free_ptr));
if ((undefined8 ****)0x1f < data_buffer_ptr) {
code_pointer = (code *)swi(0x29);
(*code_pointer)(5);
status_buffer = status_buffer + 8;
temp_free_ptr = data_buffer_ptr;
}
}
*(undefined8 *)(status_buffer + -8) = 0x1400014b7;
free(temp_free_ptr);
}
*(undefined8 *)(status_buffer + -8) = 0x1400014c9;
return 0;
}
Bitte lesen Sie die Decompiler-Listing und vervollständigen Sie meinen C++-Code unten, um das korrekte Passwort für einen eingegebenen Benutzernamen zu berechnen:``` #include #include #include #include
int main() { std::string username; std::cout << "User: "; std::cin >> username;
uint32_t password_value = 0;
std::cout << "Pass: " << password_value << "\n";
return 0;
}
======================================================================
======================================================================
======================================================================
## Die generierte Lösung
Beim ersten Mal war die generierte Lösung falsch. Ich habe sie in [tut1.txt](https://github.com/markoglasgow/gemma_crackme_tutorial/blob/HEAD/tut1.txt) in diesem Repository abgelegt. Auf den ersten Blick _sieht_ die Lösung korrekt aus, funktioniert aber nicht, wenn wir sie ausprobieren. KI sind grundsätzlich unzuverlässig, und das ist ein perfektes Beispiel dafür.
Egal, wir können einfach einen neuen Chat öffnen, den obigen Prompt kopieren und einfügen und versuchen, die KI dazu zu bringen, eine neue Lösung für uns zu generieren. In meinem Fall habe ich beim zweiten Versuch eine funktionierende Lösung erhalten:```
#include <cstdio>
#include <cstdint>
#include <string>
#include <iostream>
#include <sstream>
#include <iomanip>
/**
* @brief Simulates the custom checksum calculation defined in the crackme function.
*
* @param username The input string (username).
* @return int The final calculated user_checksum.
*/
int calculate_username_checksum(const std::string& username) {
// user_checksum is treated as an 'int' (32-bit signed integer) during the core calculation.
int user_checksum = 0;
// pass_input_length_1 acts as the state variable and is a ulonglong (64-bit).
// It starts at 0, as pass_input_length_1 = user_input_byte_index (which is 0) initially.
uint64_t state_pass_length_1 = 0;
for (size_t i = 0; i < username.length(); ++i) {
// i = user_input_byte_index
// Step 1: Calculate the intermediate value T1.
// The decompiler implies this calculation happens using standard int math
// (truncating/wrapping if intermediate values exceed 32 bits).
// (int)user_input_byte_index + 1
int index_term = (int)(i + 1);
// (int)*(char *)((longlong)temp_free_ptr + user_input_byte_index)
// This is just the ASCII value of the character.
int char_term = (int)username[i];
// (int)pass_input_length_1
// The state is cast down to an int for this arithmetic operation.
int state_term = (int)state_pass_length_1;
// T1 = ((int)user_input_byte_index + 1) * (int)char + (int)state
int intermediate_t1 = index_term * char_term + state_term;
// Step 2: Apply the XOR/Multiplication hash function.
// user_checksum = user_checksum * 8 ^ user_checksum;
// Note: The original code structure suggests the update happens here,
// but since we are recalculating the hash step-by-step, we use the definition:
// user_checksum = T1 * 8 ^ T1;
// Since the original code overwrites user_checksum completely with the hash
// of the current character, we use the T1 derived above.
int current_checksum = intermediate_t1;
current_checksum = current_checksum * 8 ^ current_checksum;
// Update the final running checksum
user_checksum = current_checksum;
// Step 3: Update the state variable for the next iteration.
state_pass_length_1 = (uint64_t)user_checksum;
}
return user_checksum;
}
int main()
{
std::string username;
std::cout << "User: ";
std::cin >> username;
// 1. Calculate the final checksum using the username
int final_user_checksum = calculate_username_checksum(username);
// 2. Calculate the final hash/secret value
// FinalHashValue = user_checksum * 0x539 ^ 0x5a5a
// Note: The arithmetic here is done on the final 32-bit 'int' checksum.
uint32_t final_hash_value = (uint32_t)final_user_checksum * 0x539 ^ 0x5a5a;
// 3. Convert the final hash value into a string (as implied by int_to_string_dynamic)
// Since the password needs to match the memory contents (memcmp), it must be the string representation.
std::stringstream ss;
ss << final_hash_value;
std::string required_password = ss.str();
// We output the required password string.
std::cout << "Pass: " << required_password << "\n";
return 0;
}
Ich habe den Chat, der die Lösung generiert hat, als solution_chat.html in diesem Repository beigefügt. Er enthielt den Denkprozess, was ich für so ein kleines, lokal laufendes Modell ziemlich cool fand.
Wie auch immer, wir können die Lösung mit Visual Studio kompilieren und dann ausführen, um eine gültige Benutzername/Passwort-Kombination zu generieren. Dann geben wir den Benutzernamen und das Passwort in das Crackme ein und überprüfen, dass wir es gelöst haben:

gg
Wie wir in diesem Tutorial mehrfach gesehen haben, ist KI von Natur aus unzuverlässig. Betrachten Sie die beiden Screenshots unten:


Der erste Screenshot stammt von früher im Tutorial, während der zweite Screenshot im gleichen Schritt aufgenommen wurde, nur mit einem anderen Durchlauf der KI-Umbenennung. Beachten Sie im Dekompilierer-Fenster, wie im oberen Screenshot der Passwort-Eingabepuffer mit &pass_input_buffer bezeichnet ist, während im unteren Screenshot derselbe Puffer mit &username_buffer_1 bezeichnet ist. Im unteren Screenshot lügt uns die KI an, und wir nennen diese Lügen „Halluzinationen“.
Alle KI wird lügen und halluzinieren. Je kleiner das Modell, desto häufiger die Halluzinantionen. Je größer das Modell, desto mehr Glück haben Sie und desto weniger wird es halluzinieren. Je mehr Sie bezahlen, desto mehr Glück haben Sie. Allerdings ist kein KI-Modell immun gegen Halluzinantionen, und Sie dürfen dies niemals vergessen, während Sie mit ihnen arbeiten.
KI eignet sich am besten für Aufgaben, bei denen Sie die Ausgabe überprüfen können oder bei denen die Ausgabe so risikoarm ist, dass perfekte Genauigkeit nicht erforderlich ist. Im Fall des Reverse Engineerings kann KI uns helfen, schnell einen Blick auf eine unbekannte Binärdatei zu werfen und uns eine Menge mühsamer manueller Arbeit zu ersparen. Aber wie wir mit der schlechten Umbenennung gesehen haben, kann sie uns auch in die falsche Richtung führen. Behandeln Sie KI-generierte Ergebnisse immer mit Vorsicht und überprüfen Sie sie, wenn möglich.
Der beste Weg, KI zu nutzen, besteht darin, ihre Ergebnisse automatisch zu verifizieren. Wenn Sie beispielsweise KI Code schreiben lassen, wäre es großartig, ein Skript zu haben, das die KI-Ausgabe gegen eine von Ihnen handgeschriebene Testsuite testet. Im Fall der KI-generierten Lösung für unser Crackme hätten wir eine Testsuite schreiben können, die jede KI-generierte Lösung automatisch gegen das Crackme testet, und dann eine Lösung akzeptiert, die für Tausende verschiedener Benutzernamen gute Passwörter generiert. Dann könnten wir die KI in einer Schleife laufen lassen, bis sie eine Lösung schreibt, die unsere Tests besteht.
Wir haben dieses Crackme mit einem relativ kleinen Modell (Gemma 4 E4B) gelöst. Dieses Modell hat 8 Milliarden Parameter, im Gegensatz zu den SotA-Modellen, die es gibt die hunderte Milliarden wenn nicht Billionen von Parametern haben. Ein kleineres Modell kann nicht gut logisch folgern, halluziniert häufiger und wird schneller unbrauchbar, wenn sich sein Kontextfenster füllt.
In einer echten Reverse-Engineering-Übung müssen wir möglicherweise Funktionen reverse-engineeren, die größer sind als das Kontextfenster unseres kleinen Modells, oder deren Logik zu verworren ist, als dass seine begrenzte Intelligenz darüber nachdenken könnte. Wir müssen mit den Einschränkungen unserer Modelle vertraut sein, wenn wir sie verwenden, damit wir im Voraus verstehen, wo und wie sie uns im Stich lassen werden.
Da die Hardware für Verbraucher Hand in Hand mit der Modellarchitektur fortschreitet, glaube ich, dass wir in der Lage sein werden, größere und bessere Modelle lokal auszuführen, was noch fortgeschrittenere Reverse-Engineering-Aufgaben für unsere lokalen Modelle erreichbar machen wird.
Das Ghidra-Plugin „Fast AI Renamer“, das ich geschrieben habe, schreibt derzeit eine Zusammenfassung für jede Funktion, die es analysiert, und es hat die Fähigkeit, die C- und ASM-Listen des Projekts, an dem es arbeitet, massenhaft zu exportieren. Ich möchte ein Tool schreiben, das ein Embedding für jede Funktionszusammenfassung berechnet und dann in einer Vektordatenbank oder SQLite indiziert, und dann können wir die Datenbank abfragen, um Funktionen gemäß natürlichsprachlichen Abfragen zu finden.
Das Ghidra-Plugin verwendet derzeit einen einfachen Workflow ohne Multi-Turn-Konversation für die Umbenennung und verlässt sich beim Parsen einer CSV aus der KI-Ausgabe, um seine Aufgabe zu erfüllen. Ich habe die Dinge so implementiert, weil ich nicht glaubte, dass ein kleines KI-Modell in der Lage wäre, die langen Konversationen und mehrfachen Tool-Aufrufe zu bewältigen, die einem agentischen Workflow innewohnen. Vielleicht liege ich falsch, und alternative Methoden des Promptings der KI sollten untersucht werden. Trotzdem denke ich, dass es für kleine lokale Modelle am besten ist, die Kontextfenstergröße so weit wie möglich zu begrenzen.
Ich bin mir nicht sicher, wie anfällig das Ghidra-Plugin für Prompt Injection ist, und Sicherheitsexperten sollten vorsichtig sein, es zur Analyse von Malware oder feindseligen Binärdateien zu verwenden. Im Allgemeinen gilt dies für alle KI-gestützten Reverse-Engineering-Tools, die möglicherweise lokale, nicht authentifizierte MCP-Server haben oder Tools für sensible Funktionen bereitstellen, die Codeausführung erreichen können. Seien Sie vorsichtig.
Angesichts der Tatsache, dass wir ein kleines Allzweckmodell für Reverse Engineering verwenden, frage ich mich, wie sehr seine Leistung verbessert werden kann, indem es auf Konversationen destilliert aus einem größeren und intelligenteren Modell feinabgestimmt wird?
Schließlich funktioniert die C/ASM-Exportfunktionalität des Ghidra-Plugins gut mit Claude Code, und ich war in der Lage, Claude Code + Sonnet zu verwenden, um beim ersten Mal eine korrekte Lösung für dieses Crackme zu schreiben, indem ich ihm einfach die markierte Dekompilierung des Crackmes zuführte und ihm sagte, eine Lösung zu finden. Ich frage mich, ob es Fähigkeiten gibt, die wir für Claude Code schreiben können, die es besser machen, eine Binärdatei durch die Textdateien seiner Dekompilierer-Ausgabe zu analysieren, ohne dass es über einen MCP-Server zu Ghidra gehen muss, um die Analyse durchzuführen. Zum Beispiel könnten wir eine -Fähigkeit für Claude Code einführen, die es ihm erlaubt, die Bytes im virtuellen Speicher der Binärdatei zu inspizieren, sodass es, wenn es sieht, dass auf einige Daten aus den Disassemblierungs-/Dekompilierer-Listen verwiesen wird, sehen kann, was dieser Speicher enthält, ohne durch Ghidra gehen zu müssen.
Dank an Kryptos für die Erstellung des Crackmes und an Crackmes.one für das Hosting. Ich schätze, dass sie dort weitergemacht haben, wo crackmes.de aufgehört hat (RIP).
Dank an Google für die Veröffentlichung eines erstaunlichen und praktischen lokalen Modells.
Dank an die NSA für die Verwendung meiner Steuergelder zur Entwicklung eines coolen Reverse-Engineering-Tools, und Dank an Ryan Kurtz für alle Unterstützung, die er auf dem Ghidra-Github bietet.