Donnerstag, 4. Dezember 2008

entwickler.com Magazine Konferenzen Akademie Entwickler-Forum Jobbörse Bücher
Software & Support Verlag




April 2006
aus Der Entwickler Ausgabe: 6.2003
DLL-Injektion über API-Hooking
Eine Einführung in das Integrieren eigenen Codes in fremde Anwendungen
von Florian Cremer

Manchmal ist der Funktionsumfang einer Anwendung nicht ausreichend und man würde gern das eine oder andere Feature dem Programm hinzufügen. Bei den meisten Softwareprodukten unter Windows handelt es sich nicht um Open Source-Projekte und überwiegend verfügen Anwendungen weder über eine PlugIn-Schnittstelle noch über eine leistungsfähige Skript-Sprache à la VB-Skript, über die man eigene Funktionalitäten integrieren kann. DLL-Injektion verspricht hier Abhilfe.


Zunächst ist es hilfreich, den Zusammenhang zwischen Prozessen und Threads zu veranschaulichen. Ein Prozess ist als Instanz eines laufenden Programms definiert und besteht aus zwei Teilen: Zum einen aus dem repräsentativen Kernel-Objekt, in dem Windows prozessrelevante Daten ablegt und zum anderen aus einem 4 GByte großen virtuellen Adressraum für Programme, DLLs und Programmdaten. Die Begrenzung auf 4 GByte ist durch den 32Bit-Adressraum der CPU begründet. Ein Prozess selber kann jedoch noch keinen Programmcode ausführen. Hier kommen die Thread-Objekte ins Spiel. Ähnlich wie Prozess-Objekte bestehen auch Thread-Objekte aus zwei Komponenten: Dem Kernel-Objekt, welches den Thread repräsentiert und in dem threatrelevante Informationen abgelegt werden und einem threadeigenen Stack, auf dem alle Parameter und lokalen Variablen abgelegt werden, die zur Thread-Ausführung benötigt werden. Threads müssen in Prozessen eingebettet sein und real wird der Stack des Threads auf dem Speicher des Prozesses abgebildet, da Thread-Objekte keinen eigenen Speicher besitzen. Ein Prozess ist also als Container für Threads zu verstehen. Im Gegensatz zu den starren Prozessen führen Threads Programmcode aus. Untereinander müssen sie sich abwechselnd Prozessorzeit teilen. Dadurch können mehrere Anwendungen scheinbar gleichzeitig abgearbeitet werden.


Nachdem ein Prozess vom Kernel generiert worden ist, wird er mit einem primären Thread-Objekt ausgestattet. Im Verlauf des Programms kann der Prozess über weitere Threads verfügen, die baumartig vom primären Thread abgespalten sind. Übrigens kann auch ein Prozess einen neuen Prozess erschaffen. Das ist zum Beispiel dann der Fall, wenn man im Explorer ein Programm startet: Der Prozess des Explorers kreiert dann den Prozess des gewählten Programms. Unabhängig vom übergeordneten Prozess läuft der neue Prozess im eigenen Kontext. Das heißt, er wird nicht automatisch zerstört, wenn der Prozess beendet wird, der ihn kreiert hat. Threads im Gegensatz können nicht länger als das Prozess-Objekt existieren, in das sie eingebettet sind. Die benötigten API-Calls um Prozesse und Threads zu erzeugen, heißen sinnvollerweise CreateProcess() und CreateThread(). Beide geben einen Handle zur Identifizierung des jeweiligen Objektes zurück, den man mit der Funktion CloseHandle() schließen sollte, sobald das Objekt nicht mehr benötigt wird. Vergisst man den Handle zu schließen, so übernimmt es das Betriebssystem beim Beenden des Prozesses. Wohlgemerkt schließt man mit CloseHandle() lediglich den Handle und gibt dadurch Betriebssystemressourcen wieder frei. Das jeweilige Objekt besteht weiter, bis es abgearbeitet ist oder durch Funktionen wie ExitThread() oder ExitProcess() beendet wird. Genaueres zu den Funktionen kann man der PlattformSDK-Dokumentation entnehmen.

Virtueller Speicher im Detail
Ein Prozess verfügt über 4 GByte virtuellen Speicher. Virtueller Speicher bedeutet, dass dem Prozess nicht von vornherein physischer Speicher, also RAM oder auch Festplattenspeicher, zugeordnet ist, sondern dieser erst bei Bedarf in den virtuellen Adressraum eingeblendet wird. Um Speicher aus dem virtuellen Prozessspeicher verwenden zu können, muss er zuvor reserviert und zugeordnet werden. Dadurch erst wird physischer Speicher zur Verfügung gestellt. Nun ist es so, dass jeder Prozess seinen eigenen virtuellen Speicher besitzt. Der Speicher anderer Prozesse ist für einen Prozess selbst unsichtbar. Aus Sicht des Prozesses existieren also nur er selbst und das Betriebssystem. Daher ist es für einen Prozess unmöglich, durch einen Zeiger auf den Adressraum eines anderen Prozesses zuzugreifen. Das ist ein wichtiges Sicherheitskriterium von Windows und garantiert so die Integrität des gesamten Systems. Dadurch bleibt das System stabil, auch wenn ein Prozess abstürzt.


Der gesamte virtuelle Speicher wird vom System in zwei Hälften aufgeteilt. Die untere Hälfte wird der Anwendung zu geteilt, die Obere wird vom Kernel genutzt. Hier legt das Betriebssystem eigenen Programmcode für den jeweiligen Prozess ab. Die der Anwendung zugeordneten 2 GByte werden wiederum in sich strukturiert.


Abb. 1: Die Aufteilung des 4 GByte großen, virtuellen Prozessspeichers

Sicherlich stellt sich die berechtigte Frage, wie ein System, welches insgesamt beispielsweise über 512 MByte RAM verfügt, einem Prozess bis zu 4 GByte Speicher zur Verfügung stellen kann. Windows stellt jedoch diesen Speicher nicht auf einmal zur Verfügung. Denn wenn eine Anwendung gestartet wird, legt Windows ein neues Prozess-Objekt an, weist diesem den virtuellen Adressraum und ein primäres Thread-Objekt zu, ermittelt nun anhand der Header-Informationen, die es der .exe-Datei der Anwendung entnimmt, wie viel realer Speicher die Anwendung inklusive angebundener DLLs benötigt und stellt diesen im virtuellen Adressraum des neu angelegten Prozesses zur Verfügung. Natürlich können die angebundenen DLLs wiederum DLLs einbeziehen. Der dafür benötigte Speicher wird ebenfalls bei der Prozessinitialisierung berücksichtigt. Auch kann das Programm während seiner Ausführung weiteren Speicher im virtuellen Adressraum allokieren. Durch einen Aufruf der Funktion VirtualAlloc() kann man für seine Anwendung zur Laufzeit entsprechend Speicher von Windows beantragen.


Die 4 GByte des virtuellen Adressraumes stellen also nur den maximal adressierbaren Speicher eines Prozesses dar, meistens wird sehr viel weniger realer Speicher benötigt. Ferner behilft sich Windows mit der berühmten Auslagerungsdatei: Der Speicher ist nämlich in 4 KByte große Speicherseiten eingeteilt, von denen stets nur die benötigten Seiten ins RAM geladen werden. Die unbenötigten Seiten werden in der Auslagerungsdatei gehalten und erst bei Bedarf geladen. Sind keine Speicherseiten im RAM frei, so muss das System welche frei geben, indem es Seiten in die Auslagerungsdatei schreibt. Erst bei Bedarf werden diese wieder zurückgeladen. Durch diesen Mechanismus ist es möglich, dass das RAM und die Auslagerungsdatei von mehreren Prozessen im System parallel verwendet werden. Natürlich kommt es ständig vor, dass Speicherseiten im physischen Speicher direkt nebeneinander liegen, obwohl sie zu verschiedenen Prozessen gehören. Das Speichermanagement von Windows führt Tabellen, mit denen die einzelnen Speicherseiten dem jeweiligen virtuellen Adressräumen zugeordnet werden. Dieses Verfahren ist ebenso sicher wie genial.


Abb. 2: Die einzelnen Speicherseiten der Prozesse können nebeneinander im RAM liegen

Mitunter erklärt dies auch, warum die Gesamtperformance des Systems wächst, je größer das verfügbare RAM ist, weil dann das System natürlich seltener auf die Auslagerungsdatei zugreifen muss.


Nun mag man annehmen, dass beim Start eines Prozesses der Anwendungs- oder auch DLL-Code in den virtuellen Speicher eingeblendet wird und nun durch nötige Auslagerungen die Speicherseiten mit eben diesem Code in die Auslagerungsdatei des Systems geschrieben werden und dadurch die Auslagerungsdatei bis ins unermessliche wächst.
Glücklicherweise ist das nicht so. Wird nämlich ein Prozess gestartet, dann wird analog zur Auslagerungsdatei der Festplattenspeicher der jeweiligen Datei, in der der Anwendungs- oder DLL-Code abgelegt ist, in den virtuellen Adressraum des jeweiligen Prozesses eingeblendet. Muss nun das System Speicherseiten im RAM freigeben, die diesen Programm-Code enthalten, so speichert es gerade diese nicht in der Auslagerungsdatei, sondern verweist auf die Quell-Datei der Anwendung oder DLL auf der Festplatte. Wenn auf die ausgelagerten Speicherseiten wieder zugegriffen werden muss, werden sie in diesem Fall nicht von der Auslagerungsdatei, sondern direkt von der Quelldatei wieder eingeladen. Noch ausgeklügelter verhält sich das System beim Start mehrerer Instanzen einer Anwendung. Der zugehörige Programmcode wird in diesem Fall nicht mehrfach für jeden damit gestarteten Prozess in den physischen Speicher geladen, sondern nur einmal und dann in die jeweiligen virtuellen Adressräume der Instanzen eingeblendet. Somit bearbeiten mehrere Prozesse gleichzeitig denselben Code im physischen Speicher. Um Missverständnisse zu vermeiden: Wie beschrieben, weiß normalerweise kein Prozess etwas von den anderen Prozessen, mit denen er sich Programmcode teilt. Variablen und sonstige dynamische Daten hält jeder Prozess in seinem eigenen zugeordneten Speicher. Wenn einer der Prozesse dann möglicherweise Programmcode oder auch Programmvariablen ändern will, wird von den betreffenden Speicherseiten vom Betriebssystem eine Kopie angefertigt und diese dem Prozess zugeordnet. Somit bleiben andere Prozesse von solchen Änderungen unberührt.


Dasselbe Verfahren, also Code in mehrere virtuelle Adressräume gleichzeitig einzublenden, wendet Windows bei DLLs an, die ja an mehrere Prozesse gleichzeitig gebunden sein können.
Denn das ist ja gerade einer der Vorzüge einer DLL: Man kann den von mehreren Anwendungen benötigten Code in DLLs bündeln und dann eben diesen Code nur einmal in den physischen Speicher laden, obwohl er von mehreren unabhängigen Prozessen verwendet wird. Dadurch kann immens Speicher gespart werden. Dies ist wohl einer der Gründe warum ein Großteil des Windowsbetriebssystems auf der DLL-Technik basiert.

DLL-Baukasten
Um eine DLL an ein Projekt zu binden gibt es zwei Möglichkeiten: Implizites und explizites Binden. Ersteres bedeutet, dass Verweise auf die DLL schon beim Kompilieren und Linken des importierenden Projektes automatisch eingebunden werden.
Die dafür nötigen Informationen beziehen der Compiler aus einer mit der DLL gelieferten Headerdatei und der Linker aus einer beim Erstellen der DLL erzeugten .lib-Datei. Beide müssen dem importierenden Projekt hinzugefügt werden..


Wenn man nun eine DLL programmieren will, sammelt man alle Funktionen und Variablen, die man in verschiedenen Prozessen verwenden möchte, in dem DLL-Projekt. Elemente einer DLL, die man nun DLL-extern anbieten möchte, müssen als exportierbar in der Header-Datei und in der .cpp-Datei der DLL gekennzeichnet werden. Dies geschieht mit der Anweisung delcspec(dllexport), mit der man die jeweiligen Signaturen und Definitionen kennzeichnet. Elemente, die man ähnlich wie private Elemente einer Klasse nur DLL-intern verwenden möchte, erhalten diese Kennzeichnung nicht.


Möchte man in einem .exe-Modul nun mit DLL-Funktionen arbeiten, muss die funktionsdeklarierende Headerdatei der DLL von dem jeweiligen CPP-File, das DLL-Funktionen benutzt, inkludiert werden. Dabei müssen nun die zu importierenden Funktionen, die eben in derselben Header-Datei im DLL-Projekt noch mit _declspec(dllexport) gekennzeichnet wurden, nun im Headerfile mit _declsepc(dllimport) gekennzeichnet werden. Dadurch weiß der Linker, dass es sich hierbei um externe Funktionen einer DLL handelt. Möchte man dieser Zweideutigkeit Herr werden, empfiehlt es sich, an dieser Stelle mit Makros zu arbeiten, die je nachdem, ob ein bestimmtes Symbol mit der #define-Anweisung im Code definiert ist oder nicht, die richtige _declspec-Anweisung vor dem Kompilieren einfügen. So wird im Headerfile, das die zu exportierenden Funktionsdefinitionen enthält und von beiden Modulen eingezogen werden muss, geprüft, ob ein bestimmtes Symbol definiert ist. Ist es noch nicht definiert, so wird es als _declsepc(dllimport)definiert und ist damit für die funktionsimportierende Anwendung angepasst. Entsprechend muss das Symbol in allen .cpp-Dateien der DLL, die die Headerdatei einbeziehen, in den .cpp-Dateien als _declspec(dllexport) definiert werden.


Das folgende Beispiel zeigt den Header der Beispiel-DLL, in der das Symbol mit der Bezeichnung INJECTIONDLL_API definiert ist. Ferner ist nur der Prototyp definiert, den die DLL exportieren soll. Alle anderen Prototypen sind in den .cpp-Dateien der DLL definiert:

#ifndef INJECTIONDLL_API
#define INJECTIONDLL_API __declspec(dllimport)
#endif

INJECTIONDLL_API int fnSetMyHook(DWORD dwThreadId);

Wie bereits erwähnt, heißt der Vorgang implizites Binden. Bei diesem Vorgang wird bei der Erstellung im Projekt eine Importtabelle erstellt, die die importierten Funktionen nebst dem Namen der zugehörenden DLL enthält.


Wenn der Lader des Betriebssystems nun beim Initialisieren eines Prozesses diese Importtabelle durchläuft, lädt er die benötigten DLLs und blendet sie in den virtuellen Adressraum des gestarteten Prozesses ein. Bevor der primäre Thread des Prozesses gestartet wird, werden die DLLs initialisiert, sofern das vom DLL-Code vorgesehen ist. Der Programmierer kann also beim impliziten Binden mit den DLL-Funktionen arbeiten, als wären sie direkter Bestandteil seines Projektes ohne sich Gedanken um das Einbinden der DLLs zu machen. An dieser Stelle sei erwähnt, dass natürlich DLLs wiederum auf DLLs verweisen können, was den Initialisierungsvorgang des Prozesses merklich verlängern kann.


Im Gegensatz zum recht automatisierten impliziten Binden muss der Entwickler beim expliziten Binden einer DLL an sein Projekt ein bisschen mehr tun. Bei dieser Technik wird die gewünschte DLL-Funktion mit der DLL erst zur Laufzeit gebunden: Wenn ein Thread während der Ausführung eine Funktion aufrufen will, die in einer DLL steht, kann er explizit die DLL in den Prozessraum laden und auf die gewünschte Funktion zugreifen. Die beim impliziten Binden einer DLL erforderliche .lib-Datei wird beim Linken des importierenden Projektes nicht benötigt, ebenso wird auch keine Importtabelle angelegt.
Um nun eine Funktion einer DLL benutzen zu können, muss ein der Signatur der gewünschten DLL-Funktion entsprechender Funktionspointer angelegt werden, der dann auf die gewünschte Funktion in der DLL zeigen soll. Diesen initialisiert man über die API-Funktion GetProcAddress(), die neben dem Namen der gewünschten Funktion einen Instanzhandle der DLL verlangt. Letzteren erhält man durch einen vorangegangenen Aufruf der Funktion LoadLibrary() bzw. LoadLibraryEx(). Diese Funktionen sind gut in der SDK-Dokumentation beschrieben und dienen dazu, die DLL in den Prozessraum der laufenden Anwendung zu laden. Eine ausführliche Beschreibung für den Umgang mit Funktionspointern liefert die Website www.function-pointer.org. Wird die geladene DLL nicht weiter benötigt, kann man sie durch einen Aufruf der Funktion FreeLibrary() wieder aus dem Speicher entfernen.


DLLs können die Einsprungsfunktion DllMain() benutzen, die, wenn sie in der DLL vorhanden ist, der DLL, ähnlich einem Klassenkonstruktor eines Objektes, die Möglichkeit zur Initialisierung gibt, indem sie vom System aufgerufen wird, wenn die DLL in einen neuen Prozessraum eingeblendet wird oder auch wenn im Prozess ein neuer Thread erstellt wird. Außerdem wird die DllMain()-Funktion für mögliche Aufräumarbeiten aufgerufen, wenn ein Thread im Prozessraum endet oder die DLL aus dem Prozessraum ausgeblendet wird. Aus welchem Grund DllMain() vom System aufgerufen worden ist, kann man dem Funktionsparameter fdwReason entnehmen und eine entsprechende Fallunterscheidung vornehmen. Wichtig ist, dass die Funktion den erfolgreichen Aufruf mit dem Wert TRUE quittiert, da die DLL sonst, wenn sie implizit an einen Prozess gebunden worden ist, mit dem gesamten Prozess, beendet wird. Ist die DLL explizit an einen Prozess mit der Funktion LoadLibrary() oder LoadLibraryEx() gebunden, so liefert die Funktion in diesem Fall lediglich den Wert null zurück. Explizites Binden ist daher sicherer, weil der importierende Prozess nicht beendet wird und man auf das missglückte Laden der DLL im Code reagieren kann.


Windows blendet eine DLL, die möglicherweise von einem anderen Prozess bereits geladen ist, in einen neuen Prozessraum ein, wenn der neue Prozess die DLL an sich binden möchte, um eine DLL-Funktion auszuführen. In der Regel, wird die DLL dann an derselben Stelle im virtuellen Adressraum eingeblendet. Ist dieser jedoch schon belegt, beispielsweise von einer anderen DLL, muss die DLL an einer anderen Adresse im virtuellen Speicher eingeblendet werden. In diesem Fall berechnet das System die relativen Adressen der in der DLL enthaltenen Funktionen, sodass der Zugriff auf diese gewährleistet ist. Diesen Mechanismus kann man ausnutzen, um gezielt Code, den man in einer DLL verpackt, in einen Prozessraum einzubinden. Jedoch muss man dafür den ausgewählten Prozess dazu bringen, die DLL zu laden.

API-Hooking
Man kann dies durch das gezielte setzen eines API-Hooks erreichen. Hooks sind Einrichtungen im Windows-Message-System, die es einem gestatten, Callback-Funktionen zu definieren. Es gibt verschiedene Arten von Hooks und manche bestehen darauf, dass die Callback-Routine in einer DLL liegen muss. Eine komplette Einführung in das Thema API_Hooking bietet der Artikel im Entwickler 1.2003 [1]. Ein auf ein bestimmtes Ereignis gesetzter Hook wird vom System über das Eintreffen des Ereignisses benachrichtigt, noch bevor die zuständige Anwendung die Nachricht über das Ereignis erhält. Jedoch sollte man sich auch über zwei entscheidende Risiken bei der Verwendung von Hooks im Klaren sein: Zum einen führen sie zu absoluten Systemabstürzen, falls der eingebrachte Code nicht fehlerfrei ist. Wobei die Auswirkung des Absturzes im Allgemeinen nicht auf den aktuellen Prozess begrenzt ist. Ferner kann eine ungeschickte Programmierung die Performance des Systems sehr stark belasten. Deshalb sollte man mit ihnen möglichst Sparsam umgehen. Hooks richtet man mit der Funktion SetWindowHookEx()ein. Die Funktion verlangt als Parameter den gewünschten Hook-Typ, einen Funktionszeiger auf die Callback-Funktion, die der Hook aufrufen soll und die das Ereignis behandelt, einen Module-Handle des Prozesses, sofern sich der Hook auf ihn selbst bezieht und sich die Callback-Funktion im Prozess selbst befindet, ansonsten eben den DLL-Handle der DLL, die die Callback-Funktion enthält und die in einen anderen Prozess geladen werden soll, und letztlich die ThreadID des Zielthreads. Letztere sollte natürlich über eine Nachrichtenschleife verfügen. Der primäre Prozess einer Windowsanwendung ist prädestiniert hierfür. Statt der ThreadID kann man auch 0 übergeben. Damit bezieht sich der Hook auf alle im System vorhandenen Threads. Die ThreadID erhält man neben dem ThreadHandle bei der Threaderstellung. Sie ist wie die ProzessID einmalig im System.


Da auf mehrere Ereignisse gleiche Hooks von verschiedenen Anwendungen installiert werden können, verwaltet Windows diese in sogenannten Hook-Chains. Deshalb ist es wichtig, am Ende der eigenen Hookprozedur die Funktion CallNextHookEx() aufzufrufen, die das Ereignis an den nächsten Hook in der Kette weiterreicht. Vergisst man CallNextHookEx() zu setzen, wird die Hook-Kette unterbrochen und andere Hooks verlieren ihre Wirkung. Unter Umständen fängt das Betriebssystem das Unterbrechen der Hook-Kette ab. Details dazu liefern die einschlägigen MSDN-Dokumentationen.


Im Beispiel wird der Hook WH_GETMESSAGE benutzt. Dieser Hook wird vom System stets aufgerufen, bevor der Ziel-Thread per GetMessage() oder PeekMessage() eine Nachricht aus seiner Messagequeue holen möchte. Eine Nachricht eines fremden Prozesses auf die der Hook abgehen soll, lässt sich leicht mit der Anwendung Spy++, die im Lieferumfang von Visual Studio enthalten ist, ermitteln. Ebenso eignen sich natürlich auch andere Hooks dafür, seine DLL in einen Prozess von extern zu integrieren.


Ist der Hook gesetzt und wird vom System aufgerufen, prüft Windows ob die DLL, die die Callback-Funktion enthält, sich bereits im Prozessraum des Zielprozesses befindet. Ist das nicht der Fall, blendet Windows die DLL in den neuen Prozessraum ein und ruft die hinterlegte Callback-Funktion in der DLL auf. In der Callback-Funktion stehen einem nun alle Möglichkeiten offen: Man kann beliebigen eigenen Code ausführen. Durch einen Aufruf der Funktion UnhookWindowsHookEx() wird der Hook wieder aus der dem System und die DLL aus dem virtuellen Adressraum des Zielprozesses entfernt.

DLL-Injektion
Als Beispielprozess für die DLL-Injektion bietet sich eine simple MFC-Dialog-Anwendung mit dem Namen MyObjectExe. Sie verfügt lediglich über einen Button, dessen Eventhandler mit der Funktion PostQuitMessage() den Prozess beendet. Die Funktion PostQuitMessage() sendet an die Zielanwendung die Nachricht WM_QUIT, auf die die Callback-Funktion der Beispiel-DLL reagieren soll. Natürlich noch bevor der Prozess selbst die Nachricht erhält und sich beendet. Um die DLL zu laden, dient die Consolen-Anwendung Injecter, die die InjectionDLL implizit bindet, dann erst den Handle des Hauptfensters des Zielprozesses über dessen Namen in Erfahrung bringt (FindWindow()) und mit diesem dann die ThreadID des primären Threads des Zielprozesses mittels der Funktion GetWindowThreadProcessId() ermittelt.

Listing 1

#include "stdafx.h"
#include "Injecter.h"
#include "InjectionDLL.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#endif


// The one and only application object

CWinApp theApp;

using namespace std;

int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
{
DWORD dwThreadID=0;
HWND hWnd=FindWindow(NULL,_T("MyObjectExe"));

dwThreadID=GetWindowThreadProcessId(hWnd,NULL);

fnSetMyHook(dwThreadID);

Sleep(10000);

return 0;
}

Die Funktion SetWindowHookEx()ist im Beispiel in die zu injizierende DLL ausgelagert und ist dort in die DLL-Funktion fnSetMyHook() eingebettet. Letztere nimmt die ermittelte ThreadID des Zielprozesses entgegen und setzt dann den Hook, der die DLL in den Zielprozess hineinzieht. Wie oben beschrieben wird die in der DLL hinterlegte Callback-Funktion fnMyCallBack() durch SetWindowHookEx()mit dem gewählten Hook im System verankert. Die Sleep-Funktion am Ende von Listing 1 dient lediglich dazu, dass die Anwendung nicht sofort beendet wird, da sonst die DLL wieder aus dem Speicher entfernt wird.


Klickt man nun den Button der Testanwendung MyObjectExe, so wird über dessen Ereignishandler die Nachricht WM_QUIT an den eigenen Prozess geschickt. Noch bevor die Nachrichtenschleife des Prozesses die Nachricht vom System abholen kann, um diese dann zu bearbeiten, wird durch den Hook die Callback-Funktion in der DLL aufgerufen, die zur Demonstration eine Messagebox einblendet. Nachdem diese quittiert wird, wird die Nachricht weitergereicht und trifft im Zielprozess ein. Dieser führt seine Aufräumarbeiten durch und beendet sich darauf. Listing 2 zeigt die .cpp-Datei der zu injizierenden DLL mit der Funktion fnSetMyHook(), die den Hook platziert und der Callback-Funktion zur Bearbeitung der entsprechenden Nachrichten.


Listing 2

#include "stdafx.h"

#define INJECTIONDLL_API __declspec(dllexport)
#include "InjectionDLL.h"

HANDLE g_DllHandle;
HHOOK g_hMyHook;

LRESULT CALLBACK fnMyCallBack(int nCode, WPARAM wParam, LPARAM lParam);

BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
g_DllHandle = hModule;
}
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}

INJECTIONDLL_API int fnSetMyHook(DWORD dwThreadId)
{
int nRetVal=1;

g_hMyHook=SetWindowsHookEx(WH_GETMESSAGE,fnMyCallBack, (HINSTANCE)g_DllHandle,dwThreadId);

if(!g_hMyHook)
nRetVal=0;

return nRetVal;
}

LRESULT CALLBACK fnMyCallBack(int nCode, WPARAM wParam, LPARAM lParam)
{
MSG * pmsg=(MSG*)lParam;
static bool fDone;

switch(pmsg->message)
{
case WM_QUIT:
{
if(!fDone)
{
MessageBox(NULL,"Diese MessageBox wird aus dem Zielprozess aufgerufen.","Injected DLL",MB_OK);
UnhookWindowsHookEx(g_hMyHook);
fDone=true;
}
}
break;
}

return (CallNextHookEx (g_hMyHook,nCode,wParam,lParam));
}

Fazit
Zum injizieren einer DLL in einen Prozess führen auch andere Wege. Ein gangbarer Weg wäre der über Remote Threading. Das Beispiel zeigt jedoch, wie man durch API-Hooking elegant eigenen Code über eine DLL in einen fremden Prozess integrieren kann. Jedoch sollte man sich der Risiken des API-Hookings bewusst sein und seinen Code sehr sauber schreiben. Schnell kann ein in einem API-Hook hinterlegter und nicht aufgeräumter Funktionspointer ins Nirvana zeigen und im gesamten System ein nichtdefiniertes Verhalten auslösen. Hat man aber seine DLL im Zielprozess integriert, ist es ein Leichtes, beispielsweise das Menü vom Hauptfenster der Zielanwendung mit eigenen Einträgen anzureichern und somit den Funktionsumfang beliebig zu erweitern.

Links und Literatur


    Hat Ihnen dieser Artikel gefallen? Dann abonnieren Sie das Entwickler Magazin direkt über unser Online-Formular.



zur vorherigen Seite
zurück
an den Anfang der Seite
nach oben
Diesen Artikel drucken
drucken
Diesen Artikel weiterempfehlen
empfehlen
Software & Support Verlag GmbH