
Technique d'exécution/injection de code utilisant la manipulation de la structure du module PEB des DLL
Exécution furtive de code via la modification du EntryPoint des modules chargés à l'exécution.
Les processus Windows ont divers modules chargés à l'exécution. Chacun de ces modules possède une fonction DllMain() définie, qui sera appelée lors de la création/destruction d'un processus ou d'un thread (quatre scénarios possibles).
Afin d'appeler correctement ces fonctions pendant la durée de vie du processus, les fonctions du chargeur Windows (ntdll!Ldrp*) se réfèrent à une liste d'entrées contenant des paramètres clés (dont le champ EntryPoint) pour chaque module.
En écrasant ce EntryPoint pour une DLL, nous garantissons que l'exécution du code sera redirigée vers un endroit de notre choix.
Cela peut être utilisé à la fois comme une primitive d'exécution de code, et pour du proxy d'API, par exemple pour exécuter certaines API avec une pile d'appels non suspecte puisqu'elles seront invoquées par des fonctions Windows légitimes.
Cela peut également être utilisé pour déclencher l'exécution dans un processus distant, à condition que l'attaquant ait la capacité de lire et d'écrire la mémoire sur ce processus cible. De manière similaire à Threadless Injection, cela offre la capacité d'exécuter du code dans un processus sans invoquer les API classiques liées à l'exécution (CreateRemoteThread, QueueUserAPC).
Le chargement/déchargement des modules au sein d'un processus Windows est un sujet complexe qui présente de nombreux défis, des risques d'instabilité, de courses critiques et de plantages. Un obstacle bien connu lié à l'exécution de code dans une fonction DllMain() par exemple, réside dans le fait qu'un verrou du chargeur (Loader Lock) est en place et que nous tournons dans un thread qui n'a pas été complètement initialisé, ou qui est en cours de terminaison.
Par conséquent, j'ai essayé de documenter correctement ce qui est possible et ce qui ne l'est pas. Par exemple, bien que la plupart des appels API habituels puissent être effectués, exécuter un beacon complet nécessite certaines contraintes pour être dans un processus séparé, afin d'éviter les interblocages causés par les fonctions utilisées dans wininet.dll ou winhttp.dll.
Chaque processus maintient une liste de structures _LDR_DATA_TABLE_ENTRY à l'exécution. Ces structures contiennent de nombreux détails pertinents sur la DLL, tels que son EntryPoint (que nous allons écraser), son nom, certains hachages, des horodatages, divers indicateurs, etc. Certaines de ces structures sont documentées, d'autres non.
On peut les visualiser via cette commande WinDbg :
dt nt!_LDR_DATA_TABLE_ENTRY 0xdeadbeef
0:006> dt nt!_LDR_DATA_TABLE_ENTRY 0x18d1c4838c0
ntdll!_LDR_DATA_TABLE_ENTRY
+0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x0000018d`1c485de0 - 0x0000018d`1c4832b0 ]
+0x010 InMemoryOrderLinks : _LIST_ENTRY [ 0x0000018d`1c485df0 - 0x0000018d`1c4832c0 ]
+0x020 InInitializationOrderLinks : _LIST_ENTRY [ 0x0000018d`1c4832d0 - 0x0000018d`1c482c80 ]
+0x030 DllBase : 0x00007ffe`e87e0000 Void
+0x038 EntryPoint : 0x00007ffe`e8838d00 Void
+0x040 SizeOfImage : 0x2fe000
+0x048 FullDllName : _UNICODE_STRING "C:\WINDOWS\System32\KERNELBASE.dll"
+0x058 BaseDllName : _UNICODE_STRING "KERNELBASE.dll"
+0x068 FlagGroup : [4] "???"
+0x068 Flags : 0x8a2cc
+0x068 PackagedBinary : 0y0
+0x068 MarkedForRemoval : 0y0
+0x068 ImageDll : 0y1
(...)
+0x0e0 MappingInfoIndexNode : _RTL_BALANCED_NODE
+0x0f8 OriginalBase : 0x00007ffe`e87e0000
+0x100 LoadTime : _LARGE_INTEGER 0x01db5f86`2fa735fc
+0x108 BaseNameHashValue : 0x235bec4
+0x10c LoadReason : 0 ( LoadReasonStaticDependency )
+0x110 ImplicitPathOptions : 0x4000
+0x114 ReferenceCount : 1
+0x118 DependentLoadFlags : 0x800
+0x11c SigningLevel : 0 ''
L'adresse de la structure peut être obtenue en parcourant une structure doublement chaînée référencée dans le PEB du processus, dans une structure PEB_LDR_DATA.
dt nt!_PEB_LDR_DATA 0xb4b4c3c3
Notez le drapeau DontCallForThreads. Comme son nom l'indique, si ce drapeau est défini, le système d'exploitation n'appellera PAS le DllMain() de ce module pour les événements de thread (c'est-à-dire DLL_THREAD_ATTACH ou DLL_THREAD_DETACH).
Lors de la création d'une DLL, le modèle suivant doit être suivi pour fonctionner de concert avec les fonctions du chargeur OS :
BOOL WINAPI DllMain(
HINSTANCE hinstDLL, // handle to DLL module
DWORD fdwReason, // reason for calling function
LPVOID lpvReserved ) // reserved
{
// Perform actions based on the reason for calling.
switch( fdwReason )
{
case DLL_PROCESS_ATTACH:
// Initialize once for each new process.
// Return FALSE to fail DLL load.
break;
case DLL_THREAD_ATTACH:
// Do thread-specific initialization.
break;
case DLL_THREAD_DETACH:
// Do thread-specific cleanup.
break;
case DLL_PROCESS_DETACH:
if (lpvReserved != nullptr)
{
break; // do not do cleanup if process termination scenario
}
// Perform any necessary cleanup.
break;
}
return TRUE; // Successful DLL_PROCESS_ATTACH.
}
Comme décrit ci-dessus, la technique écrase temporairement le EntryPoint d'une DLL afin de rediriger l'exécution. Puisque nous n'avons aucun contrôle au-delà de la redirection de l'exécution, certains arrangements doivent être faits du côté pour gérer ce que nous voulons exécuter, avec quels arguments, et comment récupérer la valeur de retour.
Cela se fait en définissant une structure DATA_T sur le tas, de manière à ce qu'elle reste accessible tout au long des différentes étapes.
Cette structure est définie comme suit :
typedef struct _DATA_T {
// LDR structures manipulation
ULONG_PTR runner; // malicious entry point to execute
ULONG_PTR bakOriginalBase; // backup of overwritten OriginalBase
ULONG_PTR bakEntryPoint; // backup of overwritten EntryPoint
HANDLE event; // event signalling that the Runner has executed
// function call
ULONG_PTR ret; // return value
DWORD createThread; // run this API call in a new thread (required for wininet/winhttp)
ULONG_PTR function; // Windows API to call
DWORD dwArgs; // number of args
ULONG_PTR args[MAX_ARGS]; // array of args
} DATA_T, * PDATA_T;
Pour configurer une exécution d'API, ces champs doivent être préparés. La valeur ret est celle qui récupérera la valeur de retour après l'exécution. L'event est utilisé pour la synchronisation, afin de signaler que l'exécution est terminée. Tous les autres champs sont des entrées définissant quelle API appeler (function), avec quels arguments (dwArgs et args[]), l'adresse de la fonction Runner() où l'exécution est redirigée, et les sauvegardes des entrées originales de la DLL écrasée (bakOriginalBase et bakEntryPoint).
Le champ createThread doit être défini à 1 pour les fonctions API complexes qui ne fonctionnent pas bien dans une configuration DllMain() (cela inclut de nombreuses bibliothèques wininet et winhttp).
Voici un exemple de configuration d'un appel à MessageBoxA() comme visible dans le PoC :