<index> / <windows-internals> / kernel-callbacks
[ en | fr ]
┌───────────────────────┐
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
└───────────────────────┘
Kernel Callbacks
~ lululufr
SOMMAIRE
  0  c'est quoi les kernel callbacks
  1  enregistrement
  2  la notification — ps_create_notify_info
  3  bypass — callback blinding
  4  bypass — altération de la ligne de commande
  5  bypass — process hollowing
  6  bypass — parent pid spoofing
  7  conclusion

──[ 0. C'est quoi les Kernel Callbacks ]──

Les kernel callbacks (a.k.a. kernel notification routines) sont un mécanisme 
intégré au kernel Windows pour les éditeurs de sécurité. Un driver kernel
enregistre une fonction, et le kernel la déclenche automatiquement quand un
événement système précis se produit — création de processus, création de thread,
chargement d'image, écriture dans le registre, etc. C'est la fondation de la télémétrie kernel des EDR modernes. Avantages sur les
anciennes approches (inline hooking, patching SSDT) :
    - Pas besoin de patcher le kernel (KPP/PatchGuard bloque de toute façon)
    - Supporté par Microsoft, stable d'une version d'OS à l'autre
    - Géré centralement par le kernel, pas de souci d'ordre des drivers
    - Contexte riche fourni à chaque notification

Un Callback Object est une structure kernel interne qui gère une liste de 
routines enregistrées pour un type d'événement donné. Les EDR doivent
enregistrer un callback séparé pour chaque type d'événement qu'ils veulent
surveiller. Tout trou dans la couverture est un angle mort potentiel.
Kernel callback object — structure interne
──[ 1. Enregistrement ]──

Pour s'abonner aux événements de création de processus, un driver appelle :
    NTSTATUS PsSetCreateProcessNotifyRoutineEx(
        PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,
        BOOLEAN                           Remove
    );
NotifyRoutine — la fonction à appeler à chaque événement
    Remove        — FALSE pour enregistrer, TRUE pour désenregistrer

Autres fonctions d'enregistrement courantes :
    PsSetCreateProcessNotifyRoutine   — création de processus (ancienne, moins d
'info)
    PsSetCreateThreadNotifyRoutine    — création/suppression de thread
    PsSetLoadImageNotifyRoutine       — chargement d'image exécutable / DLL
    CmRegisterCallback                — opérations sur le registre
    FltRegisterFilter                 — minifilter système de fichiers (cf. mini
filters)
flowchart LR
    D[Driver EDR] -->|PsSetCreateProcessNotifyRoutineEx| K[Kernel]
    K -->|stocke le pointeur| T[Table de Callbacks]
    E[Nouveau Processus] -->|déclenche| T
    T -->|appelle| R[Routine EDR]
    R -->|log / bloque| D
──[ 2. La Notification — PS_CREATE_NOTIFY_INFO ]──

Quand un processus est créé, la routine enregistrée reçoit un pointeur vers 
PS_CREATE_NOTIFY_INFO :
    typedef struct _PS_CREATE_NOTIFY_INFO {
      SIZE_T           Size;
      union {
        ULONG Flags;
        struct {
          ULONG FileOpenNameAvailable : 1;
          ULONG IsSubsystemProcess    : 1;
          ULONG Reserved              : 30;
        };
      };
      HANDLE           ParentProcessId;
      CLIENT_ID        CreatingThreadId;
      FILE_OBJECT     *FileObject;
      PCUNICODE_STRING ImageFileName;
      PCUNICODE_STRING CommandLine;
      NTSTATUS         CreationStatus;
    } PS_CREATE_NOTIFY_INFO;
Champs clés pour la détection :
    CommandLine       Ligne de commande complète
    ImageFileName     Chemin de l'image exécutable
    ParentProcessId   PID du parent

Un EDR peut flagger les patterns suspects :
    cmd.exe lancé par outlook.exe   — probablement de l'exécution de macro
    powershell.exe avec un argument -enc encodé
    notepad.exe qui spawn un processus enfant

Un HANDLE sous Windows est un entier opaque qui référence un objet kernel sans 
exposer son adresse. À partir d'un ParentProcessId, un EDR peut ouvrir le
parent, inspecter sa mémoire, énumérer son propre parent — et construire un
arbre de processus complet.
──[ 3. Bypass — Callback Blinding ]──

Le bypass le plus direct : trouver la table de callbacks kernel et écraser le 
pointeur de fonction enregistré par l'EDR avec des zéros. Demande du Ring 0 — en pratique, via BYOVD (voir l'article BYOVD). Déroulé :
    1. Charger un driver signé mais vulnérable (BYOVD).
    2. Utiliser ses primitives R/W pour scanner la mémoire kernel.
    3. Trouver les tableaux internes de callbacks pour chaque type de
       notification.
    4. Pour chaque slot, lire le pointeur de fonction, résoudre le nom du
       driver.
    5. Si le driver ressemble à un composant EDR, écraser l'entrée avec 0.

Les entrées de la table de callbacks, c'est juste un tableau de pointeurs de 
fonctions en mémoire kernel. Avec du write kernel, zéroïser une entrée sort
l'EDR de la chaîne de notification pour de bon (jusqu'au reboot). Quelques détails issus de PoCs BYOVD open-source :
    GetPspNotifyRoutineArray()  — pattern-scan la mémoire kernel pour
                                  trouver le tableau de callbacks
    IsEDR()                    — vérifie si un nom de driver est un EDR
                                  connu
    PrintAndClearCallBack()    — parcourt le tableau et zéroïse les
                                  entrées EDR

Tables de callbacks ciblées :
    CmRegisterCallback
    ObRegisterCallbacks
    PsSetCreateProcessNotifyRoutine
    PsSetCreateThreadNotifyRoutine
    PsSetLoadImageNotifyRoutine

──[ 4. Bypass — Altération de la Ligne de Commande ]──

Les EDR s'appuient beaucoup sur CommandLine de PS_CREATE_NOTIFY_INFO pour 
flagger les commandes malveillantes. Cette technique abuse du fait que la ligne
de commande est en mémoire user-mode (PEB.ProcessParameters.CommandLine) avant
que le callback kernel ne se déclenche. Attaque :
    1. Créer un processus enfant SUSPENDED avec une ligne de commande
       inoffensive. Le processus existe mais ne peut pas encore tourner
       (flag CREATE_SUSPENDED).
        CreateProcessA(target, "benign args", ..., CREATE_SUSPENDED, ..., &pi);
Altération de la ligne de commande — flux de création du processus
2. Trouver l'adresse du PEB de l'enfant :
        NtQueryInformationProcess(pi.hProcess, ProcessBasicInformation, ...);
Driver chargé — callback EDR enregistré
3. Écraser CommandLine dans le PEB de l'enfant avec la vraie ligne
       de commande malveillante :
        WriteProcessMemory(pi.hProcess, cmdLineAddr, realCmd, realCmdLen, NULL);
Démarrage du service — sc start EDRtest
4. Reprendre :
        ResumeThread(pi.hThread);
Processus altéré lancé — fausse ligne de commande vue par le callback
Sortie DebugView — log du callback montrant les args inoffensifs
Le callback kernel s'est déjà déclenché à la création avec la fausse ligne. 
Quand le processus tourne pour de vrai, le PEB a la vraie ligne de commande
remise dedans. Contraintes :
    - Tu dois contrôler le parent
    - La nouvelle ligne doit avoir une longueur égale ou inférieure au
      placeholder (agrandir le buffer demande du boulot en plus)
Processus confirmé — PID et vraie ligne de commande en mémoire
Le fork-and-run de Cobalt Strike utilise une variante de ça.
──[ 5. Bypass — Process Hollowing ]──

Le Process Hollowing remplace le code d'un processus légitime par un payload. Du 
point de vue du callback kernel, le callback se déclenche avec le nom d'image
légitime (notepad.exe), donc les checks statiques passent.
    1. Créer le processus cible en suspendu (CREATE_SUSPENDED). Le
       callback se déclenche ici — avec image et ligne de commande
       légitimes.
        CreateProcessA("C:\\Windows\\System32\\notepad.exe", NULL, ...,
                       CREATE_SUSPENDED, ..., &pi);
2. Allouer de la mémoire RWX dans le processus distant :
        exec_mem = VirtualAllocEx(pi.hProcess, NULL, shellcodeSize,
                                  MEM_COMMIT | MEM_RESERVE,
                                  PAGE_EXECUTE_READWRITE);
3. Écrire le shellcode / payload :
        WriteProcessMemory(pi.hProcess, exec_mem, shellcode, shellcodeSize, NULL);
4. Reprendre via un thread distant qui pointe sur le payload :
        CreateRemoteThread(pi.hProcess, NULL, 0,
                           (LPTHREAD_START_ROUTINE)exec_mem,
                           NULL, 0, NULL);
Shellcode injecté dans un notepad.exe suspendu
Payload Donut généré — octets de shellcode prêts
Arbre de processus — reverse shell tournant sous notepad.exe
L'arbre montre notepad.exe, le callback kernel a reporté une image légitime, ton 
code injecté tourne sous cette couverture sans être détecté.
Process hollowing — notepad.exe avec un enfant cmd dans l'arbre
──[ 6. Bypass — Parent PID Spoofing ]──

Les EDR construisent un arbre de processus à partir des ParentProcessId. Un 
parent suspect (Word → PowerShell) déclenche des règles comportementales. Tu
spoof le parent ID, la règle saute. CreateProcessW accepte une STARTUPINFOEX étendue qui permet de spécifier un
handle de parent arbitraire via PROC_THREAD_ATTRIBUTE_PARENT_PROCESS.
    STARTUPINFOEXA si = { 0 };
    si.StartupInfo.cb = sizeof(STARTUPINFOEXA);
    PROCESS_INFORMATION pi;
    SIZE_T size;

    // Ouvrir le processus "parent" cible (droits admin nécessaires)
    HANDLE hFakeParent = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, targetPid);

    // Construire la liste d'attributs
    InitializeProcThreadAttributeList(NULL, 1, 0, &size);
    si.lpAttributeList = malloc(size);
    InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &size);

    // Définir le parent spoofé
    UpdateProcThreadAttribute(si.lpAttributeList, 0,
        PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
        &hFakeParent, sizeof(HANDLE), NULL, NULL);

    // Lancer l'enfant — le kernel enregistrera hFakeParent comme parent
    CreateProcessA(NULL, "notepad.exe", NULL, NULL, FALSE,
        EXTENDED_STARTUPINFO_PRESENT, NULL, NULL,
        &si.StartupInfo, &pi);
PID spoofing — arbre de processus avant spoof
PID spoofing — faux parent montré dans l'arbre
PID spoofing — l'EDR voit une chaîne parent légitime
Le notepad.exe que tu lances apparaîtra dans l'arbre comme enfant du processus 
auquel targetPid appartient, peu importe le vrai appelant.
──[ 7. Conclusion ]──

Les kernel callbacks sont le mécanisme de télémétrie le plus robuste que Windows 
fournit aux éditeurs de sécurité — robuste parce qu'ils n'ont besoin d'aucun
patching kernel et qu'ils transmettent du contexte riche. Les bypasser, c'est
plus dur que les techniques userland :
    Callback blinding              demande Ring 0 (BYOVD)
    Altération PEB/ligne commande  demande le contrôle du parent
    Process hollowing              visible aux heuristiques de scan mémoire
    PID spoofing                   visible si l'EDR cross-check les handles pare
nts

Les EDR modernes combinent plusieurs couches de détection, donc vaincre un seul 
type de callback est rarement suffisant. Comprendre toute la surface d'attaque —
et quels callbacks un EDR utilise vraiment — est essentiel pour les red et les
blue teams.