Introduction
Le chargement latéral de DLL (« DLL side loading ») sert depuis un bon moment à obtenir l’exécution de code dans un processus signé et fiable. Le sujet a été largement abordé dans d’autres blogues, et je vous invite à les lire d’abord, car je ne les répéterai pas ici. La façon la plus simple et la plus répandue consiste à placer du code dans DllMain pour faire une injection distante de code machine dès que la DLL est chargée dans un processus.
L’injection distante est devenue de moins en moins facile face aux bons EDR, mais elle reste réalisable avec des techniques récentes, par exemple la façon dont « Pool Party » s’y prend actuellement.
L’exécution locale du code machine dans le processus d’origine, en sautant à son début, demeure une approche plus stable et plus prudente sur le plan de la sécurité opérationnelle dans la plupart des scénarios. Elle comporte toutefois une limite lorsqu’on fait du chargement latéral: le « loader lock ». Se trouver à l’intérieur de ce verrou revient essentiellement à être considérablement restreint quant aux fonctions que l’on peut appeler dans cet état.

Source de la figure 1: https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-best-practices
En effet, la plupart des codes machine de commande et contrôle, comme celui de Cobalt Strike, tenteront de charger d’autres DLL pendant leur initialisation, ce qui n’est ni permis ni recommandé à l’intérieur du loader lock. Le résultat, la plupart du temps: un processus bloqué et aucun accès distant.
Quelles sont donc nos options? Certains créent un fil d’exécution distinct et y exécutent leur code avec succès, mais Microsoft ne recommande pas cette solution, puisque selon ce qui est fait dans ce fil, cela peut créer un interblocage ou un plantage. Personnellement, je n’utilise jamais DllMain pour le chargement latéral: je préfère réimplémenter et exporter la première méthode exportée par la DLL légitime et appelée par le processus visé. C’est particulièrement utile lorsqu’on ne veut pas que le programme d’origine affiche le moindre indicateur visuel à l’utilisateur possiblement connecté, et lorsque détourner complètement les fonctions du programme est l’objectif.
Ce billet présente la méthode que j’emploie pour trouver de bons candidats et pour contourner les problèmes qui peuvent survenir à la compilation.
Trouver des candidats au chargement latéral
D’abord, je lance Process Monitor avec les paramètres habituels pour repérer les DLL qui sont cherchées en premier dans le répertoire courant, sans y être trouvées.

Ensuite, j’exécute des binaires signés arbitraires sur mon système d’essai jusqu’à voir une DLL manquante que le programme tente de charger depuis le dossier de test:

Les candidats idéaux sont des binaires signés qui peuvent être lancés sans exiger des dizaines d’autres fichiers dans la même arborescence, ce qui les rend plus portables.
Dans cet exemple, j’ai trouvé un binaire de HP qui semblait vulnérable au chargement latéral lorsqu’on plaçait une VERSION.dll sur mesure dans le même dossier. Sur le plan de la sécurité opérationnelle, cependant, version.dll est une cible courante des détections et devrait être évitée dans un scénario réel, si possible.
Trouver des fonctions exportées détournables
Ensuite, avec API Monitor (certains préfèrent Frida ici, mais dans ce cas je trouve cet outil plus simple), il est possible de voir quelles fonctions le processus signé d’origine appelle dans la DLL visée, en filtrant sur les appels à GetProcAddress et à LoadLibrary. Ici, trois fonctions étaient recherchées dans la DLL visée.

Dans les cas d’usage décrits au début de ce billet, l’idéal est de détourner la première fonction appelée et de sauter directement au code machine, ce qui évite d’avoir à réimplémenter quoi que ce soit de la DLL d’origine.
Ici, la documentation de Microsoft sur VerQueryValueW permet de confirmer laquelle est appelée en premier: « Récupère l’information de version demandée dans la ressource d’information de version indiquée. Pour récupérer la bonne ressource, avant d’appeler VerQueryValue, vous devez d’abord appeler la fonction GetFileVersionInfoSize, puis la fonction GetFileVersionInfo. » Cela correspond aussi à ce que montre API Monitor.
Écrire la charge utile
À ce stade, je connais le nom à donner à ma DLL et le nom de la fonction exportée à utiliser. Il est temps d’écrire le code malveillant et de le compiler.
Pour cela, seule compte la reproduction fidèle de la signature de la fonction d’origine. Ici, comme elle provient d’une DLL système, la signature se trouve dans MSDN.

Par préférence personnelle, l’exemple qui suit utilise l’équivalent en Nim. Voici le code final:

Dans cette version, rien n’est fait dans DllMain à l’initialisation, mais le code d’injection locale s’exécuterait peu après l’appel à « GetFileVersionInfoSizeW ».
Après la compilation, il est bon de vérifier que la DLL exporte correctement la fonction voulue:

Déjouer le compilateur
Parfois, comme c’est le cas dans cet exemple, le compilateur empêchera la compilation parce que le nom de la fonction exportée est déjà défini dans l’un de ses fichiers:

La façon simple de contourner ce problème est de commenter temporairement cette fonction dans le fichier du compilateur, puis de réessayer. En compilation croisée depuis Linux, le fichier à modifier est: /usr/share/mingw-w64/include/winver.h

Les fichiers obtenus:

Conclusion
Avec cette méthode, je trouve qu’il devient trivial de réussir un chargement latéral de DLL dans le processus d’origine, tout en évitant systématiquement DllMain et sa limite d’interblocage.
Si vous vous êtes déjà buté à cette difficulté, ou si vous commencez tout juste à explorer le chargement latéral de DLL, j’espère que cela vous fera gagner du temps. Bons accès distants!
![]()
