Pointerscan Algorithmus

  • Wie funktioniert der Pointerscan von Cheat Engine?
    Hi ho,
    ich habe mich immer gefragt, wie Cheat Engine den Pointerscan realisiert hat.
    Als ich vor ca. einem Jahr eine einfache Memorysuche in C# entwickelt habe, kam mir dann eine Idee.

    Das einzige, was man dafür braucht, ist ein "between value X and Y"-Scan und viel Rechenpower.

    Ich wollte den Gedankengang unbedingt mal aufschreiben.
    Bin jetzt ne Woche im Urlaub, ich werde Fehler nach der Woche berichtigen.

    Zunächst muss man verstehen, warum es überhaupt soetwas wie multilevel Pointer gibt.
    Angenommen wir haben folgende Klassen:

    C-Quellcode

    1. struct Player{
    2. char bar;
    3. int team;
    4. int health;
    5. //weitere Eigenschaften
    6. }
    7. struct Client{
    8. int foo;
    9. Player *playerpointer;
    10. //weitere Eigenschaften
    11. }

    und folgenden Code:

    C-Quellcode

    1. Client gameclient;
    2. main(){
    3. gameclient.playerpointer = new Player();
    4. gameclient.playerpointer->health = 1337;
    5. //hier ist der Rest vom Spiel programmiert
    6. }

    Wir haben einen Spielclienten, der beinhaltet einen Player, der Player hat dann noch Eigenschaften: Leben,Team, Name...
    Hier sieht man bereits, wie so ein multilevel Pointer zustande kommt.
    Der gameclient verweist auf das playerobjekt welches health beinhaltet:

    C-Quellcode

    1. int* healthPtr = &(gameclient.playerpointer->health)

    Die Adresse von health ändert sich nach dem Neustart des Spiels, das liegt an Zeile 3 der Main.
    Warum?
    Ganz einfach. Bei jedem Neustart, wird das Player Objekt neu auf dem Heap platziert. Die Adresse, an der das Playerobjekt liegt ändert sich und somit auch die Adresse von health
    (nennt man dynamic allocated memory).

    Ein multilevel Pointer muss her.
    Wir starten das Spiel und scannen mit Cheat Engine nach unserem Leben (1337):

    Quellcode

    1. 0x080015 = 1337

    Klasse, genau ein Treffer ;) Leider finden wir keinen Zeiger, der direkt auf 0x080005 zeigt. Wir vermuten aber, dass es einen Zeiger gibt, der in die Nähe von 0x080005 zeigt.
    Warum vermuten wir das ? Ganz einfach, health gehört zu einem Objekt. Irgendetwas muss auf dieses Objekt verweisen. Wenn es keinen Verweis gibt, ist das Objekt tot.
    Deshalb scannen wir nicht nach "exact value", sondern nach "value between 0x080000 and 0x080015"
    Wir erhalten 3 Treffer:

    Quellcode

    1. 0x011111 = 0x080010
    2. 0x012115 = 0x080003
    3. 0x013113 = 0x080011

    Hey, das ist unser erstes Level, aus der Differenz der Treffer und der gesuchten Adresse, können wir unsere ersten Offsets berechnen:
    1. Pointer: MemRead(0x011111) + 0x5
    2. Pointer: MemRead(0x012115) + 0x12
    3. Pointer: MemRead(0x013113) + 0x04

    1. Offset = 0x080015 - 0x080010 = 0x05
    2. Offset = 0x080015 - 0x080003 = 0x12
    3. Offset = 0x080015 - 0x080011 = 0x04

    Leider ändern sich die Werte von den 3 Pointern nach dem Neustart des Programms, deshalb müssen wir jetzt den Prozess für jeden gefundenen Pointer wiederholen:
    1. "value between 0x011011 and 0x011111" -> 3 Treffer
    2. "value between 0x012015 and 0x012115" -> 2 Treffer
    3. "value between 0x013013 and 0x013113" -> 1 Treffer

    Aus den Treffern lassen sich dann erneut Offsets berechnen.
    Angenommen die Offsets sind:
    1. 0x1337, 0x1338, 0x1339
    2. 0x7331, 0x123
    3. 0xabc

    Scannt man weiter, baut sich eine Baumstruktur auf mit Offsets auf:

    Der Baum wächst sehr schnell, deshalb sollte man nur ein paar Level durchlaufen.

    Doch woran erkennt man jetzt, dass wir einen kompletten Zeiger haben ?
    Das erkennen wir an den Adressen, die wir finden.
    Der Speicher von Win32 Anwendungen ist in mehrere Bereiche eingeteilt. In der Regel kommt man irgendwann auf Adressen, die im Textbereich des Programms liegen, d.h. in dem Bereich, in dem der Quellcode liegt.
    Das tolle an diesem Textbereich ist, dass er sich nur verändert, wenn das Spiel umprogrammiert wird.

    Wenn man diese Adresse erreicht hat, kann man einfach den Namen des Moduls auslesen, zu dem der Quellcode gehört (z.B: "Game.exe" oder "Client.dll").
    Die Differenz der Startadresse des Moduls und der gefundenen Adresse ist dann das finale Offset:
    MemRead( ...MemRead(MemRead((MemRead((GetModuleBaseAddress("Game.exe") + offsetN)) + offsetN-1) + offsetN-2) + offsetN-3)... + offset1)

    Jedes Blatt am Baumende besteht jetzt aus einem Modulnamen, in dem die feste Startadresse liegt. Jeder folgende Vaterknoten besitzt ein Offset.
    Jedes Blatt steht für einen gefundenen multilevel Pointer.
    Geht man den Baum vom Blatt bis zur Wurzel, kommt man bei der gesuchten Adresse an.

    Stonstiges:
    In dem Beispiel wurde als Different der "between value" Werte 0x15 verwendet:
    Deshalb scannen wir nicht nach "exact value", sondern nach "value between 0x080000 and 0x080015"

    Man sollte einen größeren Bereich wählen. Als Differenz nimmt man den maximalen Wert, den ein Offset haben darf, also die maximale Distanz, die ein Wert von der Referenz entfernt sein darf.
    Je größer dieser Wert ist, desto mehr Ergebnisse wird der Pointerscan liefern.
    Je kleiner der Wert ist, desto unwahrscheinlicher ist es, eine Adresse als Referenz zu finden.
    Beispiel,
    angenommen wir wählen 0x15 als maximales Offset, dann würden wir in dieser Klasse das Leben finden können:

    C-Quellcode

    1. struct Player{
    2. char *name; // <- Offset 0
    3. int team; // <- Offset 4
    4. int health; // <- Offset 8
    5. // weitere Eigenschaften
    6. }

    In folgender Klassen würden wir das Leben wahrscheinlich nicht finde, weil das Offset vom Leben mehr als 0x15 Bytes vom Beginn des Playerobjektes entfernt ist:

    C-Quellcode

    1. struct Player{
    2. char *name; // <- Offset 0x0
    3. int team; // <- Offset 0x4
    4. float x; // <- Offset 0x8
    5. float y; // <- Offset 0xC
    6. float z; // <- Offset 0x10
    7. float pitch; // <- Offset 0x14
    8. float yaw; // <- Offset 0x18
    9. float roll; // <- Offset 0x1C
    10. int health; // <- Offset 0x20 größer als 0x15
    11. // weitere Eigenschaften
    12. }

    11.913 mal gelesen