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.
──[ 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)
──[ 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);
2. Trouver l'adresse du PEB de l'enfant :
NtQueryInformationProcess(pi.hProcess, ProcessBasicInformation, ...);
3. Écraser CommandLine dans le PEB de l'enfant avec la vraie ligne
de commande malveillante :
WriteProcessMemory(pi.hProcess, cmdLineAddr, realCmd, realCmdLen, NULL);
4. Reprendre :
ResumeThread(pi.hThread);
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)
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);
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é.
──[ 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);
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.