Skip to content

Leçon 5.2 · Windows pour analystes· 40 min

Processus, threads et DLL

Processus, threads et DLL vus par un observateur — PEB, TEB, jetons, handles, arbres de processus et modules — et comment les lire comme des preuves.

Objectifs

  • Décrire ce que possède un processus (espace d’adressage, table des handles, jeton, PEB) et ce que possède un thread (contexte, piles, TEB)
  • Inspecter processus, threads, handles et modules avec Process Explorer, System Informer et Process Monitor
  • Lire les relations parent/enfant et les lignes de commande comme éléments d’analyse, et savoir où elles peuvent mentir
  • Reconnaître la famille CreateProcess et les options qui changent ce que vous devez vous attendre à observer
  • Expliquer comment les DLL arrivent dans un processus et repérer les modules ou threads qui n’ont rien à y faire

Comment un binaire est chargé et exécuté a suivi un exécutable de CreateProcess jusqu’à main. Cette leçon change de point de vue. Vous n’êtes plus le loader ; vous êtes l’analyste qui observe une machine sur laquelle un échantillon vient de s’exécuter. Ce que vous voyez, c’est un ensemble de processus, chacun contenant des threads et des modules, reliés entre eux par des relations parent/enfant et par des handles. Presque toutes les observations comportementales que vous rapporterez — « il a lancé PowerShell », « il s’est injecté dans explorer.exe », « il a chargé une DLL depuis %TEMP% » — portent sur ces trois objets. Savoir ce qu’est chacun d’eux, et lesquels de ses attributs sont fiables, c’est ce qui transforme une capture d’écran d’un arbre de processus en preuve.

Ce qu’est un processus

Un processus est un conteneur. Il n’exécute pas de code lui-même ; ce sont ses threads qui le font. Ce que le processus détient, c’est tout ce que ces threads partagent :

ComposantCe que c’estCe que vous cherchez
Espace d’adressage virtuelCarte mémoire privée : l’image, les DLL, les tas, les piles, les autres allocationsDes régions exécutables qui ne sont adossées à aucun fichier sur disque
Table des handlesLes références ouvertes du processus vers des objets noyau : fichiers, clés de registre, autres processus, threads, mutex, sectionsHandles vers d’autres processus, mutex nommés, fichiers à des emplacements inhabituels
Jeton d’accèsL’identité de sécurité : SID de l’utilisateur, groupes, privilèges, niveau d’intégritéSous quelle identité il tourne, s’il est élevé, quels privilèges sont activés
PEBProcess Environment Block en mode utilisateur : base de l’image, listes des modules chargés, paramètres du processusLigne de commande, chemin de l’image, répertoire courant, environnement
Au moins un threadCe qui s’exécute réellementL’endroit où chaque thread a démarré
IdentifiantsID de processus (PID), PID parent, ID de session, heure de créationPosition dans l’arbre, chronologie

Deux couches décrivent chaque processus. Le noyau tient son propre registre (une structure EPROCESS) que le code en mode utilisateur ne peut pas toucher. Le PEB, en revanche, vit dans l’espace d’adressage du processus lui-même, où le processus — ou quiconque peut écrire dans sa mémoire — peut le modifier. Cette séparation compte pour un observateur : une valeur qu’un outil lit dans le PEB (la ligne de commande, la liste des modules, le chemin d’image affiché dans certaines colonnes) n’est pas plus fiable que le processus lui-même. Les champs du PEB que vous avez rencontrés dans la leçon sur le chargement (BeingDebugged, ImageBaseAddress, Ldr) côtoient un autre champ qui compte ici :

Champ du PEB x64OffsetPourquoi les analystes s’y intéressent
ProcessParameters0x20Pointeur vers RTL_USER_PROCESS_PARAMETERS : ImagePathName, CommandLine, CurrentDirectory, Environment

Le jeton et les niveaux d’intégrité

Chaque processus porte un jeton d’accès principal. Pour le triage, quatre éléments méritent d’être lus dans l’onglet Security de Process Explorer ou de System Informer :

  • Utilisateur. NT AUTHORITY\SYSTEM, un compte de service ou l’utilisateur connecté.
  • Niveau d’intégrité. Low (moteurs de rendu en sandbox (bac à sable), processus en mode protégé), Medium (programmes utilisateur normaux), High (élevé, « Exécuter en tant qu’administrateur »), System (services et processus centraux de l’OS). Un échantillon qui a démarré en Medium et qui a ensuite un enfant en intégrité High sans invite UAC dans votre session vous montre un contournement de l’UAC.
  • Privilèges. SeDebugPrivilege activé dans un programme ordinaire signifie qu’il compte ouvrir des processus d’autres utilisateurs ou du système. La plupart des privilèges sont présents mais désactivés jusqu’à ce que le code appelle AdjustTokenPrivileges.
  • Groupes. L’appartenance à BUILTIN\Administrators, et si ce groupe est activé ou « deny-only » (un jeton administrateur non élevé).

La table des handles

Un handle est un numéro local au processus qui désigne un objet noyau. Les handles sont les relations actuelles du processus, ce qui en fait une vue en direct de ce qu’il est en train de faire. Dans l’onglet Handles de System Informer (ou dans le panneau inférieur de Process Explorer en vue handles, Ctrl+H), les types qui comptent le plus souvent sont :

  • Les handles Process et Thread vers d’autres processus — un préalable pour lire ou écrire leur mémoire. Un échantillon qui détient un handle à accès complet sur explorer.exe mérite un examen attentif.
  • Mutant (un mutex) nommé — souvent un marqueur « ne s’exécuter qu’une fois » propre à une famille, et un bon IOC (indicateur de compromission) ; la leçon Mutex, événements et communication inter-processus les traite.
  • Les handles File et Key — les fichiers qu’il garde ouverts (journaux, charges utiles préparées) et les clés de registre qu’il surveille ou écrit.
  • Les handles Section — mémoire partagée, y compris les fichiers mappés et la mémoire partagée entre processus.

Les handles sont éphémères. Un programme qui ouvre une clé, écrit une valeur et la referme ne laisse aucun handle à trouver ; c’est à cela que sert le journal d’événements de Process Monitor.

Ce qu’est un thread

Un thread est l’unité qu’exécute l’ordonnanceur. Chaque thread possède :

  • un contexte — l’état des registres sauvegardé (RIP, RSP, registres généraux, flags) que le noyau restaure quand le thread est ordonnancé ;
  • une pile en mode utilisateur dans l’espace d’adressage du processus et une pile noyau distincte (voir la pile) ;
  • un Thread Environment Block (TEB), pointé par gs en x64 (fs en x86, voir les registres de segment) ;
  • un ID de thread (TID), une priorité et une adresse de départ.
Champ du TEB x64OffsetContenu
NtTib.StackBase / StackLimit0x08 / 0x10Bornes de la pile utilisateur du thread
NtTib.Self0x30Adresse linéaire du TEB lui-même
ClientId0x40PID et TID de ce thread
ProcessEnvironmentBlock0x60Pointeur vers le PEB
LastErrorValue0x68Ce que renvoie GetLastError

C’est pourquoi mov rax, gs:[0x60] dans le désassemblage signifie « récupérer le PEB », et gs:[0x30] signifie « récupérer mon propre TEB ».

L’adresse de départ est l’attribut de thread le plus utile

Quand un thread est créé, le noyau enregistre l’endroit où on lui a demandé de commencer. Les outils l’affichent comme adresse de départ, résolue en module!fonction+offset lorsqu’elle tombe dans une image chargée. Sur un processus sain, vous verrez des motifs comme :

text
  TID    Start address
  4812   sample.exe+0x14e0                 main thread, the image's entry point
  6120   ntdll.dll!TppWorkerThread         thread-pool worker
  6344   combase.dll!CRpcThreadCache::…    COM plumbing

Celle qui doit vous arrêter, c’est une adresse de départ qui ne se résout dans aucun module — une adresse hexadécimale nue à l’intérieur d’une région mémoire privée et exécutable. Le code légitime démarre presque toujours ses threads sur des fonctions situées dans des images ; du code écrit en mémoire à l’exécution n’a aucune image à laquelle appartenir. Cette seule observation est l’indice côté hôte le plus courant de shellcode, de charges utiles dépaquetées et de code injecté (voir l’injection de DLL réflective et l’injection d’APC). Deux réserves :

  • L’adresse de départ est enregistrée à la création. Un thread démarré sur une fonction légitime puis redirigé par SetThreadContext conserve son adresse de départ innocente — c’est l’essence du détournement d’exécution de thread.
  • Les environnements JIT (.NET, navigateurs, Java) exécutent légitimement du code depuis de la mémoire privée. Jugez les threads non adossés à un fichier en fonction de la nature du processus.

Pour aller au-delà de l’adresse de départ, ouvrez la pile d’un thread dans Process Explorer ou System Informer. Avec les symboles configurés, la pile d’appels montre où se trouve le thread maintenant — en attente dans NtDelayExecution (en sommeil), NtWaitForSingleObject ou une réception sur socket — ce qui suffit souvent à dire ce qu’attend un implant inactif.

Outils d’observation, sans débogueur noyau

Tout ce qui figure dans cette leçon peut être observé avec des outils gratuits en mode utilisateur. WinDbg devient utile plus tard ; vous n’en avez pas besoin pour lire un arbre de processus.

OutilIdéal pour
Process Explorer (Sysinternals)Arbre en direct, lignes de commande, signatures des images, recherche VirusTotal, threads et piles, vues DLL et handles (Ctrl+D / Ctrl+H)
System Informer (anciennement Process Hacker)Les mêmes vues en direct, plus un onglet Memory détaillé (type de région, protection, adossement ou non à une image), l’édition des jetons et le détail par module
Process Monitor (Sysinternals)Historique enregistré : démarrage et fin des processus, chargements d’images, événements fichier, registre et réseau. Tools → Process Tree montre les processus déjà terminés
SysmonJournal d’événements persistant et interrogeable : Event ID 1 (création de processus), 5 (fin), 7 (image chargée), 8 (CreateRemoteThread), 10 (accès à un processus)
ListDLLs / Handle (CLI Sysinternals)Listes de modules et de handles scriptables ; listdlls -u ne liste que les DLL non signées
PowerShell / CIMExtractions texte rapides du PID, du PID parent et de la ligne de commande

Pour obtenir rapidement un arbre depuis un shell :

powershell
Get-CimInstance Win32_Process |
  Select-Object ProcessId, ParentProcessId, Name, CommandLine |
  Sort-Object ParentProcessId | Format-Table -AutoSize -Wrap

Quelques habitudes dans Process Explorer sont immédiatement rentables :

  • Activez Options → Verify Image Signatures et ajoutez les colonnes Verified Signer, Command Line et Integrity.
  • Apprenez le code couleur : rose pour les services, bleu clair pour les processus appartenant à votre utilisateur, violet pour les images que Process Explorer estime heuristiquement packées, et des flashs vert/rouge pour les processus qui démarrent et se terminent.
  • Les outils en direct ne montrent que le présent. Les échantillons qui lancent un processus auxiliaire pendant deux secondes puis se terminent y sont invisibles — laissez Process Monitor ou Sysmon enregistrer avant de lancer quoi que ce soit. La prochaine leçon, Surveillance comportementale, bâtit une routine de capture complète sur ce principe.

La création de processus comme preuve

Chaque processus a un parent

Quand un processus est créé, le noyau enregistre le PID du créateur comme PID parent du nouveau processus. Les outils dessinent l’arbre à partir de ce seul nombre. L’arbre a de la valeur parce que les logiciels légitimes ont des parents prévisibles, ce qui n’est pas le cas de la plupart des chaînes de distribution de malware (logiciel malveillant) :

text
  expected                                  suspicious
  ───────────────────────────────────       ──────────────────────────────────────
  services.exe ─► svchost.exe -k …          winword.exe ─► cmd.exe ─► powershell.exe
  wininit.exe  ─► services.exe, lsass.exe   excel.exe   ─► rundll32.exe  %TEMP%\a.dll
  explorer.exe ─► chrome.exe, notepad.exe   outlook.exe ─► mshta.exe http://…
  svchost.exe  ─► taskhostw.exe             wscript.exe ─► powershell.exe -enc …
  (Schedule)                                explorer.exe ─► svchost.exe
                                            svchost.exe  (in C:\Users\…\AppData\)

Motifs à vérifier sur chaque arbre :

  • Office, lecteurs PDF, navigateurs et clients de messagerie qui lancent des shells ou des hôtes de scripts (cmd.exe, powershell.exe, wscript.exe, cscript.exe, mshta.exe, rundll32.exe, regsvr32.exe). C’est la signature d’un document malveillant ou d’un exploit, et un grand classique des règles de détection.
  • Des noms de processus système avec le mauvais parent, le mauvais chemin ou le mauvais nombre d’instances. Un svchost.exe dont le parent n’est pas services.exe ; un lsass.exe ailleurs que dans C:\Windows\System32 ou en plus d’un exemplaire ; des noms trompeurs comme scvhost.exe ou lsasss.exe.
  • Des auxiliaires éphémères. Des enfants cmd.exe /c qui exécutent une commande puis se terminent, souvent pour de la reconnaissance (whoami, ipconfig /all, net group) ou du nettoyage.
  • L’échantillon qui se relance lui-même depuis un nouvel emplacement (%APPDATA%, %TEMP%, ProgramData), souvent avec un argument qui indique à la seconde copie qu’elle est l’instance « installée ».

Connaissez aussi les bizarreries normales. userinit.exe lance explorer.exe puis se termine, si bien qu’explorer.exe n’a normalement aucun parent vivant. smss.exe lance des copies de lui-même par session qui démarrent csrss.exe et winlogon.exe puis se terminent. Les processus lancés via WMI apparaissent sous WmiPrvSE.exe, les tâches planifiées sous le svchost.exe qui héberge le service Schedule, et les serveurs COM sous le svchost.exe qui héberge DcomLaunch — un lancement indirect casse donc délibérément la chaîne que vous suivriez autrement.

Là où le PID parent peut mentir

Le PID parent est un nombre enregistré une seule fois, à la création. Trois choses le fragilisent :

  1. La réutilisation des PID. Quand le parent se termine, son PID finit par être réutilisé. Un nouveau processus sans rapport peut alors ressembler au parent. Process Explorer compare les heures de création avant d’imbriquer ; quand vous corrélez à la main, faites de même, ou utilisez les ProcessGuid/ParentProcessGuid de Sysmon, qui ne sont jamais réutilisés.
  2. L’usurpation du PID parent. Un créateur peut désigner comme « parent » n’importe quel processus qu’il peut ouvrir, via l’attribut PROC_THREAD_ATTRIBUTE_PARENT_PROCESS d’une structure STARTUPINFOEX. L’enfant apparaît alors sous, par exemple, explorer.exe, hérite à certains égards du contexte de jeton de ce processus, et le vrai créateur disparaît de l’arbre. L’Event 1 de Sysmon enregistre le parent usurpé ; le fournisseur ETW de processus du noyau enregistre le vrai créateur dans l’en-tête de l’événement, et c’est ainsi que les EDR le repèrent (ATT&CK T1134.004).
  3. Le lancement indirect. WMI, les tâches planifiées, les services et COM font volontairement d’un processus système le parent, comme décrit plus haut.

Cette même relation de parenté est aussi utilisée contre vous : des échantillons vérifient si leur propre parent est explorer.exe et restent dormants s’il s’agit d’un débogueur ou d’un lanceur de sandbox — voir la détection du processus parent.

Les lignes de commande sont des IOC

Une ligne de commande est souvent l’indicateur le plus spécifique que vous donne un processus. C’est là qu’apparaissent en clair les charges utiles encodées, les chemins des fichiers déposés, les URL de C2 et les noms de fonctions exportées :

text
powershell.exe -nop -w hidden -enc SQBFAFgAIAAoAE4AZQB3AC0ATwBi…   base64 of UTF-16LE script
rundll32.exe C:\Users\a\AppData\Local\Temp\x4.dat,DllRegisterServer  DLL with a non-DLL extension
regsvr32.exe /s /n /u /i:http://… scrobj.dll                     remote scriptlet
cmd.exe /c ping 127.0.0.1 -n 3 > nul & del "C:\…\sample.exe"     delay, then self-delete
vssadmin.exe delete shadows /all /quiet                          ransomware pre-encryption step
schtasks.exe /create /sc minute /mo 15 /tn "Updater" /tr "…"     scheduled-task persistence

Deux points de précision :

  • Capturez-la à la création. L’Event 1 de Sysmon et l’événement de sécurité 4688 (avec la stratégie Include command line in process creation events activée) enregistrent la ligne de commande reçue par le noyau. Les outils en direct la lisent généralement dans les ProcessParameters du PEB au moment où vous regardez — et comme le PEB est modifiable, un processus peut écraser sa propre ligne de commande après son démarrage. Si la valeur en direct et la valeur journalisée diffèrent, cette différence est en soi une trouvaille.
  • Normalisez avant de comparer. -enc, -EncodedCommand, -e et -ec sont le même commutateur PowerShell ; ^ et " peuvent être parsemés dans les lignes de commande cmd.exe sans en changer le sens. Les détections fondées sur des chaînes exactes sont faciles à contourner.

La famille CreateProcess

Toutes les manières de démarrer un programme en mode utilisateur aboutissent à l’appel natif NtCreateUserProcess vu dans la leçon sur le chargement, mais l’API qu’utilise un échantillon vous dit ce qu’il cherche à faire et ce que vous devez vous attendre à voir :

APICe qu’elle faitCe que vous observez
CreateProcessA/WLe cas général : image, ligne de commande, options, informations de démarrageEnfant directement sous l’échantillon
CreateProcessAsUserWIdem, avec un jeton fourni par l’appelantEnfant tournant sous un autre utilisateur ou un autre niveau d’intégrité
CreateProcessWithTokenW, CreateProcessWithLogonWDémarrer sous un jeton dupliqué ou de nouveaux identifiants ; le travail est fait par le service Secondary LogonEnfant dont le jeton diffère de celui de l’appelant
ShellExecute(Ex)WPasser par le shell : associations de fichiers, URL, verbe runas pour l’élévationLe gestionnaire associé démarre (par exemple .js → wscript.exe) ; runas déclenche une invite UAC
WinExec, system() / _popen de la CRTWrappers historiques ou de commodité ; system exécute cmd.exe /c …Une couche cmd.exe supplémentaire dans l’arbre
WMI Win32_Process.Create, Planificateur de tâches, création de serviceDemander à un autre composant de démarrer le processusLe parent est WmiPrvSE.exe, svchost.exe ou services.exe

Les options de création et les informations de démarrage changent l’apparence de l’enfant :

  • CREATE_SUSPENDED (0x4) — le thread initial de l’enfant ne s’exécute pas avant ResumeThread. Il existe des usages légitimes, mais un enfant suspendu d’un binaire système courant, créé par un programme inconnu, est le coup d’ouverture du process hollowing. Dans un outil en direct, l’enfant montre tous ses threads à l’état Suspended.
  • CREATE_NO_WINDOW (0x08000000) et DETACHED_PROCESS (0x8), ou STARTF_USESHOWWINDOW avec SW_HIDE — exécuter un programme console sans fenêtre visible.
  • EXTENDED_STARTUPINFO_PRESENT (0x00080000) — une STARTUPINFOEX avec une liste d’attributs suit ; cherchez-y PROC_THREAD_ATTRIBUTE_PARENT_PROCESS (usurpation du parent) ou des attributs de politique d’atténuation, comme le blocage des DLL non Microsoft.
  • lpApplicationName = NULL avec un chemin sans guillemets contenant des espaces — Windows essaie chaque préfixe délimité par un espace, si bien que C:\Program Files\App\app.exe peut d’abord essayer C:\Program.exe. À remarquer à la fois comme bug exploité par les malwares et comme artefact sur disque.

Quand vous voyez ces API dans les imports d’un échantillon (voir Lire les capacités dans les imports), l’argument des options est la première chose à retrouver dans le désassembleur.

Les DLL : du code qui vit dans le processus d’un autre

Une DLL est une image PE dotée d’une table d’exports, chargée dans un processus plutôt qu’exécutée comme tel. Elle n’a ni processus, ni PID, ni jeton propres ; elle s’exécute sur les threads du processus hôte avec les privilèges de l’hôte. C’est précisément pour cela que les auteurs de malware l’apprécient : l’activité d’une DLL malveillante est attribuée, dans la plupart des télémétries, au processus qui l’a chargée.

Comment une DLL arrive dans un processus

CheminQuandObservable sous la forme
Import statiqueAu chargement, par le loader, depuis la table des importsPrésente dès le départ ; listée dans les imports du PE
LoadLibrary(Ex)À l’exécution, quand le code le demandeÉvénement de chargement d’image après le démarrage du processus (Procmon Load Image, Sysmon Event 7)
Chargement différéAu premier appel à l’une de ses fonctionsComme LoadLibrary, mais listée dans le répertoire des imports différés
Un binaire hôterundll32.exe dll,Export, regsvr32.exe dll (appelle DllRegisterServer), une DLL de service sous svchost.exeUn binaire Windows signé dont la ligne de commande nomme la DLL
Abus de l’ordre de rechercheUn programme légitime charge une DLL plantée portant le nom attenduEXE signé dans un dossier accessible en écriture, DLL à côté — voir le détournement de l’ordre de recherche des DLL
Mappage manuelDu code copie et reloge lui-même une DLL, sans le loaderRien dans la liste des modules — voir plus bas

Quand une DLL est chargée, le loader appelle son point d’entrée, DllMain, avec DLL_PROCESS_ATTACH — puis avec DLL_THREAD_ATTACH / DETACH pour chaque thread, et DLL_PROCESS_DETACH au déchargement. Une DLL malveillante n’a donc pas besoin d’être « appelée » : être chargée suffit pour exécuter du code, tout comme les callbacks TLS s’exécutent avant le point d’entrée d’un exécutable. Comme DllMain s’exécute pendant que le loader détient un verrou, un code bien élevé y démarre un thread et rend la main ; un nouveau thread dont l’adresse de départ se trouve dans une DLL fraîchement chargée en est la trace habituelle.

Pour les appels à LoadLibrary qui nomment la DLL sans chemin complet, Windows cherche dans un ordre fixe. Avec le mode de recherche sûr (par défaut), l’ordre pour les applications de bureau est à peu près :

text
  1. DLL redirection / API sets / already-loaded modules
  2. KnownDLLs (HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs)
  3. the directory the application was loaded from
  4. C:\Windows\System32
  5. C:\Windows\System (16-bit)
  6. C:\Windows
  7. the current directory
  8. directories on %PATH%

L’étape 3 explique pourquoi le DLL side-loading fonctionne : copiez un exécutable légitime et signé dans un dossier accessible en écriture par l’utilisateur, déposez à côté une DLL malveillante portant le nom qu’il attend, et le loader choisit celle qui a été plantée. De l’extérieur, vous voyez un processus de confiance et signé — dont la liste des modules contient une DLL non signée provenant du même dossier inhabituel.

Lire la liste des modules

Ouvrez la vue DLL (Process Explorer Ctrl+D, onglet Modules de System Informer, ou listdlls -u <pid>) et posez-vous, pour chaque module, les questions suivantes :

  • Chemin. Les DLL système viennent de C:\Windows\System32 (ou de SysWOW64 pour les processus 32 bits) et de WinSxS. Tout ce qui se trouve sous %TEMP%, %APPDATA%, ProgramData ou dans le dossier de l’échantillon doit avoir une raison d’être.
  • Signature. Les modules non signés dans un processus Microsoft signé sont rares. Activez la vérification des signatures et triez par Verified Signer.
  • Nom ou emplacement. Une version.dll ou une winhttp.dll chargée depuis le dossier de l’application plutôt que depuis System32, c’est le motif du side-loading.
  • Moment du chargement. Dans Procmon, une DLL chargée plusieurs minutes après le démarrage, juste avant une activité réseau, vous indique quand une capacité est arrivée.

La liste des modules elle-même provient des listes Ldr du PEB — des données en mode utilisateur — si bien qu’un échantillon déterminé peut en délier une entrée pour la cacher, et qu’une DLL mappée manuellement n’y figure jamais. La vérification croisée, c’est la carte mémoire : l’onglet Memory de System Informer étiquette chaque région comme Image (adossée à un fichier, mappée par le loader), Mapped ou Private. Une région Private exécutable qui commence par MZ, ou vers laquelle pointe l’adresse de départ d’un thread, est un module dont la liste ne vous parle pas. Les prochaines leçons Comprendre l’injection de processus et Dumper la mémoire et extraire les charges utiles s’appuient précisément sur cette vérification.

Lab : construire un petit arbre de processus et le lire

Ce lab utilise des programmes bénins que vous compilez vous-même. Exécutez-les dans votre VM d’analyse Windows de Construire un laboratoire d’analyse sûr, avec Process Explorer, System Informer et Process Monitor installés (FLARE-VM inclut les trois). Compilez sur n’importe quelle machine disposant de mingw-w64.

  1. Créez une petite DLL avec un DllMain et un export compatible rundll32 :

    c
    // labdll.c — announces itself when loaded; Hello() is callable via rundll32
    #include <windows.h>
    
    BOOL WINAPI DllMain(HINSTANCE inst, DWORD reason, LPVOID reserved) {
        if (reason == DLL_PROCESS_ATTACH)
            OutputDebugStringA("labdll: DLL_PROCESS_ATTACH");
        return TRUE;
    }
    
    __declspec(dllexport) void CALLBACK Hello(HWND hwnd, HINSTANCE inst,
                                               LPSTR args, int show) {
        MessageBoxA(NULL, args && *args ? args : "hello from labdll",
                    "labdll", MB_OK);
    }
  2. Créez un lanceur qui démarre un enfant avec une fenêtre masquée, exécute un second thread, charge la DLL à l’exécution puis attend pour que vous puissiez l’inspecter :

    c
    // lab.c — spawns a child, starts a worker thread, loads labdll.dll, waits
    #include <windows.h>
    #include <stdio.h>
    
    static DWORD WINAPI worker(LPVOID arg) {
        Sleep(INFINITE);                      /* idle thread to find later */
        return 0;
    }
    
    int main(void) {
        char cmd[] = "cmd.exe /c ping -n 120 127.0.0.1 > nul";
        STARTUPINFOA si = { .cb = sizeof(si) };
        PROCESS_INFORMATION pi;
    
        if (!CreateProcessA(NULL, cmd, NULL, NULL, FALSE, CREATE_NO_WINDOW,
                            NULL, NULL, &si, &pi))
            return 1;
        printf("lab pid %lu, child cmd.exe pid %lu\n",
               GetCurrentProcessId(), pi.dwProcessId);
        CloseHandle(pi.hThread);              /* keep hProcess open on purpose */
    
        HANDLE t = CreateThread(NULL, 0, worker, NULL, 0, NULL);
        HMODULE dll = LoadLibraryA("labdll.dll");
        printf("worker thread %lu, labdll at %p\n", GetThreadId(t), (void *)dll);
    
        puts("press Enter to exit");
        getchar();
        CloseHandle(pi.hProcess);
        return 0;
    }
  3. Compilez les deux et copiez-les dans le même dossier de la VM :

    bash
    x86_64-w64-mingw32-gcc -shared -s -o labdll.dll labdll.c -luser32
    x86_64-w64-mingw32-gcc -s -o lab.exe lab.c
  4. Lancez d’abord l’enregistrement. Ouvrez Process Monitor et videz le journal. Laissez-le sans filtre pour l’instant ; vous affinerez la vue ensuite avec Tools → Process Tree et des filtres. Lancez aussi DebugView si vous l’avez, pour voir le message OutputDebugString émis par DllMain.

  5. Exécutez lab.exe depuis une invite de commandes. Laissez-le en attente.

  6. Lisez l’arbre. Dans Process Explorer, trouvez cmd.exe → lab.exe → cmd.exe → PING.EXE. Un conhost.exe peut apparaître à côté de l’enfant console masqué. Survolez ou ajoutez la colonne Command Line et lisez la ligne de commande complète de l’enfant. Vérifiez la colonne Integrity : tous doivent être en Medium.

  7. Lisez les threads. Ouvrez lab.exe → Threads. Vous devriez trouver le thread principal démarrant dans lab.exe, votre thread de travail démarrant à un autre offset lab.exe+0x…, et éventuellement des workers du pool de threads dans ntdll.dll. Sélectionnez le thread de travail et cliquez sur Stack : il attend dans NtDelayExecution, appelé depuis SleepEx.

  8. Lisez les modules et les handles. Appuyez sur Ctrl+D : labdll.dll est listée avec le dossier du lab comme chemin et sans signataire vérifié, au milieu des DLL de System32. Appuyez sur Ctrl+H : trouvez le handle Process vers l’enfant cmd.exe — le handle que vous avez délibérément gardé ouvert.

  9. Vérifiez la mémoire. Dans System Informer, ouvrez lab.exe → Memory. Trouvez la région à l’adresse de base que le programme a affichée pour labdll.dll. Notez qu’elle est de type Image avec un chemin de fichier — et qu’aucune région Private n’est exécutable. C’est la référence que vous donne un processus sain.

  10. Observez un binaire hôte. Exécutez rundll32.exe labdll.dll,Hello "loaded by rundll32". Dans Process Explorer, le processus est le rundll32.exe signé par Microsoft ; seules sa ligne de commande et sa liste de modules révèlent ce qu’il exécute réellement.

  11. Retrouvez l’historique. Une fois PING.EXE terminé (deux minutes) ou après avoir appuyé sur Entrée, l’arbre en direct perd ces processus. Dans Process Monitor, ouvrez Tools → Process Tree : ils y sont toujours, avec lignes de commande, heures de début et de fin. Filtrez sur Operation is Load Image pour voir à quel moment labdll.dll a été chargée par rapport au démarrage du processus.

Questions : Lesquelles de vos observations rapporteriez-vous comme IOC, et lesquelles ne valent que pour cette compilation ? Si lab.exe avait passé CREATE_SUSPENDED, qu’aurait montré l’onglet Threads de l’enfant ? Si le thread de travail avait démarré à une adresse nue sans nom de module, que vérifieriez-vous ensuite dans l’onglet Memory ? Pourquoi le fait que rundll32.exe soit signé par Microsoft ne vous apprend-il rien sur la DLL qu’il exécute ?

À retenir

  • Un processus est un conteneur — espace d’adressage, table des handles, jeton, PEB — et ce sont ses threads qui travaillent ; les DLL s’exécutent sur ces threads avec l’identité de l’hôte.
  • Les enregistrements côté noyau (PID, PID parent à la création, mémoire adossée à une image) sont plus fiables que les données du PEB en mode utilisateur (ligne de commande, liste des modules), qu’un processus peut réécrire.
  • Les arbres de processus sont des preuves parce que les logiciels légitimes ont des parents prévisibles ; connaissez les bizarreries normales et les façons dont un parent peut être réutilisé, usurpé ou délibérément indirect.
  • Les lignes de commande sont souvent l’IOC le plus spécifique que vous donne un processus ; capturez-les à la création avec Sysmon ou 4688 plutôt que de vous fier à une vue en direct.
  • Les options de création comme CREATE_SUSPENDED et les listes d’attributs changent ce que vous devez vous attendre à voir — et sont la première chose à retrouver quand un échantillon importe CreateProcess.
  • Les threads qui démarrent hors de tout module, et la mémoire privée exécutable que la liste des modules n’explique pas, sont les signes côté hôte les plus courants d’un code qui ne provient pas d’un fichier sur disque.