Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
LdrShuffle — Technique d'exécution/injection de code utilisant la manipulation de la structure du module PEB des DLL | Kitploit
Outils/GitHubGitHub/rwxstoned/ldrshuffle
Analyse de CodeExploitationMouvement LatéralShellcodePost-ExploitationTests d'IntrusionRed TeamingDéveloppement de Charges UtilesExploitation de Binaires
GitHubrwxstoned/ldrshuffle

LdrShuffle

Technique d'exécution/injection de code utilisant la manipulation de la structure du module PEB des DLL

289441il y a 1 anVérifié par Kitploit

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager
Voir le dépôt

LdrShuffle

Exécution furtive de code via la modification du EntryPoint des modules chargés à l'exécution.

Résumé

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.

Cas d'utilisation

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).

Défis

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.

Implémentation

Rappel sur le chargement des DLL sous Windows

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

root@kitploit:~
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 :

root@kitploit:~
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.
}

Détails techniques sur l'implémentation

Configuration d'un appel API

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 :

root@kitploit:~
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 :

root@kitploit:~
 pDataT->dwArgs = 4;
 pDataT->runner = (ULONG_PTR)Runner;
 pDataT->function = (ULONG_PTR)MessageBoxA;
 pDataT->args[0] = (ULONG_PTR)0;
 pDataT->args[1] = (ULONG_PTR)"Hello";
 pDataT->args[2] = (ULONG_PTR)"LDRSHUFFLE";
 pDataT->args[3] = (ULONG_PTR)MB_OKCANCEL;
 pDataT->event = CreateEventA(NULL, FALSE, FALSE, "ExecEvt");

Modification du _LDR_DATA_TABLE_ENTRY

La fonction UpdateLdr() est responsable de l'exécution de la modification appropriée dans le _LDR_DATA_TABLE_ENTRY du module cible.

RestoreLdr() restaurera ces modifications à un stade ultérieur (invoquée par Runner()).

Ces fonctions localisent essentiellement le PEB et parcourent les structures de modules pour identifier la bonne DLL et ses champs. Dans les fichiers d'en-tête, je réutilise les définitions utilisées par Batsec dans son DarkLoadLibrary et j'encourage les lecteurs à consulter ce projet et le billet de blog MDSec associé pour bénéficier de l'excellent travail qu'il a effectué sur les mécanismes internes du chargement de modules sous Windows.

Remarque : ce PoC charge une DLL sacrificielle (SACRIFICIAL_DLL_NAME) et effectue ces modifications sur cette DLL. Cependant, il est tout à fait possible de modifier une DLL déjà chargée. C'est d'ailleurs l'approche adoptée pour l'injection inter-processus. Pour des raisons de stabilité, je recommande d'éviter de toucher aux DLL importantes comme ntdll ou kernel32, qui ont également tendance à être plus scrutées par les solutions de sécurité.

Exécution

Lors de la création ou de la destruction d'un thread, l'exécution est redirigée vers Runner(), qui agit comme un faux DllMain() pour le module. Cette fonction va alors :

  • localiser sur le tas où se trouve la structure de données utilisée pour exécuter les appels / récupérer la valeur de retour (structure PDATA_T)
  • restaurer le PEB RestoreLdr() à son état d'origine
  • effectuer l'appel normal DllMain() (en faisant essentiellement un proxy de l'appel DLL normal).

À ce stade, l'exécution "normale" du système d'exploitation a été effectuée. Elle continue ensuite avec nos charges utiles :

  • exécuter notre appel API malveillant, selon les valeurs et arguments stockés dans la structure DATA_T. Si cette API a été marquée pour s'exécuter dans un nouveau thread (createThread = 1), cet appel sera effectué dans un nouveau thread.
  • enfin, signaler un événement (pDataT->event) pour que notre code principal sache que l'appel a été effectué.

Lorsque Windows finit par invoquer notre faux EntryPoint (qui est l'adresse de la fonction Runner()), la pile d'appels ressemble à ceci :

Callstack on MessageBoxA()

Exemple de proxy d'API

Le PoC fourni contient un exemple invoquant MessageBoxA().

Il contient également une démonstration d'un téléchargement HTTP utilisant wininet. Définissez la variable HTTP pour activer ce code.

Callstack on MessageBoxA()

Exemple d'injection inter-processus

Les principes décrits ci-dessus se résument à lire et écrire dans l'espace mémoire du processus, afin de provoquer une exécution de code à un moment arbitraire du futur.

Avec quelques ajustements, ces opérations de lecture et d'écriture peuvent être appliquées à un processus distant pour écraser le EntryPoint de l'une de ses DLL.

Un prérequis est la capacité de lire et d'écrire dans l'espace mémoire du processus, c'est-à-dire :

OpenProcess(PROCESS_VM_READ, FALSE, dwPid) et OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE, PID)

Un projet supplémentaire est présent dans le PoC, appelé LdrInject, démontrant comment effectuer ces étapes. En résumé, il fait ce qui suit :

  • dans ReadPEB(), il parcourt la liste _LDR_DATA_TABLE_ENTRY du processus cible pour identifier une DLL appropriée à écraser. Notez que cette DLL doit avoir DontCallForThreads == 0 car nous souhaitons que Windows invoque le EntryPoint de cette DLL lors de la création d'un thread. Nous ne choisissons pas non plus les premières DLL de la liste car elles ont tendance à être plus scrutées par les produits de sécurité (ntdll.dll, kernel32.dll…).

  • les détails de cette DLL sont stockés dans une structure de données PEBINJ_DATA.

  • du shellcode (dans ce cas un beacon) est écrit dans l'espace du processus distant avec InjectShellcodeToRemoteProcess()

  • deux appels WriteProcessMemory() écrasent le EntryPoint de la DLL et le sauvegardent dans OriginalBase afin qu'il puisse être restauré ultérieurement.

À ce stade, le prochain événement DLL_THREAD_ATTACH ou DLL_THREAD_DETACH entraînera l'invocation du shellcode. Cela comporte certaines limitations et mises en garde dans le contexte de l'exécution d'un beacon, détaillées dans la section suivante.

Beacon Cobalt Strike

Cette technique entraîne l'exécution d'un shellcode dans une situation très spécifique. Le verrou du chargeur (Loader Lock) est actif (car le système d'exploitation pense être en train de charger/décharger une DLL) ; un thread est soit en cours de création, soit en cours de destruction ; et d'une manière générale, il existe un risque de problèmes de synchronisation de threads, d'interblocages, etc.

Lors des tests, deux défis ont été observés :

  • l'exécution d'un beacon Cobalt Strike typique entraînerait un interblocage lors de l'utilisation d'API dans wininet.dll ou winhttp.dll.

  • l'exécution lors de la destruction d'un thread provoque des problèmes de stabilité car nous tournons dans un thread en cours de destruction.

Pour augmenter la stabilité, nous devons :

  • nous assurer que le beacon s'exécutera dans un nouveau thread. Par conséquent, l'UDRL fera un CreateThread avant d'invoquer le point d'entrée habituel de la DLL réflexive de Cobalt Strike.

  • ne s'exécuter que dans un thread en cours de création, pas dans un thread mourant. Pour ce faire, nous nous assurons que lorsque le EntryPoint est appelé par le système d'exploitation, la raison invoquée est fdwReason == DLL_THREAD_ATTACH :

winApi.CreateThread(NULL, 4096, (LPTHREAD_START_ROUTINE)&runner, (LPVOID)&ct_data, 0, &dwThId);

au lieu de l'habituel

((DLLMAIN)entryPoint)((HINSTANCE)loaderStart, 4, NULL);

root@kitploit:~
    ULONG_PTR __cdecl ReflectiveLoader(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
        // only run for a Thread creation event
        
        if (fdwReason != DLL_THREAD_ATTACH) {
            return TRUE;
        }

        ...
    }

Ces deux étapes supplémentaires ont été intégrées dans une démo pour un UDRL.

Tests

Liste des API testées pour LdrShuffle

VirtualAlloc

VirtualProtect

CreateThread

Sleep

MessageBoxA

InternetOpenW (doit s'exécuter avec createThread = 1)

InternetOpenUrlA (doit s'exécuter avec createThread = 1)

TODO

  • Continuer à tester davantage d'API pour LdrShuffle
Télécharger l’outil