Vue d’ensemble
Cette suite d’outils implémente une chaîne d’attaque complète, de la génération du payload jusqu’à son exécution furtive en mémoire sur un système Windows x64 :
| Composant | Rôle | Artefact produit |
|---|---|---|
| clipboard-logger | Payload DLL — capture le presse-papiers et écrit les données dans un fichier de log | clipboard-logger.dll |
| dll-injector | Injecte la DLL dans un processus cible par Manual Mapping — produit un shellcode autonome | shellcode.bin |
| cryptic-runner | Chiffre le shellcode (Rolling XOR) et génère un exécutable d’apparence anodine qui le déchiffre et l’exécute en mémoire | executable.exe |
Ce projet a été développé par Ruben Petteng Ngongang et Ryan Bouchou dans un cadre strictement académique (exercice Red Team). L'utilisation de ces outils en dehors d'un environnement de test autorisé est illégale et passible de poursuites pénales. Les auteurs déclinent toute responsabilité en cas d'usage malveillant.
Code source Le code source des projets mentionnées est disponible sur Github
Résultats VirusTotal
| Artefact | Lien VirusTotal | SHA-256 | Archive |
|---|---|---|---|
executable.exe (chaîne complète) | VirusTotal | 2db91608cabc9fbaf945a36887560712926f93bf84560f044db8d3ef3adaa893 | Archive.org |
helloworld.exe (test libc) | VirusTotal | — | |
score9 (version pré-correction) | VirusTotal | Archive.org |
Contexte des missions Red Team
Dans le cadre d’une mission Red Team, disposer d’un injecteur de DLL indétectable est indispensable pour assurer la persistance de l’accès obtenu sur un système et procéder ultérieurement aux différents pivots. En pratique, on injecte un payload C2 (Command and Control) afin de réaliser des actions distantes sur la machine infectée depuis un serveur d’attaque.
Ce faisant, un attaquant est à même d’opérer en toute discrétion depuis une base subrepticement cachée au sein d’un processus anodin. Tout l’enjeu réside dans l’obfuscation instillée au sein de l’injecteur afin de garantir l’indétectabilité de l’injection.
Architecture & Workflow
Flux d’exécution complet
Le pipeline de build orchestre les trois composants dans un ordre précis. Le script racine make.sh automatise l’ensemble :
Flux d’exécution au runtime (Windows)
Détail des composants
1. clipboard-logger — Payload DLL
Le payload est une DLL Windows x64 qui surveille le presse-papiers du système et exfiltre son contenu dans un fichier texte.
Fonctionnement
DllMain: à l’attachement (DLL_PROCESS_ATTACH), initialise les APIs dynamiques et crée un thread de polling.- Thread de polling : interroge
GetClipboardSequenceNumber()toutes les 500 ms. Lorsqu’un changement est détecté, ouvre le presse-papiers, extrait le texte (CF_TEXT) et appelle la fonction de journalisation. - Journalisation : écrit le contenu capturé dans
C:\Users\<USER>\Desktop\clip.txtvia les syscalls directsNtCreateFileetNtWriteFile— aucun appel aux APIs Win32 de fichier. - Arrêt propre : sur
DLL_PROCESS_DETACH, signale au thread de s’arrêter et attend sa terminaison.
Techniques d’évasion
| Technique | Détail |
|---|---|
| Résolution dynamique (PEB + FNV-1a) | Aucune entrée suspecte dans l’IAT de la DLL |
Direct Syscalls (NtCreateFile, NtWriteFile) | Écriture fichier sans transiter par kernel32.dll |
| Fonctions string custom | _strlen, _wcslen, _wcscpy, _wcscat — aucune dépendance CRT |
| Symbol stripping | Compilation avec -s — aucun symbole de debug |
Structure
clipboard-logger/
├── make.sh # Script de build → build/clipboard-logger.dll
├── src/
│ ├── clipboard-logger.c # Logique principale (polling + logging)
│ ├── direct-syscalls.asm # Stubs NT : NtCreateFile, NtWriteFile, etc.
│ └── peb-lookup.c # Résolution dynamique APIs via PEB
└── include/
├── peb-lookup.h # Structures PEB, hashes FNV-1a des APIs
└── direct-syscalls.h # Prototypes des stubs syscall
2. dll-injector — Injecteur par Manual Mapping
L’injecteur copie manuellement les sections PE de la DLL payload dans la mémoire d’un processus cible (explorer.exe par défaut), sans passer par LoadLibrary. La DLL injectée n’apparaît dans aucune liste de modules du système.
Orchestration de l’injection
ProcessWalking: Énumération des processus viaCreateToolhelp32Snapshot+Process32First/Next→ PID cible.MannualMappingDll: Ouverture du handle, allocation distante (NtAllocateVirtualMemory), écriture des headers PE et sections (NtWriteVirtualMemory).injectManualMappingStub: Injection d’un bloc contigu[ASM stub | C stub | MANUAL_MAPPING_DATA]+ création du thread distant (NtCreateThreadEx).
ASM Stub (576 octets, PIC x64)
Shellcode position-independent exécuté dans le processus cible :
- Alignement de pile (
and rsp, -16) - PEB Walk →
InMemoryOrderModuleList - Recherche de
KERNEL32.DLLpar comparaison WCHAR - Résolution EAT →
LoadLibraryA,GetProcAddress,GetModuleHandleA - Complétion de
MANUAL_MAPPING_DATA - Appel de
C_LoaderStub(pData) ExitThread(0)— résolu dynamiquement
C Stub (loader PIC, compilé sans CRT)
- Relocation de base : parcours
.reloc, patch des entréesIMAGE_REL_BASED_DIR64 - Résolution IAT :
LoadLibraryA+GetProcAddresspour chaque import (par nom ou ordinal) - Appel
DllMain:(HMODULE)pBase, DLL_PROCESS_ATTACH, NULL
DLL embarquée dans .text
La DLL payload est embarquée sous forme de tableau unsigned char bytecode[] dans la section .text via encrypt.py. Aucun accès disque au runtime. L’ensemble (injecteur + DLL) est extrait en un seul blob binaire par objcopy --only-section=.text → shellcode.bin.
Direct Syscalls
| Fonction NT | Syscall | Rôle |
|---|---|---|
NtOpenProcess | 0x0026 | Ouverture du handle sur le processus cible |
NtAllocateVirtualMemory | 0x0018 | Allocation de mémoire distante |
NtWriteVirtualMemory | 0x003a | Écriture DLL + stubs en mémoire distante |
NtReadVirtualMemory | 0x003f | Lecture de la mémoire distante |
NtProtectVirtualMemory | 0x0050 | Modification des permissions mémoire |
NtCreateThreadEx | 0x00c9 | Création du thread distant |
Stabilité des direct syscalls Les numéros de syscall sont hardcodés pour la version de Windows cible. En production, il faudrait les résoudre dynamiquement en parsant
ntdll.dllou via une méthode de type FreshyCalls^2.
Fallback APIs (résolution dynamique via PEB)
| Fonction Win32 | Rôle |
|---|---|
CreateToolhelp32Snapshot | Snapshot de la liste des processus |
Process32First / Process32Next | Itération sur les processus |
VirtualFreeEx | Libération mémoire distante |
CloseHandle | Fermeture des handles |
Structure
dll-injector/
├── make.sh # Build shellcode (PIC + objcopy)
├── src/
│ ├── main.tpl.c # Template d'entrée (DLL embarquée)
│ ├── encrypt.py # Embed DLL en tableau C
│ ├── dll-injector.c # Orchestration : mapping + injection
│ ├── pe-parser.c # Chargement PE depuis buffer mémoire
│ ├── loader-stub.c # Stub C PIC (reloc + IAT + DllMain)
│ ├── asm-stub.asm # Stub ASM PIC (PEB walk dans la cible)
│ ├── simple-dll.c # DLL de démonstration (notification systray)
│ └── utils/
│ ├── peb-lookup.c # Résolution dynamique APIs via PEB + FNV-1a
│ ├── memory.c # Wrappers : direct syscalls NT + fallback
│ ├── direct-syscalls.asm # Stubs ASM : appels NT directs
│ └── windows-memory.c # Wrappers Windows Memory API
├── include/
│ ├── dll-injector/ # En-têtes principaux
│ ├── utils/ # PEB lookup, macros, direct-syscalls, memory
│ └── windows/ # Structures PE custom
├── docs/ # Doxyfile + thème doxygen-awesome-css
└── test/ # Tests unitaires (PE parser)
3. cryptic-runner — Crypter / Packer
Le runner chiffre un shellcode binaire brut avec un algorithme de Rolling XOR (clé aléatoire de 16 octets) et l’enveloppe dans un exécutable Windows qui le déchiffre et l’exécute en mémoire au runtime.
Chiffrement (build time — format.py)
- Lecture du binaire d’entrée (shellcode brut)
- Génération d’une clé aléatoire de 16 octets
- Chiffrement :
encrypted[i] = plaintext[i] ^ key[i % 16] - Génération de
build/main.cà partir du templatesrc/main.tpl.cavec les placeholders :SET_BYTECODE_SIZE→ taille du shellcodeSET_BYTECODE_ARRAY→ tableau C du bytecode chiffréSET_KEY_SIZE→16SET_KEY_ARRAY→ tableau C de la clé
Déchiffrement & exécution (runtime — main.tpl.c)
- Anti-sandbox (
check_available_ressources()) : interrogeGlobalMemoryStatusEx()— si la RAM < 5 Go, exécute unhello_world()anodin et termine. - Déchiffrement (
sort_array()) :bytecode[i] ^= key[i % 16] - Exécution : cast du buffer en pointeur de fonction et saut direct. Le bytecode est placé en section
.textvia--omagic, ce qui rend la mémoire exécutable sans appel àVirtualProtect.
Flags de compilation notables
| Flag | Effet |
|---|---|
-Wl,--omagic | .text et .data partagent le même segment — le bytecode est exécutable sans VirtualProtect |
-Wl,--disable-nxcompat | Désactive le marquage DEP dans le PE |
-Wl,--disable-dynamicbase | Désactive l’ASLR |
Structure
cryptic-runner/
├── make.sh # Build : format.py + mingw gcc → build/executable.exe
├── test.sh # Build des tests (PEB lookup + extraction shellcode)
├── helloworld.c # Programme de test simple (Hello world)
├── src/
│ ├── main.tpl.c # Template du wrapper (anti-sandbox + XOR + exec)
│ ├── format.py # Chiffrement Rolling XOR + génération main.c
│ └── peb-lookup.c # Résolution dynamique APIs via PEB (pour les tests)
├── include/
│ └── peb-lookup.h # En-tête PEB lookup
└── test/
└── test.c # Tests unitaires PEB lookup
Techniques mises en œuvre
| Technique | Composant | Description |
|---|---|---|
| Manual Mapping | dll-injector | Copie manuelle du PE en mémoire distante, sans LoadLibrary — la DLL n’apparaît dans aucune liste de modules |
| API Hashing (FNV-1a) | dll-injector, clipboard-logger | Résolution des APIs au runtime via PEB — aucun import suspect dans l’IAT |
| ASM Stub PIC | dll-injector | Shellcode x64 position-independent : PEB walk dans le processus cible |
| C Loader Stub PIC | dll-injector | Relocation de base, résolution IAT, appel DllMain — compilé sans CRT |
| Direct Syscalls (NT) | dll-injector, clipboard-logger | Appels directs au noyau via syscall, court-circuitant les hooks userland |
DLL embarquée (.text) | dll-injector | Payload compilé dans l’injecteur — aucun accès disque au runtime |
| Extraction shellcode | dll-injector | objcopy --only-section=.text → blob binaire sans en-têtes PE |
| Rolling XOR Crypter | cryptic-runner | Chiffrement du shellcode avec clé aléatoire 16 octets — signature différente à chaque build |
| Anti-Sandbox | cryptic-runner | Vérification de la RAM physique (≥ 5 Go) avant déchiffrement |
| Linker Evasion | cryptic-runner | --omagic + --disable-nxcompat + --disable-dynamicbase — exécution sans appels mémoire suspects |
Prérequis & Compilation
Dépendances
| Outil | Version | Usage |
|---|---|---|
x86_64-w64-mingw32-gcc | ≥ 12 | Cross-compilation Linux → Windows x64 |
x86_64-w64-mingw32-ld | — | Linkage PIC sans CRT (dll-injector) |
x86_64-w64-mingw32-objcopy | — | Extraction section .text |
nasm | ≥ 2.15 | Assemblage des stubs ASM (format win64) |
python3 | ≥ 3.6 | Scripts d’embed (encrypt.py) et de chiffrement (format.py) |
doxygen | — | Documentation (optionnel) |
Sur Arch Linux :
sudo pacman -S mingw-w64-gcc nasm pythonSur Debian/Ubuntu :
sudo apt install gcc-mingw-w64-x86-64 binutils-mingw-w64-x86-64 nasm python3Compilation de la chaîne complète
Le script racine make.sh orchestre le build des trois composants dans l’ordre :
# Depuis la racine du projet :
./make.shCe script exécute séquentiellement :
# 1. Build du payload DLL
cd clipboard-logger && ./make.sh
# → clipboard-logger/build/clipboard-logger.dll
# 2. Build de l'injecteur + extraction shellcode
cd dll-injector && ./make.sh ../clipboard-logger/build/clipboard-logger.dll
# → dll-injector/build/shellcode.bin
# 3. Build du runner chiffré
cd cryptic-runner && ./make.sh ../dll-injector/build/shellcode.bin
# → cryptic-runner/build/executable.exeCompilation individuelle
clipboard-logger
cd clipboard-logger
./make.sh
# → build/clipboard-logger.dlldll-injector
cd dll-injector
# Mode shellcode (PIC pur → .bin)
./make.sh /chemin/vers/payload.dll
# → build/shellcode.bincryptic-runner
cd cryptic-runner
# Chiffre et emballe un shellcode quelconque
./make.sh /chemin/vers/shellcode.bin
# → build/executable.exe
# Test avec un simple Hello World
./make.sh helloworld.c
# → build/executable.exeArtefacts produits
| Répertoire | Fichier | Description |
|---|---|---|
clipboard-logger/build/ | clipboard-logger.dll | Payload DLL (monitoring presse-papiers) |
dll-injector/build/ | shellcode.bin | Shellcode PIC autonome (injecteur + DLL embarquée) |
cryptic-runner/build/ | main.c | Source généré (bytecode chiffré + clé) |
cryptic-runner/build/ | executable.exe | Livrable final — exécutable FUD |
Usage / Ligne de commande
Lancer la chaîne complète (build + déploiement)
# 1. Compiler la suite
./make.sh
# 2. Transférer executable.exe vers la machine Windows cible
cp cryptic-runner/build/executable.exe ~/windows_share/
# 3. Sur la machine Windows (avec explorer.exe en cours d'exécution) :
executable.exeVérification
Après exécution sur la cible Windows :
executable.exes’exécute et vérifie l’environnement (RAM ≥ 5 Go)- Le shellcode est déchiffré en mémoire et exécuté
- L’injecteur résout les APIs via PEB et localise
explorer.exe - La DLL
clipboard-loggerest injectée par manual mapping dansexplorer.exe - Un fichier
clip.txtapparaît sur le Bureau de l’utilisateur, contenant les données du presse-papiers
Test unitaire — DLL de démonstration
Pour tester l’injection sans le payload clipboard, la DLL de démonstration (simple-dll.c) affiche une notification dans le system tray :
cd dll-injector
# Compiler avec la DLL de démo au lieu du clipboard-logger
./make.sh build/injected-dll.dllInjection distante — Détail
Étapes de l’injection
L’injection se déroule en six étapes séquentielles, dont les quatre dernières impliquent des opérations sur la mémoire du processus cible.
Diagramme d’architecture interne
Erreurs possibles et mitigations
| Étape | Erreur | Mitigation |
|---|---|---|
NtOpenProcess | STATUS_ACCESS_DENIED — processus protégé (PPL) | Cibler un processus non-protégé dans le même contexte d’intégrité |
NtAllocateVirtualMemory | STATUS_QUOTA_EXCEEDED | Passer NULL comme adresse pour déléguer le choix au noyau |
NtWriteVirtualMemory | STATUS_PARTIAL_COPY — région non-accessible | S’assurer de l’allocation préalable avec PAGE_EXECUTE_READWRITE |
NtCreateThreadEx | STATUS_PROCESS_IS_TERMINATING | Valider que le processus cible est actif avant injection |
CreateToolhelp32Snapshot | INVALID_HANDLE_VALUE | Vérifier que le processus cible est en cours d’exécution |
Justification du processus cible
Le processus explorer.exe est choisi car :
- il est toujours en cours d’exécution sur un poste Windows ;
- il est détenu par l’utilisateur courant (pas besoin de privilèges élevés) ;
- il possède déjà de nombreuses DLL chargées, ce qui rend une allocation mémoire supplémentaire moins suspecte.
Choix techniques
Pourquoi le Manual Mapping ?
Contrairement à l’utilisation classique de LoadLibrary, le manual mapping réimplémente le chargeur Windows (Ldr) en mode utilisateur pour copier manuellement les sections de la DLL dans l’espace mémoire de la cible. Ce faisant, on évite :
- de faire appel à une fonction critique de l’API Windows (centrale et surveillée) ;
- de recenser la DLL injectée dans la
InLoadOrderModuleList.
| Avantage | Inconvénient |
|---|---|
| Furtivité accrue : la DLL n’apparaît pas dans la liste des modules chargés | Complexité : gestion manuelle des relocalisations, IAT et callbacks TLS |
Bypass ETW : évite les événements générés par LoadLibrary | Stabilité : sensible aux variations de structures PE complexes |
Alternatives écartées
- LoadLibrary (DLL Injection standard) : extrêmement surveillée par les EDR, détectable via l’analyse des modules chargés.
- Process Hollowing : le remplacement de l’image d’un processus légitime est une signature comportementale forte pour les antivirus modernes.
Stratégie FUD (Fully Undetectable)
Actions entreprises
- Résolution Dynamique d’API (PEB Hashing FNV-1a) : suppression de l’IAT — fonctions résolues au runtime par parcours du PEB.
- Direct Syscalls : appels NT directs via
syscall, court-circuitantntdll.dllet ses hooks userland. - Anti-Sandbox : vérification de la RAM physique (< 5 Go → comportement anodin).
- Rolling XOR Crypter : signature binaire différente à chaque build grâce à la clé aléatoire.
- DLL embarquée + extraction shellcode : aucun fichier DLL sur le disque au runtime ; l’ensemble est un blob
.binautonome intégré dans le runner. - Compilation PIC pure :
-ffreestanding -nostdlib— aucune dépendance CRT, aucun import PE classique. - Linker flags :
--omagic(pas deVirtualProtect),--disable-nxcompat,--disable-dynamicbase.
Résultats de tests
- Tests Locaux : exécution réussie sur Windows 11 avec Windows Defender (Protection en temps réel activée). Aucune alerte déclenchée, payload opérationnel.
- VirusTotal : voir le tableau en tête de document.
Limites connues
Ce qui reste détectable
- Analyse de la Call Stack : un EDR peut détecter que l’exécution provient d’une région mémoire unbacked (non associée à un fichier sur disque).
- Scanner de mémoire (YARA) : si les chaînes du payload sont déchiffrées en mémoire sans nettoyage, elles peuvent être détectées par un scan périodique de la RAM.
- Analyse comportementale : l’accès répété au presse-papiers par un processus injecté peut être considéré comme suspect sur une longue période.
Points à améliorer
- Stack Spoofing : masquer l’origine de l’exécution dans la pile d’appels.
- Polymorphisme : chiffrer le stub assembleur pour changer sa signature à chaque exécution.
- Module Overloading : écraser un module légitime peu utilisé au lieu d’allouer une nouvelle page mémoire.
- Chiffrement AES : remplacer le Rolling XOR par un chiffrement plus robuste (AES-CTR).
- Résolution dynamique des SSN : parser
ntdll.dllau runtime au lieu de hardcoder les numéros de syscall (compatibilité multi-versions Windows).
Bibliographie
- Maldev Academy : documentation sur le Manual Mapping et le PEB Walking.
- Alice Climent-Pommeret (FreshyCalls)^2 : tri de l’Export Address Table pour la résolution de SSN.
- Jackson_T (SysWhispers) : implémentation des stubs assembleur pour les syscalls directs.
- Microsoft Technical Documentation : structures internes
ntdll.dllet Native API. - Ired.team : techniques d’évasion et API Hashing.
- j00ru/windows-syscalls^1 : tables de numéros de syscalls par version de Windows.