Page principale ; Index ;

Cybercarnet

Injection manuelle de DLL

L’injection manuelle

Qu’est ce qu’une injection manuelle ?

L’injection manuelle, ou manual mapping, est une technique d’injection de DLL utilisée par les maliciels et les programmes de triche pour se rendre discret respectivement auprès des antivirus et des programmes antitriches.

Pourquoi ne pas effectuer une injection classique ?

Lorsqu’une bibliothèque malicieuse est injectée, donc ajoutée, à l’execution d’un programme légitime, il existe plusieurs façon de la détecter. Il est possible d’énumerer programmatiquement les modules (bibliothèques) d’un processus en utilisant la fonction de l’API Windows CreateToolhelp32Snapshot, ou visuellement d’identifier les modules avec un outils comme Process Hacker.

module-processhacker.png
Figure 1: Visualisation des modules d’un processus avec Process Hacker

Il est aussi possible pour les programmes de détection d’observer certains appels à l’API Windows, notamment CreateRemoteThread et LoadLibrary. En effet si on voulait injecter une bibliothèque sans se soucier de sa furtivitée, la méthode la plus commune serait d’effectuer la suite d’instruction suivante :

// Ouverture du programme cible avec son PID
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, 0, dwTargetPid);
// Récupération de l'adresse de la fonction LoadLibraryA pour créer un thread ayant
// LoadLibrary en entry point
LPVOID lpLoadLibraryAddress = (LPVOID)GetProcAddress(GetModuleHandle(L”kernel32.dll”), “LoadLibraryA”);
// Ecriture du chemin la DLL à injecter dans la mémoire du programme cible
char *dllName = "C:\\chemin\\injection.dll";
LPVOID dllNameAddress = (LPVOID)VirtualAllocEx(hProc, NULL, strlen(dllName), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(hProc, dllNameAddress, dllName, strlen(dllName), NULL);
// Execution d'un thread ayant pour but de charger la DLL
CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)lpLoadLibraryAddress, dllNameAddress, NULL, NULL);

Cette façon d’injecter possède un désavantage majeure, l’appel de LoadLibrary, responsable d’enregistrer le nom de la bibliothèque dans la liste des modules.

Ce que permet donc l’injection manuelle

L’injection manuelle permet de résoudre ce désavantage majeure tout simplement en écrivant un bout de code effectuant le meme travail que LoadLibrary sans effectuer l’enregistrement de la bibliothèque dans la structure du processus.

Les étapes d’un chargement de bibliothèque

La fonction de l’API Windows ne fait pas qu’ajouter le code dans la mémoire du processus, il effectue aussi d’autre actions notamment la relocation et la résolution des imports.

La ’relocation’

Une fois compilé, un programme assume son adresse de base. Par exemple, il estime que son adresse de base en mémoire sera 0x400000 et que toutes les autres adresses seront déduite en fonction du décalage avec cette adresse. Cependant, une fois la bibliothèque chargée, celle-ci sera le plus vraisemblablement chargée à une adresse mémoire aléatoire. Il faut donc modifier les adresses pour indiquer la vrai adresse après chargement en mémoire.

Les imports

Les fonctions importées sont des fonctions utilisées dans le module, mais n’y étant pas présentes. Une bibliothèque peut faire appel à des fonctions d’autre bibliothèque, et doit donc effectuer un travail d’importation.

Structure du format PE

Les executables et bibliothèques dynamiques Windows sont stockés dans un format appelé Portable Executable (PE). Ce format dispose notamment d’une en-tete au début du fichier, permettant notamment d’effectuer les étapes nécessaires au moment d’un chargement de bibliothèque.

pe-header.png
Figure 2: En-tete du format Portable Executable

Comment coder une injection manuelle ?

Concretement parlant, pour effecuter une injection manuelle, il faut:

  • Insérer le contenu de notre bibliothèque dans la mémoire du programme
  • Insérer un code de chargement de bibliothèque (effectuant relocation et import)

Des données necessaires au code de chargement devront lui être passé, ces données sont:

  • L’adresse où a été chargé la bibliothèque
  • L’adresse de LoadLibrary et GetProcAddress pour charger les dépendances (au moment de l’import, les bibliothèques nécessaires)

En pseudo code, voici à quoi cela ressemble.

fonction_principale()
{
    insertion_dll();
    insertion_injecteur();
}

insertion_dll(__out__ données)
{
    // Ouverture de la DLL et conversion en format PE
    // Allocation memoire dans le programme cible
    // La taille de la DLL est contenu dans les en-tete de la DLL
    // Ecriture des entete dans le programme cible
    // Ecriture des sections de code dans le programme cible

    // Les entete contiennent les informations sur les sections:
    // - l'adresse ou trouver la section dans la DLL
    // - l'adresse ou placer la section en memoire du programme cible
    // - la taille de la section

    // Cette fonction va remplir une structure de données contenant
    // toutes les information que necessite l'injecteur:
    // - L'adresse memoire de la DLL
    // - Les adresses de GetProcAdress et LoadLibrary
}

insertion_injecteur(__in__ données)
{
    // Allocation de memoire pour l'injecteur
    // Ecriture de la fonction injecteur dans le programme cible
    // Lancement d'execution de l'injecteur

    // Il est possible par exemple d'utiliser CreateRemoteThread mais ce
    // n'est pas l'option la plus discrete
}

injecteur()
{
    // Effectuer les relocations
    // Effectuer les imports
    // Effectuer la resolution des callbacks TLS
}

Pour l’étape de l’import des dépendances, il est aussi possible de ré-effectuer de l’injection manuelle au lieu d’utiliser LoadLibrary.

Limites et détection

Cette méthode n’est cependant pas optimale en discretion à elle seule. Par exemple en utilisant CreateRemoteThread dans le code, un nouveau thread est allouée à l’execution de notre code de bibliothèque. Ce thread peut apparaitre étrange en observant sa stacktrace, qui dispose de fonctions sans noms par rapport à un thread légitime Windows (pas de noms car pas de symboles).

threads2.png
Figure 3: Stacktrace du thread de la bibliothèque injectée
threads3.png
Figure 4: Stacktrace d’un thread légitime

Il est aussi possible de scanner la mémoire entière d’un processus, et d’identifier les en-tetes PE qui ne semblent pas enregistrée par le processus, signe évident d’une injection malicieuse. Une technique pourrait donc par exemple de supprimer le header après le chargement.

Ressources

Un tutoriel de 4 vidéos de 20 minutes montrer comment coder un injecteur manuel en C++ à été effectué par Guided Hacking. De plus le forum Guided Hacking propse aussi un post montrant toutes les techniques d’injection et des informations complémentaires sur les actions a effectué pour éviter d’appeler CreateRemoteThread.

Créé avec 💟 par GNU Emacs et 🦄 org mode