<index> / <windows-internals> / shellcode
[ en | fr ]
┌───────────────────────┐
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
└───────────────────────┘
Shellcode sur Windows
~ lululufr
SOMMAIRE
  0  le problème — aslr et pas d'adresses fixes
  1  pourquoi pas les syscalls directement
  2  trouver kernel32.dll à l'exécution
  3  parser l'export directory
  4  donut — pe vers shellcode
  5  tester un shellcode

──[ 0. Le Problème — ASLR et Pas d'Adresses Fixes ]──

Un shellcode est injecté dans une région mémoire dont l'adresse est inconnue à 
la compilation — merci ASLR. ASLR randomise les adresses de base de la stack, du heap, et de chaque module
chargé à chaque lancement de processus. Ton shellcode ne peut pas contenir des
jumps en dur comme JMP 0x12345678 — il crashe direct. Donc le shellcode doit être Position Independent Code (PIC) : que des adresses
relatives, et tout ce dont il a besoin se résout dynamiquement à l'exécution.
──[ 1. Pourquoi Pas les Syscalls Directement ]──

Sous Linux, les shellcodes appellent le kernel direct avec l'instruction syscall 
et des numéros en dur. Simple, portable. Sous Windows, ça ne marche pas :
    - Les SSN (numéros de syscall de la Windows Native API) changent
      entre versions d'OS (XP → 7 → 10 → 11). Un SSN en dur pour
      NtAllocateVirtualMemory sur Windows 10 22H2 n'est pas le même
      sur Windows 11 23H2.
    - Microsoft ne documente ni ne garantit la stabilité des SSN.

La bonne approche : appeler l'API Win32 documentée à travers les DLLs :
    kernel32.dll — gestion mémoire, création de processus/threads, chargement de
 DLL
    ntdll.dll    — bridge fin vers le kernel (Native API)

──[ 2. Trouver kernel32.dll à l'Exécution ]──

Le shellcode a besoin de l'adresse de base de kernel32.dll pour récupérer 
GetProcAddress, et ensuite trouver le reste. La méthode passe par le PEB et le
TEB — deux structures Windows à des emplacements prévisibles.
flowchart TD
    A["FS:[0x30] (x86)\nGS:[0x60] (x64)"] --> B[PEB]
    B --> C[PEB.Ldr]
    C --> D[InMemoryOrderModuleList]
    D --> E["[0] — l'exécutable lui-même"]
    E --> F["[1] — ntdll.dll"]
    F --> G["[2] — kernel32.dll ← adresse de base ici"]
Étapes :
    1. Lire l'adresse du TEB depuis FS (x86) ou GS (x64).
    2. Le TEB contient un pointeur vers le PEB à un offset connu.
    3. PEB.Ldr pointe vers PEB_LDR_DATA qui contient InMemoryOrderModuleList.
    4. Parcourir la liste : entrée 0 c'est l'exe, entrée 1 c'est ntdll.dll,
       entrée 2 c'est kernel32.dll. Le champ DllBase de cette entrée
       donne l'adresse de base.

Cet ordre de chargement est stable pour les processus Windows standard.
──[ 3. Parser l'Export Directory ]──

Une fois kernel32.dll localisée, le shellcode doit trouver GetProcAddress 
dedans. Ce qui veut dire parser la table d'exports PE à la main.
    1. À l'offset 0x3C depuis la base de la DLL : e_lfanew — offset des NT Heade
rs.
    2. NT Headers → Optional Header → DataDirectory[0] : RVA de l'Export Directo
ry.
    3. IMAGE_EXPORT_DIRECTORY contient trois tableaux parallèles :

        AddressOfNames        — RVAs des strings de noms de fonctions
        AddressOfNameOrdinals — index dans AddressOfFunctions pour chaque nom
        AddressOfFunctions    — RVAs du code des fonctions

    4. Boucler sur AddressOfNames, comparer chaque nom avec "GetProcAddress".
       Une fois trouvé, utiliser l'ordinal correspondant pour indexer
       AddressOfFunctions, récupérer la RVA, ajouter la base de la DLL
       pour la VA.

Avec GetProcAddress en main, on résout n'importe quoi d'autre par son nom depuis 
n'importe quelle DLL chargée.
──[ 4. Donut — PE vers Shellcode ]──

Écrire à la main les stubs de parcours du PEB et de parsing des exports, c'est 
chiant. L'outil open-source Donut (github.com/TheWover/donut) fait tout : tu lui
files un PE (EXE ou DLL), il l'enveloppe dans un loader shellcode
position-indépendant.
    ./donut.exe -i "rev_shell.exe" -a 3 -f 3 -o payload.c
-i fichier d'entrée (PE)
    -a 3 architecture : x64
    -f 3 format de sortie : tableau C
Sortie Donut — payload.c généré
Sortie Donut — aperçu des octets du shellcode
Donut embarque le PE, le compresse, gère la relocation, résout les imports à 
l'exécution, et appelle le point d'entrée — le tout dans un blob shellcode
autonome.
──[ 5. Tester un Shellcode ]──

Harnais de test minimal in-process :
    int main() {
        void* mem;

        // Allouer de la mémoire RWX
        mem = VirtualAlloc(
            NULL,
            sizeof(buf),
            MEM_COMMIT | MEM_RESERVE,
            PAGE_EXECUTE_READWRITE
        );

        // Copier le shellcode
        RtlMoveMemory(mem, buf, sizeof(buf));

        // Exécuter via un pointeur de fonction
        ((void(*)())mem)();

        return 0;
    }
buf[] contient les octets bruts du shellcode (ex. depuis Donut).

Loader simple : alloc une page RWX, copie les octets, appel dedans. Dans un vrai 
scénario d'injection, on fait pareil dans un processus distant : VirtualAllocEx
+ WriteProcessMemory + CreateRemoteThread.