Funktionen einer DLL in einem laufenden Programm umleiten

ChiefWiggum

New member
Howto: Funktionen einer DLL in einem laufenden Programm umleiten
Am Beispiel von OpenClipboard / EmptyClipboard der user32.dll

Da in einem Thread[1] gefragt wurde, wie man das dauerhafte Leeren des Zwischenspeichers(Clipboard)
durch ein gewisses Programm verhindert, habe ich mir gedacht, generell für Detours(Funktionsumleitungen)
ein kleines Tutorial zu schreiben welches nicht an zusätzliche Librarys gebunden ist.

Nun, wie schaffen wir es, die Funktionsaufrufe umzuleiten?
Das geht, indem wir uns eine eigene DLL programmieren, die wir dann in den Zielprozess reinladen.
Unsere DLL wird dann die Aufgabe haben, die ersten Bytes der zu blockierenden Funktionen so umzuschreiben,
dass stattdessen eigene Funktionen der DLL aufgerufen werden.
Also, zeitlicher Ablauf:
1.) DLL wird in das Zielprogramm geladen (injiziert)
2.) DllMain() wird mit reason = DLL_PROCESS_ATTACH aufgerufen, unser Signal zum einnisten.
3.) Zielfunktionen werden mit einem kleinen Assembler-Sprungbefehl überschrieben, sodass ein Aufruf dieser einen indirekten Aufruf der eigenen Funktion bedeutet
Dies kann man sich am besten so vorstellen:

detour.png



Nun beschäftigen wir uns erstmal mit dem Kern des ganzen: Dem Assembler-Jump.
Wenn wir uns beispielsweise in OllyDbg einen langen Jump anschauen, sehen wir in etwa sowas:

jumpc.png


Aha, wir sehen also dieser Sprungbefehl ist in Bytes geschrieben: 0xE9 40FBFFFF
Wenn wir uns noch weitere Jumps ansehen, werden wir feststellen, dass bei einem normalen! (keine Short-Jumps!)
das erste Byte immer identisch ist. 0xE9 "kodiert" damit den normalen JUMP.
(Short Jumps haben nur eine Reichweite von 2^8 Bytes da diese insgesamt nur 2 Bytes lang sind)
Wie kommen wir jetzt auf die restlichen 4 Bytes?
Die Zieladresse des Jumps ist offensichtlich 0x00479537, jedoch stehen als zusätzliche Informationen nur 0x40FBFFFF zur Verfügung.
Die Auflösung des Rätsels bekommen wir wenn wir noch die aktuelle Adresse mit einbeziehen: 0x004799F2.
Jumps sind bei direkten Angaben immer relativ! (Mit einem jmp [ebx] z.b. erreicht man einen Sprung zu einer statischen Adresse)
D.h. wir ziehen von der Zieladresse unsere Adresse ab (sowie 5 Bytes für den Jump-Befehl) und dann wissen wir wie viele Bytes wir springen müssen:
0x00479537
-0x004799F2
-0x00000005
-----------
0xFFFFFB40

Okay, vielleicht fragt ihr euch, wie 0xFFFFFB40 == 0x40FBFFFF sein können. Dies hängt alles mit der Big/Little-Endian-Architektur zusammen. [2]
Demnach steht bei little Endian immer das Byte mit dem kleinsten Wert vorne und beim big Endian das mit dem größten Wert(so wie wir es kennen)
Unsere CPUs sind jedoch auf little Endian angewiesen[3], was bedeutet, dass wir unseren berechneten (byteweise!) Wert umkehren müssen:
big endian: 0x FF FF FB 40
little endian: 0x 40 FB FF FF
Und siehe da, wir haben den Jump-Befehl aufgeschlüsselt!

Der Rest ist relativ einfach, erstmal erstellen wir unsere Funktion, die uns den Assemblercode eines Sprungs von X nach Y generiert:
Code:
/**
calculateJump(src, dst, jump)
Berechnet den Assembler-Code für den Sprung von src nach dst und schreibt den Code in jump.
Jump muss mindestens 5 Bytes groß sein! Sinnvoll wären exakt 5 Bytes(1 Byte JUMP-OP, 4 Byte Zieladresse)
32-Bit only!
**/
void calculateJump(int src, int dst, char* jump){
    int distance = (int)dst - (int)src - 5;//Die 5 Bytes müssen wegen des Jump-Befehls noch abgezogen werden ;)
    //Code für einen Sprung:  E9 FF EE DD CC für 0 -> 0xCCDDEEFF, demnach:
    //Bytes rückwärts schreiben und abhängig von der aktuellen Position!
    jump[0] = 0xE9; //Assembler => JMP
    jump[1] = distance & 0x000000FF;            //Die letzten 2 Bytes kommen als erstes (Siehe Big/Little Endian)
    jump[2] = ((distance & 0x0000FF00) >> 8);   //Die nächsten 2 Bytes als zweites. Da das ganze dann 0xEE00 wäre müssen wir es noch um ein Byte nach rechts verschieben.
    jump[3] = ((distance & 0x00FF0000) >> 16);  //Hierbei wären es schon 0xDD0000, d.h. um zwei Bytes verschieben
    jump[4] = ((distance & 0xFF000000) >> 24);  //Und dann nochmal um drei. Schon sind wir fertig =)
}
Ich hoffe der Code ist jetzt soweit nachvollziehbar, denn jetzt kommt der Part mit dem Einbauen in den Code:
Unser Code ist im Speicher in der Regel als PAGE_EXECUTE_READ gespeichert.
Das bedeutet, dass man nicht einfach ein paar Codestellen bearbeiten kann, wie man will.
Die Funktion VirtualProtect() erlaubt es uns jedoch, diesen Protection-Flag zu bearbeiten,
d.h. unser Ablauf wird jetzt wie folgt sein:
1.) Sprungcode berechnen
2.) Protection-Flag durch PAGE_EXECUTE_READWRITE ersetzen
3.) Sprungcode reinschreiben
4.) Alten Protection-Flag wiederherstellen

Um später ein entfernen unserer DLL möglich zu machen, sichern wir uns die ersten 5 Bytes (die wir durch unseren Sprungcode überschreiben),
da das Programm sonst später versuchen würde, unsere Funktionen aufzurufen, obwohl wir uns schon längst verabschiedet haben. Das wäre ja nicht Sinn der Sache ;)
Also kommt jetzt nochmal was Code:
Erstmal unsere Funktion, die (in diesem Beispiel jetzt) OpenClipboard ersetzen soll:
Code:
BOOL WINAPI _OpenClipboard(HWND hWndNewOwner){ //Ersetzungsfunktion
    return false;//Einfach nur den Fehlerwert zurückggeben ;) Die Originalfunktion wird nicht aufgerufen, d.h. das Programm hat keine Chance das Clipboard zu modifizieren
}

//Sowie der Sicherungsbereich für die 5 Bytes:
unsigned char openclipboard[5];
Der Funktionskopf muss identisch mit der zu ersetzenden Funktion sein.
(Wen es interessiert: Es gibt verschiedene "calling conventions", eg stdcall, cdecl, fastcall etc. Diese Verwenden alle unterschiedliche Methoden um ihre Parameter und Rückgabewerte
zu übergeben. Wenn wir nun z.b. unterschiedliche Calling-Convetions benutzen, heißt das nicht umbedingt was gutes für unser Programm ;)
Genauso wie wenn die Anzahl der Parameter nicht stimmt. Diese werden bei cdecl vor dem Funktionsaufruf auf den Stack gepusht und wenn die Funktion nun auf eine andere Anzahl
an Parameter angewiesen ist, könnte man einen sehr schönen Absturz miterleben^^)

Und nun der Code zum Einbauen des Sprungcodes:
Code:
//OpenClipboard bearbeiten:
    //5 Bytes sichern:
    unsigned char* src = (unsigned char*)OpenClipboard;        //Originale Funktion die umgeleitet werden soll.
    void* dst = (void*)_OpenClipboard;                        //Die Funktion zu der gesprungen wird. Also quasi unsere Ersetzungsfunktion.
    unsigned char jump[5];                                    //Unser Assembly-JUMP-Code. 5 Bytes, Erklärung siehe calculateJump()
    DWORD oldProtect = 0;                                    //Um den alten Protection-Flag zu sichern
    DWORD dummy = 0;                                        //Um den "alten" neuen Protection-Flag zu erhalten, da der oldProtect-Parameter nicht optional ist

    //Erstmal sichern wir die ersten 5 Bytes um sie später eventuell zurückzuschreiben, wenn unsere DLL entfernt wird:
    for(int i = 0; i < 5; i++)
        openclipboard[i] = *(src+i);
    
    //Nun berechnen wir den Assembler-Sprungcode:
    calculateJump((int)src, (int)dst, (char*)&jump);

    //Um unseren Sprungcode einzufügen, benötigen wir erstmal Schreibrechte auf den Speicherbereich:
    if(!VirtualProtect((void*)src, 5, PAGE_EXECUTE_READWRITE, &oldProtect)){
        MessageBoxA(0, "Virtual Protect returned 0!", "Detour-Error!", MB_OK | MB_ICONERROR);
        return false;
    }
    //Nun schreiben wir unseren JMP-Befehl direkt in die ersten 5 Bytes des Originalcodes:
    for(int i = 0; i < 5; i++)
        *(src+i) = jump[i];

    //Nun wieder den alten Speicherflag setzen, wir wollen ja ordentlich arbeiten ;)
    if(!VirtualProtect((void*)src, 5, oldProtect, &dummy)){
        //Ein Fehlschlag hierbei ist nicht kritisch, auch wenn er eigentlich nicht auftreten dürfte.
        MessageBoxA(0, "VirtualProtect returned 0 @ reprotect. This is not critical!", "Detour-Warning!", MB_OK | MB_ICONWARNING);
    }
Die return false's existieren, da ich den Code für die Umleitungen in eine seperate funktion installDetours() gelegt habe und diese je nachdem ob sie erfolgreich war oder nicht
true bzw false zurückgibt ;-)
Das gleiche müssen wir für jede Funktion die wir umleiten wollen ausführen.
Jetzt fehlt uns noch der Code zum entfernen der Detour's:
Code:
//Und hier entfernen wir unsere Umleitungen:
    unsigned char* ptr = (unsigned char*)OpenClipboard;
    DWORD oldProtect = 0;
    DWORD dummy = 0;

    //Deprotecten
    if(!VirtualProtect((void*)ptr, 5, PAGE_EXECUTE_READWRITE, &oldProtect)){
        MessageBoxA(0, "Virtual Protect returned 0!", "Detour-Error!", MB_OK | MB_ICONERROR);
        return false;
    }

    //Sicherungskopie von oben wieder einfügen:
    for(int i = 0; i < 5; i++)
        *(ptr+i) = openclipboard[i];

    //VirtualProtect rückgängig machen:
    if(!VirtualProtect((void*)ptr, 5, oldProtect, &dummy)){
        MessageBoxA(0, "VirtualProtect returned 0 @ reprotect. This is not critical!", "Detour-Warning!", MB_OK | MB_ICONWARNING);
    }
Perfekt!
Damit steht unsere DLL schon fast, jetzt schreiben wir uns noch ein kleines "Opferprogramm":
Code:
#include <windows.h>
#include <cstdio>

int main()
{
    printf("[EVIL CLIPBOARDCLEANER]\n");

    while(1){
        printf("Emptying clipboard...");

        if(!OpenClipboard(0) || !EmptyClipboard()){
            printf("failed!\n");
        } else {
            printf("OK\n");
        }
        CloseClipboard();
        Sleep(1000);
    }
    return 0;
}
Sollte soweit verständlich sein, kurz gesagt: Es leert jede Sekunde das Clipboard.
Wenn wir jetzt das Programm laufen lassen und per Winject[4] oder sonstigen Programmen die DLL injizieren,
wird die DllMain aufgerufen und wir können unsere Umleitungen installieren.
Dann wird quasi OpenClipboard zu _OpenClipboard und EmptyClipboard zu _EmptyClipboard.
Und wie wir oben gesehen haben, returnt _OpenClipboard IMMER false, d.h. es schlägt immer "fehl" => Leeren Verhindert.
Auch könnten wir einfach return true; nehmen und dem Programm vorgaukeln, als ob der Zwischenspeicher gelöscht worden wäre, was er jedoch nicht ist ;)
Die Möglichkeiten sind schier unendlich und so als Nebeninformation, diese Technik zum Funktionsumleiten wird oft und gerne in der
Cheatproduktion benutzt. Eine kleine Umleitung auf die OpenGL-glViewport-Funktion lässt einen beliebige benutzerdefinierte "Dinger" zeichnen
(z.b. Overlays). Aber das sei nur so am Rande erwähnt.
Damit wäre ich dann auch schon am Ende meines kleinen Tutorials, ich hoffe es war soweit verständlich =p
Wie legal das ganze ist kann ich jedoch leider nicht genau sagen. Aber wo kein Kläger da keine Klage^^.


ChiefWiggum






Code:
#include "stdafx.h"
#include <Windows.h>


//Unsere beiden Prototypen zur Einrichtung/Entfernung von den Detours
bool insertDetours();
bool removeDetours();

//Funktion zum Berechnen des Sprungcodes:
void calculateJump(int src, int dst, char* jump);

//Für jede Funktion die wir umleiten benötigen wir jetzt einen "Backup" der ersten 5 Bytes sowie unsere Ersetzungsfunktion:
unsigned char openclipboard[5]; //Backup der ersten 5 bytes

BOOL WINAPI _OpenClipboard(HWND hWndNewOwner){ //Ersetzungsfunktion
    return false;//Einfach nur den Fehlerwert zurückggeben ;) Die Originalfunktion wird nicht aufgerufen, d.h. das Programm hat keine Chance das Clipboard zu modifizieren
}

unsigned char emptyclipboard[5]; //S.o.

BOOL WINAPI _EmptyClipboard(){
    return true;//Auch die leeren-Funktion wird "abgeändert". Rein Theoretisch sollte es reichen, nur diese Funktion zu ersetzen um eine Leerung des Clipboards zu verhindern
    //Außer wenn Programme vielleicht versuchen, den Clipboardinhalt durch ein Leerzeichen zu ersetzen.
}


bool setup = false;

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        //Wenn wir uns den Prozess anschließen, installieren wir unsere Umleitungen:
        if(!setup){//Damit wir uns auch nur einmal einrichten ;)
            if(insertDetours())
                MessageBoxA(0, "Attach successfully completed", "Attach-Info", MB_OK);
            else
                MessageBoxA(0, "Attach error: Could not insert detours", "Attach-Info", MB_OK | MB_ICONERROR);
            setup = true;//Auch bei einem Fehler setup = true setzen, um ggf. teilweise eingerichtete Umleitungen nicht außer betracht zu lassen (eg. beim entfernen)
        }
    break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:

    break;

    case DLL_PROCESS_DETACH:
        if(setup){//Wenn wir eingerichtet sind, Detours entfernen:
            if(removeDetours())
                MessageBoxA(0, "Detach successfully completed", "Detach-Info", MB_OK);
            else 
                MessageBoxA(0, "Detach error: Could not remove detours", "Detach-Info", MB_OK | MB_ICONERROR);
            setup = false;//Voilá 
        }
    break;
    }

    return TRUE;
}


bool insertDetours(){
    //OpenClipboard bearbeiten:
    //5 Bytes sichern:
    unsigned char* src = (unsigned char*)OpenClipboard;        //Originale Funktion die umgeleitet werden soll.
    void* dst = (void*)_OpenClipboard;                        //Die Funktion zu der gesprungen wird. Also quasi unsere Ersetzungsfunktion.
    unsigned char jump[5];                                    //Unser Assembly-JUMP-Code. 5 Bytes, Erklärung siehe calculateJump()
    DWORD oldProtect = 0;                                    //Um den alten Protection-Flag zu sichern
    DWORD dummy = 0;                                        //Um den "alten" neuen Protection-Flag zu erhalten, da der oldProtect-Parameter nicht optional ist

    //Erstmal sichern wir die ersten 5 Bytes um sie später eventuell zurückzuschreiben, wenn unsere DLL entfernt wird:
    for(int i = 0; i < 5; i++)
        openclipboard[i] = *(src+i);
    
    //Nun berechnen wir den Assembler-Sprungcode:
    calculateJump((int)src, (int)dst, (char*)&jump);

    //Um unseren Sprungcode einzufügen, benötigen wir erstmal Schreibrechte auf den Speicherbereich:
    if(!VirtualProtect((void*)src, 5, PAGE_EXECUTE_READWRITE, &oldProtect)){
        MessageBoxA(0, "Virtual Protect returned 0!", "Detour-Error!", MB_OK | MB_ICONERROR);
        return false;
    }
    //Nun schreiben wir unseren JMP-Befehl direkt in die ersten 5 Bytes des Originalcodes:
    for(int i = 0; i < 5; i++)
        *(src+i) = jump[i];

    //Nun wieder den alten Speicherflag setzen, wir wollen ja ordentlich arbeiten ;)
    if(!VirtualProtect((void*)src, 5, oldProtect, &dummy)){
        //Ein Fehlschlag hierbei ist nicht kritisch, auch wenn er eigentlich nicht auftreten dürfte.
        MessageBoxA(0, "VirtualProtect returned 0 @ reprotect. This is not critical!", "Detour-Warning!", MB_OK | MB_ICONWARNING);
    }


    //Und jetzt das volle Programm noch einmal für EmptyClipboard =p
    src = (unsigned char*)EmptyClipboard;
    dst = (void*)_EmptyClipboard;
    for(int i = 0; i < 5; i++)
        emptyclipboard[i] = *(src+i);

    calculateJump((int)src, (int)dst, (char*)&jump);

    if(!VirtualProtect((void*)src, 5, PAGE_EXECUTE_READWRITE, &oldProtect)){
        MessageBoxA(0, "Virtual Protect returned 0!", "Detour-Error!", MB_OK | MB_ICONERROR);
        return false;
    }
    for(int i = 0; i < 5; i++)
        *(src+i) = jump[i];
    if(!VirtualProtect((void*)src, 5, oldProtect, &dummy)){
        MessageBoxA(0, "VirtualProtect returned 0 @ reprotect. This is not critical!", "Detour-Warning!", MB_OK | MB_ICONWARNING);
    }

    //Alles ordungsgemäß gelaufen:
    return true;
}

bool removeDetours(){
    //Und hier entfernen wir unsere Umleitungen:
    unsigned char* ptr = (unsigned char*)OpenClipboard;
    DWORD oldProtect = 0;
    DWORD dummy = 0;

    //Deprotecten
    if(!VirtualProtect((void*)ptr, 5, PAGE_EXECUTE_READWRITE, &oldProtect)){
        MessageBoxA(0, "Virtual Protect returned 0!", "Detour-Error!", MB_OK | MB_ICONERROR);
        return false;
    }

    //Sicherungskopie von oben wieder einfügen:
    for(int i = 0; i < 5; i++)
        *(ptr+i) = openclipboard[i];

    //VirtualProtect rückgängig machen:
    if(!VirtualProtect((void*)ptr, 5, oldProtect, &dummy)){
        MessageBoxA(0, "VirtualProtect returned 0 @ reprotect. This is not critical!", "Detour-Warning!", MB_OK | MB_ICONWARNING);
    }

    //Und das ganze nocheinmal für EmptyClipboard:
    ptr = (unsigned char*)EmptyClipboard;
    
    if(!VirtualProtect((void*)ptr, 5, PAGE_EXECUTE_READWRITE, &oldProtect)){
        MessageBoxA(0, "Virtual Protect returned 0!", "Detour-Error!", MB_OK | MB_ICONERROR);
        return false;
    }
    
    for(int i = 0; i < 5; i++)
        *(ptr+i) = emptyclipboard[i];

    if(!VirtualProtect((void*)ptr, 5, oldProtect, &dummy)){
        MessageBoxA(0, "VirtualProtect returned 0 @ reprotect. This is not critical!", "Detour-Warning!", MB_OK | MB_ICONWARNING);
    }
    //Alles hat funktioniert:
    return true;
}

/**
calculateJump(src, dst, jump)
Berechnet den Assembler-Code für den Sprung von src nach dst und schreibt den Code in jump.
Jump muss mindestens 5 Bytes groß sein! Sinnvoll wären exakt 5 Bytes(1 Byte JUMP-OP, 4 Byte Zieladresse)
32-Bit only!
**/
void calculateJump(int src, int dst, char* jump){
    int distance = (int)dst - (int)src - 5;//Die 5 Bytes müssen wegen des Jump-Befehls noch abgezogen werden ;)
    //Code für einen Sprung:  E9 FF EE DD CC für 0 -> 0xCCDDEEFF, demnach:
    //Bytes rückwärts schreiben und abhängig von der aktuellen Position!
    jump[0] = 0xE9; //Assembler => JMP
    jump[1] = distance & 0x000000FF;            //Die letzten 2 Bytes kommen als erstes (Siehe Big/Little Endian)
    jump[2] = ((distance & 0x0000FF00) >> 8);   //Die nächsten 2 Bytes als zweites. Da das ganze dann 0xEE00 wäre müssen wir es noch um ein Byte nach rechts verschieben.
    jump[3] = ((distance & 0x00FF0000) >> 16);  //Hierbei wären es schon 0xDD0000, d.h. um zwei Bytes verschieben
    jump[4] = ((distance & 0xFF000000) >> 24);  //Und dann nochmal um drei. Schon sind wir fertig =)
}
[1] http://www.hackerboard.de/windows-7/43855-programm-unterdrueckt-copy-paste-o.html
[2] Byte-Reihenfolge
[3] Byte-Reihenfolge
[4] Cheat-Hack - WinJect 1.7 for [ Tools ]
 

ChiefWiggum

New member
Kleiner Nachtrag:
Versucht bitte nicht, in einer umgeleiteten Funktion die originale nochmal aufzurufen, dass geht schief ;) Das Programm schmiert dann mit einem Stack Overflow ab, da man damit ja eine Unendlichschleife zwischen der Originalfunktion und der eigenen Produziert ;)
 

enkore

New member
Es sei denn man kopiert die Originalfunktion vor dem Verändern irgendwo anders hin, dann ist das kein Problem...
 

ChiefWiggum

New member
Jep, das könnte ich wenn bedarf ja noch als Erweiterung dazumachen, dass man sich speicher von 10 Bytes Allociert, dort die 5 bytes backup und einen Sprung auf funktionsbeginn+5 macht und diese Funktion dann bei Bedarf aufruft.
Sollte eig kein Problem sein ;)

(Mit diesem Trick lässt sich btw der GameGuard verarschen. Vor dem start die ersten 5 bytes und den jump in den eigenen Code sichern und dann, wenn der gg die Funktionen detourt ist man selbst nicht von betroffen^^)
 
Oben