0x01. PRESENTATION
Les API Windows représentent une liste de fonctions disponibles pour mieux développer nos programmes. Elles sont disponibles à travers plusieurs outils, dont Depends . Elles ne sont pas toutes documentées, c'est à dire que les paramètres ne sont pas toujours connus, les valeurs retournées non plus, c'est alors au développeur de les "deviner". Lors de l'injection de DLL, les API Windows sont utilisées par l'injecteur et parfois par la DLL à injecter. Ils s'agit d'une poignée de fonction nécessaire qui vont permettre de copier le contenu d'un fichier binaire - bibliothèque de fonction (DLL) - dans le processus, grâce à un thread.
0x02. DEFINITIONS
- Processus
Un processus est l'image mémoire du résultat de l'exécution d'un programme. Lorsqu'un programme s'exécute il charge l'équivalent binaire de sa section .data - donc ses données - puis de même pour la section .text qui contiendra le flot de commandes assembleur à exécuter. Un processus peut contenir des handles et des thread qui vont lui permettre de ne pas grossir en mémoire ou alors rapidement d'occuper toutes les ressources systèmes. Les handle permettent au système d'exploitation de s'associer aux processus et de les faire interagir avec l'utilisateur (l'écran).
- Thread
Le thread est une partie du programme utilisant une partie plus minime des ressources du processeur ou bien de la mémoire, le processus va gérer le thread de manière à effectuer plusieurs tâches parallèles. La programmation avec les threads permet donc d'effectuer plus de tâches on utilisant moins de ressources, mais cela est bien entendu relatif à son environnement.
- Handle
Un handle est une poignée qui permet à une fonction de s'associer avec un objet. Par exemple les FILE HANDLE, par exemple en langage C, il serait représenter par un pointeur de type FILE : FILE* handle_for_a_file; c'est ensuite sur à travers ce handle que seront effectuées les opérations concernant le fichier : fread(handle_for_a_file, ... ); fscanf(handle_for_a_file, ... );
- PID
Le PID, pour Process IDentifiant est la valeur numérique du processus. Il permet d'identifier un processus rapidement et facilement qu'il ait un nom ou pas.
- PPID
Le PPID est l'identifiant du processus père, par exemple si la calculatrice est lancée, son père sera l'explorateur (explorer.exe) car pour lancer la calculatrice il faut avoir chargé l'explorateur pour trouver oà se trouve la calculatrice. Ainsi on peut expliquer comment se donne les droit d'un processus à un autre, en lançant cmd.exe en tant que administrateur, ses enfants (netstat.exe par exemple) auront les même droits.
- API
Une API est une interface de programmation entre le système d'exploitation et les programmes. Il s'agit plus précisément de fonctions/modules utiles (voir nécessaires dans la plus part des cas) aux autres programmes. Elle permet de se documenter quant à l'utilisation de fonctions système sur notre programme et inversement, pour que le programme interfère avec le système. Les API de Microsoft ne sont pas toutes documentées, ce qui veut dire que le développeur doit parfois tester les arguments d'une fonction afin de pouvoir l'utiliser.
- DLL
Une DLL va permettre au programme de charger une bibliothèque de fonctions. DLL signifie "Dynamic Load Library", cette acronyme démontre l'utilité de tels fichiers. Les DLL sont propres à Windows, c'est à dire que ces ensembles de fonctions, de programmes utilitaires ne sont utilisable qu'à partir de programmes Windows. Les DLL sont des fichiers binaires tout comme les exécutables à trois différences près :
- Une DLL a besoin d'un programme externe pour être utilisée ;
- Du fait que c'est une bibliothèque de fonctions, il faut un programme externe pour connaitre et exploiter ses fonctions ;
- Par défaut elles ne sont exploitables que par le programme qui les utilise.
0x03. API
Pour l'injection de DLL, il est nécessaire d'appeler des fonctions Windows - donc d'utiliser des API, notamment celle concernant les processus ( dans
// Pour l'élévation des droits
OpenProcessToken
LookupPrivilegeValue
AdjustTokenPrivileges
CloseHandle
Ensuite, nous aurons évidemment besoin de savoir dans quel processu nous allons injecter notre DLL:
// Pour l'exploration des processus
CreateToolhelp32Snapshot
Process32First
Process32Next
Puis finalement les fonctions appelées pour l'injection. Vous remarquerez l'utilisation de "CreateRemoteThread" qui est la fonction qui permettra d'attacher au processus (distant) un thread: notre DLL.
// Pour l'injection à proprement parlé
OpenProcess
VirtualAllocEx
WriteProcessMemory
GetProcAddress
CreateRemoteThread
WaitForSingleObject
VirtualFreeEx
Voici un petit schéma afin de vous aider à mieux comprendre:
1 - hProc = OpenProcess( PROCESS_ALL_ACCESS, FALSE, ProcPID ) 2 - AllocMemo = (char*)VirtualAllocEx( hProc, NULL, strlen(strDLLFileName)+1, MEM_COMMIT, PAGE_READWRITE ); 5 - CreateRemoteThread (hProc,NULL,0,GPA, (void*) AllocMemo , THREAD_SUSPEND_RESUME, NULL ); -----------------+ | | | | | @: 0x7??????? | +---------+ | | PROCESS | | |=========| | | PID | | | PPID | | | Threads | | | MID | | | Flags | | +----+----+ | | @: 0x???????? | <-- handle --+ @: 0x???????? +-----+ V | | DLL | ---+----> <-- thread --+ @: 0x???????? +-----+ | | | ^ 3 - WriteProcessMemory(hProc, // Bloc AllocMemo, // concernant strDLLFileName, // l'écriture strlen(strDLLFileName)+1, // dans le NULL ) // process distant 4 - (DWORD*)GetProcAddress( GetModuleHandle("kernel32.dll"), "LoadLibraryA" ); 1- Ouverture du process, avec tout les accès possible, c'est pour cela que le programme doit avant s'élever en privilèges 2- Maintenant qu'on a notre poignée sur le process par la fonction précédente on peut allouer autant de mémoire que la taille du fichier 3- Une fois l'allocation faite, on peut écrire les données, donc notre fichier dans la mémoire du process. WriteProcessMemory prend en paramètre un nom de fichier. 4- Pour utiliser ce qui est écrit, il nous faut un thread , pas de problème une API Windows nous le permet grâce à la fonction CreateRemoteThread , mais avant il nous faut l'adresse du process qui sera donnée par GetProcAddress . 5- Il est enfin temps d'appeler la DLL chargée via CreateRemoteThread .
0x04. INJECTION
Les fonctions du programme sont largement commentées ce qui permet de comprendre le but de certains appels ainsi que des API sans avoir à s'étendre sur la théorie. Le nom des fonctions ainsi que les valeurs utilisées sont retrouvables dans la documentation Microsoft.
La fonction permettant l'élévation de privilèges:
int UpPrivileges() { HANDLE hToken; LUID Value; TOKEN_PRIVILEGES tp; // On ouvre un handle le process courant, c'est à dire notre programme if( ! OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken) ) return ERROR_NOT_UPGRADE; // On vérifie que notre programme a les droits de débogages // s'il a déjà les droits, inutile de continuer if( ! LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &Value) ) return ERROR_NOT_UPGRADE; /* On initialise la structure afin de redéfinir les droits */ // Nombre de privilège, effectivement un process peut avoir plusieurs privilèges tp.PrivilegeCount = 1; // On conserve l'identifiant de privilège tp.Privileges[0].Luid = Value; // Définit le privilège en lui même tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; // Ajuste les privilèges définis par la structure ci-dessus // si l'ajustement ne s'est pas fait avec succès, on quitte la fonction if( ! AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL) ) return ERROR_NOT_UPGRADE; CloseHandle(hToken); return SUCESSFUL; }
Pour aider à l'injection il nous faut la liste des process :
int ProcessList() { int i_NbrProcess = 0; // Une poignée pour communiquer avec les process HANDLE h_Sys = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if( ! h_Sys ) { // Impossible d'accéder aux fonctions de Toolhelp32 return(ERROR_TH32_ACCESS); } PROCESSENTRY32 p_Proc; // Structure of a process ZeroMemory(&p_Proc, sizeof(p_Proc)); // Initialize to NULL the proc structure p_Proc.dwSize = sizeof(p_Proc); // Dword Size = Size Of Structure proc // On entre dans la liste des process, on commence par le premier if( ! Process32First(h_Sys, &p_Proc) ) { return(ERROR_P32F_ACCESS); } printf("\n"); printf("%-6s \t| %-6s \t| %-4s \t| %-20s\n","PPID","PID","USAGE","NAME"); printf("%-6s \t+-%-6s \t+-%-4s \t+-%-20s\n","----","---","-----","----"); do { printf("%-6d \t| %-6d \t| %-4d \t\t| %-20s\n",p_Proc.th32ParentProcessID , p_Proc.th32ProcessID , p_Proc.cntUsage , p_Proc.szExeFile ); i_NbrProcess ++; // On affiche tant qu'on est sur un process, en avançant de process en process } while( Process32Next(h_Sys, &p_Proc) ); return(i_NbrProcess); }
Puis voici l'attache au process de notre DLL, c'est à dire l'injection en elle même. Vous remarquerez que le process est appelé par son PID et que la DLL par son chemin.
int AttachToProcess(DWORD ProcPID, char* strDLLFileName) { HANDLE hProc; HANDLE hThread; void* GPA; char* AllocMemo; int res; // Elevation de privilèges... printf("[-] Setting up higher privilege..."); if( UpPrivileges() ) { no_printf(); printf("\n[!] Try to continue load of the DLL..."); } else printf("\b\b\b Ok."); hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcPID); AllocMemo = (char*)VirtualAllocEx(hProc, NULL, strlen(strDLLFileName)+1, MEM_COMMIT, PAGE_READWRITE); // Permet de verifier la structure de la DLL if( ! CheckDLLStructure(strDLLFileName) ) { printf("\n[+] Checking for DLL structure : No !\n\n"); exit(DLL_NOT_VALID); } else printf("\n[-] Checking for DLL structure : Ok."); printf("\n[-] DLL filename to load : %s" , strDLLFileName); printf("\n[-] Writting DLL %d bytes into process memory..." , N_SizeFile(strDLLFileName)); Sleep(1000); if( WriteProcessMemory(hProc, AllocMemo, strDLLFileName, strlen(strDLLFileName)+1, NULL ) ) { printf("\b\b\b Ok.\n"); } else { no_printf(); exit(ERROR_NOT_WRITTEN); } printf("[-] Getting Process Address..."); GPA = (DWORD*)GetProcAddress( GetModuleHandle("kernel32.dll"), "LoadLibraryA" ); // Laisse un temps pour s'accrocher à l'adresse du processus Sleep(2000); printf("\b\b\b at %X.",GPA); printf("\n[-] Creating Thread..."); hThread = CreateRemoteThread ( hProc,NULL,0,GPA, (void*)AllocMemo , THREAD_SUSPEND_RESUME , NULL ); printf("\b\b\b %X at %X.",hThread,&hThread); // Pour vérifier que le process est bien attaché, il faut // attendre sa synchronisation avec la DLL injectée printf("\n[-] Waiting for synchronisation..."); res = WaitForSingleObject(hThread, INFINITE); // L'attente de synchronisation Sleep(2000); printf("\b\b\b Ok.\n"); // On purge la mémoire allouée au handle VirtualFreeEx(hProc, (void *) AllocMemo, 0, MEM_RELEASE); CloseHandle(hThread); return 0; }
0x05. CREATION DE DLL
Nous allons créer une DLL des plus simplistes dont le code permettra de quitter l'application dans la quelle elle a été injectée. Il s'agit simplement d'utiliser l'API ExitProcess( UINT ) afin que le process "s'auto-termine". La DLL ci-dessous se limite au processus nouvellement créé.
#include <windows.h> #include <stdio.h> #include <stdlib.h> #if BUILDING_DLL # define DLLIMPORT __declspec (dllexport) #else /* Not BUILDING_DLL */ # define DLLIMPORT __declspec (dllimport) #endif /* Not BUILDING_DLL */ DLLIMPORT void SelfKill () { MessageBox( NULL, "Attempt to self kill process.", "DLL injected", MB_OK ); ExitProcess( 0 ); } BOOL APIENTRY DllMain (HINSTANCE hInst /* Library instance handle. */ , DWORD reason /* Reason this function is being called. */ , LPVOID reserved /* Not used. */ ) { SelfKill () return TRUE; }
Afin de tester notre programme, nous allons à partir de la console MSDOS lancer le programme de la manière suivante :
c:\dll_injexion\> calc.exe && injexion -n calc.exe self_kill.dll
Ce qui aura pour effet de lancer la calculatrice, puis (si elle est bien lancée (&&)) d'injecter la DLL "self_kill.dll", qui doit donc afficher un message puis quitter la caculatrice. Il se peut que l'antivirus ou le firewall (ou votre HIPS) réagisse, mais ayant le code sous les yeux vous pouvez voir qu'il n'y a aucun code malsain dans la DLL. L'air de rien, cette petite DLL peut servir à "tuer" un processus d'une autre manière que TerminateProcess .
0x06. CONCLUSION
L'injection de DLL n'est pas forcément utilisé dans le contournement de (vieux) firewall ou pour la création de virus/rootkits, mais également dans des programmes par exemple de surveillance: on peut très bien imaginer une DLL qui vérifie l'intégrité de l'éxecutable dans lequel elle a été injectée.
=> Écrit par : Nicolas, le 10 novembre 2007