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
- 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.
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.
Une chaîne C wide classique ressemble à :
Code: Select all
L"Kernel"
Code: Select all
L'\0'
Code: Select all
UNICODE_STRING
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;
- : longueur actuelle de la chaîne ;
Code: Select all
Length - : capacité maximale du buffer ;
Code: Select all
MaximumLength - : adresse du contenu Unicode.
Code: Select all
Buffer
3. Exemple simple de UNICODE_STRING
Exemple :
Code: Select all
UNICODE_STRING str;
RtlInitUnicodeString(
&str,
L"Kernel"
);
Code: Select all
Kernel
Code: Select all
6 * sizeof(WCHAR)
4. MaximumLength
`MaximumLength` représente la taille maximale disponible dans le buffer.
Exemple conceptuel :
Code: Select all
Length = 12
MaximumLength = 14
Il faut toujours distinguer :
Code: Select all
taille actuellement utilisée
```
et :
[code]
capacité disponible
Une erreur fréquente est de supposer qu'on peut toujours faire :
Code: Select all
KdPrint(("%ws\n", str.Buffer));
Or une `UNICODE_STRING` peut représenter un buffer dont la longueur est connue uniquement via :
Code: Select all
str.Length
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;
- longueur explicite ;
- capacité explicite ;
- pointeur vers le buffer.
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
Cela évite :
- les conversions répétées ;
- les problèmes de code page ;
- la perte d'information ;
- les dépendances inutiles à l'ANSI.
La fonction la plus classique est :
Code: Select all
RtlInitUnicodeString
Code: Select all
UNICODE_STRING DeviceName;
RtlInitUnicodeString(
&DeviceName,
L"\\Device\\MyDriver"
);
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"
);
En revanche, ceci peut devenir dangereux :
Code: Select all
WCHAR temp[64];
UNICODE_STRING str;
RtlInitUnicodeString(
&str,
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"
);
- pas d'allocation ;
- référence vers le buffer fourni ;
- calcul automatique des longueurs.
Dans du code plus moderne, on peut également rencontrer :
Code: Select all
RtlUnicodeStringInit
RtlUnicodeStringInitEx
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;
}
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);
13. Erreur classique : caractères vs octets
Supposons :
Code: Select all
WCHAR Buffer[64];
Code: Select all
sizeof(Buffer)
Code: Select all
64
Donc :
Code: Select all
str.MaximumLength =
sizeof(Buffer);
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
);
15. Exemple : symbolic link
Code: Select all
UNICODE_STRING SymbolicLink;
RtlInitUnicodeString(
&SymbolicLink,
L"\\DosDevices\\MyCounter"
);
IoCreateSymbolicLink(
&SymbolicLink,
&DeviceName
);
16. OBJECT_ATTRIBUTES et UNICODE_STRING
Les API natives utilisent souvent :
Code: Select all
OBJECT_ATTRIBUTES
Exemple :
Code: Select all
UNICODE_STRING Name;
OBJECT_ATTRIBUTES oa;
RtlInitUnicodeString(
&Name,
L"\\Registry\\Machine\\..."
);
InitializeObjectAttributes(
&oa,
&Name,
OBJ_CASE_INSENSITIVE,
nullptr,
nullptr
);
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
);
18. Allocation automatique lors d'une conversion
Avec :
Code: Select all
TRUE
Code: Select all
RtlAnsiStringToUnicodeString
Code: Select all
unicode.Buffer
Exemple :
Code: Select all
RtlFreeUnicodeString(
&unicode
);
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);
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
Code: Select all
ANSI_STRING ansi;
NTSTATUS status =
RtlUnicodeStringToAnsiString(
&ansi,
&unicode,
TRUE
);
Code: Select all
RtlFreeAnsiString(
&ansi
);
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*`.
Code: Select all
RtlInitUnicodeString(
&str,
L"Kernel"
);
Code: Select all
str.Buffer
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
);
Code: Select all
Qui alloue ?
↓
Qui libère ?
23. RtlFreeAnsiString
Même principe pour :
Code: Select all
ANSI_STRING
Code: Select all
RtlFreeAnsiString
Code: Select all
RtlFreeAnsiString(
&ansi
);
Pour copier une `UNICODE_STRING` vers une autre :
Code: Select all
RtlCopyUnicodeString
Exemple :
Code: Select all
WCHAR Buffer[128];
UNICODE_STRING destination;
destination.Buffer = Buffer;
destination.Length = 0;
destination.MaximumLength =
sizeof(Buffer);
RtlCopyUnicodeString(
&destination,
&source
);
Exemple :
Code: Select all
UNICODE_STRING a;
UNICODE_STRING b;
b = a;
- Length ;
- MaximumLength ;
- Buffer.
Ce n'est pas une copie profonde du texte.
26. Risque avec une copie superficielle
Exemple :
Code: Select all
b = a;
Code: Select all
a.Buffer
Code: Select all
b.Buffer
Il faut donc comprendre la différence entre :
Code: Select all
copier la structure
Code: Select all
copier le contenu
Cette fonction permet d'ajouter une `UNICODE_STRING` à une autre.
Exemple :
Code: Select all
NTSTATUS status =
RtlAppendUnicodeStringToString(
&Destination,
&Source
);
Code: Select all
MaximumLength
Cette variante accepte directement une chaîne wide classique.
Exemple :
Code: Select all
NTSTATUS status =
RtlAppendUnicodeToString(
&Destination,
L"\\Child"
);
29. RtlCompareUnicodeString
Pour comparer deux chaînes :
Code: Select all
LONG result =
RtlCompareUnicodeString(
&A,
&B,
TRUE
);
Typiquement :
- TRUE : case-insensitive ;
- FALSE : case-sensitive.
Le résultat suit la logique classique :
Code: Select all
< 0
== 0
> 0
31. RtlEqualUnicodeString
Exemple :
Code: Select all
BOOLEAN equal =
RtlEqualUnicodeString(
&A,
&B,
TRUE
);
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
);
- chemins ;
- noms d'objets ;
- préfixes de registre ;
- classifications de noms.
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"
);
34. Buffers locaux
Exemple dangereux :
Code: Select all
UNICODE_STRING MakeName()
{
WCHAR Buffer[64];
UNICODE_STRING str;
RtlInitUnicodeString(
&str,
Buffer
);
return str;
}
Code: Select all
Buffer
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;
- `Name.Buffer` référence une chaîne externe ;
- le buffer appartient à `MY_DATA` ;
- une copie profonde est créée.
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"
);
37. Chaînes et fichiers
Les API natives de fichiers utilisent des chemins NT.
Exemple :
Code: Select all
\??\C:\Temp\File.txt
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*
- : tailles exprimées en bytes ;
Code: Select all
Cb - : tailles exprimées en caractères.
Code: Select all
Cch
Exemple :
Code: Select all
WCHAR Buffer[64];
NTSTATUS status =
RtlStringCbCopyW(
Buffer,
sizeof(Buffer),
L"Kernel"
);
40. RtlStringCchCopyW
Exemple :
Code: Select all
WCHAR Buffer[64];
NTSTATUS status =
RtlStringCchCopyW(
Buffer,
RTL_NUMBER_OF(Buffer),
L"Kernel"
);
41. Différence Cb / Cch
Exemple :
Code: Select all
WCHAR Buffer[64];
Code: Select all
sizeof(Buffer)
Code: Select all
RTL_NUMBER_OF(Buffer)
42. RtlStringCbCatW
Pour concaténer en utilisant une taille en octets :
Code: Select all
RtlStringCbCatW(
Buffer,
sizeof(Buffer),
L"\\Child"
);
Même principe avec une taille exprimée en caractères :
Code: Select all
RtlStringCchCatW(
Buffer,
RTL_NUMBER_OF(Buffer),
L"\\Child"
);
Pour du formatage :
Code: Select all
RtlStringCbPrintfW(
Buffer,
sizeof(Buffer),
L"PID=%lu",
Pid
);
Version en nombre de caractères :
Code: Select all
RtlStringCchPrintfW(
Buffer,
RTL_NUMBER_OF(Buffer),
L"Value=%ld",
Value
);
Des fonctions comme :
Code: Select all
wcscpy
wcscat
sprintf
Dans le kernel, un dépassement peut provoquer :
- corruption mémoire ;
- écrasement d'une structure ;
- bug check ;
- vulnérabilité de sécurité.
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;
}
Code: Select all
UNICODE_STRING DeviceName;
RtlInitUnicodeString(
&DeviceName,
Buffer
);
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
);
Exemple incorrect :
Code: Select all
RtlAnsiStringToUnicodeString(
&unicode,
&ansi,
TRUE
);
// aucune libération
Code: Select all
unicode.Buffer
Il faut appeler :
Code: Select all
RtlFreeUnicodeString(
&unicode
);
Exemple :
Code: Select all
UNICODE_STRING str;
RtlInitUnicodeString(
&str,
L"Kernel"
);
RtlFreeUnicodeString(
&str
);
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;
La bonne valeur est :
Code: Select all
str.MaximumLength =
sizeof(Buffer);
Ne pas supposer que :
Code: Select all
str.Buffer[str.Length / sizeof(WCHAR)]
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
Code: Select all
str.Buffer
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.
Pour les structures NT :
Code: Select all
UNICODE_STRING
ANSI_STRING
Code: Select all
RtlInitUnicodeString
RtlInitAnsiString
RtlUnicodeStringInit
RtlUnicodeStringInitEx
Code: Select all
RtlAnsiStringToUnicodeString
RtlUnicodeStringToAnsiString
RtlFreeUnicodeString
RtlFreeAnsiString
Code: Select all
RtlCopyUnicodeString
RtlAppendUnicodeStringToString
RtlAppendUnicodeToString
RtlCompareUnicodeString
RtlEqualUnicodeString
RtlPrefixUnicodeString
Code: Select all
RtlStringCbCopyW
RtlStringCchCopyW
RtlStringCbCatW
RtlStringCchCatW
RtlStringCbPrintfW
RtlStringCchPrintfW
Une `UNICODE_STRING` peut être visualisée ainsi :
Code: Select all
UNICODE_STRING
|
+--> Length
|
+--> MaximumLength
|
+--> Buffer
|
v
texte Unicode
Avant toute libération :
Code: Select all
Qui a créé Buffer ?
↓
Cette API l'a-t-elle alloué ?
↓
Quelle fonction doit le libérer ?
Le kernel Windows utilise principalement :
Code: Select all
UNICODE_STRING
Cette structure contient :
Code: Select all
Length
MaximumLength
Buffer
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
Code: Select all
RtlAnsiStringToUnicodeString
Code: Select all
RtlFreeUnicodeString
Code: Select all
RtlStringCb*
RtlStringCch*
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.
