Registre Windows 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: 120
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Registre Windows en Kernel Mode

Post by Hydraxx »

Registre Windows en Kernel Mode

Le registre Windows peut être utilisé depuis un driver kernel pour charger ou conserver des paramètres persistants. En kernel mode, on manipule principalement des

Code: Select all

UNICODE_STRING
, des

Code: Select all

OBJECT_ATTRIBUTES
, des handles et les familles

Code: Select all

Zw*
,

Code: Select all

Rtl*
et

Code: Select all

Io*
.

1. Namespace NT du registre

Les chemins kernel utilisent le namespace NT, par exemple :

Code: Select all

\Registry\Machine\System\CurrentControlSet\Services\MyDriver
Les API kernel ne travaillent pas directement avec les constantes Win32 comme

Code: Select all

HKEY_LOCAL_MACHINE
.

2. UNICODE_STRING et OBJECT_ATTRIBUTES

Les noms de clés et de valeurs sont généralement représentés avec

Code: Select all

UNICODE_STRING
.

Code: Select all

UNICODE_STRING keyName;

RtlInitUnicodeString(
    &keyName,
    L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\MyDriver"
);
Pour ouvrir une clé, on initialise ensuite un

Code: Select all

OBJECT_ATTRIBUTES
:

Code: Select all

OBJECT_ATTRIBUTES oa;

InitializeObjectAttributes(
    &oa,
    &keyName,
    OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE,
    nullptr,
    nullptr
);
OBJ_KERNEL_HANDLE est important lorsqu'un driver peut s'exécuter dans le contexte d'un processus user mode.

3. Droits d'accès

Quelques droits importants :
Il faut demander uniquement les droits nécessaires.

4. Ouvrir une clé avec ZwOpenKey

Code: Select all

HANDLE key = nullptr;

NTSTATUS status =
    ZwOpenKey(
        &key,
        KEY_READ,
        &oa
    );

if (!NT_SUCCESS(status))
{
    return status;
}
Une clé ouverte doit être fermée avec :

Code: Select all

ZwClose(key);
5. Créer une clé avec ZwCreateKey

Code: Select all

ULONG disposition = 0;

status =
    ZwCreateKey(
        &key,
        KEY_READ | KEY_WRITE,
        &oa,
        0,
        nullptr,
        REG_OPTION_NON_VOLATILE,
        &disposition
    );
La variable

Code: Select all

disposition
permet de savoir si la clé existait déjà ou vient d'être créée.

6. Zw* et Nt*

Depuis un driver, on utilise généralement les variantes

Code: Select all

Zw*
. Les familles

Code: Select all

Nt*
et

Code: Select all

Zw*
peuvent aboutir aux mêmes services internes mais n'ont pas exactement les mêmes règles concernant le mode de l'appelant.

7. Clés associées aux périphériques

Pour un PDO, on peut utiliser :

Code: Select all

IoOpenDeviceRegistryKey
avec notamment :

Code: Select all

PLUGPLAY_REGKEY_DEVICE
PLUGPLAY_REGKEY_DRIVER
Le premier concerne les paramètres propres à l'instance du périphérique, le second ceux associés au driver.

Pour une interface de périphérique :

Code: Select all

IoOpenDeviceInterfaceRegistryKey
8. Lire une valeur avec ZwQueryValueKey

Le pattern le plus important est celui des deux appels :

Code: Select all

ULONG size = 0;

status =
    ZwQueryValueKey(
        key,
        &valueName,
        KeyValuePartialInformation,
        nullptr,
        0,
        &size
    );
Le premier appel permet de déterminer la taille nécessaire. On alloue ensuite le buffer, puis on rappelle la fonction.

9. STATUS_BUFFER_TOO_SMALL et STATUS_BUFFER_OVERFLOW

Lors du premier appel, ces statuts peuvent être attendus :

Code: Select all

STATUS_BUFFER_TOO_SMALL
STATUS_BUFFER_OVERFLOW
Ils indiquent que le buffer fourni n'est pas suffisant.

10. Allocation moderne du buffer

Code: Select all

PKEY_VALUE_PARTIAL_INFORMATION info =
    (PKEY_VALUE_PARTIAL_INFORMATION)
    ExAllocatePool2(
        POOL_FLAG_PAGED,
        size,
        'geRK'
    );

if (info == nullptr)
{
    return STATUS_INSUFFICIENT_RESOURCES;
}
Dans du vieux code WDM, on rencontrera souvent

Code: Select all

ExAllocatePool
ou

Code: Select all

ExAllocatePoolWithTag
.

11. Deuxième appel de ZwQueryValueKey

Code: Select all

status =
    ZwQueryValueKey(
        key,
        &valueName,
        KeyValuePartialInformation,
        info,
        size,
        &size
    );
Après utilisation :

Code: Select all

ExFreePool(info);
12. KEY_VALUE_PARTIAL_INFORMATION

Cette structure contient notamment :

Code: Select all

Type
DataLength
Data
Le champ

Code: Select all

Type
indique le type de la valeur et

Code: Select all

DataLength
sa taille.

13. Types de valeurs importants
Il faut toujours vérifier Type et DataLength avant d'interpréter Data.

14. Lire un REG_DWORD

Code: Select all

if (info->Type == REG_DWORD &&
    info->DataLength == sizeof(ULONG))
{
    ULONG value =
        *(PULONG)info->Data;
}
15. Lire un REG_QWORD

Code: Select all

if (info->Type == REG_QWORD &&
    info->DataLength == sizeof(ULONGLONG))
{
    ULONGLONG value =
        *(PULONGLONG)info->Data;
}
16. REG_SZ, REG_EXPAND_SZ et REG_MULTI_SZ

Code: Select all

REG_SZ
contient une chaîne Unicode.

Code: Select all

REG_EXPAND_SZ
peut contenir des références à des variables d'environnement.

Code: Select all

REG_MULTI_SZ
contient plusieurs chaînes Unicode terminées par zéro puis un zéro supplémentaire.

Exemple conceptuel :

Code: Select all

"One\0Two\0Three\0\0"
17. REG_BINARY

Code: Select all

REG_BINARY
contient des octets arbitraires. Le driver doit connaître le format attendu et vérifier la taille avant toute conversion ou cast.

18. Valider les tailles

Une taille lue dans le registre ne doit pas être utilisée sans limite.

Code: Select all

if (size == 0 || size > MAX_ALLOWED_SIZE)
{
    return STATUS_INVALID_BUFFER_SIZE;
}
Cela évite des allocations excessives ou des erreurs de calcul.

19. Écrire une valeur avec ZwSetValueKey

Code: Select all

UNICODE_STRING valueName;

RtlInitUnicodeString(
    &valueName,
    L"MyValue"
);

ULONG value = 42;

status =
    ZwSetValueKey(
        key,
        &valueName,
        0,
        REG_DWORD,
        &value,
        sizeof(value)
    );
Le handle doit posséder

Code: Select all

KEY_SET_VALUE
.

20. Supprimer une valeur

Code: Select all

ZwDeleteValueKey(
    key,
    &valueName
);
21. Supprimer une clé

Code: Select all

ZwDeleteKey(key);
ZwClose(key);
Le fait de supprimer la clé ne dispense pas de fermer le handle.

22. Fonctions Rtl* du registre

On peut aussi rencontrer :

Code: Select all

RtlQueryRegistryValues
RtlWriteRegistryValue
RtlDeleteRegistryValue
Ces fonctions fournissent des accès plus spécialisés ou simplifiés. Les API

Code: Select all

Zw*
restent importantes à maîtriser car elles exposent directement le modèle handles + objets.

23. ZwQueryKey

Code: Select all

ZwQueryKey
permet d'obtenir des informations sur une clé, par exemple avec :

Code: Select all

KeyFullInformation
24. KEY_FULL_INFORMATION

On y trouve notamment :
  • le nombre de sous-clés ;
  • le nombre de valeurs ;
  • la longueur maximale des noms ;
  • la longueur maximale des données ;
  • la date de dernière modification.
25. Énumérer les sous-clés

On utilise :

Code: Select all

ZwEnumerateKey
avec un index.

Exemple conceptuel :

Code: Select all

for (ULONG index = 0; ; ++index)
{
    status =
        ZwEnumerateKey(
            key,
            index,
            KeyBasicInformation,
            buffer,
            bufferSize,
            &resultLength
        );

    if (status == STATUS_NO_MORE_ENTRIES)
        break;

    if (!NT_SUCCESS(status))
        break;
}
26. KEY_BASIC_INFORMATION

Cette structure contient notamment :

Code: Select all

LastWriteTime
NameLength
Name
Le nom n'est pas forcément terminé par zéro. Sa longueur doit être déterminée avec

Code: Select all

NameLength
.

27. Énumérer les valeurs

On utilise :

Code: Select all

ZwEnumerateValueKey
On rencontre notamment :

Code: Select all

KeyValueBasicInformation
KeyValuePartialInformation
KeyValueFullInformation
28. KEY_VALUE_BASIC_INFORMATION

Cette structure contient notamment :

Code: Select all

Type
NameLength
Name
Elle est pratique lorsqu'on souhaite surtout connaître les noms et types des valeurs.

29. ZwFlushKey

Code: Select all

ZwFlushKey
permet de demander la persistance des modifications.

Il ne faut pas l'appeler systématiquement : Windows gère normalement lui-même l'écriture différée du registre.

30. IRQL

Les opérations de registre sont des opérations pouvant bloquer.

Elles sont généralement effectuées à :

Code: Select all

PASSIVE_LEVEL
Un accès au registre ne doit pas être réalisé depuis une ISR ou une DPC.

31. RegistryPath dans DriverEntry

Code: Select all

extern "C"
NTSTATUS DriverEntry(
    PDRIVER_OBJECT DriverObject,
    PUNICODE_STRING RegistryPath
)

Code: Select all

RegistryPath
indique la clé de service du driver.

Si ce chemin doit être conservé après

Code: Select all

DriverEntry
, il faut en créer une copie dont le driver contrôle la durée de vie.

32. Registre et PnP

Il faut distinguer :
  • configuration globale du driver ;
  • configuration d'une instance de périphérique ;
  • configuration d'une interface.
Les fonctions

Code: Select all

IoOpenDeviceRegistryKey
et

Code: Select all

IoOpenDeviceInterfaceRegistryKey
existent pour ces cas PnP.

33. Gestion des erreurs

Statuts fréquents :

Code: Select all

STATUS_OBJECT_NAME_NOT_FOUND
STATUS_OBJECT_PATH_NOT_FOUND
STATUS_BUFFER_TOO_SMALL
STATUS_BUFFER_OVERFLOW
STATUS_ACCESS_DENIED
STATUS_NO_MORE_ENTRIES
Toujours utiliser :

Code: Select all

NT_SUCCESS(status)
pour les tests généraux de réussite.

34. Nettoyage

Lorsqu'une fonction possède plusieurs ressources :

Code: Select all

HANDLE key;
PVOID buffer;
chaque chemin d'erreur doit correctement nettoyer ce qui a déjà été acquis.

Le modèle mental est :

Code: Select all

ouvrir
    ↓
allouer
    ↓
utiliser
    ↓
libérer
    ↓
fermer
35. Exemple complet : lire un DWORD

Code: Select all

NTSTATUS ReadDword(
    HANDLE Key,
    PUNICODE_STRING ValueName,
    PULONG Result
)
{
    ULONG size = 0;

    NTSTATUS status =
        ZwQueryValueKey(
            Key,
            ValueName,
            KeyValuePartialInformation,
            nullptr,
            0,
            &size
        );

    if (status != STATUS_BUFFER_TOO_SMALL &&
        status != STATUS_BUFFER_OVERFLOW)
    {
        return status;
    }

    if (size == 0 || size > 4096)
    {
        return STATUS_INVALID_BUFFER_SIZE;
    }

    PKEY_VALUE_PARTIAL_INFORMATION info =
        (PKEY_VALUE_PARTIAL_INFORMATION)
        ExAllocatePool2(
            POOL_FLAG_PAGED,
            size,
            'geRK'
        );

    if (info == nullptr)
    {
        return STATUS_INSUFFICIENT_RESOURCES;
    }

    status =
        ZwQueryValueKey(
            Key,
            ValueName,
            KeyValuePartialInformation,
            info,
            size,
            &size
        );

    if (NT_SUCCESS(status))
    {
        if (info->Type != REG_DWORD ||
            info->DataLength != sizeof(ULONG))
        {
            status = STATUS_OBJECT_TYPE_MISMATCH;
        }
        else
        {
            *Result =
                *(PULONG)info->Data;
        }
    }

    ExFreePool(info);

    return status;
}
36. Exemple complet : écrire un DWORD

Code: Select all

NTSTATUS WriteDword(
    HANDLE Key,
    PCWSTR Name,
    ULONG Value
)
{
    UNICODE_STRING valueName;

    RtlInitUnicodeString(
        &valueName,
        Name
    );

    return ZwSetValueKey(
        Key,
        &valueName,
        0,
        REG_DWORD,
        &Value,
        sizeof(Value)
    );
}
37. Erreurs classiques
  • oublier

    Code: Select all

    ZwClose
    ;
  • demander

    Code: Select all

    KEY_ALL_ACCESS
    sans nécessité ;
  • oublier

    Code: Select all

    OBJ_KERNEL_HANDLE
    dans un contexte où il est nécessaire ;
  • ne pas vérifier

    Code: Select all

    NTSTATUS
    ;
  • ne pas vérifier le type

    Code: Select all

    REG_*
    ;
  • ne pas vérifier

    Code: Select all

    DataLength
    ;
  • faire confiance à une taille gigantesque ;
  • confondre octets et caractères ;
  • supposer que tous les noms retournés sont terminés par zéro ;
  • accéder au registre à un IRQL trop élevé ;
  • conserver

    Code: Select all

    RegistryPath
    sans gérer la durée de vie de son buffer.
38. API essentielles à retenir

Code: Select all

ZwOpenKey
ZwCreateKey
ZwQueryValueKey
ZwSetValueKey
ZwDeleteValueKey
ZwDeleteKey
ZwEnumerateKey
ZwEnumerateValueKey
ZwQueryKey
ZwClose

IoOpenDeviceRegistryKey
IoOpenDeviceInterfaceRegistryKey
39. Structures essentielles

Code: Select all

UNICODE_STRING
OBJECT_ATTRIBUTES

KEY_VALUE_PARTIAL_INFORMATION
KEY_VALUE_BASIC_INFORMATION
KEY_VALUE_FULL_INFORMATION

KEY_BASIC_INFORMATION
KEY_FULL_INFORMATION
40. Modèle mental à retenir

Ouverture :

Code: Select all

UNICODE_STRING
      ↓
OBJECT_ATTRIBUTES
      ↓
ZwOpenKey
      ↓
HANDLE
Lecture :

Code: Select all

ZwQueryValueKey
      ↓
taille nécessaire
      ↓
ExAllocatePool2
      ↓
ZwQueryValueKey
      ↓
valider Type + DataLength
      ↓
traiter
      ↓
ExFreePool
Puis :

Code: Select all

ZwClose
41. Résumé

Le registre kernel repose principalement sur :

Code: Select all

UNICODE_STRING
OBJECT_ATTRIBUTES
HANDLE
Zw*
Le pattern fondamental est :

Code: Select all

initialiser le nom
      ↓
initialiser OBJECT_ATTRIBUTES
      ↓
ouvrir
      ↓
query / set / enumerate
      ↓
fermer
Pour les données de taille variable, le pattern en deux appels est essentiel.

Enfin, le driver doit systématiquement valider :
  • les tailles ;
  • les types ;
  • les droits ;
  • les handles ;
  • les allocations ;
  • l'IRQL ;
  • la durée de vie des buffers.
Pour du code moderne, les buffers temporaires seront typiquement alloués avec :

Code: Select all

ExAllocatePool2
tout en sachant reconnaître les anciennes API :

Code: Select all

ExAllocatePool
ExAllocatePoolWithTag
dans les drivers historiques.

Who is online

Users browsing this forum: No registered users and 0 guests