Approfondissement des bases du kernel WDM

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

Moderator: Rick

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

Approfondissement des bases du kernel WDM

Post by Hydraxx »

Chapitre 3 — Techniques de programmation de base en kernel Windows (WDM)

Salut l’équipe 8-)

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
C’est un chapitre fondamental, parce qu’en kernel, le problème n’est pas seulement de faire marcher du code.
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
1) Introduction — le mindset kernel

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
Une erreur kernel peut provoquer :
  • corruption mémoire
  • invalidation de structures système
  • deadlock
  • fuite de mémoire non paginée
  • violation d’IRQL
  • BSOD
En kernel, on ne pense pas seulement :

Code: Select all

“Est-ce que mon code compile ?”
On pense surtout :

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 ?”
Piliers du chapitre :
  • IRQL
  • mémoire paged / nonpaged
  • structures kernel spécifiques
  • NTSTATUS
  • debugging
2) IRQL — le concept central du kernel

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
Niveaux à retenir au début :
PASSIVE_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
DISPATCH_LEVEL

À 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();
Exemple :

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"));
}
Avant de choisir ton pool, tes locks, tes API ou tes sections de code, il faut toujours se demander :

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 :
J’insiste surtout sur

Code: Select all

ExAllocatePoolWithTag
, parce que le tag est très utile pour l’analyse et le debug.

Deux grandes familles :
  • NonPagedPool : reste en RAM, utilisable dans des contextes sensibles
  • PagedPool : peut être paginé, réservé aux contextes sûrs
La vraie question n’est pas :

Code: Select all

“Est-ce que je suis dans une IRP ?”
La vraie question est :

Code: Select all

“Dans quels contextes ce buffer sera-t-il utilisé ?”
Exemple NonPagedPool :

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));
Exemple PagedPool :

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));
Toujours vérifier l’échec :

Code: Select all

PVOID buffer = ExAllocatePoolWithTag(NonPagedPool, 1024, 'buf1');

if (!buffer)
    return STATUS_INSUFFICIENT_RESOURCES;
Toute allocation doit être libérée :

Code: Select all

ExFreePool(buffer);
Exemple complet :

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);
}
À retenir :
  • Code: Select all

    ExAllocatePoolWithTag
    est la base
  • Code: Select all

    NonPagedPool
    = toujours en RAM
  • Code: Select all

    PagedPool
    = pageable
  • toujours vérifier le retour
  • toujours libérer
  • toujours mettre un tag
4) Code pageable vs non pageable

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

DriverEntry
.

Code: Select all

#ifdef ALLOC_PRAGMA
#pragma alloc_text(INIT, DriverEntry)
#endif
Une fonction placée en

Code: Select all

INIT
ne doit pas être rappelée plus tard.

PAGE

Pour une routine pageable :

Code: Select all

#ifdef ALLOC_PRAGMA
#pragma alloc_text(PAGE, MyPagedRoutine)
#endif
Cette routine ne doit tourner qu’à bas IRQL.

PAGED_CODE()

Code: Select all

NTSTATUS MyPagedRoutine()
{
    PAGED_CODE();

    return STATUS_SUCCESS;
}
En build de debug, cela vérifie que la routine est appelée à un IRQL compatible avec le paging.

Autres routines à connaître :
À retenir :
5) Optimisation mémoire — les lookaside lists

Les lookaside lists servent à optimiser les allocations / libérations répétées de petits objets de taille fixe.

APIs importantes :
Exemple :

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);
}
À retenir :

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;
Signification :
La liste est doublement chaînée et souvent circulaire.

Tu intègres

Code: Select all

LIST_ENTRY
dans ta propre structure :

Code: Select all

typedef struct _MY_ITEM
{
    ULONG Value;
    LIST_ENTRY Link;
} MY_ITEM, *PMY_ITEM;
Initialisation :

Code: Select all

LIST_ENTRY Head;
InitializeListHead(&Head);
Insertion :

Code: Select all

InsertHeadList(&Head, &Item->Link);
InsertTailList(&Head, &Item->Link);
Retrait :

Code: Select all

RemoveEntryList(&Item->Link);
Macro clé :

Code: Select all

PMY_ITEM item = CONTAINING_RECORD(entry, MY_ITEM, Link);
Parcours :

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;
}
À retenir :
  • Code: Select all

    LIST_ENTRY
    = mécanisme bas niveau de chaînage
  • tu l’intègres dans tes structures
  • Code: Select all

    CONTAINING_RECORD
    est indispensable pour revenir à ta structure
7) SINGLE_LIST_ENTRY

Version plus simple :

Code: Select all

typedef struct _SINGLE_LIST_ENTRY {
    struct _SINGLE_LIST_ENTRY* Next;
} SINGLE_LIST_ENTRY, *PSINGLE_LIST_ENTRY;
APIs typiques :

Code: Select all

LIST_ENTRY
est plus flexible.

Code: Select all

SINGLE_LIST_ENTRY
est plus simple et plus léger.

8) Strings kernel — structures et routines RTL

Structures importantes :
Définition de

Code: Select all

UNICODE_STRING
:

Code: Select all

typedef struct _UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
    PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
Piège majeur :

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");
Ou avec un buffer fourni :

Code: Select all

WCHAR buffer[128];
UNICODE_STRING us;

RtlInitEmptyUnicodeString(&us, buffer, sizeof(buffer));
Routines utiles :
Conversion ANSI ↔ Unicode :

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);
}
Exemple complet :

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));
}
À retenir :
  • Code: Select all

    UNICODE_STRING
    est partout dans le noyau
  • il faut utiliser les routines

    Code: Select all

    Rtl*
  • il faut raisonner en termes de buffer et de taille
9) Accès au registry depuis le kernel

En kernel, on utilise les routines

Code: Select all

Zw*
:
Les chemins ressemblent à :

Code: Select all

OBJECT_ATTRIBUTES
est fondamental.

Exemple :

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
);
Ouverture :

Code: Select all

HANDLE hKey;
NTSTATUS status;

status = ZwOpenKey(&hKey, KEY_READ, &oa);
if (!NT_SUCCESS(status))
    return status;
Lecture avec pattern en deux appels :

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
);
Écriture :

Code: Select all

ULONG value = 1234;
UNICODE_STRING valueName;

RtlInitUnicodeString(&valueName, L"MyDword");

status = ZwSetValueKey(
    hKey,
    &valueName,
    0,
    REG_DWORD,
    &value,
    sizeof(value)
);
Toujours fermer :

Code: Select all

ZwClose(hKey);
Types classiques :
Exemple complet minimaliste :

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;
}
À retenir :
  • les routines

    Code: Select all

    Zw*
    servent au registry noyau
  • Code: Select all

    UNICODE_STRING
    +

    Code: Select all

    OBJECT_ATTRIBUTES
    sont la base
  • la lecture de valeur suit souvent un pattern en deux appels
  • il faut fermer les handles et libérer les buffers
10) NTSTATUS — le langage des erreurs du kernel

En kernel, on renvoie très souvent un

Code: Select all

NTSTATUS
.

Exemple :

Code: Select all

NTSTATUS status;
status = ZwOpenKey(&hKey, KEY_READ, &oa);
Macros importantes :
La plus importante au début :

Code: Select all

if (!NT_SUCCESS(status))
    return status;
Codes fréquents :
Exemple :

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;
À retenir :
  • Code: Select all

    NTSTATUS
    est partout
  • on teste avec

    Code: Select all

    NT_SUCCESS
  • les erreurs doivent être propagées proprement
  • il faut nettoyer avant de retourner
11) Exceptions kernel

Le noyau Windows supporte le SEH.

On peut donc écrire :
Exemple :

Code: Select all

NTSTATUS SafeReadUlong(PULONG UserLikePointer, PULONG OutValue)
{
    __try
    {
        *OutValue = *UserLikePointer;
        return STATUS_SUCCESS;
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        return GetExceptionCode();
    }
}
À retenir :
  • les exceptions sont un outil de sécurité
  • ce n’est pas une excuse pour écrire du code paresseux
  • à utiliser avec parcimonie
12) Debug kernel

Un driver kernel n’a pas de console classique.

Le debug passe généralement par :

Code: Select all

KdPrint
:

Code: Select all

KdPrint(("[+] Hello from kernel\n"));
Exemple avec

Code: Select all

UNICODE_STRING
:

Code: Select all

KdPrint(("[+] Device name = %wZ\n", &DeviceName));

Code: Select all

ASSERT
:

Code: Select all

ASSERT(ptr != NULL);
Cela sert à exprimer une hypothèse qui doit être vraie en build de debug.

À retenir :
  • le debug kernel n’est pas du luxe
  • sans logs ni assertions, un bug kernel est très difficile à diagnostiquer
13) Driver Verifier

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
Vérifications importantes :
  • Special Pool
  • Pool Tracking
  • IRQL checking
Idée mentale :

Code: Select all

“Ton driver a l’air de marcher ? On va voir s’il marche encore quand on arrête d’être gentil.”
Si ton driver ne supporte pas Driver Verifier, il y a souvent encore des problèmes sérieux à régler.

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

    Code: Select all

    __try/__except
    partout
15) Résumé mental final

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
  • Code: Select all

    LIST_ENTRY
    relie les objets kernel entre eux
  • Code: Select all

    UNICODE_STRING
    est le format de string central du noyau
  • les routines

    Code: Select all

    Zw*
    servent à manipuler le registry et d’autres objets noyau
  • Code: Select all

    NTSTATUS
    est le langage des retours d’erreur
  • Code: Select all

    KdPrint
    ,

    Code: Select all

    ASSERT
    et Driver Verifier sont tes meilleurs alliés pour trouver les bugs
Autrement dit :

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

    Code: Select all

    LIST_ENTRY
    , allouer trois objets, les insérer dans une liste, les parcourir et les libérer proprement.
  • Défi 2 — Pool
    Écrire une routine qui alloue un contexte avec

    Code: Select all

    ExAllocatePoolWithTag
    , initialise ses champs, puis une seconde routine qui le détruit proprement.
  • Défi 3 — Strings
    Construire un

    Code: Select all

    UNICODE_STRING
    de sortie en concaténant une chaîne initiale et un suffixe avec

    Code: Select all

    RtlAppendUnicodeToString
    .
  • Défi 4 — Registry
    Lire une valeur

    Code: Select all

    REG_DWORD
    dans le registry via

    Code: Select all

    ZwOpenKey
    et

    Code: Select all

    ZwQueryValueKey
    , en respectant le pattern en deux appels.
  • 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
    .
Conclusion

Si tu comprends vraiment :
alors tu as déjà les vraies fondations pour écrire du code kernel propre.

APIs / routines importantes à retenir de ce chapitre

IRQL
Mémoire
Sections pageable
Lookaside
Listes
Strings
Registry
Debug
Gestion des statuts / exceptions
Ad92
Posts: 1
Joined: Thu Apr 09, 2026 3:48 pm
Contact:

Re: Approfondissement des bases du kernel WDM

Post by Ad92 »

Très interessant merci

Who is online

Users browsing this forum: No registered users and 0 guests