Ein kleines Writeup das ich vor laaaanger Zeit einmal geschrieben habe. Es erklärt wie man virtuelle Methoden detoured.
Die meisten anderen Tutorials und Snippets befassen sich nur damit die Adresse in der VTable zu überschreiben, während dieses Konzept erläutert wie man eine richtige Funktionsumleitung á la MS Detours implementiert. Nur halt für virtuelle Methoden.
Original geposted auf B2H, aber vllt. interessiert sich hier ja auch jemand dafür. Ihr wisst schon ... aktivität in der Coding section, und so. ![Wink](images/smilies/wink.png)
---
Sicherlich kennen es viele von euch: Hooken einer Member-Funktion also Methode, indem man die Funktionsaddresse in der VTable entsprechend abändert. Das einzige Problem dabei ist, das es u. U. schwer wird die Parameter abzufangen und das Callen der echten Funktion sich dementsprechend auch nicht unbedingt als das einfachste erweist.
Wahrscheinlich kennen aber auch viele das Verfahren mit dem normale Funktionen meist gehookt werden. Detouring.
Das ist einfach erklärt. Zunächst lokalisiert man die zu hookende Funktion, und dann überschreibt man die ersten 5 (oder falls nötig mehr) Bytes mit einem JMP zu seiner eigenen. Damit man die Funktion aufrufen kann, werden die ersten 5 Bytes der Funktion wiederum an eine Stelle in den Speicher geschrieben und ein JMP an die Addresse der Funktion zzgl. der 5 Bytes angehängt.
So kann man die Funktion nun ganz normal aufrufen, indem man die stelle mit den gespeicherten 5 Bytes callt. Wenn das Programm aber wie üblich die originale Funktionsaddresse aufruft, landet es bei unserem ersten JMP und wird auf unsere Funktion umgeleitet.
Worauf wir achten müssen ist das die Typen der Parameter und natürlich die Convention stimmen, denn ansonsten werden die Parameter entweder falschrum eingelesen, oder es wird Müll aus dem Speicher kopiert.
Mit dieser Methode haben wir gegenüber dem direkten manipulieren der VTable einige Vorteile, da wir die Parameter bequem abändern und dann trotzdem die alte Funktion aufrufen können. Das geht zwar bei der anderen auch (siehe anderer Thread) erfordert aber Inline-Assembler und Casts bei den Rückgabewerten und ist im Übrigen alles andere als schön.
Warum verwendet man (meistens) also nicht auch die Detouring Methode bei VTables? Das wird wohl daher rühren, das es im Netz kaum Quellen gibt die diese Methode für VTables erläutern, da es wohl mehr oder weniger trivial ist, wenn man sich mit der Sache beschäftigt - leider vergessen die meisten heutzutage, sich mit einer Sache zu beschäftigen.
Und jede arme Seele die Versucht hat, eine Member-Funktion mit einem normalen Detour zu hooken, wird wohl an den Exceptions zerbrochen sein. Amen.
Warum?
Funktionen die als Methode deklariert sind, werden automatisch als thiscall aufgerufen. Diese Calling-Convention wurde speziell für Klassen und Objekte ausgelegt und funktioniert im Grunde wie ein stdcall, mit der Ausnahme das beim thiscall zusätzlich der this-Pointer in ECX - wer nicht weis was das ist schaut bitte in einer ASM Referenz nach und verbrennt anschließend seinen PC - mitübergeben.
Somit wird sozusagen ein Argument übergeben, das im Code nicht sichtbar ist. Wenn man nun die Funktion normal detouren würde, ist es ja logisch das alles crasht. Was also tun? Richtig! Den this-Pointer "manuell" übergeben!
Ich hatte ja eben schon erklärt, das ein thiscall im Grunde wie ein stdcall funktioniert. Das ist sehr praktisch, da wir für unsere Neue Funktion einfach stdcall als Convention nehmen können, wenn wir daran denken, das es ein Parameter mehr gibt. Die Funktionsdefinition sähe dann ungefähr so aus:
Code:
void __stdcall newYourFunc(void * _this, T1 var1, T2 var2);
So weit zur Funktion. Wie kann man nun den this-Pointer vernünftig übergeben? Erstmal sollten wir uns ein klares Bild vom Stack machen.
Code:
Stack-Modell:
3A0F244C ; Return-Adresse, wird vor dem Funktionsaufruf auf den Stack gelegt
0013DB14 ; Parameter Nr. 1 <--- Wird als zweites auf den Stack gelegt
00000284 ; Parameter Nr. 2 <--- Wird zuerst auf den Stack gelegt - LIFO Prinzip
Wir wissen dass der zusätzliche Parameter in ECX gespeichert wurde, also müssten wir das theoretisch nur unter die Return-Adresse auf den Stack pushen:
Code:
ASM:
POP EAX ; Speichern der Return-Addresse in EAX
PUSH ECX ; Pushen des this-Parameters
PUSH EAX ; Pushen der zuvor gespeicherten Return-Adresse
Dannach sollte der Stack so aussehen:
Code:
Stack-Modell:
3A0F244C ; Return-Adresse, wird vor dem Funktionsaufruf auf den Stack gelegt
03889FF0 ; Gerade eben gepushter this-Pointer
0013DB14 ; Parameter Nr. 1 <--- Wird als zweites auf den Stack gelegt
00000284 ; Parameter Nr. 2 <--- Wird zuerst auf den Stack gelegt - LIFO Prinzip
Nun brauchen wir noch nen JMP zu unserer neuen Funktion und, schwupp, haben wir die Funktion gehookt und können uns ein Ei backen. Da wir aber u. U. die echte Funktion wieder aufrufen möchten, müssen wir die überschriebenen Bytes irgendwo sichern, am ende einen JMP zum Rest der Funktion einbauen und das ganze Aufrufbar machen.
Also brauchen wir erstmal eine Typedefinition:
Code:
typedef void (__stdcall *YourFunc_t)(void * _this, T1 var1, T2 var2);
Dann brauchen wir den Funktionszeiger:
Code:
YourFunc_t origYourFunc;
Und nun müssten wir dort via new, malloc o. ä. Platz schaffen usw. aber das übernimmt zum Glück die Funktion welche ich am Ende dieses Artikels Posten werde, ich bleibe daher bei der Funktionsweise:
Wir schreiben die in der originalen Funktion überschriebenen Bytes, an die Adresse auf die der soeben definierte Pointer zeigt.
Dann müssen wir den this-Pointer wieder nach ECX bekommen, also zunächst wieder das Modell:
Code:
Stack-Modell:
3A0F244C ; Return-Adresse, wird vor dem Funktionsaufruf auf den Stack gelegt
03889FF0 ; Gerade eben gepushter this-Pointer
0013DB14 ; Parameter Nr. 1 <--- Wird als zweites auf den Stack gelegt
00000284 ; Parameter Nr. 2 <--- Wird zuerst auf den Stack gelegt - LIFO Prinzip
Und hier der ASM-Code
Code:
ASM:
POP EAX ; Speichern der Return-Adresse in EAX
POP ECX ; Speichern des this-Pointer in ECX
PUSH EAX ; Pushen der zuvor gespeicherten Return-Adresse
Und nun sieht der Stack so aus wie zuvor, während der this-Pointer in ECX gespeichert ist:
Code:
Stack-Modell:
3A0F244C ; Return-Adresse, wird vor dem Funktionsaufruf auf den Stack gelegt
0013DB14 ; Parameter Nr. 1 <--- Wird als zweites auf den Stack gelegt
00000284 ; Parameter Nr. 2 <--- Wird zuerst auf den Stack gelegt - LIFO Prinzip
Also praktisch, wie oben nur andersrum. Den JMP zur Position der alten Funktion, plus den überschriebenen Bytes nicht vergessen!
PS.
Konstruktive Kritik erwünscht.