Windows repose sur un modèle objet centralisé. Processus, threads, fichiers, événements, mutex, sections, devices et plusieurs autres ressources sont représentés par des objets kernel.
Ce cours regroupe deux notions étroitement liées : l'Object Manager et la durée de vie des objets.
Sommaire
- 1. Rôle de l'Object Manager
- 2. Objet kernel et handle
- 3. Tables de handles
- 4. Namespace NT
- 5. OBJECT_ATTRIBUTES
- 6. UNICODE_STRING
- 7. DRIVER_OBJECT et DEVICE_OBJECT
- 8. Handles kernel
- 9. ObReferenceObjectByHandle
- 10. ObReferenceObject et ObDereferenceObject
- 11. Handle Count et Pointer Count
- 12. Pourquoi le lifetime est critique
- 13. IRP_MJ_CLEANUP et IRP_MJ_CLOSE
- 14. Rundown Protection
- 15. Fuites de références
- 16. Use-after-free
- 17. Symétrie des ressources
- 18. Sécurité des objets
- 19. Bonnes pratiques
1. Rôle de l'Object Manager
L'Object Manager fournit une infrastructure commune pour :
- créer les objets
- les nommer
- les stocker dans le namespace
- gérer leurs handles
- appliquer la sécurité
- gérer les références
- déterminer leur durée de vie
2. Objet kernel et handle
Un handle n'est pas un pointeur direct vers l'objet.
Conceptuellement :
Code: Select all
HANDLE
|
v
Handle Table Entry
|
v
Kernel Object
3. Tables de handles
Chaque processus possède une table de handles.
Un handle utilisateur n'est donc pas une adresse kernel exploitable directement.
Le kernel doit utiliser les API prévues pour résoudre ou référencer l'objet.
4. Namespace NT
Les objets nommés apparaissent dans un namespace hiérarchique.
Exemples :
Code: Select all
\Device
\Driver
\KnownDlls
\BaseNamedObjects
\Sessions
\GLOBAL??
5. OBJECT_ATTRIBUTES
De nombreuses API natives prennent :
Code: Select all
OBJECT_ATTRIBUTES
Code: Select all
InitializeObjectAttributes(
&oa,
&name,
OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE,
nullptr,
nullptr
);
- ObjectName
- RootDirectory
- Attributes
- SecurityDescriptor
6. UNICODE_STRING
Les noms kernel utilisent souvent :
Code: Select all
UNICODE_STRING
Code: Select all
UNICODE_STRING name;
RtlInitUnicodeString(&name, L"\\Device\\MyDevice");
- Length est en octets
- MaximumLength est en octets
- la chaîne n'est pas toujours null-terminated
7. DRIVER_OBJECT et DEVICE_OBJECT
Un driver possède un :
Code: Select all
DRIVER_OBJECT
Code: Select all
DEVICE_OBJECT
Code: Select all
IoCreateDevice(
DriverObject,
0,
&DeviceName,
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&DeviceObject
);
8. Handles kernel
Lorsqu'un driver ouvre un objet via une API Zw*, il peut utiliser :
Code: Select all
OBJ_KERNEL_HANDLE
9. ObReferenceObjectByHandle
Cette fonction permet d'obtenir un pointeur d'objet à partir d'un handle.
Exemple conceptuel :
Code: Select all
PVOID object = nullptr;
status = ObReferenceObjectByHandle(
Handle,
DesiredAccess,
ObjectType,
KernelMode,
&object,
nullptr
);
10. ObReferenceObject et ObDereferenceObject
Une référence explicite garantit que l'objet reste vivant.
Code: Select all
ObReferenceObject(Object);
// utilisation
ObDereferenceObject(Object);
11. Handle Count et Pointer Count
Un objet peut avoir :
- des handles ouverts
- des références kernel
Fermer le dernier handle ne signifie pas nécessairement que l'objet est immédiatement détruit.
12. Pourquoi le lifetime est critique
Un driver peut garder un pointeur pendant qu'un autre thread détruit l'objet.
Le problème classique :
Code: Select all
if (GlobalObject != nullptr)
Use(GlobalObject);
Il faut donc une synchronisation correcte et/ou une référence.
13. IRP_MJ_CLEANUP et IRP_MJ_CLOSE
Pour les objets fichier :
CLEANUP correspond généralement à la fin de l'utilisation des handles associés à l'ouverture.
CLOSE arrive lorsque le FILE_OBJECT peut réellement être détruit.
Cette distinction est directement liée au lifetime.
14. Rundown Protection
La rundown protection permet de garantir qu'une structure reste valide pendant qu'un traitement l'utilise.
Principe :
Code: Select all
Acquire
|
v
Use object
|
v
Release
Code: Select all
bloquer les nouvelles acquisitions
attendre les utilisations en cours
détruire
15. Fuites de références
Une référence oubliée peut empêcher la destruction.
Symptômes possibles :
- driver qui ne s'unload pas correctement
- objet qui reste vivant
- mémoire non libérée
- FILE_OBJECT ou DEVICE_OBJECT conservé trop longtemps
16. Use-after-free
L'erreur inverse est encore plus grave : utiliser un objet après libération.
Exemple conceptuel :
Code: Select all
ObDereferenceObject(Object);
// Object peut maintenant être détruit
Use(Object); // DANGER
17. Symétrie des ressources
Bonne règle :
Code: Select all
ObReferenceObject <-> ObDereferenceObject
ZwOpen... <-> ZwClose
ExAllocatePool2 <-> ExFreePool
IoCreateDevice <-> IoDeleteDevice
IoCreateSymbolicLink <-> IoDeleteSymbolicLink
18. Sécurité des objets
Les objets nommés peuvent avoir un Security Descriptor.
Le simple fait qu'un objet soit visible sous :
Code: Select all
\Device
\BaseNamedObjects
Les droits sont évalués lors de la création ou de l'ouverture du handle.
19. Bonnes pratiques
- Ne jamais conserver un pointeur sans comprendre son lifetime
- Prendre une référence si l'objet doit survivre à l'appel courant
- Libérer exactement une fois chaque référence acquise
- Synchroniser la destruction des structures globales
- Distinguer handles et pointeurs d'objets
- Utiliser Driver Verifier pendant les tests
Conclusion
L'Object Manager et le lifetime sont inséparables. Les handles permettent d'accéder aux objets, les références garantissent leur survie et le namespace permet de les retrouver. Un driver robuste doit savoir exactement qui possède chaque ressource et quand sa destruction devient possible.
Exercices
- Créer un Device Object nommé et le retrouver avec WinObj.
- Utiliser ObReferenceObjectByHandle sur un handle obtenu dans un driver de test.
- Tracer toutes les acquisitions/libérations d'un objet pour vérifier qu'elles sont équilibrées.
