Gestion des chaînes de caractères en Kernel Windows

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

Moderator: Rick

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

Gestion des chaînes de caractères en Kernel Windows

Post by Hydraxx »

Gestion des chaînes de caractères en Kernel Windows

Dans le kernel Windows, les chaînes de caractères sont très souvent manipulées avec les structures NT natives :

Code: Select all

UNICODE_STRING
ANSI_STRING
Le noyau utilise principalement Unicode, notamment pour :
  • les noms de périphériques ;
  • les symbolic links ;
  • les chemins d'objets ;
  • les clés de registre ;
  • les noms de fichiers ;
  • de nombreuses API natives.
Une bonne compréhension de `UNICODE_STRING` est donc indispensable en WDM.

Le point fondamental est que une UNICODE_STRING n'est pas simplement un wchar_t*.

Elle contient :
  • une longueur courante ;
  • une longueur maximale ;
  • un pointeur vers le buffer.
1. Chaînes C et chaînes NT

Une chaîne C wide classique ressemble à :

Code: Select all

L"Kernel"
et repose généralement sur un caractère nul final :

Code: Select all

L'\0'
Une chaîne NT utilise une structure :

Code: Select all

UNICODE_STRING
qui stocke explicitement la longueur du contenu.

Cela permet au kernel de manipuler des chaînes sans dépendre systématiquement d'un terminateur nul.

2. Structure UNICODE_STRING

Conceptuellement :

Code: Select all

typedef struct _UNICODE_STRING
{
    USHORT Length;
    USHORT MaximumLength;
    PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
Les champs sont :
  • Code: Select all

    Length
    : longueur actuelle de la chaîne ;
  • Code: Select all

    MaximumLength
    : capacité maximale du buffer ;
  • Code: Select all

    Buffer
    : adresse du contenu Unicode.
Length et MaximumLength sont exprimés en octets, pas en caractères.

3. Exemple simple de UNICODE_STRING

Exemple :

Code: Select all

UNICODE_STRING str;

RtlInitUnicodeString(
    &str,
    L"Kernel"
);
Pour une chaîne de 6 caractères Unicode :

Code: Select all

Kernel
la longueur utile correspond conceptuellement à :

Code: Select all

6 * sizeof(WCHAR)
Le terminateur nul éventuel n'est pas compté dans `Length`.

4. MaximumLength

`MaximumLength` représente la taille maximale disponible dans le buffer.

Exemple conceptuel :

Code: Select all

Length        = 12
MaximumLength = 14
sur une chaîne wide pouvant contenir 6 caractères utiles plus un terminateur nul.

Il faut toujours distinguer :

Code: Select all

taille actuellement utilisée
```

et :

[code]
capacité disponible
5. UNICODE_STRING n'est pas forcément terminée par NULL

Une erreur fréquente est de supposer qu'on peut toujours faire :

Code: Select all

KdPrint(("%ws\n", str.Buffer));
Cela suppose implicitement que `Buffer` pointe vers une chaîne wide terminée par `L'\0'`.

Or une `UNICODE_STRING` peut représenter un buffer dont la longueur est connue uniquement via :

Code: Select all

str.Length
Il faut donc utiliser les API prévues pour ce type de structure ou être certain que le buffer est bien terminé par zéro.

6. Structure ANSI_STRING

Le modèle est très similaire :

Code: Select all

typedef struct _ANSI_STRING
{
    USHORT Length;
    USHORT MaximumLength;
    PCHAR  Buffer;
} ANSI_STRING, *PANSI_STRING;
Les mêmes principes s'appliquent :
  • longueur explicite ;
  • capacité explicite ;
  • pointeur vers le buffer.
Mais le contenu est ANSI au lieu d'Unicode.

7. Pourquoi Unicode est privilégié

Les API kernel Windows utilisent massivement Unicode.

Il est donc préférable d'utiliser :

Code: Select all

UNICODE_STRING
dès que possible.

Cela évite :
  • les conversions répétées ;
  • les problèmes de code page ;
  • la perte d'information ;
  • les dépendances inutiles à l'ANSI.
8. RtlInitUnicodeString

La fonction la plus classique est :

Code: Select all

RtlInitUnicodeString
Exemple :

Code: Select all

UNICODE_STRING DeviceName;

RtlInitUnicodeString(
    &DeviceName,
    L"\\Device\\MyDriver"
);
Cette fonction initialise la structure pour référencer une chaîne existante.

Elle n'alloue pas un nouveau buffer pour copier le texte.

La structure référence simplement le buffer fourni.

9. Durée de vie du buffer avec RtlInitUnicodeString

Exemple sûr :

Code: Select all

RtlInitUnicodeString(
    &DeviceName,
    L"\\Device\\MyDriver"
);
La chaîne littérale existe pendant toute la durée de vie du module.

En revanche, ceci peut devenir dangereux :

Code: Select all

WCHAR temp[64];

UNICODE_STRING str;

RtlInitUnicodeString(
    &str,
    temp
);
si `str` est conservée après la disparition de `temp`.

Le buffer doit rester valide aussi longtemps que la `UNICODE_STRING` l'utilise.

10. RtlInitAnsiString

Pour une chaîne ANSI :

Code: Select all

ANSI_STRING str;

RtlInitAnsiString(
    &str,
    "Kernel"
);
Même principe :
  • pas d'allocation ;
  • référence vers le buffer fourni ;
  • calcul automatique des longueurs.
11. API d'initialisation plus modernes

Dans du code plus moderne, on peut également rencontrer :

Code: Select all

RtlUnicodeStringInit
RtlUnicodeStringInitEx
Ces fonctions font partie des API de chaînes sûres du kernel.

Elles permettent notamment une gestion d'erreur plus explicite.

Exemple conceptuel :

Code: Select all

UNICODE_STRING str;

NTSTATUS status =
    RtlUnicodeStringInit(
        &str,
        L"\\Device\\Example"
    );

if (!NT_SUCCESS(status))
{
    return status;
}
12. Initialisation manuelle

Il est possible de remplir une `UNICODE_STRING` manuellement.

Exemple :

Code: Select all

WCHAR Buffer[64];

UNICODE_STRING str;

str.Buffer = Buffer;
str.Length = 0;
str.MaximumLength = sizeof(Buffer);
Cette approche est utile lorsqu'on veut remplir progressivement un buffer déjà alloué.

13. Erreur classique : caractères vs octets

Supposons :

Code: Select all

WCHAR Buffer[64];
La capacité en octets est :

Code: Select all

sizeof(Buffer)
et non :

Code: Select all

64
car chaque `WCHAR` occupe plus d'un octet.

Donc :

Code: Select all

str.MaximumLength =
    sizeof(Buffer);
est correct.

14. Exemple : nom de périphérique

Code: Select all

UNICODE_STRING DeviceName;

RtlInitUnicodeString(
    &DeviceName,
    L"\\Device\\MyCounter"
);

NTSTATUS status =
    IoCreateDevice(
        DriverObject,
        sizeof(DEVICE_EXTENSION),
        &DeviceName,
        FILE_DEVICE_UNKNOWN,
        0,
        FALSE,
        &DeviceObject
    );
Le nom du périphérique est fourni sous forme de `UNICODE_STRING`.

15. Exemple : symbolic link

Code: Select all

UNICODE_STRING SymbolicLink;

RtlInitUnicodeString(
    &SymbolicLink,
    L"\\DosDevices\\MyCounter"
);

IoCreateSymbolicLink(
    &SymbolicLink,
    &DeviceName
);
C'est un usage extrêmement classique en WDM.

16. OBJECT_ATTRIBUTES et UNICODE_STRING

Les API natives utilisent souvent :

Code: Select all

OBJECT_ATTRIBUTES
qui référence elle-même une `UNICODE_STRING`.

Exemple :

Code: Select all

UNICODE_STRING Name;
OBJECT_ATTRIBUTES oa;

RtlInitUnicodeString(
    &Name,
    L"\\Registry\\Machine\\..."
);

InitializeObjectAttributes(
    &oa,
    &Name,
    OBJ_CASE_INSENSITIVE,
    nullptr,
    nullptr
);
Ici encore, la durée de vie du buffer référencé par `Name` doit être correcte pendant l'utilisation de `oa`.

17. RtlAnsiStringToUnicodeString

Cette fonction convertit une chaîne ANSI vers Unicode.

Exemple :

Code: Select all

ANSI_STRING ansi;
UNICODE_STRING unicode;

RtlInitAnsiString(
    &ansi,
    "Kernel"
);

NTSTATUS status =
    RtlAnsiStringToUnicodeString(
        &unicode,
        &ansi,
        TRUE
    );
Le troisième paramètre indique ici que la fonction doit allouer le buffer destination.

18. Allocation automatique lors d'une conversion

Avec :

Code: Select all

TRUE
dans :

Code: Select all

RtlAnsiStringToUnicodeString
Windows alloue la mémoire nécessaire pour :

Code: Select all

unicode.Buffer
Il faut donc libérer cette mémoire ensuite.

Exemple :

Code: Select all

RtlFreeUnicodeString(
    &unicode
);
Sinon il y a fuite mémoire.

19. Conversion avec buffer fourni

On peut aussi fournir soi-même le buffer destination.

Exemple conceptuel :

Code: Select all

WCHAR Buffer[128];

UNICODE_STRING unicode;

unicode.Buffer = Buffer;
unicode.Length = 0;
unicode.MaximumLength =
    sizeof(Buffer);
Puis utiliser une fonction de conversion configurée pour ne pas allouer.

Dans ce cas :

il ne faut pas appeler RtlFreeUnicodeString sur un buffer qui ne vient pas de cette allocation.

20. RtlUnicodeStringToAnsiString

Conversion inverse :

Code: Select all

RtlUnicodeStringToAnsiString
Exemple :

Code: Select all

ANSI_STRING ansi;

NTSTATUS status =
    RtlUnicodeStringToAnsiString(
        &ansi,
        &unicode,
        TRUE
    );
Puis :

Code: Select all

RtlFreeAnsiString(
    &ansi
);
si la fonction a alloué le buffer.

21. Ownership des buffers

Avant de libérer une chaîne, il faut savoir d'où vient son buffer.

Trois cas fréquents :
  • buffer littéral / constant ;
  • buffer fourni manuellement par le driver ;
  • buffer alloué par une fonction `Rtl*`.
Exemple :

Code: Select all

RtlInitUnicodeString(
    &str,
    L"Kernel"
);
Ici :

Code: Select all

str.Buffer
ne doit pas être libéré avec `RtlFreeUnicodeString`.

22. RtlFreeUnicodeString

Cette fonction sert lorsque le buffer a été alloué par une API compatible, par exemple lors d'une conversion demandant une allocation.

Exemple :

Code: Select all

RtlFreeUnicodeString(
    &unicode
);
Le principe général est :

Code: Select all

Qui alloue ?
    ↓
Qui libère ?
Il faut respecter la convention de l'API.

23. RtlFreeAnsiString

Même principe pour :

Code: Select all

ANSI_STRING
avec :

Code: Select all

RtlFreeAnsiString
Exemple :

Code: Select all

RtlFreeAnsiString(
    &ansi
);
24. RtlCopyUnicodeString

Pour copier une `UNICODE_STRING` vers une autre :

Code: Select all

RtlCopyUnicodeString
La destination doit avoir un buffer suffisamment grand.

Exemple :

Code: Select all

WCHAR Buffer[128];

UNICODE_STRING destination;

destination.Buffer = Buffer;
destination.Length = 0;
destination.MaximumLength =
    sizeof(Buffer);

RtlCopyUnicodeString(
    &destination,
    &source
);
25. Copie profonde vs simple référence

Exemple :

Code: Select all

UNICODE_STRING a;
UNICODE_STRING b;

b = a;
Cette affectation copie uniquement :
  • Length ;
  • MaximumLength ;
  • Buffer.
Les deux structures référencent alors le même buffer.

Ce n'est pas une copie profonde du texte.

26. Risque avec une copie superficielle

Exemple :

Code: Select all

b = a;
Puis si le buffer d'origine disparaît :

Code: Select all

a.Buffer
et :

Code: Select all

b.Buffer
peuvent tous deux devenir invalides.

Il faut donc comprendre la différence entre :

Code: Select all

copier la structure
et :

Code: Select all

copier le contenu
27. RtlAppendUnicodeStringToString

Cette fonction permet d'ajouter une `UNICODE_STRING` à une autre.

Exemple :

Code: Select all

NTSTATUS status =
    RtlAppendUnicodeStringToString(
        &Destination,
        &Source
    );
La destination doit disposer de suffisamment de place dans :

Code: Select all

MaximumLength
28. RtlAppendUnicodeToString

Cette variante accepte directement une chaîne wide classique.

Exemple :

Code: Select all

NTSTATUS status =
    RtlAppendUnicodeToString(
        &Destination,
        L"\\Child"
    );
Très pratique lorsqu'on construit un chemin ou un nom d'objet.

29. RtlCompareUnicodeString

Pour comparer deux chaînes :

Code: Select all

LONG result =
    RtlCompareUnicodeString(
        &A,
        &B,
        TRUE
    );
Le dernier paramètre indique si la comparaison doit ignorer la casse.

Typiquement :
  • TRUE : case-insensitive ;
  • FALSE : case-sensitive.
30. Interpréter le résultat de comparaison

Le résultat suit la logique classique :

Code: Select all

< 0
== 0
> 0
Pour tester une égalité simple, il existe une API plus directe.

31. RtlEqualUnicodeString

Exemple :

Code: Select all

BOOLEAN equal =
    RtlEqualUnicodeString(
        &A,
        &B,
        TRUE
    );
Cette fonction est plus claire lorsqu'on veut simplement savoir si deux chaînes sont égales.

32. RtlPrefixUnicodeString

Cette fonction permet de vérifier si une chaîne commence par une autre.

Exemple conceptuel :

Code: Select all

UNICODE_STRING Prefix;
UNICODE_STRING Full;

BOOLEAN result =
    RtlPrefixUnicodeString(
        &Prefix,
        &Full,
        TRUE
    );
C'est utile pour :
  • chemins ;
  • noms d'objets ;
  • préfixes de registre ;
  • classifications de noms.
33. Chaînes constantes

Lorsqu'un texte est constant, il est préférable d'éviter une allocation dynamique inutile.

Exemple :

Code: Select all

UNICODE_STRING DeviceName;

RtlInitUnicodeString(
    &DeviceName,
    L"\\Device\\Example"
);
La chaîne littérale a une durée de vie compatible avec celle du module.

34. Buffers locaux

Exemple dangereux :

Code: Select all

UNICODE_STRING MakeName()
{
    WCHAR Buffer[64];

    UNICODE_STRING str;

    RtlInitUnicodeString(
        &str,
        Buffer
    );

    return str;
}
Quand la fonction retourne :

Code: Select all

Buffer
n'existe plus.

La `UNICODE_STRING` retournée contient alors un pointeur invalide.

35. Chaînes dans une structure kernel

Exemple :

Code: Select all

typedef struct _MY_DATA
{
    UNICODE_STRING Name;

} MY_DATA, *PMY_DATA;
Il faut décider si :
  • `Name.Buffer` référence une chaîne externe ;
  • le buffer appartient à `MY_DATA` ;
  • une copie profonde est créée.
Sans règle d'ownership claire, les bugs deviennent probables.

36. Chaînes et registre

Les API kernel de registre travaillent très souvent avec Unicode.

Exemple conceptuel :

Code: Select all

UNICODE_STRING ValueName;

RtlInitUnicodeString(
    &ValueName,
    L"MyValue"
);
Les chemins de clés et noms de valeurs doivent donc être manipulés avec les structures adaptées.

37. Chaînes et fichiers

Les API natives de fichiers utilisent des chemins NT.

Exemple :

Code: Select all

\??\C:\Temp\File.txt
ou d'autres formes d'objets dans le namespace NT.

Ces chemins ne sont pas toujours identiques aux chemins Win32 classiques vus en user mode.

38. API sûres de manipulation de chaînes

Windows fournit des familles d'API destinées à éviter de nombreux dépassements de buffer.

Deux familles importantes sont :

Code: Select all

RtlStringCb*
RtlStringCch*
La différence principale est :
  • Code: Select all

    Cb
    : tailles exprimées en bytes ;
  • Code: Select all

    Cch
    : tailles exprimées en caractères.
39. RtlStringCbCopyW

Exemple :

Code: Select all

WCHAR Buffer[64];

NTSTATUS status =
    RtlStringCbCopyW(
        Buffer,
        sizeof(Buffer),
        L"Kernel"
    );
Ici, la taille est donnée en octets.

40. RtlStringCchCopyW

Exemple :

Code: Select all

WCHAR Buffer[64];

NTSTATUS status =
    RtlStringCchCopyW(
        Buffer,
        RTL_NUMBER_OF(Buffer),
        L"Kernel"
    );
Ici, la taille est donnée en nombre de caractères.

41. Différence Cb / Cch

Exemple :

Code: Select all

WCHAR Buffer[64];
Avec une API `Cb` :

Code: Select all

sizeof(Buffer)
Avec une API `Cch` :

Code: Select all

RTL_NUMBER_OF(Buffer)
Confondre les deux peut provoquer de graves erreurs de taille.

42. RtlStringCbCatW

Pour concaténer en utilisant une taille en octets :

Code: Select all

RtlStringCbCatW(
    Buffer,
    sizeof(Buffer),
    L"\\Child"
);
43. RtlStringCchCatW

Même principe avec une taille exprimée en caractères :

Code: Select all

RtlStringCchCatW(
    Buffer,
    RTL_NUMBER_OF(Buffer),
    L"\\Child"
);
44. RtlStringCbPrintfW

Pour du formatage :

Code: Select all

RtlStringCbPrintfW(
    Buffer,
    sizeof(Buffer),
    L"PID=%lu",
    Pid
);
45. RtlStringCchPrintfW

Version en nombre de caractères :

Code: Select all

RtlStringCchPrintfW(
    Buffer,
    RTL_NUMBER_OF(Buffer),
    L"Value=%ld",
    Value
);
46. Pourquoi éviter les fonctions C non sûres

Des fonctions comme :

Code: Select all

wcscpy
wcscat
sprintf
peuvent être dangereuses si la taille de destination n'est pas correctement contrôlée.

Dans le kernel, un dépassement peut provoquer :
  • corruption mémoire ;
  • écrasement d'une structure ;
  • bug check ;
  • vulnérabilité de sécurité.
Les variantes `RtlString*` limitent ce risque.

47. Exemple complet : construire un nom

Code: Select all

WCHAR Buffer[128];

NTSTATUS status =
    RtlStringCchCopyW(
        Buffer,
        RTL_NUMBER_OF(Buffer),
        L"\\Device\\"
    );

if (!NT_SUCCESS(status))
{
    return status;
}

status =
    RtlStringCchCatW(
        Buffer,
        RTL_NUMBER_OF(Buffer),
        L"MyDriver"
    );

if (!NT_SUCCESS(status))
{
    return status;
}
Puis :

Code: Select all

UNICODE_STRING DeviceName;

RtlInitUnicodeString(
    &DeviceName,
    Buffer
);
48. Exemple complet : conversion ANSI vers Unicode

Code: Select all

ANSI_STRING ansi;
UNICODE_STRING unicode;

RtlInitAnsiString(
    &ansi,
    "KernelDriver"
);

NTSTATUS status =
    RtlAnsiStringToUnicodeString(
        &unicode,
        &ansi,
        TRUE
    );

if (!NT_SUCCESS(status))
{
    return status;
}

KdPrint((
    "Unicode length = %hu\n",
    unicode.Length
));

RtlFreeUnicodeString(
    &unicode
);
49. Erreur : oublier la libération

Exemple incorrect :

Code: Select all

RtlAnsiStringToUnicodeString(
    &unicode,
    &ansi,
    TRUE
);

// aucune libération
La mémoire allouée pour :

Code: Select all

unicode.Buffer
reste allouée.

Il faut appeler :

Code: Select all

RtlFreeUnicodeString(
    &unicode
);
50. Erreur : libérer une chaîne non allouée

Exemple :

Code: Select all

UNICODE_STRING str;

RtlInitUnicodeString(
    &str,
    L"Kernel"
);

RtlFreeUnicodeString(
    &str
);
C'est incorrect si le buffer n'a pas été alloué par l'API correspondante.

La structure ne possède pas nécessairement la mémoire qu'elle référence.

51. Erreur : MaximumLength incorrect

Exemple :

Code: Select all

WCHAR Buffer[64];

UNICODE_STRING str;

str.Buffer = Buffer;
str.Length = 0;
str.MaximumLength = 64;
Ici, `MaximumLength` représente 64 octets, pas 64 caractères.

La bonne valeur est :

Code: Select all

str.MaximumLength =
    sizeof(Buffer);
52. Erreur : supposer une terminaison NULL

Ne pas supposer que :

Code: Select all

str.Buffer[str.Length / sizeof(WCHAR)]
est toujours un `L'\0'`.

La `UNICODE_STRING` est définie par ses champs de longueur.

53. Erreur : buffer local expiré

Exemple :

Code: Select all

UNICODE_STRING str;

{
    WCHAR Buffer[32];

    RtlInitUnicodeString(
        &str,
        Buffer
    );
}

// Buffer n'existe plus
À partir de ce moment :

Code: Select all

str.Buffer
est invalide.

54. Bonnes pratiques
  • Privilégier Unicode.
  • Comprendre que `UNICODE_STRING` décrit un buffer ; elle ne possède pas forcément ce buffer.
  • Toujours savoir qui a alloué la mémoire.
  • Toujours savoir qui doit la libérer.
  • Utiliser les API `Rtl*` plutôt que des fonctions C dangereuses.
  • Faire très attention aux unités : octets vs caractères.
  • Éviter les conversions ANSI inutiles.
  • Ne pas garder un pointeur vers un buffer local expiré.
  • Vérifier les `NTSTATUS` retournés par les fonctions sûres.
55. API essentielles à retenir

Pour les structures NT :

Code: Select all

UNICODE_STRING
ANSI_STRING
Pour l'initialisation :

Code: Select all

RtlInitUnicodeString
RtlInitAnsiString
RtlUnicodeStringInit
RtlUnicodeStringInitEx
Pour les conversions :

Code: Select all

RtlAnsiStringToUnicodeString
RtlUnicodeStringToAnsiString
RtlFreeUnicodeString
RtlFreeAnsiString
Pour les opérations sur `UNICODE_STRING` :

Code: Select all

RtlCopyUnicodeString
RtlAppendUnicodeStringToString
RtlAppendUnicodeToString
RtlCompareUnicodeString
RtlEqualUnicodeString
RtlPrefixUnicodeString
Pour les buffers wide classiques :

Code: Select all

RtlStringCbCopyW
RtlStringCchCopyW
RtlStringCbCatW
RtlStringCchCatW
RtlStringCbPrintfW
RtlStringCchPrintfW
56. Modèle mental à retenir

Une `UNICODE_STRING` peut être visualisée ainsi :

Code: Select all

UNICODE_STRING
   |
   +--> Length
   |
   +--> MaximumLength
   |
   +--> Buffer
           |
           v
       texte Unicode
La structure ne dit pas à elle seule qui possède la mémoire.

Avant toute libération :

Code: Select all

Qui a créé Buffer ?
        ↓
Cette API l'a-t-elle alloué ?
        ↓
Quelle fonction doit le libérer ?
57. Résumé

Le kernel Windows utilise principalement :

Code: Select all

UNICODE_STRING
pour les chaînes de caractères.

Cette structure contient :

Code: Select all

Length
MaximumLength
Buffer
Les longueurs sont exprimées en octets.

Une `UNICODE_STRING` n'est pas obligatoirement terminée par un caractère nul.

Pour référencer une chaîne constante :

Code: Select all

RtlInitUnicodeString
Pour convertir ANSI vers Unicode :

Code: Select all

RtlAnsiStringToUnicodeString
et si la fonction alloue le buffer :

Code: Select all

RtlFreeUnicodeString
Pour les manipulations de buffers classiques, les API sûres :

Code: Select all

RtlStringCb*
RtlStringCch*
sont préférables aux fonctions C non bornées.

Enfin, le concept le plus important est l'ownership du buffer :

une structure UNICODE_STRING peut simplement référencer une mémoire existante sans en être propriétaire.

La bonne gestion des longueurs, du lifetime et de l'ownership évite les fuites, les use-after-free et les corruptions mémoire dans les drivers.

Who is online

Users browsing this forum: No registered users and 0 guests