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