Remote Function Calling

  • Aufrufen einer Funktion eines anderen Prozesses
    Remote Function Calling - Aufrufen einer Funktion eines anderen Prozesses

    Inhaltsverzeichnis

    1. Vorwort
    2. Finden der gesuchten Funktion mithilfe von OllyDbg
    3. Implementierung in eine Programmiersprache (hier C/C++)
    4. Schlusswort


    1. Vorwort

    Dieses Tutorial soll dazu dienen, euch das Aufrufen einer Funktion eines fremden Prozesses aus eurem eigenen Programm näherzubringen.
    Das Ziel in diesem Fall wird das Finden und Ausführen der "Gewinnen"-Funktion in Minesweeper (.exe liegt im Anhang) sein.
    Die hier verwendete Methodik ist logischerweise auf andere Spiele anwendbar, allerdings kann das Finden der Funktion und die letztendliche Implementierung im eigenen Programm
    deutlich schwerer ausfallen.

    Obwohl ich versuche den Reverse Engineering (RE) Part recht ausführlich zu erklären, sollten trotzdem Grundkenntnisse in RE und Memory Manipulation sowie der Windows API mitgebracht werden (folglich sollte OllyDbg sowie eine IDE eurer Programmiersprache auf eurem System installiert sein).
    Ungeachtet der Tatsache, dass ich eine Implementierung in C/C++ vornehmen werde, ist die Familiarität mit einer Programmiersprache, die die Windows API unterstützt, essentiell, wenn ihr es auf andere Programmiersprachen ausweiten wollt (C#, AutoIt etc).



    2. Finden der gesuchten Funktion mithilfe von OllyDbg

    Direkt am Anfang des Tutorials beginnen wir auch schon mit der schwersten und kompliziertesten Aufgabe: Das Finden der Funktion, die wir aufrufen möchten.
    Abhängig vom Programm, welches man analysiert, kann dies ein langer oder kurzer Prozess sein. Laut Erfahrung dauert es leider oft ziemlich lang im Vergleich zur eigentlichen Implementierung in einer Programmiersprache.

    In diesem Beispiel wollen wir die Funktion finden, die aufgerufen wird, wenn wir ein Minesweeper Spiel gewonnen haben und unseren Highscore eintragen können.
    Zu Allererst macht man sich Gedanken, welche Windows API Funktionen verwendet werden, da dies oft ein guter Weg ist, um nahe an die Funktion heranzukommen.

    Beispiel: Es öffnet sich eine Messagebox, wenn der Loginvorgang beginnt. Über einen Breakpoint auf MessageBoxA oder MessageBoxW, ist es nun möglich, den Suchbereich im Speicher des Programmes drastisch einzugrenzen, da wir wissen, dass unsere Funktion bald aufgerufen werden muss. Wir müssen lediglich nach ihrer Charakteristika Ausschau halten (z.B. beim Login das Senden des Paketes zum Server).

    Im Falle von Minesweeper öffnet sich ein Dialog beim Gewinnen des Spiels, welcher wie folgt aussieht:



    Diese Information können wir verwenden, um den Suchbereich, wie oben beschrieben, einzugrenzen.
    In diesem Fall setz ich einen Breakpoint auf jeden Call von "EndDialog" und klicke auf OK, um den Breakpoint auszulösen (das Wissen fürs Finden/Auflisten von verwendeten Windows API Funktionen im Hauptmodul setz ich voraus).

    Wir landen hier:



    Es fällt am Stack auf, dass wir in der Funktion NICHT unmittelbar aus dem Hauptmodul winmine.exe gelandet sind, sondern aus der USER32.dll. Dies ist insofern unglücklich, da wir so nicht direkt zum CALL springen können.

    Erinnerung: Bei einem CALL wird die Adresse der Instruction direkt NACH dem CALL auf den Stack gepusht, damit bei einem RET oder RETN dorthin zurückgekehrt wird. Dies kann man sich zunutze machen, um schnell den CALL der Funktion zu finden, da oftmals dieselbe Funktion von mehreren Stellen im Programmcode aufgerufen wird. Somit sparen wir uns das durchsteppen bis zum RET bzw. RETN und kommen direkt zum CALL im Hauptmodul.


    Glücklicherweise liefert uns OllyDbg die Möglichkeit Referenzen zu Funktion o.ä. zu finden. Dazu müssen wir lediglich an deren Anfang gehen und Ctrl + R bzw. Rechtsklick -> Find references to -> Selected Command drücken/klicken.


    Diese Funktionalität machen wir uns nun zunutze, um zur Wurzel der Funktion zu kommen, bzw. in die Funktion zu springen, wo überprüft wird, ob wir das Spiel gewonnen haben.

    | |

    In den oberen Bildern bin ich einfach jede Funktion bis zum Anfang hochgegangen und hab ihre Referenzen aufgelistet und auf geprüft, ob sie die Funktion ist die wir suchen (Prüfung, ob wir gewonnen oder verloren haben).
    Es sei zu erwähnen, dass ich in diesem Fall nach "SetTimer" Ausschau halte, da die Zeit startet, sobald wir ein Feld angeklickt haben. Die Überprüfung, ob wir gewonnen haben, sowie die Überprüfung der Felder muss folglich in derselben Funktion bzw. nahe beieinander liegen, außerdem muss es einen konditionalen Jump unmittelbar vor dem CALL geben.

    In dem letzten Bild ist zu erkennen, dass wir an eine Funktion gestoßen sind, zu der mehrere Referenzen existieren. In diesem Falle könnte man jeden einzelnen CALL selbst untersuchen, oder aber die einfachere und langsamere Methode mit Breakpoints verwenden.
    Dazu setzen wir nun einen Breakpoint auf jeden der CALLs und gewinnen das Spiel erneut und schauen, welcher ausgelöst wird.

    Sobald wir das Spiel erneut gewonnen haben, löst einer der Breakpoints, wie gewollt, aus.
    Wir finden zwar hier bereits einen konditionalen Jump vor dem Breakpoint:



    allerdings schau ich mir trotzdem noch an, ob es nur eine Referenz zu dieser Funktion gibt.

    Dies ist hier sogar der Fall:




    Sofort fällt auf, dass wir direkt einen konditionalen Jump vor unserem CALL haben sowie die "SetTimer" Funktion, die wir bereits zuvor angenommen hatten!





    Es ist also sehr wahrscheinlich, dass der CALL auf dem der Breakpoint zuletzt ausgelöst ist, unser Aufruf der Funktion ist, die wir suchen.



    Das

    Quellcode

    1. 010035A9 |. 6A 01 PUSH 1


    könnte auf einen boolschen Wert als Parameter hindeuten, welches unsere Vermutung nochmals unterstützt.

    Erinnerung: Werte die VOR einem CALL auf den Stack gepusht werden, sind (nicht immer, aber meistens) Argumente der Funktion, die durch den CALL aufgerufen wird. Dies ist zwar nicht bei allen Calling Conventions zwangsläufig der fall (__fastcall), aber oftmals liegt man richtig, wenn man einfach annimmt, dass es Argumente sind.


    An dieser Stelle könnten wir jetzt die Funktion mit dem "SetTimer" Aufruf auseinandernehmen (wenn man sich die Funktion mit dem "SetTimer" anschaut, wird man feststellen, dass dies die Routine ist, wo geprüft wird, ob man auf eine Mine geklickt hat oder nicht. Ich werde denke ich mal ein weiteres Tutorial erstellen, um eine Art "Wallhack" zu erstellen mit dem wir dann sehen können, auf welchen Feldern Minen sind und welche nicht. Dort werde ich dann näher auf die Funktion eingehen) und reversen, oder aber einfach unsere Vermutung testen, dass dies der richtige CALL ist:

    Quellcode

    1. 010035AB |> \E8 CCFEFFFF CALL winmine.0100347C




    3. Implementierung in eine Programmiersprache (hier C/C++)

    Da ich dieses Tutorial "Remote Function Calling" werde ich hier ausschließlich auf eine Methode eingehen, die das Aufrufen der gewünschten Funktion von außerhalb des Fremdprozesses bewerkstelligt.
    Die Windows API Funktion, die letztenendes für das Ausführen zuständig ist, nennt sich "CreateRemoteThread". Es sei erwähnt, dass viele Hackshields diese Funktion überwachen, um mögliche DLL Injektionen oder Funktionsaufrufe abzufangen.
    Es gibt zwar noch undokumentierte API Funktionen, die dasselbe bewerkstelligen und sicherer sind, auf die ich hier jedoch nicht weiter eingehen werde.

    Um letztendlich unsere gewollte Funktion im Zielprozess auszuführen, könnten wir zwar direkt CreateRemoteThread mit der Funktionsadresse callen, allerdings funktioniert dies nicht immer.
    Stattdessen schreiben wir einen sogenannten Shellcode. Dieser ist Assemblercode, der in den Zielprozess injiziert und dann ausgeführt wird. Dieser wird dann letztendlich unsere Zielfunktion ausführen.

    Der Code dafür sieht im Beispiel von C so aus:

    C-Quellcode

    1. #include <Windows.h>
    2. int main() {
    3. BYTE buf[] = { 0x6A, 0x01, // PUSH 1
    4. 0xE8, 0x90, 0x90, 0x90, 0x90, // CALL Function
    5. 0xE9, 0x90, 0x90, 0x90, 0x90 }; // JMP Return
    6. DWORD dwFunction = 0x0100347C, // Zielfunktion
    7. dwReturn = 0x010038B6, // Passende Rücksprungadresse
    8. dwPId, // Prozess ID
    9. dwWritten; // benötigt für WriteProzessMemory
    10. HWND hWnd = FindWindowA(NULL, "MineSweeper"); // wird für die PId benötigt
    11. if (hWnd) {
    12. GetWindowThreadProcessId(hWnd, &dwPId); // PId vom Prozess erhalten
    13. HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPId); // Prozess öffnen
    14. if (hProcess) {
    15. LPVOID lpMemory = VirtualAllocEx(hProcess, NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE); // Speicher für unseren Shellcode reservieren
    16. *(DWORD*)&buf[3] = (dwFunction - (DWORD)(lpMemory) - sizeof(buf) + 5); // Buffer mit passender Sprungweite füllen
    17. *(DWORD*)&buf[8] = (dwReturn - (DWORD)(lpMemory) - sizeof(buf)); // Buffer mit passender Sprungweite füllen
    18. WriteProcessMemory(hProcess, lpMemory, buf, sizeof(buf), &dwWritten); // Alles in den Zielprozess schreiben...
    19. HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)lpMemory, NULL, NULL, NULL); // ... und ausführen
    20. WaitForSingleObject(hThread, INFINITE); // Warten bis unser CALL zuende ist
    21. VirtualFreeEx(hProcess, lpMemory, 0, MEM_RELEASE); // Reservierten Speicher freigeben
    22. CloseHandle(hThread); // Handle schließen
    23. CloseHandle(hProcess); // Handle schließen
    24. }
    25. }
    26. return 0;
    27. }


    Das Schöne an einem Shellcode ist, dass man einfach alles notwendige vorm eigentlichen CALL unserer Zielfunktion im Prozess übernehmen kann und man sich keine Gedanken über Parameter, Calling Convention und Rückgabedatentyp machen brauch.

    Die Hexadezimalwerte, die ihr in dem Array seht, sind die Opcodes unserer Assemblerinstruktionen. So steht ein 0x6A01 für ein PUSH 1. Dies könnt ihr in den oberen Bildern neben den Instruktionen erkennen.
    Die zweimal vier 0x90 in dem Array sind Platzhalter, die später durch die Sprungweiten für unsere Zielfunktion und Rücksprungadresse ersetzt werden.
    Ich nehme gerne 0x90, da dies der Opcode für ein NOP (No Operation) ist. Manchmal hab ich so mein Zielprogramm nicht aus Versehen gecrasht, weil ich einen Fehler gemacht habe.

    Da wir in unserem Falle nicht auf den Stack des Zielrprogrammes achten, ist es notwendig, dass wir an die richtige Stelle im Programmablauf, nach unserem Shellcode, zurückspringen, damit die Applikation nicht crasht.
    Wie auf dem Bild 9 zu erkennen ist, will das Programm nach unserer Funktion verschiedene Registerwerte wiederherstellen, die wir allerdings nicht wissen. Deshalb ist es wichtig als Rücksprungadresse die Adresse von der Instruktion NACH dem CALL zur Subroutine, die dann unsere Funktion callt, zu nehmen. Die hier verwendete Adresse ist auf dem Bild 8 zu erkennen.

    Der Ablauf ist normalerweise folgender (stark vereinfacht dargestellt):
    • 010038B1 CALL 01003512 (Aufruf einer Subroutine, die dann unsere Ziel- bzw. die Gewinnfunktion aufruft)
      • Werte aus Register auf den Stack sichern
      • Prüfen ob gewonnen
      • Ja: 010035AB CALL 0100347C (0100347C ist die Adresse unserer Funktion)
        • Gewinnfunktion ausführen
      • Nein: Aufrufen der Gewinnfunktion überspringen
      • Werte in die Register vom Stack wiederherstellen
      • Zum CALL dieser Subroutine zurückspringen
    • 010038B6 hier ist nun die Instruktion nach dem Call der Subroutine

    Es fällt auf, dass es erst ganz am Ende keinen Versuch gab, Werte von einem Punkt am Anfang der Funktion wiederherzustellen. Deshalb ist dies unsere Rücksprungadresse.
    Mit diesem Wissen und der Information, wo unser Speicher im Zielprozess reserviert wurde, können wir nun unsere Sprungweiten berechnen.

    Erinnerung: Bei einem CALL oder einem JMP wird nicht direkt die Adresse als Opcode angegeben, sondern lediglich die Sprungweite zur Zieladresse. Folglich wird die Sprungweite so berechnet:
    Sprungweite = Zielfunktion - CALL Adresse - 5
    Zudem ist die Sprungweite immer ein 4 Byte Signed Integer (bei x86 Anwendungen) und wird immer im Little Endian Format gespeichert. Die -5 sind zu beachten, da ein CALL 5 Byte groß ist und wir deshalb lediglich vom Ende des CALLs zum Ziel springen möchten.


    Diese Sprungweiten werden dann in den Shellcode eingesetzt und der gesamte Buffer wird via WriteProcessMemory und CreateRemoteThread in den Zielprozess geschrieben und ausgeführt.
    Zum Schluss warten wir nur noch, bis unser Thread ausgelaufen ist und geben alle Handles und Speicher frei.

    Um nun unseren fertigen Code testen zu können, starten wir die Anwendung einfach wie gewohnt in OllyDbg, setzen einen Breakpoint auf unsere Zielfunktion und lassen uns bei unserem Programmcode "lpMemory", also die Adresse des reservierten Speichers ausgeben. Falls das Zielprogramm nun crasht, können wir zur Adresse springen, wo wir den Shellcode hingeschrieben haben und diesen auf Integrität (also auf Fehler) überprüfen.

    In Olly sollte in unserem Fall also sowas wie

    C-Quellcode

    1. PUSH 1
    2. CALL winmine.0100347C
    3. JMP winmine.010038B6


    stehen. Wenn dies nicht der Fall ist, dann überprüfen wir die Opcodes daneben und schauen, ob alles richtig berechnet/in den Prozess geschrieben wurde und bügeln dementsprechend Fehler aus.
    Wird allerdings unser zuvor gesetzter Breakpoint ausgelöst, so ist dies die Bestätigung, dass unsere Zielfunktion erfolgreich durch den Shellcode aufgerufen wurde. Es kann allerdings trotzdem sein, dass das Programm crasht, deshalb ist es wichtig die Register genau im Blick zu behalten, ob nicht irgendwo eine Access Violation passiert. Ist dies der Fall, dann haben wir Werte vor dem CALL der Zielfunktion übersehen, die wir zwingend unserem Shellcode hinzufügen müssen.



    4. Schlusswort

    So das war dann wohl mein erstes Tutorial hier.
    Ausgehend davon, wie der Anklang zu diesem Tutorial ist, werde ich mir Gedanken machen, ob ich nicht öfter eines schreibe. Hier habe ich ja bereits eine Andeutung gemacht, dass ich unter Umständen ein Weiteres zu der Funktion mit dem SetTimer erstelle, also quasi ein "Wallhack" für Minesweeper. Dies lass ich mir jedoch erst noch durch den Kopf gehen.
    Kritik an diesem Tutorial ist gern gesehen, solange es konstruktiv und angemessen bleibt.
    Ich hoffe ich konnte euch diese ganze Thematik etwas näher bringen. Der Schwierigkeitsgrad dürfte sich so zwischen Anfänger und Fortgeschritten bewegen, was denke ich angemessen sein sollte.
    Bei Fragen bitte die Kommentarfunktion unten benutzen, ich beantworte ungerne separat fragen via PN.


    Mit freundlichen Grüßen
    - Peng
    Dateien
    • winmine.exe

      (119,81 kB, 6 mal heruntergeladen, zuletzt: )

    10.652 mal gelesen

Kommentare 2