Salut l’équipe
Dans cette partie, on quitte progressivement le simple “driver qui compile” pour entrer dans le vrai développement kernel.
Ici, on parle surtout de :
- contraintes d’exécution
- gestion mémoire
- structures internes du noyau
- chaînes de caractères kernel
- registry
- NTSTATUS
- exceptions
- debugging
Le vrai problème, c’est d’écrire du code qui :
- marche
- reste stable
- respecte l’IRQL
- ne corrompt pas la mémoire
- ne provoque pas de BSOD
En user mode, un programme tourne dans un environnement isolé. S’il se trompe, il peut planter, mais le système continue souvent de vivre.
En kernel mode, le code tourne avec des privilèges très élevés. Le driver peut :
- accéder à la mémoire noyau
- parler au matériel
- manipuler des objets critiques
- influencer l’ensemble du système
- corruption mémoire
- invalidation de structures système
- deadlock
- fuite de mémoire non paginée
- violation d’IRQL
- BSOD
Code: Select all
“Est-ce que mon code compile ?”
Code: Select all
“Dans quel contexte ma fonction est appelée ?”
“Quel IRQL ?”
“Est-ce que cette mémoire peut paginer ?”
“Est-ce que cette API est autorisée ici ?”
- IRQL
- mémoire paged / nonpaged
- structures kernel spécifiques
- NTSTATUS
- debugging
IRQL signifie Interrupt Request Level.
Il détermine concrètement :
- ce que ton code a le droit de faire
- quelles API sont autorisées
- si la mémoire pageable est accessible
- si ton thread peut être interrompu par autre chose
Code: Select all
PASSIVE_LEVELCode: Select all
APC_LEVELCode: Select all
DISPATCH_LEVEL
À ce niveau, tu peux généralement :
- accéder à la mémoire pageable
- appeler des routines susceptibles de bloquer
- faire des opérations lentes
- utiliser beaucoup d’API kernel classiques
À ce niveau, le code doit répondre rapidement.
On ne peut plus se permettre des opérations qui pourraient bloquer ou demander le chargement d’une page absente de la RAM.
Règle essentielle :
à DISPATCH_LEVEL, pas de mémoire pageable.
Si tu ne retiens qu’une phrase :
IRQL élevé = pas de paging.
API utile :
Code: Select all
KIRQL irql = KeGetCurrentIrql();
Code: Select all
VOID ShowCurrentIrql()
{
KIRQL irql = KeGetCurrentIrql();
KdPrint(("[+] Current IRQL = %u\n", irql));
if (irql == PASSIVE_LEVEL)
KdPrint(("[+] PASSIVE_LEVEL\n"));
else if (irql == APC_LEVEL)
KdPrint(("[+] APC_LEVEL\n"));
else if (irql >= DISPATCH_LEVEL)
KdPrint(("[+] DISPATCH_LEVEL or above\n"));
}
dans quel contexte cette routine peut être appelée ?
3) Gestion mémoire — le pool kernel
En kernel, on utilise le pool kernel.
Fonctions importantes :
Code: Select all
ExAllocatePoolCode: Select all
ExAllocatePoolWithTagCode: Select all
ExFreePool
Code: Select all
ExAllocatePoolWithTagDeux grandes familles :
- NonPagedPool : reste en RAM, utilisable dans des contextes sensibles
- PagedPool : peut être paginé, réservé aux contextes sûrs
Code: Select all
“Est-ce que je suis dans une IRP ?”
Code: Select all
“Dans quels contextes ce buffer sera-t-il utilisé ?”
Code: Select all
typedef struct _MY_CONTEXT
{
ULONG Value;
LIST_ENTRY Link;
} MY_CONTEXT, *PMY_CONTEXT;
PMY_CONTEXT ctx = ExAllocatePoolWithTag(
NonPagedPool,
sizeof(MY_CONTEXT),
'txCM'
);
if (!ctx)
return STATUS_INSUFFICIENT_RESOURCES;
RtlZeroMemory(ctx, sizeof(MY_CONTEXT));
Code: Select all
typedef struct _MY_CONFIG
{
ULONG Flags;
WCHAR Name[64];
} MY_CONFIG, *PMY_CONFIG;
PMY_CONFIG cfg = ExAllocatePoolWithTag(
PagedPool,
sizeof(MY_CONFIG),
'gfCM'
);
if (!cfg)
return STATUS_INSUFFICIENT_RESOURCES;
RtlZeroMemory(cfg, sizeof(MY_CONFIG));
Code: Select all
PVOID buffer = ExAllocatePoolWithTag(NonPagedPool, 1024, 'buf1');
if (!buffer)
return STATUS_INSUFFICIENT_RESOURCES;
Code: Select all
ExFreePool(buffer);
Code: Select all
NTSTATUS CreateSmallContext(PMY_CONTEXT* OutContext)
{
PMY_CONTEXT ctx;
if (!OutContext)
return STATUS_INVALID_PARAMETER;
*OutContext = NULL;
ctx = ExAllocatePoolWithTag(NonPagedPool, sizeof(MY_CONTEXT), 'txCM');
if (!ctx)
return STATUS_INSUFFICIENT_RESOURCES;
RtlZeroMemory(ctx, sizeof(MY_CONTEXT));
ctx->Value = 1234;
InitializeListHead(&ctx->Link);
*OutContext = ctx;
return STATUS_SUCCESS;
}
VOID DestroySmallContext(PMY_CONTEXT Context)
{
if (Context)
ExFreePool(Context);
}
- est la base
Code: Select all
ExAllocatePoolWithTag - = toujours en RAM
Code: Select all
NonPagedPool - = pageable
Code: Select all
PagedPool - toujours vérifier le retour
- toujours libérer
- toujours mettre un tag
Le code du driver lui-même peut être organisé en sections spéciales.
INIT
Pour le code d’initialisation, par exemple
Code: Select all
DriverEntryCode: Select all
#ifdef ALLOC_PRAGMA
#pragma alloc_text(INIT, DriverEntry)
#endif
Code: Select all
INITPAGE
Pour une routine pageable :
Code: Select all
#ifdef ALLOC_PRAGMA
#pragma alloc_text(PAGE, MyPagedRoutine)
#endif
PAGED_CODE()
Code: Select all
NTSTATUS MyPagedRoutine()
{
PAGED_CODE();
return STATUS_SUCCESS;
}
Autres routines à connaître :
Code: Select all
MmLockPagableCodeSectionCode: Select all
MmUnlockPagableImageSection
- : code d’initialisation
Code: Select all
INIT - : code pageable à bas IRQL
Code: Select all
PAGE - : garde-fou utile
Code: Select all
PAGED_CODE()
Les lookaside lists servent à optimiser les allocations / libérations répétées de petits objets de taille fixe.
APIs importantes :
Code: Select all
ExInitializeNPagedLookasideListCode: Select all
ExAllocateFromNPagedLookasideListCode: Select all
ExFreeToNPagedLookasideListCode: Select all
ExDeleteNPagedLookasideList
Code: Select all
NPAGED_LOOKASIDE_LIST g_Lookaside;
typedef struct _MY_NODE
{
ULONG Id;
LIST_ENTRY Link;
} MY_NODE, *PMY_NODE;
VOID InitLookaside()
{
ExInitializeNPagedLookasideList(
&g_Lookaside,
NULL,
NULL,
0,
sizeof(MY_NODE),
'doNM',
0
);
}
PMY_NODE AllocateNode()
{
PMY_NODE node = ExAllocateFromNPagedLookasideList(&g_Lookaside);
if (node)
RtlZeroMemory(node, sizeof(MY_NODE));
return node;
}
VOID FreeNode(PMY_NODE Node)
{
if (Node)
ExFreeToNPagedLookasideList(&g_Lookaside, Node);
}
beaucoup de petits objets de taille fixe, alloués/libérés fréquemment = lookaside list intéressante.
6) LIST_ENTRY — la fondation des listes kernel
Définition :
Code: Select all
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY* Flink;
struct _LIST_ENTRY* Blink;
} LIST_ENTRY, *PLIST_ENTRY;
- : élément suivant
Code: Select all
Flink - : élément précédent
Code: Select all
Blink
Tu intègres
Code: Select all
LIST_ENTRYCode: Select all
typedef struct _MY_ITEM
{
ULONG Value;
LIST_ENTRY Link;
} MY_ITEM, *PMY_ITEM;
Code: Select all
LIST_ENTRY Head;
InitializeListHead(&Head);
Code: Select all
InsertHeadList(&Head, &Item->Link);
InsertTailList(&Head, &Item->Link);
Code: Select all
RemoveEntryList(&Item->Link);
Code: Select all
PMY_ITEM item = CONTAINING_RECORD(entry, MY_ITEM, Link);
Code: Select all
PLIST_ENTRY entry;
PMY_ITEM item;
entry = Head.Flink;
while (entry != &Head)
{
item = CONTAINING_RECORD(entry, MY_ITEM, Link);
KdPrint(("Value = %lu\n", item->Value));
entry = entry->Flink;
}
- = mécanisme bas niveau de chaînage
Code: Select all
LIST_ENTRY - tu l’intègres dans tes structures
- est indispensable pour revenir à ta structure
Code: Select all
CONTAINING_RECORD
Version plus simple :
Code: Select all
typedef struct _SINGLE_LIST_ENTRY {
struct _SINGLE_LIST_ENTRY* Next;
} SINGLE_LIST_ENTRY, *PSINGLE_LIST_ENTRY;
Code: Select all
PushEntryListCode: Select all
PopEntryList
Code: Select all
LIST_ENTRYCode: Select all
SINGLE_LIST_ENTRY8) Strings kernel — structures et routines RTL
Structures importantes :
Code: Select all
ANSI_STRINGCode: Select all
UNICODE_STRING
Code: Select all
UNICODE_STRINGCode: Select all
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
Length et MaximumLength sont en octets, pas en nombre de caractères.
Le buffer n’est pas forcément nul-terminé.
Initialisation :
Code: Select all
UNICODE_STRING us;
RtlInitUnicodeString(&us, L"Hello");
Code: Select all
WCHAR buffer[128];
UNICODE_STRING us;
RtlInitEmptyUnicodeString(&us, buffer, sizeof(buffer));
Code: Select all
RtlCopyUnicodeStringCode: Select all
RtlAppendUnicodeStringToStringCode: Select all
RtlAppendUnicodeToStringCode: Select all
RtlCompareUnicodeStringCode: Select all
RtlCopyMemoryCode: Select all
RtlZeroMemory
Code: Select all
ANSI_STRING ansi;
UNICODE_STRING uni;
RtlInitAnsiString(&ansi, "test");
NTSTATUS status = RtlAnsiStringToUnicodeString(&uni, &ansi, TRUE);
if (NT_SUCCESS(status))
{
KdPrint(("[+] Converted: %wZ\n", &uni));
RtlFreeUnicodeString(&uni);
}
Code: Select all
VOID DemoUnicodeString()
{
UNICODE_STRING Prefix;
UNICODE_STRING Result;
WCHAR Buffer[128];
RtlInitUnicodeString(&Prefix, L"Kernel ");
RtlInitEmptyUnicodeString(&Result, Buffer, sizeof(Buffer));
RtlCopyUnicodeString(&Result, &Prefix);
RtlAppendUnicodeToString(&Result, L"World");
KdPrint(("[+] Result = %wZ\n", &Result));
}
- est partout dans le noyau
Code: Select all
UNICODE_STRING - il faut utiliser les routines
Code: Select all
Rtl* - il faut raisonner en termes de buffer et de taille
En kernel, on utilise les routines
Code: Select all
Zw*Code: Select all
ZwOpenKeyCode: Select all
ZwCreateKeyCode: Select all
ZwQueryValueKeyCode: Select all
ZwSetValueKeyCode: Select all
ZwDeleteKey
Code: Select all
\Registry\Machine\...Code: Select all
\Registry\User\...
Code: Select all
OBJECT_ATTRIBUTESExemple :
Code: Select all
UNICODE_STRING path;
OBJECT_ATTRIBUTES oa;
RtlInitUnicodeString(&path, L"\Registry\Machine\SOFTWARE\Test");
InitializeObjectAttributes(
&oa,
&path,
OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE,
NULL,
NULL
);
Code: Select all
HANDLE hKey;
NTSTATUS status;
status = ZwOpenKey(&hKey, KEY_READ, &oa);
if (!NT_SUCCESS(status))
return status;
Code: Select all
UNICODE_STRING valueName;
ULONG size = 0;
PKEY_VALUE_PARTIAL_INFORMATION info;
RtlInitUnicodeString(&valueName, L"MyValue");
status = ZwQueryValueKey(
hKey,
&valueName,
KeyValuePartialInformation,
NULL,
0,
&size
);
if (status != STATUS_BUFFER_TOO_SMALL && status != STATUS_BUFFER_OVERFLOW)
{
ZwClose(hKey);
return status;
}
info = ExAllocatePoolWithTag(PagedPool, size, 'gvRK');
if (!info)
{
ZwClose(hKey);
return STATUS_INSUFFICIENT_RESOURCES;
}
status = ZwQueryValueKey(
hKey,
&valueName,
KeyValuePartialInformation,
info,
size,
&size
);
Code: Select all
ULONG value = 1234;
UNICODE_STRING valueName;
RtlInitUnicodeString(&valueName, L"MyDword");
status = ZwSetValueKey(
hKey,
&valueName,
0,
REG_DWORD,
&value,
sizeof(value)
);
Code: Select all
ZwClose(hKey);
Code: Select all
REG_SZCode: Select all
REG_DWORDCode: Select all
REG_BINARYCode: Select all
REG_MULTI_SZCode: Select all
REG_EXPAND_SZ
Code: Select all
NTSTATUS ReadMyDword(ULONG* OutValue)
{
UNICODE_STRING keyPath;
UNICODE_STRING valueName;
OBJECT_ATTRIBUTES oa;
HANDLE hKey;
NTSTATUS status;
ULONG size = 0;
PKEY_VALUE_PARTIAL_INFORMATION info = NULL;
if (!OutValue)
return STATUS_INVALID_PARAMETER;
RtlInitUnicodeString(&keyPath, L"\\Registry\\Machine\\SOFTWARE\\Test");
RtlInitUnicodeString(&valueName, L"MyDword");
InitializeObjectAttributes(
&oa,
&keyPath,
OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE,
NULL,
NULL
);
status = ZwOpenKey(&hKey, KEY_READ, &oa);
if (!NT_SUCCESS(status))
return status;
status = ZwQueryValueKey(hKey, &valueName, KeyValuePartialInformation, NULL, 0, &size);
if (status != STATUS_BUFFER_TOO_SMALL && status != STATUS_BUFFER_OVERFLOW)
{
ZwClose(hKey);
return status;
}
info = ExAllocatePoolWithTag(PagedPool, size, 'gvRK');
if (!info)
{
ZwClose(hKey);
return STATUS_INSUFFICIENT_RESOURCES;
}
status = ZwQueryValueKey(hKey, &valueName, KeyValuePartialInformation, info, size, &size);
if (NT_SUCCESS(status) && info->Type == REG_DWORD && info->DataLength == sizeof(ULONG))
{
*OutValue = *(ULONG*)info->Data;
}
ExFreePool(info);
ZwClose(hKey);
return status;
}
- les routines servent au registry noyau
Code: Select all
Zw* - +
Code: Select all
UNICODE_STRINGsont la baseCode: Select all
OBJECT_ATTRIBUTES - la lecture de valeur suit souvent un pattern en deux appels
- il faut fermer les handles et libérer les buffers
En kernel, on renvoie très souvent un
Code: Select all
NTSTATUSExemple :
Code: Select all
NTSTATUS status;
status = ZwOpenKey(&hKey, KEY_READ, &oa);
Code: Select all
NT_SUCCESSCode: Select all
NT_ERROR
Code: Select all
if (!NT_SUCCESS(status))
return status;
Code: Select all
STATUS_SUCCESSCode: Select all
STATUS_UNSUCCESSFULCode: Select all
STATUS_INSUFFICIENT_RESOURCESCode: Select all
STATUS_ACCESS_DENIEDCode: Select all
STATUS_INVALID_PARAMETERCode: Select all
STATUS_BUFFER_TOO_SMALLCode: Select all
STATUS_BUFFER_OVERFLOW
Code: Select all
NTSTATUS status;
PVOID buffer = NULL;
buffer = ExAllocatePoolWithTag(NonPagedPool, 512, 'buf1');
if (!buffer)
return STATUS_INSUFFICIENT_RESOURCES;
status = SomeKernelRoutine(buffer);
if (!NT_SUCCESS(status))
{
ExFreePool(buffer);
return status;
}
ExFreePool(buffer);
return STATUS_SUCCESS;
- est partout
Code: Select all
NTSTATUS - on teste avec
Code: Select all
NT_SUCCESS - les erreurs doivent être propagées proprement
- il faut nettoyer avant de retourner
Le noyau Windows supporte le SEH.
On peut donc écrire :
Code: Select all
__tryCode: Select all
__except
Code: Select all
NTSTATUS SafeReadUlong(PULONG UserLikePointer, PULONG OutValue)
{
__try
{
*OutValue = *UserLikePointer;
return STATUS_SUCCESS;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return GetExceptionCode();
}
}
- les exceptions sont un outil de sécurité
- ce n’est pas une excuse pour écrire du code paresseux
- à utiliser avec parcimonie
Un driver kernel n’a pas de console classique.
Le debug passe généralement par :
Code: Select all
KdPrintCode: Select all
ASSERT- WinDbg
- Driver Verifier
Code: Select all
KdPrintCode: Select all
KdPrint(("[+] Hello from kernel\n"));
Code: Select all
UNICODE_STRINGCode: Select all
KdPrint(("[+] Device name = %wZ\n", &DeviceName));
Code: Select all
ASSERTCode: Select all
ASSERT(ptr != NULL);
À retenir :
- le debug kernel n’est pas du luxe
- sans logs ni assertions, un bug kernel est très difficile à diagnostiquer
Driver Verifier sert à pousser les drivers dans leurs retranchements.
Il peut :
- surveiller les allocations
- forcer certains scénarios
- détecter les violations d’IRQL
- repérer les corruptions de pool
- signaler des fuites ou usages incorrects
- Special Pool
- Pool Tracking
- IRQL checking
Code: Select all
“Ton driver a l’air de marcher ? On va voir s’il marche encore quand on arrête d’être gentil.”
14) Erreurs critiques à éviter absolument
- accéder à de la mémoire paged à IRQL élevé
- oublier
Code: Select all
ExFreePool - mal manipuler
Code: Select all
LIST_ENTRY - oublier
Code: Select all
CONTAINING_RECORD - mal gérer les tailles de buffer dans les strings
- ignorer les
Code: Select all
NTSTATUS - oublier
Code: Select all
ZwClose - mettre partout
Code: Select all
__try/__except
Si on devait condenser tout le chapitre :
- IRQL décide de ce que tu peux faire
- le pool décide où vivent tes données
- les sections pageable décident où vit ton code
- relie les objets kernel entre eux
Code: Select all
LIST_ENTRY - est le format de string central du noyau
Code: Select all
UNICODE_STRING - les routines servent à manipuler le registry et d’autres objets noyau
Code: Select all
Zw* - est le langage des retours d’erreur
Code: Select all
NTSTATUS - ,
Code: Select all
KdPrintet Driver Verifier sont tes meilleurs alliés pour trouver les bugsCode: Select all
ASSERT
le développement kernel, c’est avant tout une discipline de contexte, de rigueur et de mémoire.
16) Mini idées de défis pour s’entraîner
- Défi 1 — LIST_ENTRY
Créer une petite structure avec un entier et un, allouer trois objets, les insérer dans une liste, les parcourir et les libérer proprement.Code: Select all
LIST_ENTRY - Défi 2 — Pool
Écrire une routine qui alloue un contexte avec, initialise ses champs, puis une seconde routine qui le détruit proprement.Code: Select all
ExAllocatePoolWithTag - Défi 3 — Strings
Construire unde sortie en concaténant une chaîne initiale et un suffixe avecCode: Select all
UNICODE_STRING.Code: Select all
RtlAppendUnicodeToString - Défi 4 — Registry
Lire une valeurdans le registry viaCode: Select all
REG_DWORDetCode: Select all
ZwOpenKey, en respectant le pattern en deux appels.Code: Select all
ZwQueryValueKey - Défi 5 — NTSTATUS
Écrire une routine qui enchaîne plusieurs opérations, gère proprement chaque erreur, nettoie les ressources au bon moment et retourne le bon.Code: Select all
NTSTATUS
Si tu comprends vraiment :
- l’IRQL
- le pool
Code: Select all
LIST_ENTRYCode: Select all
UNICODE_STRING- le registry
Code: Select all
NTSTATUS- le debug
APIs / routines importantes à retenir de ce chapitre
IRQL
Code: Select all
KeGetCurrentIrql
Code: Select all
ExAllocatePoolCode: Select all
ExAllocatePoolWithTagCode: Select all
ExFreePoolCode: Select all
RtlZeroMemoryCode: Select all
RtlCopyMemory
Code: Select all
PAGED_CODECode: Select all
MmLockPagableCodeSectionCode: Select all
MmUnlockPagableImageSection
Code: Select all
ExInitializeNPagedLookasideListCode: Select all
ExAllocateFromNPagedLookasideListCode: Select all
ExFreeToNPagedLookasideListCode: Select all
ExDeleteNPagedLookasideList
Code: Select all
InitializeListHeadCode: Select all
InsertHeadListCode: Select all
InsertTailListCode: Select all
RemoveEntryListCode: Select all
PushEntryListCode: Select all
PopEntryListCode: Select all
CONTAINING_RECORD
Code: Select all
RtlInitUnicodeStringCode: Select all
RtlInitEmptyUnicodeStringCode: Select all
RtlInitAnsiStringCode: Select all
RtlCopyUnicodeStringCode: Select all
RtlAppendUnicodeStringToStringCode: Select all
RtlAppendUnicodeToStringCode: Select all
RtlCompareUnicodeStringCode: Select all
RtlAnsiStringToUnicodeStringCode: Select all
RtlUnicodeStringToAnsiStringCode: Select all
RtlFreeUnicodeStringCode: Select all
RtlFreeAnsiString
Code: Select all
InitializeObjectAttributesCode: Select all
ZwOpenKeyCode: Select all
ZwCreateKeyCode: Select all
ZwQueryValueKeyCode: Select all
ZwSetValueKeyCode: Select all
ZwDeleteKeyCode: Select all
ZwClose
Code: Select all
KdPrintCode: Select all
ASSERT
Code: Select all
NT_SUCCESSCode: Select all
NT_ERRORCode: Select all
__tryCode: Select all
__exceptCode: Select all
GetExceptionCode
