Un shellcode Windows autonome ne peut pas se contenter d’appeler MessageBoxA comme le ferait un programme C ordinaire : il n’a, par construction, ni table d’imports ni éditeur de liens pour lui fournir les adresses des fonctions dont il a besoin. Toute la technique consiste donc à résoudre dynamiquement, au sein même du shellcode, l’adresse des API qu’il veut appeler — en partant des seules structures que Windows garantit accessibles depuis n’importe quel processus.
Le point de départ : le PEB
Chaque processus Windows dispose d’un Process Environment Block (PEB), accessible sans appel système depuis le segment fs/gs selon l’architecture. C’est de lui que tout part.
Le PEB pointe vers une structure PEB_LDR_DATA, laquelle référence plusieurs listes chaînées de modules chargés — notamment l’InMemoryOrderModuleList, que l’on parcourt pour retrouver, module par module, une structure LDR_DATA_TABLE_ENTRY. C’est là que se lisent le nom (BaseDllName) et l’adresse de base (DllBase) de chaque bibliothèque chargée — typiquement kernel32.dll, présente dans tout processus Windows.
Résoudre les fonctions par l’EAT
Une fois l’adresse de base d’une DLL connue, il reste à y localiser une fonction précise. C’est le rôle de l’Export Address Table (EAT), une structure exposée par tout module PE exportant des symboles, qui associe noms de fonctions et adresses relatives.
Comparer des chaînes de caractères en dur dans le shellcode serait coûteux et signant. On lui préfère un hachage des noms d’API : le shellcode calcule un hash à la volée pour chaque export rencontré et le compare à un hash cible codé en dur, sans jamais faire apparaître le nom littéral de la fonction recherchée. C’est une technique d’évasion à part entière, dans l’esprit de celles décrites dans la note sur l’évasion : elle prive l’analyste statique d’une chaîne de caractères exploitable.
Se retrouver soi-même : CALL/POP
Un shellcode, une fois injecté, ne connaît pas nécessairement sa propre adresse en mémoire. L’idiome classique CALL/POP contourne le problème : un call vers l’instruction suivante empile l’adresse de retour, qu’un pop immédiat récupère — fournissant ainsi, sans dépendance externe, l’adresse courante d’exécution, utile pour référencer des données relatives (chaînes, hashs) embarquées dans le shellcode lui-même.
Assembler la chaîne complète
L’enchaînement typique d’un shellcode Windows autonome suit donc ce schéma :
- localiser le PEB, puis la liste des modules chargés ;
- identifier
kernel32.dll(ou une autre DLL cible) via son nom ou son hash ; - parcourir son EAT pour résoudre, par hachage, les fonctions nécessaires (
LoadLibraryA,GetProcAddress, puis l’API finale visée) ; - charger les arguments selon la convention d’appel de la plateforme ;
- appeler la fonction résolue —
MessageBoxAdans l’exemple canonique, mais tout autantWinExecouCreateProcessAdans un cas réel.
Reconstituer ce cheminement en assembleur lisible, annoté et segmenté par blocs fonctionnels est l’exercice qui donne toute sa valeur pédagogique à l’analyse : on ne comprend vraiment cette mécanique qu’en la réécrivant soi-même.
Sources et lectures complémentaires
- PEB / LDR (documentation Win32)
- PEB walk et résolution dynamique
- Hachage des API en x86 (Metasploit)
- Payload Windows x64 MessageBox (Metasploit)
- PEB Walk — Fareed Fauzi
- Finding kernel32 base & API addresses in shellcode
- Analysing shellcode to understand how Windows APIs are called
- Universal Windows Shellcode — Part 1
- x64 WinAPI shellcoding
- Basics of shellcode analysis
- BendyBear shellcode (BlackTech), Unit 42
- Windows API hashing in malware
- From a C project through assembly to shellcode (VX Underground)