Inhaltsverzeichnis

Nachforschungen in der Engine

Ikarus und LeGo haben die Türen geöffnet für allerhand Änderungen an der Spielmechanik. Ein großer Teil davon ist es, vom Zugriff auf Elemente der Spielengine Gebrauch zu machen. Mit Ikarus lassen sich Enginefunktionen aufrufen, mit LeGo lassen sie sich hooken. Doch werden die Vorstellungen etwas präziser, reicht vielleicht nicht die Liste von Enginefunktionen, sondern es wird ein Blick in die Engine notwendig.

Dieser Eintrag soll dabei ein Wegweiser sein; von generellen Erklärungen zu einem detaillierten Schritt-für-Schritt-Beispiel über Erforschen einer Adresse zum Hooken der Focus-Bar (angelehnt an das overrideBars-Skript).

Ansätze

Es gibt Programme, die es einem ermöglichen in die kompilierte Exe-Datei hineinzuschauen. Hier sind vier verschieden Ansätze bei denen sie uns helfen können.

  1. Eine passende Adresse für einen Hook finden
  2. Die Adresse einer Enginefunktion herausfinden, um sie aus Daedalus aufzurufen
  3. Nachschauen, was eine Enginefunktion genau macht
  4. Code in der Engine überschreiben (Stichwort MemoryProtectionOverride)

Für Punkt 2 brauchen wir gar nicht in die Engine hineinschauen, denn dafür wurde bereits eine Liste von allen Enginefunktionen zusammengestellt. Solange wir also wissen, wie unsere gewünschte Enginefunktion heißt sind wir damit gut bedient. Für Hooks (Punkt 1) gilt das selbe - fast: Denn wir können mit Hilfe der Liste jegliche Enginefunktion nur zu ihrem Beginn hooken. In den meisten Fällen ist das genau das richtige, insbesondere wenn wir auf die übergebenen Argumente scharf sind, in anderen Fällen wollen wir etwas innerhalb der Funktion hooken.

Vorbereitung

Zu erst brauchen wir IDA („Interactive Disassembler“). Natürlich gibt es viele Alternativen, aber es gibt stets eine kostenlose vollwertige Version von IDA (seit einem halben Jahr sogar IDA 7.0!) und diese Anleitung ist auf IDA ausgelegt. Hier ist ein Link zur kostenlosen Version auf der offiziellen Webseite. Die Gratisversionen haben alles was wir brauchen. NicoDE hat schon eine Anleitung geschrieben wie man die Analyse in IDA vorbereitet und die EXE in IDA hineinlädt. Nicos Post bezieht sich auf Version 5. Wenn einige Schritte in seiner Anleitung nicht möglich sind (aufgrund von anderer Version), kann man sie bedenkenlos überspringen, es sollte trotzdem alles funktionieren.

Des Weiteren sind noch einige hilfreiche Einstellungen vorzunehmen:

  1. OptionsGeneral, im Disassembly Tab:
    • Alle Checkboxen bei Display disassembly line parts aktivieren
    • Number of opcode bytes (non-graph) auf 8 setzen
  2. Alle Views (Unterfenster) schließen außer IDA View-A
  3. Falls im IDA View-A ein Graph mit Kästen und Linien angezeigt wird, mit Leertaste in Non-Graph-View wechseln.
  4. Functions-Fenster schließen und stattdessen unter ViewOpen subviews die Names- und Strings-Fenter öffnen.

Anschließend die Datenbank speichern.

IDA Interface

Wir sollten nun das IDA View (mit vier Spalten von Textfragmenten), ein Names-Fenster, ein Strings-Fenster und unten das Output Window sehen.

Das ist grundsätzlich alles was wir brauchen.

 IDA Fenster

IDA View (Disassembly)

Hier sehen wir die gesamte EXE von Anfang bis Ende. Die Zeilen werden von oben nach unten vom Prozessor abgearbeitet (solange nicht ein Jump eine neue Position diktiert; der „Entrypoint“ ist allerdings nicht am Anfang).

Ein wertvoller Tipp ist, dass man mit G zu bestimmten Adressen springen kann.

Jede Zeile enthält eine Instruktion. Die vier Spalten von links nach rechts sind:

 Zeile aus IDA

Adresse

Der rechte Teil ist die Adresse im Speicher als Hex-Zahl, die man in Daedalus-Skripten in Hooks oder Calls sieht. Der linke Teil sagt uns, was für eine Art von Informationen an dieser Adresse steht. .text steht für ausführbare Instruktionen.

Stackoffset

Das ist eine Hex-Zahl, die mitzählt wie viele Bytes seit dem Beginn einer Funktion auf den Stack gelegt werden. Den Stackoffset an einer bestimmen Adresse zu wissen ist ggf. innerhalb von Hookfunktionen im Zusammenhang mit ESP wichtig. Dazu später mehr.

Opcode

Das ist Maschinencode bestehend aus Hex-Zahlen. Jedes Grüppchen aus zwei Ziffern entspricht einem Byte. Die Anzahl der Grüppchen/Bytes ist die Differenz zwischen der Adresse in dieser Zeile und der nächsten. Und das ist größtenteils auch wofür wir diese Anzeige in den Optionen angeschaltet haben: Um mit schnellem Blick zu sehen, wie lang eine Instruktion an einer bestimmten Adresse ist. Das ist die „Länge“ eines Hooks, die man häufig in Daedalus als zweites Argument von HookEngine-Aufrufen finden (in Ausnahmefällen muss die Länge etwas größer sein). Ansonsten brauchen wir für unsere Zwecke die Bedeutung der einzelnen Bytes erst einmal nicht verstehen. Einen weiteren Gebrauch für uns haben sie, wenn wir mit MemoryProtectionOverride Code in der Engine umschreiben wollen. Für jetzt konzentrieren wir uns aber erst einmal auf die Basics.

Assembly-Instruktion

Hier steht das selbe wie in der vorherigen Spalte, nur in Assembly-Code, welcher bedeutend einfacher zu lesen ist. Eine Instruktion kann man als einen drei-oder-vier-Buchstaben-langen „Befehl“ und ggf. einigen „Argumenten“ verstehen. Anders als den Opcode wollen wir diese Instruktionen verstehen - keine Sorge, viele verschiedene „Befehle“ gibt es nicht und man kann sich mit ein bisschen Halbwissen viel zusammenreimen.

Names-Fenster

Anders als das Function-Fenster, was wir zuvor geschlossen haben, sind hier alle von IDA gefundenen Namen enthalten, also Funktionen (ausführbare Segmente) und Pointer und Objekte (statische Datensegmente). Das ist wörtlich das Fenster und meist der Startpunkt, wenn wir etwas in der Engine suchen.

Zunächst klicken wir irgendwo in das Fenster und drücken STRG+F, um den Quick-Filter zu öffnen. Das ist eine Eingabezeile am unteren Rand mit der wir schnell nach z.B. Funktionen suchen können. Mit einem Doppelklick auf einen Namen gelangen wir im IDA View an die entsprechende Adresse, mit STRG+C im Names-Fenster kopieren wir Name und Adresse. Das ist nützlich um die Adresse direkt in unser Daedalus-Skript einzufügen, anstatt sie im IDA View erst auszuwählen und zu kopieren.

Strings-Fenster

Fast ebenso wichtig ist die Suche nach Strings (also den Inhalt von Strings). Hier sollten wir auch mit STRG+F den Quick-Filter öffnen. Dieses Fenster ist extrem hilfreich, wenn wir irgendwo im Spiel eine Textausgabe haben und herausfinden wollen, wo in der Engine diese Ausgabe erstellt wird. In den meisten Fällen lässt sich das durch einen Doppelklick auf den entsprechend gesuchten String heraus finden.

Suchen wir aber z.B. nach „Unknown command“ sehen wir dass uns ein Doppelklick auf den gefundenen String nicht an eine Adresse in einer Funktion führt, sondern in einem Datensegment. IDA listet aber freundlicherweise alle Referenzen zu dieser Adresse darunter auf. Falls es zu viele sind, so dass IDA die Liste mit abkürzt, hilft ein Rechtsklick mit List cross references to…. Die Referenzen zeigen direkt den Namen der Funktion, die den String verwendet. Mit erneutem Doppelklick auf eine dieser Referenzen gelangen wir an die entsprechende Adresse.

Die Referenzen werden übrigens genauso in ausführbaren Segmenten angezeigt und sind dort ebenso nützlich, um zwischen aufgerufenen und aufrufenden Funktionen oder Jumps zu springen.

Output Window

Das Output Window enthält gelegentlich interessante Informationen. Je nach IDA Version ist auch ein Python Interpreter enthalten mit dem man z.B. Adressen direkt vom Hex- ins Dezimalformat umwandeln kann. Ansonsten ist das Fenster für uns eher uninteressant.

Assemly-Code

Hier ein paar Worte zu Assembly-Code (speziell unter Intel 80386).

Auch wenn man nicht viel Wissen mitbringen braucht, sollte man sich zumindest etwas über die generelle Funktionsweise schlau machen.

Maschinenstack

Ein Stack ist wie ein Papierstapel: Neues ablegen tut man immer oben drauf, abgearbeitet wird er auch von oben, angefangen mit dem obersten Zettel. Was als letztes dazu kommt, wird auch als erstes wieder weggenommen. Auf den Maschinenstack werden Daten abgelegt um sie zwischenzuspeichern, bis man sie nicht mehr braucht und vom Stack wieder herunter nimmt. Einen Stack bedarf es deshalb, weil es auf dieser Ebene keine Variablen gibt (nur Register). Anstatt 42 in Variable x zu speichern, legen wir 42 auf den Stack. Im Nachfolgenden können wir immer auf die 42 zurückgreifen, weil wir uns die Position im Stack merken.

Achtung: Der Stack in Daedalus hat nichts mit dem Maschinenstack zu tun!

Register

Zu und vom Stack wandern Werte über Register. Darin werden kleine Werte kurzweilig gespeichert, um sie zu manipulieren oder weiterzugeben. Die Register enthalten oder zeigen also auf relevante Daten. Die einzelnen Register haben allerdings unterschiedliche Funktionen. Die drei wichtigsten, die man kennen sollte sind SP (stack pointer), CX (counter) und AX (accumulator). Hier in IDA finden wir sie jeweils mit einem E für „extended“ davor (also ESP, ECX und EAX), weil es sich hier um 32-Bit-Register handelt.

ESP

 Maschinenstack und ESP

ESP zeigt auf den derzeitigen Stackanfang. Den brauchen wir also um an auf dem Stack abgelegte Daten heran zu kommen. Deshalb finden wir in Hookfunktionen in Daedalus-Skripten ESP recht häufig. So kann die Hookfunktion Daten auslesen, die zum Zeitpunkt des Hooks auf dem Stack liegen sind. „Derzeitigen Stackanfang“ deshalb, weil beim Ablegen (push) auf den Stack der Stackpointer um die abgelegte Anzahl an Bytes verringert wird (der Maschinenstack wächst nach unten) und beim Herunternehmen (pop) wieder entsprechend erhöht wird. In IDA finden wir häufig ESP+X. Das X steht dabei für den Stackoffset, den wir auch in IDA in der zweiten Spalte der Zeile einsehen können. Jedes mal wenn etwas auf den Stack gelegt wird (mit der Instruktion push), wird ESP entsprechend verringert und der Stackoffset erhöht. Nach beliebig vielen neuen Werten auf dem Stack können wir später trotzdem immer (solange unserer Wert nicht gepoppt ist) mit Hilfe des Stackoffsets X an die Daten kommen: ESP+X.

ECX

ECX ist zwar ein Zählerregister für Schleifen, ist aber hier aus anderem Grund für uns interessant. Die Calling Conventions, die man evtl. aus C und C++ kennt, sind verschiedene Arten wie eine Funktion aufgerufen wird (z.B. wird der Stack von der Funktion selbst aufgeräumt oder muss das der Aufrufer machen?, usw.). Eine dieser Calling Conventions ist __thiscall. Während normale Argumente immer auf den Stack (siehe ESP) gelegt und so der aufgerufenen Funktion bereitgestellt werden, wird this für eine solche Funktion in ECX abgelegt.

 oCNpc::GetName

Speziell bedeutet das, dass in der Funktion oCNpc::GetName der NPC in Frage in ECX zur Verfügung steht. ECX ist in diesem Falle also ein Zeiger auf ein oCNpc-Objekt. Schauen wir in die Funktion sehen wir, dass an Adresse 0x72F825 auf ecx+124h Bezug genommen wird. 124h ist hexadezimal für 292. In der Klasse oCNpc (dokumentiert in Ikarus) finden wir an Offset 292 die Klassenvariable name. Die Funktion tut also was sie verspricht: Sie holt den Namen eines des NPC. Würden wir oCNpc::GetName hooken (aus welchem irrsinnigen Grund auch immer) könnten wir in Daedalus über ECX an den NPC herankommen.

EAX

EAX enthält nach einem Funktionsaufruf ggf. einen entsprechenden Rückgabewert. Nach einem Aufruf von oCNpc::IsDead (mit einem bestimmten NPC in ECX) würde EAX entweder 0 oder 1 sein. Hookt man eine Funktion an einer passenden Adresse kann man so also ihren Rückgabewert überschreiben. Beim Aufrufen von Enginefunktionen aus Daedalus (Call) ist das allerdings nur bedingt wichtig zu wissen, denn dort holt man sich den Rückgabewert z.B. mittles CALL_RetValAsInt() (siehe Ikarus Dokumentation).

Instruktionen

Hier eine kleine Übersicht über die häufigsten/wichtigsten Assembly Instruktionen und groben Erklärungen.

Assembly Code Erklärung
mov dst, src Kopiere src nach dst, wobei src und dst Register oder Adressen sein können.
push a Lege a auf den Stack und verringere ESP um die entsprechende Anzahl an Bytes.
pop a Hole das oberste Element vom Stack und speichere es in a und erhöhe ESP um die entsprechende Anzahl an Bytes.
sub a, b a = a - b
add a, b a = a + b
call dist Rufe Funktion, die dist Bytes von hier ist auf und kehre anschließend wieder an die jetzige Adresse zurück.
jmp dist Grob: Springe dist bytes.
test a, b Führe einen bestimmten Vergleich zwischen den Operanden a und b aus und speichere das Ergebnis in einem Flag. Google für Details.
cmp a, b
jz dist Konditioneller Jump um dist Bytes, wenn das Zero-Flag gesetzt ist, sprich: eine vorheriger Vergleich ergab 0.
jnz dist Wie jz nur bei leerem Zero-Flag
[a] Solche Klammern um etwas (a) herum lesen dessen Wert aus. Man kann es etwa vergleichen mit MEM_ReadInt/MEM_ReadByte aus Ikarus.

Für Details oder weitere Instruktionen sind simple Googlesuchen sehr hilfreich.

Beispiel: Adresse zum Hooken Finden

Los geht's. Wir wollen die Stelle in der Engine finden, an der die Fokusbar gezeichnet wird und diese hooken, um die Bartextur zu ändern.

Teil I

 Focussuche wie in alten Zeiten

Wo fangen wir an? Ein erster Gedanke ist „Vielleicht passiert das in irgendeiner Funktion die sich mit dem 'Focus' beschäftigt.“. Geben wir also in den Quick-Filter des Names-Fensters Focus ein und schauen in einige der gelisteten Funktionen. Dort können wir ein bisschen im IDA View herumscrollen und nach verdächtigen Funktionsaufrufen Ausschau halten:

Denn ab und zu werden andere Funktionen aus einer Funktion mit der Instruktion call aufgerufen. IDA sucht dazu den Namen der referenzierten Funktion und schreibt ihn in die selbe Zeile. Das ermöglicht es, uns an den Namen der aufgerufenen Funktionen zu orientieren, was die Engine an der Stelle vermutlich so macht.

In den Funktionen hier geht es aber scheinbar einfach viel darum den Focus zu ermitteln und einzugrenzen usw. Das ist für unsere Absichten also eine Sackgasse.

Ein weiterer Gedanke: Es gibt ja das hidePlayerStatus-Skript, das alle On-Screen Informationen, inklusive der Bars ausschalten kann. Darin wird sich der Enginefunktion oCGame::SetShowPlayerStatus bedient, die das kontrolliert. Also suchen wir doch mal im Names-Fenster nach Funktionen mit „PlayerStatus“ im Namen. oCGame::UpdatePlayerStatus hört sich gut an.

Da dies eine __thiscall-Funktion ist, wissen wir, dass (zumindest am Anfang der Funktion) ECX auf das oCGame-Objekt zeigt, in dem die Bars referenziert werden (siehe oCGame Klasse, dokumentiert in Ikarus).

Der Zeiger auf das Objekt wird ein paar Zeilen nach Beginn der Funktion (Adresse 0x6C3163) mit mov esi, ecx in das Register ESI umgespeichert, das behalten wir mal im Hinterkopf: ESI = Zeiger auf oCGame-Objekt. Kurz darauf wird vier mal hintereinander die Funktion zCView::RemoveItem(zCView *) aufgerufen. Dabei wird in ECX (this) immer der Zeiger auf das globale zCView-Objekt screen gespeichert und als Argument (auch ein Zeiger auf ein zCView-Objekt) wird jeweils ESI+etwas (erst in EAX gespeichert und von da aus) auf den Stack gelegt (ESI+8Ch, ESI+90h, ESI+94h und ESI+98h). Wir erinnern uns, dass ESI auf das oCGame-Objekt zeigt. Ein Blick in die dokumentierten Klassen aus Ikarus verrät, dass diese Offsets die vier Bars (hpBar, manaBar, swimBar, focusBar) in der oCGame-Klasse sind! Das scheint also die richtige Stelle; Hier werden alle Bars vom screen-Object entfernt, bevor sie vermutlich gleich darauf aktualisiert neu gezeichnet werden.

Hier eine kleine Randbemerkung: Viele der dokumentierten Klassen in Ikarus enthalten neben den Klassenvariablen in einem Kommentar auch deren Offset (siehe z.B. oCNpc). Unglücklich für dieses Beispiel ist, dass das für die Klasse oCGame leider gerade nicht der Fall ist. Aber man kann diese Offsets abzählen: Erste Klassenvariable beginnend mit 0, Integer sind 4 Bytes groß, Strings 20 Bytes und Arrays ein entsprechendes Vielfaches. Nach Abzählen kommt man für die die Variablen hpBar, manaBar, swimBar und focusBar auf 140, 144, 148 und 152, was den Hex-Zahlen 0x8C, 0x90, 0x94 und 0x98 entspricht.

Wir sind aber nicht an der Stelle interessiert, wo die Focus-Bar entfernt wird, denn das wird sie scheinbar jedes Frame, sondern wo sie neu gezeichnet wird. Das sollte nämlich nur der Fall sein, wenn der Spieler einen NPC im Fokus hat.
Da [esi+98h] die Focus-Bar referenziert (oCGame* + 0x98 = oCGame.focusBar), markieren wir esi+98h an Adresse 0x6C31AF. Dank der Highlightingfunktion wird jedes weitere esi+98h auch gelb hervorgehoben. Nach vorsichtigem Herunterscrollen, entdecken wir an Adresse 0x6C3707 das esi+98h wieder: Hier wird etwas mit der Focus-Bar gemacht!

Weitere kleine Bemerkung: Das Funktioniert nicht immer, weil die Inhalte der Register manchmal in andere Register verschoben werden. Bei keinem Fund, hätten wir nur +98h markiert (ohne „esi“) und gesucht.

Ein paar Zeilen darunter die Bestätigung: ein Aufruf von zCView::InsertItem(zCView *,int). Hier wird die Focus-Bar wieder eingefügt. Wir haben unsere Stelle gefunden. Jetzt wollen wir dort eine optimale Adresse zum Hooken finden, von der aus wir auch an den Focus-NPC heran kommen.

Einige Zeilen zuvor sehen wir Aufrufe zu oCNpc::GetAttribute(int) (an Adressen 0x6C36C3 und 0x6C36D2). Hier werden die HP-Werte des NPC für die Focus-Bar zusammengesucht. Weil dies eine __thiscall-Funktion ist, wird kurz vor Aufruf ein Zeiger auf den NPC in Frage in ECX abgelegt. Das wird erreicht mit mov ecx, edi (an Adresse 0x6C36CC), d.h. auch EDI enthält den Zeiger auf den NPC. Da EDI bis zum Hinzufügen der Focus-Bar nicht mehr verändert wird, können wir (vorsichtig) davon ausgehen, dass EDI bis dahin noch den Zeiger auf den NPC enthält. Das sollten wir später überprüfen.

Hooks

Für eine geeignete Adresse zum Hooken müssen wir folgendes beachten:

  1. Die Instruktion an der Adresse, darf kein call oder jegliche Art von Jump sein (jmp, jz, jnz, jge, …), da sich sonst relative Adressen verschieben.
  2. Der Hook sollte nicht zwischen Vergleichsinstruktionen (test, cmp, …) und darauf folgenden konditionalen Jumps (jz, jnz, jge, …) liegen, denn die Vergleiche setzen Flags, die durch den Hook evtl. verändert werden.
  3. Für einen Hook muss die Instruktion an der Adresse muss mindestens 5 Bytes lang sein. Wenn sie zu kurz ist, kann man die Anzahl der Bytes der darauf folgende(n) Instruktion(en) mit einbeziehen. Wichtig ist, dass man mit dem Hook keine Instruktion zerschneidet.
  4. Man darf keine darauf folgend(n) Instruktion(en) über Code-Blöcke hinweg mit einbeziehen, d.h. es darf kein loc_xxxxxx zwischen den Instruktionen stehen.
  5. Der entsprechende Daedalus-Code wird vor der Instruktion an der Hookadresse ausgeführt.

Teil II

Als eine geeignete Adresse bietet sich hier 0x6C370D an. Die Instruktion hier enthält nichts kritisches und ist sechs Bytes lang. Kurz davor wird ein Zeiger auf die Focus-Bar in EDX abgelegt, das könnte für uns auch nützlich sein.

Nun brauchen wir die Adresse noch als Dezimalzahl (umrechnen können wir das z.B. einfach mit dem Windows-Taschenrechner, manche Text-Editoren haben auch Plugins für solche Umrechnungen) und initialisieren den Hook in der Init_Global (oder wo wir möchten) mit:

const int oCGame__UpdatePlayerStatus_focusbar = 7091981; //0x6C370D
HookEngineF(oCGame__UpdatePlayerStatus_focusbar, 6, _updateFocusBar);

Wichtig ist hier, dass wir den „Umweg“ mit der Konstanten plus Kommentar mit Hex-Zahl machen, denn wenn wir 7091981 direkt an HookEngineF übergeben, kann diese „Magic Number“ keiner mehr nachvollziehen und z.B. auch ein möglicher Gothic 1 Port wird um einiges aufwendiger.

Für unsere Hookfunktion haben wir uns zwei Sachen gemerkt: Zum Zeitpunkt des Hooks ist ein Zeiger auf den NPC in EDI und ein Zeiger auf die Focus-Bar in EDX. Alle Register werden von LeGo beim Hooken in Form von gleichnamigen Daedalus-Variablen bereitgestellt (und Vorsicht, sie sind auch modifizierbar!). Bevor wir aber jetzt alles finalisieren, sollten wir überprüfen, ob unsere Nachforschungen stimmen:

func void _updateFocusBar() {
    // Das ganze bitte nur einmal
    const int once = 0;
    if (once) {
        return;
    };
    once = 1;
 
    MEM_InfoBox("Betrete Hook");
 
    var oCNpc npc; npc = _^(EDI);
    MEM_InfoBox(ConcatStrings("NPC Name: ", npc.name));
 
    var oCViewStatusBar bar; bar = _^(EDX);
    MEM_InfoBox(ConcatStrings("Focus-Bar Textur: ", ViewPtr_GetTexture(bar.value_bar)));
};

Wenn wir beim Spielstart einen NPC fokussieren und beide Infoboxen mit vernünftigen Ausgaben erhalten, scheinen wir alles richtig gemacht zu haben. Wenn nicht oder wenn es zu einem Absturz kommt, was wir bei solchen Prozessen sehr häufig in Kauf nehmen müssen, können wir mit den InfoBoxen eingrenzen, woran es liegt:

  1. Absturz ohne jegliche Infobox: Die Hookadresse ist falsch.
  2. Absturz nach „Betrete Hook“: EDI enthält keinen Zeiger auf eine gülte Adresse.
  3. Absturz nach „NPC Name: XYZ“: EDX enthält keinen Zeiger auf ein gültes oCViewStatusBar-Objekt.
  4. Absturz nach allen Infoboxen: Die Hooklänge (2. Argument in HookEngineF) ist inkorrekt.

Aber: Es klappt!

Nun können wir in der Daedalus-Funktion überprüfen, ob der NPC vergiftet ist oder nicht und entsprechend die Textur ändern.

Für eine ausgereiftere Version des Skriptes, siehe overrideBars.

Hinweise