Callbacks en Kernel Mode

Ce cours dédiée au développement de drivers en kernel mode sans Framework

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 144
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Callbacks en Kernel Mode

Post by Hydraxx »

Callbacks en Kernel Mode

Objectif

Les callbacks permettent à un driver d'être appelé automatiquement lorsqu'un événement se produit. Selon la famille utilisée, ils servent à recevoir une notification, inspecter une opération, modifier certains paramètres, bloquer certaines opérations ou transmettre ensuite l'événement à un client user-mode.

1. Principe général

Un callback est une fonction fournie par le driver au système.

Code: Select all

DriverEntry
   |
   | RegisterCallback(...)
   v
Windows mémorise la routine
   |
   | événement
   v
Callback(...)
Le callback évite de faire du polling permanent.

2. Callback vs thread système

Un thread système est créé volontairement par le driver avec une API comme :

Code: Select all

PsCreateSystemThread(...)
Un callback, lui, est appelé par Windows lorsqu'un événement précis survient.

3. Cycle de vie

Un callback suit généralement :

Code: Select all

Register
   |
   v
Event
   |
   v
Callback
   |
   v
Unregister
Le désenregistrement avant unload est essentiel. Sinon Windows pourrait appeler une routine située dans un driver déjà déchargé.

4. Règles générales
  • connaître l'IRQL ;
  • savoir si l'attente est autorisée ;
  • connaître la durée de vie des pointeurs reçus ;
  • protéger les structures globales ;
  • éviter le travail lourd ;
  • copier les données qu'on veut conserver ;
  • désenregistrer proprement au unload.
5. Executive Callback Objects

Les principales APIs sont :

Code: Select all

ExCreateCallback
ExRegisterCallback
ExNotifyCallback
ExUnregisterCallback
Ces callbacks sont génériques et peuvent servir de mécanisme publish/subscribe entre composants kernel.

6. ExCreateCallback

`ExCreateCallback` crée ou ouvre un objet callback.

Code: Select all

Callback Object
    |
    +--> Driver A
    +--> Driver B
    +--> Driver C
L'objet peut être nommé afin d'être retrouvé par plusieurs composants.

7. ExRegisterCallback

Un driver peut s'enregistrer sur l'objet :

Code: Select all

registration = ExRegisterCallback(
    callbackObject,
    MyCallback,
    context
);
Routine typique :

Code: Select all

VOID MyCallback(
    PVOID CallbackContext,
    PVOID Argument1,
    PVOID Argument2
);
8. ExNotifyCallback

Un composant déclenche la notification :

Code: Select all

ExNotifyCallback(
    callbackObject,
    argument1,
    argument2
);
Toutes les routines enregistrées sont appelées.

9. ExUnregisterCallback

Lorsqu'un driver ne veut plus recevoir les notifications :

Code: Select all

ExUnregisterCallback(registration);
Il faut également gérer correctement la référence sur l'objet callback.

10. Callbacks de processus

APIs importantes :

Code: Select all

PsSetCreateProcessNotifyRoutine
PsSetCreateProcessNotifyRoutineEx
La version `Ex` fournit davantage d'informations.

11. Process callback

Prototype :

Code: Select all

VOID ProcessNotify(
    PEPROCESS Process,
    HANDLE ProcessId,
    PPS_CREATE_NOTIFY_INFO CreateInfo
);
Si :

Code: Select all

CreateInfo != NULL
le processus est en création.

Si :

Code: Select all

CreateInfo == NULL
le processus se termine.

12. PS_CREATE_NOTIFY_INFO

Cette structure permet notamment d'obtenir des informations sur :
  • l'image ;
  • la command line ;
  • le parent ;
  • le contexte de création ;
  • le statut de création.
Si une donnée doit survivre après le callback, il faut la copier dans une allocation appartenant au driver.

13. Callbacks de threads

API classique :

Code: Select all

PsSetCreateThreadNotifyRoutine
Le callback reçoit généralement :

Code: Select all

HANDLE ProcessId
HANDLE ThreadId
BOOLEAN Create
`Create == TRUE` indique une création, sinon une terminaison.

14. Remote Thread Detection

En combinant contexte créateur et processus cible, on peut journaliser les créations de threads distants.

Attention : un remote thread n'est pas automatiquement malveillant. Des débogueurs, outils système ou logiciels de sécurité peuvent en produire.

15. Callbacks de chargement d'images

APIs :

Code: Select all

PsSetLoadImageNotifyRoutine
PsRemoveLoadImageNotifyRoutine
Ils permettent d'observer le chargement d'EXE, DLL et images kernel.

16. IMAGE_INFO

Le callback reçoit une structure `IMAGE_INFO` permettant notamment de connaître l'adresse de base et la taille de l'image.

En combinant Process + Thread + Image, on peut construire un petit système de monitoring.

17. Object Manager callbacks

API principale :

Code: Select all

ObRegisterCallbacks
Cette famille permet d'intercepter certaines opérations de handles sur des objets comme les processus et threads.

18. OB_CALLBACK_REGISTRATION

La structure contient notamment :
  • version ;
  • nombre d'opérations ;
  • altitude ;
  • contexte ;
  • tableau d'opérations.
L'altitude sert à ordonner plusieurs callbacks.

19. OB_OPERATION_REGISTRATION

Cette structure décrit :
  • le type d'objet ;
  • les opérations surveillées ;
  • le pre-callback ;
  • le post-callback.
Exemples de types :

Code: Select all

PsProcessType
PsThreadType
20. Handle Create / Duplicate

Opérations importantes :

Code: Select all

OB_OPERATION_HANDLE_CREATE
OB_OPERATION_HANDLE_DUPLICATE
Le driver peut donc surveiller création et duplication de handles.

21. Pre-operation callback

Le pre-callback reçoit :

Code: Select all

POB_PRE_OPERATION_INFORMATION
Il permet d'inspecter l'objet cible, l'opération, les paramètres et les droits demandés.

22. DesiredAccess

Lors de l'ouverture d'un processus :

Code: Select all

PROCESS_QUERY_INFORMATION
PROCESS_VM_READ
PROCESS_TERMINATE
peuvent faire partie du masque demandé.

Un pre-callback peut réduire certains droits :

Code: Select all

DesiredAccess &= ~PROCESS_TERMINATE;
Il ne faut pas inventer des droits supplémentaires qui n'ont pas été demandés.

23. Post-operation callback

Le post-callback reçoit :

Code: Select all

POB_POST_OPERATION_INFORMATION
Il est surtout utile pour observer le statut final et le résultat de l'opération.

24. ObUnRegisterCallbacks

Au cleanup :

Code: Select all

ObUnRegisterCallbacks(RegistrationHandle);
25. Projet : Process Protector

Architecture :

Code: Select all

Application
    |
    | IOCTL_ADD_PID
    v
Driver
    |
    | liste de PID protégés
    |
    | ObRegisterCallbacks
    v
Ouverture handle processus
    |
    | PreCallback
    v
retire PROCESS_TERMINATE
Ce projet combine IOCTL, synchronisation et Object Manager callbacks.

26. ERESOURCE

`ERESOURCE` est intéressant lorsque beaucoup de lectures ont lieu mais peu d'écritures.

Exemple :
  • callbacks : lecture de la liste de PID ;
  • IOCTL add/remove : écriture dans la liste.
27. Registry callbacks

Le Configuration Manager expose :

Code: Select all

CmRegisterCallbackEx
CmUnRegisterCallback
Un driver peut observer ou filtrer des opérations Registry.

28. REG_NOTIFY_CLASS

Le callback reçoit une classe décrivant l'opération.

Exemples :
  • création de clé ;
  • ouverture ;
  • suppression ;
  • modification d'une valeur ;
  • suppression d'une valeur ;
  • renommage ;
  • query ;
  • pre-operation ;
  • post-operation.
29. Pre-notifications Registry

Avant l'opération, le driver peut :
  • inspecter ;
  • logger ;
  • laisser continuer ;
  • retourner une erreur ;
  • dans certains cas modifier certaines données.
30. Post-notifications Registry

Après l'opération, le driver peut obtenir des informations telles que le statut final via des structures comme :

Code: Select all

REG_POST_OPERATION_INFORMATION
31. Exemple RegSetValue

Classes utiles :

Code: Select all

RegNtPreSetValueKey
RegNtPostSetValueKey
Le callback peut récupérer le nom de valeur, le type, la taille et les données.

32. Données de taille variable

Les noms et valeurs peuvent être de taille variable.

Une stratégie robuste consiste à allouer :

Code: Select all

sizeof(EVENT)
+ keyNameSize
+ valueNameSize
+ dataSize
et à utiliser des offsets.

33. Structure avec offsets

Exemple :

Code: Select all

struct EVENT
{
    ULONG Type;
    ULONG KeyNameOffset;
    ULONG ValueNameOffset;
    ULONG DataOffset;
    ULONG DataSize;
};
Layout :

Code: Select all

[EVENT][KeyName][ValueName][Data]
34. CmCallbackGetKeyObjectIDEx

Cette API peut servir à obtenir une représentation exploitable du nom d'une clé à partir de l'objet Registry.

Il faut respecter la routine de libération documentée pour les données retournées.

35. Performance Registry

Le Registry est très actif.

Le callback doit donc :
  • filtrer tôt ;
  • éviter les gros traitements ;
  • limiter les allocations ;
  • différer le travail lourd.
36. Stockage des événements

Un driver de monitoring peut utiliser une structure :

Code: Select all

struct EVENT_HEADER
{
    LIST_ENTRY Entry;
    LARGE_INTEGER Time;
    ULONG Type;
};
Puis ajouter chaque événement à une file.

37. LIST_ENTRY

APIs classiques :

Code: Select all

InsertTailList
RemoveHeadList
IsListEmpty
La liste doit être protégée si plusieurs callbacks peuvent l'utiliser simultanément.

38. Limiter la file

Si le producer est plus rapide que le consumer, la file peut grossir sans limite.

Il faut prévoir une stratégie :
  • limite maximale ;
  • drop ancien ;
  • drop nouveau ;
  • compteur de pertes.
39. Communication kernel vers user-mode

Le client peut récupérer les événements via :
  • `IRP_MJ_READ` ;
  • IOCTL ;
  • mécanisme asynchrone.
Architecture :

Code: Select all

Callbacks
   |
   v
Queue
   |
   v
IRP_MJ_READ
   |
   v
Client user-mode
40. Projet SysMon

Combiner :

Code: Select all

Process
Thread
Image
Registry
dans une file commune et afficher côté user-mode :

Code: Select all

[PROCESS] PID=1200 notepad.exe
[THREAD] PID=1200 TID=4200
[IMAGE] PID=1200 kernel32.dll
[REG] PID=1200 HKLM\...\Test = 42
41. Synchronisation

Les callbacks peuvent s'exécuter en concurrence.

Selon le contexte :
  • spin lock ;
  • fast mutex ;
  • ERESOURCE ;
  • autres mécanismes adaptés.
Toujours respecter l'IRQL du callback.

42. Garder les sections critiques courtes

Mauvais :

Code: Select all

AcquireLock();

allocation complexe
formatage
attente
traitement lourd

ReleaseLock();
Meilleur :

Code: Select all

AcquireLock();

modifier structure minimale

ReleaseLock();

traitement lourd ailleurs
43. Pointeurs temporaires

Ne jamais supposer qu'un pointeur reçu dans un callback reste valide après le retour.

Mauvais :

Code: Select all

g_Data = CallbackData;
Correct :

Code: Select all

AllocateOwnBuffer();
CopyData();
44. Cleanup et unload

Ordre général :

Code: Select all

1. empêcher nouvelles opérations internes
2. désenregistrer les callbacks
3. attendre les travaux internes si nécessaire
4. vider les files
5. libérer la mémoire
6. supprimer les devices
7. unload
45. Minifilter callbacks

Les minifilters utilisent eux aussi un modèle callback :
  • pre-create ;
  • post-create ;
  • pre-read ;
  • post-read ;
  • pre-write ;
  • post-write.
C'est cependant un domaine suffisamment vaste pour un cours séparé.

46. WFP callbacks

Windows Filtering Platform utilise des callouts pour le réseau.

Conceptuellement :

Code: Select all

Network event
    |
    v
WFP
    |
    v
Callout
Là encore, c'est un sous-domaine spécialisé.

47. Comparaison des familles

Executive
Callbacks génériques entre composants kernel.

Ps*
Processus, threads, images.

Ob*
Handles et Object Manager.

Cm*
Registry.

Minifilter
Filesystem.

WFP
Réseau.

48. Notification vs interception

Certaines callbacks sont surtout informatives :

Code: Select all

Process create
Thread create
Image load
D'autres permettent davantage d'interception :

Code: Select all

Ob pre-operation
Registry pre-operation
Minifilter pre-operation
49. Projet pratique 1

Callback Object entre deux drivers :
  • Driver A crée l'objet callback ;
  • Driver B s'enregistre ;
  • Driver A appelle `ExNotifyCallback` ;
  • Driver B reçoit la notification.
50. Projet pratique 2

Process Monitor :
  • création/terminaison ;
  • PID ;
  • parent PID ;
  • image ;
  • command line ;
  • transfert user-mode.
51. Projet pratique 3

Process Protector :
  • liste de PID ;
  • `ObRegisterCallbacks` ;
  • interception des handles ;
  • suppression de `PROCESS_TERMINATE`.
52. Projet pratique 4

Registry Monitor :
  • `CmRegisterCallbackEx` ;
  • filtre sur writes ;
  • nom de clé ;
  • nom de valeur ;
  • type ;
  • taille ;
  • données.
53. Projet avancé

Mini SysMon :

Code: Select all

Process callback -----Thread callback -------Image callback ---------> Queue ---> User-mode client
Registry callback -----/
54. Pièges classiques
  • oublier le désenregistrement ;
  • conserver un pointeur temporaire ;
  • faire trop de travail ;
  • mauvais IRQL ;
  • mauvais lock ;
  • deadlock ;
  • fuite mémoire ;
  • file illimitée ;
  • confusion pre/post ;
  • filtrage trop large ;
  • oublier la concurrence.
55. APIs principales à retenir

Executive

Code: Select all

ExCreateCallback
ExRegisterCallback
ExNotifyCallback
ExUnregisterCallback
Process / Thread / Image

Code: Select all

PsSetCreateProcessNotifyRoutineEx
PsSetCreateThreadNotifyRoutine
PsSetLoadImageNotifyRoutine
PsRemoveLoadImageNotifyRoutine
Object Manager

Code: Select all

ObRegisterCallbacks
ObUnRegisterCallbacks
Registry

Code: Select all

CmRegisterCallbackEx
CmUnRegisterCallback
CmCallbackGetKeyObjectIDEx
Conclusion

Les callbacks sont un mécanisme fondamental du kernel Windows.

Pour un développeur kernel orienté logiciel, les familles les plus importantes sont :

Code: Select all

Process
Thread
Image
Object Manager
Registry
Executive Callback Objects
La difficulté réelle n'est pas simplement d'enregistrer une fonction.

Il faut surtout maîtriser :
  • le contexte d'exécution ;
  • l'IRQL ;
  • le lifetime des données ;
  • la concurrence ;
  • le cleanup ;
  • les performances ;
  • les règles propres à chaque sous-système.
Un bon callback doit être court, prévisible, correctement synchronisé et toujours proprement désenregistré.

Who is online

Users browsing this forum: No registered users and 1 guest