Aller au contenu

API Win32 et noyau en profondeur

Ce que vous allez apprendre

  • Comment surveiller les modifications du registre en temps reel avec RegNotifyChangeKeyValue
  • Les transactions KTM pour des modifications atomiques du registre
  • Les operations en masse : chargement, sauvegarde et remplacement de ruches
  • La gestion avancee de la securite via les API (DACL, SACL, prise de possession)
  • Les callbacks noyau avec CmRegisterCallbackEx pour intercepter toute operation registre
  • Les API non documentees (NtAPI) : renommer des cles, surveiller plusieurs cles simultanement
  • Les techniques d'optimisation et les benchmarks
  • L'interoperabilite avec C#, Python, Rust et Go
  • Le depannage des codes d'erreur et les pieges classiques

En 30 secondes

// Watch a registry key for changes in real time
HKEY hKey;
RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\MonApp", 0, KEY_NOTIFY, &hKey);
RegNotifyChangeKeyValue(hKey, TRUE, REG_NOTIFY_CHANGE_LAST_SET, NULL, FALSE);
printf("Something changed!\n");
RegCloseKey(hKey);

Analogie

Si les API de base sont le volant et les pedales d'une voiture, les API avancees de ce chapitre sont le turbo, l'ABS et le controle de traction. Vous pouvez conduire sans eux, mais ils deviennent indispensables quand vous poussez la machine a ses limites.

En resume

  • Ce chapitre couvre les API avancees : surveillance en temps reel, transactions, operations en masse, securite, callbacks noyau et API non documentees
  • RegNotifyChangeKeyValue est la fonction cle pour detecter les modifications du registre sans interrogation repetee

Rappel des API de base

Le chapitre 05 presente les API fondamentales : RegOpenKeyEx, RegQueryValueEx, RegSetValueEx, RegCloseKey. Les sections suivantes couvrent des fonctions que la plupart des developpeurs ne touchent jamais, mais qui sont essentielles pour les outils systeme, la securite et l'administration avancee.

En resume

  • Les API de base (RegOpenKeyEx, RegQueryValueEx, RegSetValueEx, RegCloseKey) sont couvertes au chapitre 05
  • Ce chapitre se concentre sur les fonctions avancees pour les outils systeme, la securite et l'administration

RegNotifyChangeKeyValue : surveillance en temps reel

Signature et parametres

LSTATUS RegNotifyChangeKeyValue(
    HKEY   hKey,            // Handle to an open key
    BOOL   bWatchSubtree,   // TRUE = watch all subkeys recursively
    DWORD  dwNotifyFilter,  // What changes to watch for
    HANDLE hEvent,          // Event object (async) or NULL (sync)
    BOOL   fAsynchronous    // TRUE = async, FALSE = blocking
);
Flag Valeur Declenchement
REG_NOTIFY_CHANGE_NAME 0x01 Ajout ou suppression d'une sous-cle
REG_NOTIFY_CHANGE_ATTRIBUTES 0x02 Modification des attributs
REG_NOTIFY_CHANGE_LAST_SET 0x04 Modification d'une valeur
REG_NOTIFY_CHANGE_SECURITY 0x08 Modification du descripteur de securite
REG_NOTIFY_THREAD_AGNOSTIC 0x10000000 Survit a la fin du thread (Windows 8+)

Implementation synchrone

#include <windows.h>
#include <stdio.h>

int main(void) {
    HKEY hKey;
    RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\MonApp",
                  0, KEY_NOTIFY, &hKey);
    printf("Watching HKCU\\Software\\MonApp...\n");

    // Block until something changes
    LSTATUS s = RegNotifyChangeKeyValue(hKey, TRUE,
        REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_LAST_SET,
        NULL, FALSE);

    if (s == ERROR_SUCCESS) printf("Change detected!\n");
    else printf("Failed: %ld\n", s);

    RegCloseKey(hKey);
    return 0;
}

Implementation asynchrone avec evenements

#include <windows.h>
#include <stdio.h>

int main(void) {
    HKEY hKey;
    RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\MonApp",
                  0, KEY_NOTIFY, &hKey);
    HANDLE hEvent = CreateEventW(NULL, TRUE, FALSE, NULL);

    printf("Watching (async). Ctrl+C to stop.\n");
    while (1) {
        ResetEvent(hEvent);
        LSTATUS s = RegNotifyChangeKeyValue(hKey, TRUE,
            REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_LAST_SET |
            REG_NOTIFY_THREAD_AGNOSTIC,
            hEvent, TRUE);
        if (s != ERROR_SUCCESS) break;

        DWORD w = WaitForSingleObject(hEvent, 5000);
        if (w == WAIT_OBJECT_0)
            printf("[CHANGE] detected at tick %lu\n", GetTickCount());
        else if (w == WAIT_TIMEOUT)
            printf("[TICK] no change (5s)\n");
        else break;
    }
    CloseHandle(hEvent);
    RegCloseKey(hKey);
    return 0;
}

Surveillance multi-cles (WaitForMultipleObjects)

#include <windows.h>
#include <stdio.h>

typedef struct { HKEY hKey; HANDLE hEvent; WCHAR path[MAX_PATH]; } WATCH;

static LSTATUS RegisterWatch(WATCH *w) {
    ResetEvent(w->hEvent);
    return RegNotifyChangeKeyValue(w->hKey, TRUE,
        REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_LAST_SET |
        REG_NOTIFY_THREAD_AGNOSTIC, w->hEvent, TRUE);
}

int main(void) {
    WATCH w[2]; HANDLE ev[2];
    LPCWSTR paths[] = { L"Software\\MonApp", L"Software\\AutreApp" };
    for (int i = 0; i < 2; i++) {
        w[i].hEvent = CreateEventW(NULL, TRUE, FALSE, NULL);
        wcscpy_s(w[i].path, MAX_PATH, paths[i]);
        RegOpenKeyExW(HKEY_CURRENT_USER, paths[i], 0, KEY_NOTIFY, &w[i].hKey);
        ev[i] = w[i].hEvent;
        RegisterWatch(&w[i]);
    }
    while (1) {
        DWORD idx = WaitForMultipleObjects(2, ev, FALSE, INFINITE);
        if (idx >= WAIT_OBJECT_0 && idx < WAIT_OBJECT_0 + 2) {
            int i = idx - WAIT_OBJECT_0;
            wprintf(L"[CHANGE] %s\n", w[i].path);
            RegisterWatch(&w[i]);
        }
    }
    return 0;
}

Limite de 64 handles

WaitForMultipleObjects accepte au maximum 64 handles. Pour surveiller plus de cles, utilisez un I/O Completion Port ou NtNotifyChangeMultipleKeys (section NtAPI).

Wrapper C# (P/Invoke)

using System;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Win32;

public static class RegistryWatcher
{
    [DllImport("advapi32.dll", SetLastError = true)]
    private static extern int RegNotifyChangeKeyValue(
        IntPtr hKey, bool bWatchSubtree, int dwNotifyFilter,
        IntPtr hEvent, bool fAsynchronous);

    public static void Watch(RegistryKey key, CancellationToken ct)
    {
        IntPtr hKey = key.Handle.DangerousGetHandle();
        using var evt = new ManualResetEvent(false);
        IntPtr hEvent = evt.SafeWaitHandle.DangerousGetHandle();
        int filter = 0x01 | 0x04 | 0x10000000; // NAME + LAST_SET + THREAD_AGNOSTIC

        while (!ct.IsCancellationRequested)
        {
            evt.Reset();
            int r = RegNotifyChangeKeyValue(hKey, true, filter, hEvent, true);
            if (r != 0) throw new System.ComponentModel.Win32Exception(r);
            while (!evt.WaitOne(1000))
                if (ct.IsCancellationRequested) return;
            Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Change detected");
        }
    }
}

Wrapper PowerShell

$job = Start-Job -ScriptBlock {
    Add-Type -TypeDefinition @"
using System; using System.Runtime.InteropServices; using Microsoft.Win32;
public static class RegWatch {
    [DllImport("advapi32.dll")]
    public static extern int RegNotifyChangeKeyValue(
        IntPtr hKey, bool watch, int filter, IntPtr evt, bool async);
}
"@
    $key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey("Software\MonApp")
    $h = $key.Handle.DangerousGetHandle()
    while ($true) {
        # 0x05 = CHANGE_NAME | CHANGE_LAST_SET; sync mode (blocks)
        $r = [RegWatch]::RegNotifyChangeKeyValue($h, $true, 0x05, [IntPtr]::Zero, $false)
        if ($r -eq 0) { Write-Output "[$(Get-Date -Format 'HH:mm:ss')] Change detected" }
    }
}
Receive-Job -Job $job -Wait
Resultat attendu
[12:34:56] Change detected

Limitation et contournement

L'API ne dit pas CE QUI a change

RegNotifyChangeKeyValue signale qu'un changement a eu lieu, mais pas lequel. Contournement : prenez un instantane des valeurs avant la notification, attendez le signal, prenez un nouvel instantane, comparez les deux (pattern snapshot + diff).

En resume

  • RegNotifyChangeKeyValue surveille une cle en mode synchrone (bloquant) ou asynchrone (avec un objet evenement)
  • WaitForMultipleObjects permet de surveiller jusqu'a 64 cles simultanement ; au-dela, utilisez un I/O Completion Port
  • L'API ne precise pas quel changement a eu lieu : le pattern snapshot + diff est necessaire pour identifier les modifications

Transactions KTM (Kernel Transaction Manager)

Les transactions regroupent plusieurs modifications en une operation atomique : tout est applique, ou rien.

Fonction Role
CreateTransaction Creer un objet transaction
RegCreateKeyTransacted Creer/ouvrir une cle dans une transaction
RegOpenKeyTransacted Ouvrir une cle existante dans une transaction
RegDeleteKeyTransacted Supprimer une cle dans une transaction
CommitTransaction Appliquer toutes les modifications
RollbackTransaction Annuler toutes les modifications

Modification atomique avec rollback

#include <windows.h>
#include <ktmw32.h>
#include <stdio.h>
#pragma comment(lib, "ktmw32.lib")
#pragma comment(lib, "advapi32.lib")

int main(void) {
    HANDLE hTx = CreateTransaction(NULL, NULL, 0, 0, 0, 0, NULL);
    if (hTx == INVALID_HANDLE_VALUE) return 1;

    HKEY hKey1 = NULL, hKey2 = NULL;
    DWORD disp;
    LSTATUS s;

    // Operation 1: create Config key
    s = RegCreateKeyTransacted(HKEY_CURRENT_USER,
        L"Software\\MonApp\\Config", 0, NULL,
        REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL,
        &hKey1, &disp, hTx, NULL);
    if (s != ERROR_SUCCESS) goto rollback;

    DWORD ver = 2;
    s = RegSetValueExW(hKey1, L"Version", 0, REG_DWORD, (LPBYTE)&ver, sizeof(ver));
    if (s != ERROR_SUCCESS) goto rollback;

    // Operation 2: create State key
    s = RegCreateKeyTransacted(HKEY_CURRENT_USER,
        L"Software\\MonApp\\State", 0, NULL,
        REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL,
        &hKey2, &disp, hTx, NULL);
    if (s != ERROR_SUCCESS) goto rollback;

    DWORD migrated = 1;
    s = RegSetValueExW(hKey2, L"Migrated", 0, REG_DWORD, (LPBYTE)&migrated, sizeof(migrated));
    if (s != ERROR_SUCCESS) goto rollback;

    // All OK: commit
    CommitTransaction(hTx);
    printf("Transaction committed.\n");
    goto cleanup;

rollback:
    RollbackTransaction(hTx);
    printf("Transaction rolled back.\n");

cleanup:
    if (hKey1) RegCloseKey(hKey1);
    if (hKey2) RegCloseKey(hKey2);
    CloseHandle(hTx);
    return 0;
}

Deprecation de TxR

Microsoft deconseille les transactions registre depuis Windows 10 : surcharge de performance, deadlocks, maintenance couteuse. Les API fonctionnent toujours mais pourraient disparaitre. Alternative : sauvegardez les valeurs existantes dans une structure en memoire, appliquez les modifications, et restaurez manuellement en cas d'echec.

En resume

  • Les transactions KTM regroupent plusieurs modifications en une operation atomique (tout ou rien) avec CommitTransaction et RollbackTransaction
  • Les API transactionnelles (RegCreateKeyTransacted, RegOpenKeyTransacted) fonctionnent toujours mais sont depreciees depuis Windows 10
  • L'alternative recommandee est de sauvegarder les valeurs en memoire et de restaurer manuellement en cas d'echec

Operations en masse

Ces API manipulent des ruches entieres -- indispensables pour l'administration et la forensique.

RegLoadKey / RegUnLoadKey : ruches hors ligne

#include <windows.h>
#include <stdio.h>

static void EnablePrivilege(LPCWSTR priv) {
    HANDLE hToken; TOKEN_PRIVILEGES tp;
    OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken);
    LookupPrivilegeValueW(NULL, priv, &tp.Privileges[0].Luid);
    tp.PrivilegeCount = 1; tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    AdjustTokenPrivileges(hToken, FALSE, &tp, 0, NULL, NULL);
    CloseHandle(hToken);
}

int main(void) {
    EnablePrivilege(SE_BACKUP_NAME);
    EnablePrivilege(SE_RESTORE_NAME);

    // Load an offline NTUSER.DAT into HKLM\TempHive
    RegLoadKeyW(HKEY_LOCAL_MACHINE, L"TempHive", L"C:\\Backup\\NTUSER.DAT");

    HKEY hKey;
    RegOpenKeyExW(HKEY_LOCAL_MACHINE,
        L"TempHive\\Software\\Microsoft\\Windows\\CurrentVersion",
        0, KEY_READ, &hKey);
    WCHAR buf[256]; DWORD sz = sizeof(buf);
    RegQueryValueExW(hKey, L"ProgramFilesDir", NULL, NULL, (LPBYTE)buf, &sz);
    wprintf(L"ProgramFilesDir = %s\n", buf);
    RegCloseKey(hKey);

    // Unload
    RegUnLoadKeyW(HKEY_LOCAL_MACHINE, L"TempHive");
    return 0;
}

RegSaveKeyEx / RegRestoreKey

// Save: requires SeBackupPrivilege
HKEY hKey;
RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\MonApp", 0, KEY_READ, &hKey);
RegSaveKeyExW(hKey, L"C:\\Backup\\monapp.hiv", NULL, REG_LATEST_FORMAT);
// REG_STANDARD_FORMAT = compatible older Windows
// REG_LATEST_FORMAT   = compact, current Windows
// REG_NO_COMPRESSION  = fastest write, largest file
RegCloseKey(hKey);

// Restore: requires SeRestorePrivilege
RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\MonApp", 0, KEY_WRITE, &hKey);
RegRestoreKeyW(hKey, L"C:\\Backup\\monapp.hiv", REG_FORCE_RESTORE);
RegCloseKey(hKey);

RegReplaceKey : remplacement atomique

// Replace the SYSTEM hive atomically at next reboot
RegReplaceKeyW(HKEY_LOCAL_MACHINE, L"SYSTEM",
    L"C:\\Backup\\system_new.hiv",  // new hive
    L"C:\\Backup\\system_old.hiv"); // backup of current

Necessite un redemarrage

Le remplacement n'a lieu qu'au prochain boot. Si le fichier est corrompu, le systeme ne demarre plus.

RegLoadMUIString : chaines localisees

HKEY hKey;
RegOpenKeyExW(HKEY_LOCAL_MACHINE,
    L"SYSTEM\\CurrentControlSet\\Services\\Spooler", 0, KEY_READ, &hKey);
WCHAR name[256]; DWORD outSize;
RegLoadMUIStringW(hKey, L"DisplayName", name, sizeof(name), &outSize, 0, NULL);
// name = "Print Spooler" (EN) or "Spouleur d'impression" (FR)
RegCloseKey(hKey);

RegLoadAppKey : ruches applicatives privees

HKEY hAppKey;
RegLoadAppKeyW(L"C:\\MonApp\\settings.dat", &hAppKey, KEY_ALL_ACCESS, 0, 0);
DWORD val = 1;
RegSetValueExW(hAppKey, L"Initialized", 0, REG_DWORD, (LPBYTE)&val, sizeof(val));
RegCloseKey(hAppKey);  // hive auto-unloaded when all handles close

Avantage

Les ruches privees sont invisibles dans Regedit et evitent les conflits avec d'autres applications. Parfait pour les configs portables ou sensibles.

En resume

  • RegLoadKey / RegUnLoadKey permettent de charger des ruches hors ligne (NTUSER.DAT d'un autre utilisateur) pour lecture ou modification
  • RegSaveKeyEx sauvegarde une sous-arborescence et RegRestoreKey la restaure ; RegReplaceKey effectue un remplacement atomique au prochain redemarrage
  • RegLoadAppKey charge une ruche privee invisible dans Regedit, ideale pour les donnees applicatives isolees

Securite avancee via API

Lire la DACL d'une cle

#include <windows.h>
#include <sddl.h>
#include <stdio.h>

void ReadKeySecurity(HKEY hKey) {
    DWORD sdSize = 0;
    RegGetKeySecurity(hKey, DACL_SECURITY_INFORMATION, NULL, &sdSize);
    PSECURITY_DESCRIPTOR pSD = malloc(sdSize);
    RegGetKeySecurity(hKey, DACL_SECURITY_INFORMATION, pSD, &sdSize);

    LPWSTR sddl = NULL;
    ConvertSecurityDescriptorToStringSecurityDescriptorW(
        pSD, SDDL_REVISION_1, DACL_SECURITY_INFORMATION, &sddl, NULL);
    wprintf(L"SDDL: %s\n", sddl);
    LocalFree(sddl); free(pSD);
}

Construire une DACL par programmation

#include <windows.h>
#include <aclapi.h>
#pragma comment(lib, "advapi32.lib")

BOOL SetKeyDacl(HKEY hKey) {
    PSID pAdmin = NULL, pUsers = NULL;
    SID_IDENTIFIER_AUTHORITY ntAuth = SECURITY_NT_AUTHORITY;

    AllocateAndInitializeSid(&ntAuth, 2, SECURITY_BUILTIN_DOMAIN_RID,
        DOMAIN_ALIAS_RID_ADMINS, 0,0,0,0,0,0, &pAdmin);
    AllocateAndInitializeSid(&ntAuth, 2, SECURITY_BUILTIN_DOMAIN_RID,
        DOMAIN_ALIAS_RID_USERS, 0,0,0,0,0,0, &pUsers);

    EXPLICIT_ACCESS ea[2] = {0};
    // Administrators: Full Control
    ea[0].grfAccessPermissions = KEY_ALL_ACCESS;
    ea[0].grfAccessMode = SET_ACCESS;
    ea[0].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
    ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID;
    ea[0].Trustee.ptstrName = (LPWSTR)pAdmin;
    // Users: Read Only
    ea[1].grfAccessPermissions = KEY_READ;
    ea[1].grfAccessMode = SET_ACCESS;
    ea[1].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
    ea[1].Trustee.TrusteeForm = TRUSTEE_IS_SID;
    ea[1].Trustee.ptstrName = (LPWSTR)pUsers;

    PACL pDacl = NULL;
    SetEntriesInAcl(2, ea, NULL, &pDacl);
    DWORD err = SetSecurityInfo(hKey, SE_REGISTRY_KEY,
        DACL_SECURITY_INFORMATION, NULL, NULL, pDacl, NULL);

    LocalFree(pDacl); FreeSid(pAdmin); FreeSid(pUsers);
    return (err == ERROR_SUCCESS);
}

Prise de possession (SeTakeOwnershipPrivilege)

BOOL TakeKeyOwnership(LPCWSTR keyPath) {
    // 1. Enable SeTakeOwnershipPrivilege (same pattern as EnablePrivilege above)
    HANDLE hToken; TOKEN_PRIVILEGES tp;
    OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken);
    LookupPrivilegeValueW(NULL, SE_TAKE_OWNERSHIP_NAME, &tp.Privileges[0].Luid);
    tp.PrivilegeCount = 1; tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    AdjustTokenPrivileges(hToken, FALSE, &tp, 0, NULL, NULL);
    CloseHandle(hToken);

    // 2. Open key with WRITE_OWNER
    HKEY hKey;
    RegOpenKeyExW(HKEY_LOCAL_MACHINE, keyPath, 0, WRITE_OWNER, &hKey);

    // 3. Set Administrators as owner
    PSID pAdmin = NULL;
    SID_IDENTIFIER_AUTHORITY ntAuth = SECURITY_NT_AUTHORITY;
    AllocateAndInitializeSid(&ntAuth, 2, SECURITY_BUILTIN_DOMAIN_RID,
        DOMAIN_ALIAS_RID_ADMINS, 0,0,0,0,0,0, &pAdmin);
    DWORD err = SetSecurityInfo(hKey, SE_REGISTRY_KEY,
        OWNER_SECURITY_INFORMATION, pAdmin, NULL, NULL, NULL);
    FreeSid(pAdmin); RegCloseKey(hKey);
    return (err == ERROR_SUCCESS);
}

SACL : audit des acces

La SACL controle quels acces generent des evenements dans le journal de securite (evenement 4663). L'activation necessite SeSecurityPrivilege et l'ouverture avec ACCESS_SYSTEM_SECURITY.

// Open with SACL access
HKEY hKey;
RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\SensitiveApp",
    0, KEY_READ | ACCESS_SYSTEM_SECURITY, &hKey);
// Read/modify SACL with RegGetKeySecurity / RegSetKeySecurity
// using SACL_SECURITY_INFORMATION flag
RegCloseKey(hKey);

En resume

  • RegGetKeySecurity lit la DACL d'une cle sous forme SDDL ; SetEntriesInAcl et SetSecurityInfo permettent de construire et appliquer une DACL par programmation
  • SeTakeOwnershipPrivilege permet de prendre possession d'une cle protegee pour en modifier ensuite les permissions
  • La SACL controle l'audit des acces et genere des evenements 4663 dans le journal de securite

Mode noyau : CmRegisterCallbackEx

Les callbacks noyau interceptent toute operation registre du systeme. Un pre-callback peut bloquer l'operation, un post-callback peut l'auditer.

sequenceDiagram
    participant App as Application
    participant CM as Configuration Manager
    participant CB as Callback (driver)
    participant Reg as Ruche

    App->>CM: RegSetValueEx()
    CM->>CB: Pre-callback
    alt Autorise
        CB-->>CM: STATUS_SUCCESS
        CM->>Reg: Ecriture
        CM->>CB: Post-callback (audit)
        CM-->>App: Succes
    else Bloque
        CB-->>CM: STATUS_ACCESS_DENIED
        CM-->>App: Echec
    end

Types de callbacks (REG_NOTIFY_CLASS)

Pre-operation Post-operation Declenchement
RegNtPreCreateKey RegNtPostCreateKey Creation
RegNtPreOpenKey RegNtPostOpenKey Ouverture
RegNtPreDeleteKey RegNtPostDeleteKey Suppression de cle
RegNtPreSetValueKey RegNtPostSetValueKey Modification de valeur
RegNtPreDeleteValueKey RegNtPostDeleteValueKey Suppression de valeur
RegNtPreQueryValueKey RegNtPostQueryValueKey Lecture de valeur
RegNtPreRenameKey RegNtPostRenameKey Renommage

Driver squelette

#include <ntddk.h>

LARGE_INTEGER g_Cookie;

NTSTATUS RegistryCallback(PVOID Ctx, PVOID Arg1, PVOID Arg2) {
    UNREFERENCED_PARAMETER(Ctx);
    REG_NOTIFY_CLASS cls = (REG_NOTIFY_CLASS)(ULONG_PTR)Arg1;

    if (cls == RegNtPreSetValueKey) {
        PREG_SET_VALUE_KEY_INFORMATION info = (PREG_SET_VALUE_KEY_INFORMATION)Arg2;
        UNICODE_STRING protected = RTL_CONSTANT_STRING(L"ProtectedValue");
        if (info->ValueName &&
            RtlEqualUnicodeString(info->ValueName, &protected, TRUE)) {
            DbgPrint("[RegFilter] Blocked write to ProtectedValue\n");
            return STATUS_ACCESS_DENIED;
        }
    }
    if (cls == RegNtPreDeleteKey) {
        PREG_DELETE_KEY_INFORMATION info = (PREG_DELETE_KEY_INFORMATION)Arg2;
        DbgPrint("[RegFilter] Delete attempt on %p\n", info->Object);
    }
    if (cls == RegNtPostSetValueKey) {
        PREG_POST_OPERATION_INFORMATION info = (PREG_POST_OPERATION_INFORMATION)Arg2;
        if (NT_SUCCESS(info->Status))
            DbgPrint("[RegFilter] Value write succeeded on %p\n", info->Object);
    }
    return STATUS_SUCCESS;
}

VOID DriverUnload(PDRIVER_OBJECT Drv) {
    UNREFERENCED_PARAMETER(Drv);
    CmUnRegisterCallback(g_Cookie);
}

NTSTATUS DriverEntry(PDRIVER_OBJECT Drv, PUNICODE_STRING Path) {
    UNREFERENCED_PARAMETER(Path);
    UNICODE_STRING alt = RTL_CONSTANT_STRING(L"380000");
    NTSTATUS s = CmRegisterCallbackEx(RegistryCallback, &alt, Drv, NULL, &g_Cookie, NULL);
    if (NT_SUCCESS(s)) Drv->DriverUnload = DriverUnload;
    return s;
}
Domaine Usage typique
Antivirus Bloquer la desactivation de Windows Defender
EDR Journaliser les modifications des cles Run/RunOnce
Protection anti-sabotage Empecher la suppression des cles du service AV
Sandbox Rediriger les ecritures vers un espace isole

Performance

Le callback est invoque pour chaque operation registre du systeme. Filtrez rapidement, evitez les allocations memoire et ne faites jamais d'I/O dans le callback.

En resume

  • CmRegisterCallbackEx enregistre un callback noyau qui intercepte toute operation registre (pre et post-operation)
  • Un pre-callback peut bloquer une operation (retourner STATUS_ACCESS_DENIED), un post-callback peut l'auditer
  • Les antivirus, EDR et sandboxes utilisent ce mecanisme pour proteger ou surveiller les cles sensibles

NtAPI : les API non documentees

Les fonctions Nt* de ntdll.dll offrent des capacites impossibles via Win32.

Fonction Equivalent Win32 Specificite
NtCreateKey RegCreateKeyEx Options supplementaires
NtOpenKey RegOpenKeyEx Support OBJ_OPENLINK
NtQueryKey aucun Metadonnees detaillees
NtRenameKey aucun Seul moyen de renommer une cle
NtNotifyChangeMultipleKeys aucun Surveille plusieurs cles en un appel
NtQueryValueKey RegQueryValueEx Modes de lecture optimises

NtRenameKey : renommer une cle

#include <windows.h>
#include <winternl.h>
#include <stdio.h>

typedef NTSTATUS (NTAPI *PFN_NtRenameKey)(HANDLE, PUNICODE_STRING);

BOOL RenameRegistryKey(HKEY hParent, LPCWSTR subKey, LPCWSTR newName) {
    HKEY hKey;
    if (RegOpenKeyExW(hParent, subKey, 0, KEY_WRITE, &hKey) != ERROR_SUCCESS)
        return FALSE;

    PFN_NtRenameKey pNtRenameKey = (PFN_NtRenameKey)
        GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtRenameKey");
    if (!pNtRenameKey) { RegCloseKey(hKey); return FALSE; }

    UNICODE_STRING us;
    us.Length = (USHORT)(wcslen(newName) * sizeof(WCHAR));
    us.MaximumLength = us.Length + sizeof(WCHAR);
    us.Buffer = (PWSTR)newName;

    NTSTATUS st = pNtRenameKey(hKey, &us);
    RegCloseKey(hKey);
    return (st == 0);
}

Le parametre est le nouveau nom seul

Pour HKCU\Software\OldName, passez L"NewName" et non L"Software\\NewName".

NtNotifyChangeMultipleKeys

typedef NTSTATUS (NTAPI *PFN_NtNotifyChangeMultipleKeys)(
    HANDLE MasterKeyHandle, ULONG Count,
    OBJECT_ATTRIBUTES SubordinateObjects[],
    HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext,
    PIO_STATUS_BLOCK IoStatusBlock, ULONG CompletionFilter,
    BOOLEAN WatchTree, PVOID Buffer, ULONG BufferSize,
    BOOLEAN Asynchronous);

En pratique, la plupart des developpeurs l'appellent avec Count = 0 (equivalent a RegNotifyChangeKeyValue mais avec moins de surcharge Win32).

Python : NtRenameKey via ctypes

import ctypes
from ctypes import wintypes

ntdll = ctypes.WinDLL("ntdll")
advapi32 = ctypes.WinDLL("advapi32")

class UNICODE_STRING(ctypes.Structure):
    _fields_ = [("Length", wintypes.USHORT),
                ("MaximumLength", wintypes.USHORT),
                ("Buffer", wintypes.LPWSTR)]

def rename_key(parent, subkey_path, new_name):
    """Rename a registry key using NtRenameKey (undocumented API)."""
    hkey = wintypes.HKEY()
    advapi32.RegOpenKeyExW(parent, subkey_path, 0, 0x20006, ctypes.byref(hkey))

    buf = ctypes.create_unicode_buffer(new_name)
    us = UNICODE_STRING(len(new_name) * 2, len(new_name) * 2 + 2,
                        ctypes.cast(buf, wintypes.LPWSTR))

    ntdll.NtRenameKey.restype = ctypes.c_long
    ntdll.NtRenameKey.argtypes = [wintypes.HANDLE, ctypes.POINTER(UNICODE_STRING)]
    status = ntdll.NtRenameKey(hkey, ctypes.byref(us))
    advapi32.RegCloseKey(hkey)
    if status != 0:
        raise OSError(f"NtRenameKey failed: 0x{status & 0xFFFFFFFF:08X}")

if __name__ == "__main__":
    rename_key(0x80000001, "Software\\OldKeyName", "NewKeyName")
    print("Key renamed.")

Non documentee = risque de rupture

Les fonctions Nt* ne font pas partie du contrat API stable de Windows. Microsoft peut les modifier entre deux versions. En pratique, NtRenameKey n'a pas change depuis Windows XP, mais il n'existe aucune garantie.

En resume

  • NtRenameKey est le seul moyen de renommer une cle de registre sans la recreer manuellement
  • NtNotifyChangeMultipleKeys surveille plusieurs cles en un seul appel, depassant la limite de WaitForMultipleObjects
  • Les fonctions Nt* sont stables en pratique depuis Windows XP mais ne font partie d'aucun contrat API officiel

Performance et optimisation

Benchmarks

Lecture de 10 000 valeurs REG_SZ de 256 octets. Temps relatif a RegQueryValueEx (base 100).

API Temps Notes
RegQueryValueEx 100 Pas de validation de type
RegGetValue 105 Validation de type, expansion auto
NtQueryValueKey (Partial) 85 Acces direct noyau
NtQueryValueKey (Full) 92 Retourne aussi le nom
C# RegistryKey.GetValue 150 Marshalling .NET
PowerShell Get-ItemPropertyValue 1200 Surcharge pipeline

RegGetValue : l'API moderne recommandee

DWORD value; DWORD size = sizeof(value);
RegGetValueW(HKEY_CURRENT_USER, L"Software\\MonApp", L"Counter",
    RRF_RT_REG_DWORD,  // reject if not DWORD
    NULL, &value, &size);
// Returns ERROR_UNSUPPORTED_TYPE if the value is not REG_DWORD
Flag Effet
RRF_RT_REG_SZ Accepte REG_SZ uniquement
RRF_RT_REG_DWORD Accepte REG_DWORD uniquement
RRF_RT_ANY Accepte tous les types
RRF_NOEXPAND Ne pas expander les %variables%
RRF_SUBKEY_WOW6464KEY Force la vue 64-bit
RRF_SUBKEY_WOW6432KEY Force la vue 32-bit

Impact de la profondeur

Profondeur Temps relatif
1 niveau 100
5 niveaux 112
10 niveaux 135
20 niveaux 185
50 niveaux 340

Le Configuration Manager maintient un cache de cellules en memoire. La premiere lecture est lente (cache miss), les suivantes sont quasi-instantanees. Les ecritures invalident la page correspondante.

Lecture par lots

// WRONG: opens/closes the key 100 times
for (int i = 0; i < 100; i++)
    RegGetValueW(HKEY_CURRENT_USER, L"Software\\MonApp",
                 names[i], RRF_RT_ANY, NULL, &data, &sz);

// RIGHT: open once, read 100 times
HKEY hKey;
RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\MonApp", 0, KEY_READ, &hKey);
for (int i = 0; i < 100; i++) {
    DWORD s = sizeof(data);
    RegQueryValueExW(hKey, names[i], NULL, NULL, (LPBYTE)&data, &s);
}
RegCloseKey(hKey);
// ~40% faster on large batches

En resume

  • RegGetValue est l'API moderne recommandee : elle valide le type de donnees et rejette les types inattendus
  • La profondeur des cles impacte les performances (x3.4 a 50 niveaux) ; le cache du Configuration Manager amortit les lectures repetees
  • Ouvrir la cle une seule fois puis lire en boucle est environ 40% plus rapide que des appels independants a RegGetValue

Interoperabilite

Rust : crate winreg

use std::io;
use winreg::enums::*;
use winreg::RegKey;

fn main() -> io::Result<()> {
    let hkcu = RegKey::predef(HKEY_CURRENT_USER);
    let (key, _) = hkcu.create_subkey("Software\\MonApp")?;

    key.set_value("Version", &42u32)?;
    key.set_value("Name", &"MonApplication")?;

    let version: u32 = key.get_value("Version")?;
    let name: String = key.get_value("Name")?;
    println!("Version: {version}, Name: {name}");

    // Enumerate subkeys
    for entry in hkcu.open_subkey("Software")?.enum_keys() {
        println!("Subkey: {}", entry?);
    }
    hkcu.delete_subkey_all("Software\\MonApp")?;
    Ok(())
}

Go : golang.org/x/sys/windows/registry

package main

import (
    "fmt"
    "log"
    "golang.org/x/sys/windows/registry"
)

func main() {
    key, _, err := registry.CreateKey(registry.CURRENT_USER,
        `Software\MonApp`, registry.ALL_ACCESS)
    if err != nil { log.Fatal(err) }
    defer key.Close()

    key.SetDWordValue("Version", 42)
    key.SetStringValue("Name", "MonApplication")

    v, _, _ := key.GetIntegerValue("Version")
    n, _, _ := key.GetStringValue("Name")
    fmt.Printf("Version: %d, Name: %s\n", v, n)

    registry.DeleteKey(registry.CURRENT_USER, `Software\MonApp`)
}

C# (.NET) : NtRenameKey via P/Invoke

using System;
using System.Runtime.InteropServices;
using Microsoft.Win32;

public static class NtRegistry
{
    [StructLayout(LayoutKind.Sequential)]
    private struct UNICODE_STRING {
        public ushort Length, MaximumLength;
        public IntPtr Buffer;
    }

    [DllImport("ntdll.dll")]
    private static extern int NtRenameKey(IntPtr KeyHandle, ref UNICODE_STRING NewName);

    public static void RenameKey(RegistryKey parent, string oldSub, string newName)
    {
        using var key = parent.OpenSubKey(oldSub,
            RegistryKeyPermissionCheck.ReadWriteSubTree);
        IntPtr hKey = key!.Handle.DangerousGetHandle();
        IntPtr buf = Marshal.StringToHGlobalUni(newName);
        var us = new UNICODE_STRING {
            Length = (ushort)(newName.Length * 2),
            MaximumLength = (ushort)(newName.Length * 2 + 2),
            Buffer = buf };
        try {
            int s = NtRenameKey(hKey, ref us);
            if (s != 0) throw new InvalidOperationException($"0x{(uint)s:X8}");
        } finally { Marshal.FreeHGlobal(buf); }
    }
}
// NtRegistry.RenameKey(Registry.CurrentUser, @"Software\OldName", "NewName");

Python : surveillance avec ctypes

import ctypes, time
from ctypes import wintypes

advapi32 = ctypes.WinDLL("advapi32")
kernel32 = ctypes.WinDLL("kernel32")

def watch_key(subkey, timeout_ms=5000, rounds=10):
    hkey = wintypes.HKEY()
    advapi32.RegOpenKeyExW(0x80000001, subkey, 0, 0x0010, ctypes.byref(hkey))
    event = kernel32.CreateEventW(None, True, False, None)
    try:
        for _ in range(rounds):
            kernel32.ResetEvent(event)
            advapi32.RegNotifyChangeKeyValue(hkey, True, 0x04, event, True)
            w = kernel32.WaitForSingleObject(event, timeout_ms)
            if w == 0: print(f"[{time.strftime('%H:%M:%S')}] Change!")
            else:      print(f"[{time.strftime('%H:%M:%S')}] Timeout")
    finally:
        kernel32.CloseHandle(event)
        advapi32.RegCloseKey(hkey)

if __name__ == "__main__":
    watch_key("Software\\MonApp")

En resume

  • Rust (crate winreg), Go (golang.org/x/sys/windows/registry) et C# (P/Invoke) offrent des wrappers natifs pour les operations registre
  • NtRenameKey est accessible depuis Python (ctypes) et C# (P/Invoke) pour renommer des cles
  • Python peut surveiller les modifications du registre en temps reel via RegNotifyChangeKeyValue et ctypes

Depannage des API

Codes d'erreur

Code Constante Signification
0 ERROR_SUCCESS Reussite
2 ERROR_FILE_NOT_FOUND Cle ou valeur inexistante
5 ERROR_ACCESS_DENIED Droits insuffisants
6 ERROR_INVALID_HANDLE Handle invalide ou deja ferme
87 ERROR_INVALID_PARAMETER Parametre incorrect
234 ERROR_MORE_DATA Buffer trop petit
259 ERROR_NO_MORE_ITEMS Fin de l'enumeration
1010 ERROR_KEY_DELETED Cle marquee pour suppression
1013 ERROR_KEY_HAS_CHILDREN Suppression impossible (sous-cles presentes)
1314 ERROR_PRIVILEGE_NOT_HELD Privilege requis manquant

Piege n°1 : dimensionnement des buffers

// WRONG: fixed buffer, fails on long values
WCHAR buf[64]; DWORD sz = sizeof(buf);
RegQueryValueExW(hKey, L"LongValue", NULL, NULL, (LPBYTE)buf, &sz);

// RIGHT: query size first, then allocate
DWORD sz = 0;
RegQueryValueExW(hKey, L"LongValue", NULL, NULL, NULL, &sz);
LPBYTE data = malloc(sz);
RegQueryValueExW(hKey, L"LongValue", NULL, NULL, data, &sz);
free(data);

Piege n°2 : Unicode vs ANSI

// WRONG: ANSI version truncates non-ASCII
RegQueryValueExA(hKey, "Param", NULL, NULL, (LPBYTE)buf, &sz);
// RIGHT: always use the W (Wide) version
RegQueryValueExW(hKey, L"Param", NULL, NULL, (LPBYTE)buf, &sz);

Piege n°3 : vue 32-bit vs 64-bit (WOW64)

Sur un systeme 64-bit, les applications 32-bit voient un registre redirige (WOW6432Node).

// 32-bit app reading the real 64-bit hive
RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft",
    0, KEY_READ | KEY_WOW64_64KEY, &hKey);

// 64-bit app reading the 32-bit (WOW6432Node) hive
RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft",
    0, KEY_READ | KEY_WOW64_32KEY, &hKey);
Flag Effet
KEY_WOW64_64KEY Force la vue 64-bit
KEY_WOW64_32KEY Force la vue 32-bit
aucun Vue du processus appelant

Debugger avec Process Monitor

Colonne Filtre recommande
Operation RegOpenKey, RegQueryValue, RegSetValue
Process Name Votre executable
Result is not SUCCESS (affiche uniquement les echecs)

Capture au boot

Process Monitor peut capturer les operations registre des le demarrage de Windows (Options > Enable Boot Logging). Indispensable pour diagnostiquer un probleme de service ou de driver.

En resume

  • ERROR_MORE_DATA (234) indique un buffer trop petit : interrogez d'abord la taille avec un buffer NULL puis allouez dynamiquement
  • Utilisez toujours les fonctions W (Wide/Unicode) au lieu des versions A (ANSI) pour eviter la corruption de caracteres
  • Sur un systeme 64 bits, KEY_WOW64_64KEY et KEY_WOW64_32KEY forcent la vue registre souhaitee independamment de l'architecture du processus

En resume

Les API avancees du registre couvrent un large spectre : surveillance en temps reel (RegNotifyChangeKeyValue), transactions atomiques (KTM), operations en masse sur les ruches, securite granulaire (DACL/SACL), callbacks noyau (CmRegisterCallbackEx) et API non documentees (NtRenameKey). Pour la majorite des cas, RegGetValue est le meilleur choix. Les NtAPI servent la performance extreme, les callbacks noyau offrent le controle total. Les trois erreurs les plus frequentes : dimensionnement des buffers, confusion Unicode/ANSI, et oubli de la redirection WOW64.