Debugging, Diagnostics, ETW et Debug en Win32

Développement système natif en c/c++ avec win32 ...

Moderator: Rick

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

Debugging, Diagnostics, ETW et Debug en Win32

Post by Hydraxx »

Salut, :D

aujourd’hui on attaque un très gros chapitre système : le debugging, le diagnostic, ETW et la Debug API Windows.

Ici on ne parle pas juste de mettre un breakpoint dans Visual Studio. On parle d’observer un processus en train de vivre, de mesurer des métriques, de capturer un état cohérent, de tracer ce que fait le système en temps réel, et dans les cas les plus avancés, de devenir soi-meme le debugger du processus cible.

C’est un bloc extrêmement important si tu veux aller vers :
  • le développement système Windows
  • les outils internes et diagnostics
  • la supervision technique
  • la sécurité, les EDR, les anti-cheat
  • le reverse engineering dynamique
  • le debugging avancé
  • l’instrumentation de services ou d’applications lourdes
Beaucoup de développeurs n’utilisent jamais ces API. Tant qu’on reste dans du code applicatif classique, on peut vivre sans. Mais dès qu’on veut comprendre ce qui se passe réellement sur une machine, ou écrire un outil sérieux, on finit presque toujours par rencontrer ces mécanismes.

Vision globale du chapitre

En réalité, tout ce chapitre repose sur une progression logique. On commence par observer simplement, puis on mesure, puis on capture un état, puis on trace en profondeur, puis on contrôle activement l’exécution.

Le modèle mental global est celui-ci :

Code: Select all

1. Observer simplement
   → OutputDebugString

2. Mesurer
   → Performance Counters / PDH

3. Capturer un état cohérent
   → Process Snapshots / PSS
   → éventuellement MiniDump dans d’autres scénarios

4. Tracer en profondeur
   → ETW

5. Contrôler activement
   → Debug API
Autrement dit :

Code: Select all

voir → mesurer → capturer → tracer → contrôler
C’est exactement la progression logique d’un développeur système qui passe d’un code “qui tourne” à un code ou à un outil capable d’expliquer ce qui se passe.

OutputDebugString : le diagnostic texte le plus simple

Le premier mécanisme de diagnostic est le plus léger : émettre un message de debug.

L’idée générale est très simple :

Code: Select all

Processus
↓
OutputDebugString
↓
Windows
↓
debugger / outil de capture
Cela permet à un processus d’émettre des informations de diagnostic sans ouvrir une console, sans écrire un fichier de log, et sans forcément posséder une interface.

API principales :
  • OutputDebugStringA
  • OutputDebugStringW
  • OutputDebugString
Exemple :

Code: Select all

OutputDebugStringW(L"Service started");
En pratique, on l’utilise pour :
  • afficher l’état d’une opération
  • signaler une erreur
  • indiquer une progression
  • dump une variable formatée
  • émettre un message que WinDbg, Visual Studio, DebugView ou un outil maison pourra capturer
Le mécanisme DBWIN historique

OutputDebugString n’est pas un “printf magique vers le vide”. Historiquement, le mécanisme de capture repose sur un schéma de communication bien connu autour de plusieurs objets nommés.

Noms à connaitre :

Code: Select all

DBWIN_BUFFER
DBWIN_BUFFER_READY
DBWIN_DATA_READY
Le modèle mental classique est celui-ci :

Code: Select all

Process A
↓
OutputDebugString
↓
écrit dans DBWIN_BUFFER
↓
signale DBWIN_DATA_READY
↓
outil de capture attend l'événement
↓
lit le buffer
↓
affiche le message
Le buffer historique est associé à une structure du style :
  • PID de l’émetteur
  • texte de debug
Dans beaucoup d’outils maison ou d’explications bas niveau, on rencontre une structure de type DBWIN_BUFFER ou équivalent logique.

Pourquoi OutputDebugString reste utile

Parce que c’est :
  • très léger
  • très simple à utiliser
  • capturable par beaucoup d’outils
  • parfait pour du diagnostic rapide
Mais il faut aussi connaitre ses limites :
  • ce n’est pas un vrai système de logging structuré
  • ce n’est pas idéal pour de gros volumes
  • ce n’est pas adapté aux analyses riches ou historiques
  • ce n’est pas une solution complète de supervision
C’est donc un excellent outil d’appoint, pas une architecture complète.

PDH et les Performance Counters : mesurer le système

Après l’observation texte, on passe à la mesure.

Windows expose énormément de compteurs système : CPU, mémoire, disque, réseau, processus, threads, objets noyau, etc. Ces compteurs sont visibles dans Performance Monitor et accessibles par API via PDH, Performance Data Helper.

Modèle mental :

Code: Select all

Kernel / services / sous-systèmes
↓
Performance Counters
↓
PDH
↓
ton programme ou PerfMon
Un compteur s’identifie par un chemin de la forme :

Code: Select all

\Objet(Instance)\Compteur
Exemples :

Code: Select all

\Processor(_Total)\% Processor Time
\Process(*)\% Processor Time
\Process(*)\ID Process
\Memory\Available MBytes
\PhysicalDisk(_Total)\Disk Reads/sec
Types et handles PDH à connaitre

Les types centraux de PDH sont :
  • PDH_HQUERY
  • PDH_HCOUNTER
  • PDH_STATUS
Le handle de query représente une requête logique contenant un ou plusieurs compteurs. Le handle de counter représente un compteur individuel ajouté à cette query.

APIs les plus importantes :
  • PdhOpenQuery
  • PdhCloseQuery
  • PdhAddCounter
  • PdhAddEnglishCounter
  • PdhRemoveCounter
  • PdhCollectQueryData
  • PdhCollectQueryDataEx
  • PdhGetFormattedCounterValue
  • PdhGetFormattedCounterArray
  • PdhGetRawCounterValue
  • PdhGetCounterInfo
  • PdhValidatePath
  • PdhExpandWildCardPath
  • PdhLookupPerfNameByIndex
  • PdhLookupPerfIndexByName
Ouvrir une query PDH

Exemple :

Code: Select all

PDH_HQUERY query = NULL;

PDH_STATUS status = PdhOpenQueryW(
    NULL,
    0,
    &query
);

if (status != ERROR_SUCCESS)
    return 1;
Erreur classique à éviter : le retour de PdhOpenQuery est un code de statut, pas le handle. Le vrai handle est dans la variable query.

Ajouter un compteur

Exemple :

Code: Select all

PDH_HCOUNTER counter = NULL;

PDH_STATUS status = PdhAddEnglishCounterW(
    query,
    L"\\Processor(_Total)\\% Processor Time",
    0,
    &counter
);

if (status != ERROR_SUCCESS)
{
    PdhCloseQuery(query);
    return 1;
}
Il faut bien comprendre la différence suivante :
  • PdhAddCounter dépend de la langue du système
  • PdhAddEnglishCounter prend un chemin anglais stable
En pratique, pour des outils techniques, on préfère très souvent la version anglaise.

Le double collect : règle fondamentale

Beaucoup de compteurs ne donnent pas une valeur instantanée brute, mais une valeur dérivée d’une variation dans le temps. C’est particulièrement vrai pour des compteurs comme :

Code: Select all

% Processor Time
Il faut donc souvent faire :

Code: Select all

PdhCollectQueryData(query);
Sleep(1000);
PdhCollectQueryData(query);
Sinon la valeur récupérée sera nulle, fausse ou inexploitable.

C’est l’un des pièges les plus classiques en PDH.

Lire une valeur simple

API :
  • PdhGetFormattedCounterValue
Structure importante :
  • PDH_FMT_COUNTERVALUE
Exemple :

Code: Select all

PDH_FMT_COUNTERVALUE value = { 0 };
DWORD type = 0;

PDH_STATUS status = PdhGetFormattedCounterValue(
    counter,
    PDH_FMT_DOUBLE,
    &type,
    &value
);

if (status != ERROR_SUCCESS)
    return 1;

/* lire value.doubleValue */
Le champ à lire dépend du format demandé :
  • PDH_FMT_DOUBLE → doubleValue
  • PDH_FMT_LONG → longValue
  • PDH_FMT_LARGE → largeValue
Il faut aussi regarder le champ CStatus dans certains scénarios.

Compteurs multi-instance

Quand un compteur contient une wildcard comme :

Code: Select all

\Process(*)\% Processor Time
on ne récupère pas une seule valeur, mais un tableau de valeurs.

Dans ce cas, il faut utiliser :
  • PdhGetFormattedCounterArray
Structure importante :
  • PDH_FMT_COUNTERVALUE_ITEM
Chaque item contient typiquement :
  • le nom de l’instance
  • la valeur formatée associée
C’est indispensable pour les compteurs multi-instance de type process, thread, disque, interface réseau, etc.

Autres structures PDH à connaitre

Parmi les structures utiles :
  • PDH_FMT_COUNTERVALUE
  • PDH_FMT_COUNTERVALUE_ITEM
  • PDH_COUNTER_INFO
  • PDH_RAW_COUNTER
  • PDH_TIME_INFO
  • PDH_BROWSE_DLG_CONFIG
PDH_COUNTER_INFO est utile avec PdhGetCounterInfo pour récupérer des métadonnées sur un compteur.

PDH_BROWSE_DLG_CONFIG est intéressante pour un outil graphique qui laisse l’utilisateur choisir ses compteurs.

Pièges classiques de PDH

Les erreurs les plus fréquentes sont :
  • confondre code de retour et vrai handle
  • oublier le double collect
  • utiliser PdhGetFormattedCounterValue sur un compteur multi-instance
  • lire le mauvais champ de l’union
  • oublier PdhCloseQuery
  • utiliser des chemins dépendants de la langue sans y faire attention
PSS : Process Snapshots, capturer un état cohérent

On passe maintenant à un sujet plus avancé : les Process Snapshots, souvent abrégés PSS.

L’idée de PSS est de capturer une vue cohérente d’un processus, puis de travailler sur cette vue capturée plutôt que sur un processus vivant qui continue de changer sous nos yeux.

Modèle mental :

Code: Select all

Processus cible
↓
PssCaptureSnapshot
↓
snapshot cohérent
↓
PssQuerySnapshot / PssWalkSnapshot
Pourquoi c’est utile :
  • un processus vivant change sans cesse
  • ses threads peuvent naitre ou mourir
  • ses handles peuvent varier
  • sa mémoire peut bouger
  • ses contextes CPU changent en permanence
Avec PSS, on fige une vue exploitable.

Handles et types PSS à connaitre

Les types centraux sont :
  • HPSS
  • HPSSWALK
HPSS représente le snapshot. HPSSWALK représente le marqueur de parcours utilisé pour itérer proprement dans les données du snapshot.

APIs centrales :
  • PssCaptureSnapshot
  • PssFreeSnapshot
  • PssQuerySnapshot
  • PssWalkMarkerCreate
  • PssWalkMarkerFree
  • PssWalkMarkerGetPosition
  • PssWalkMarkerSetPosition
  • PssWalkSnapshot
  • PssDuplicateSnapshot
Capturer un snapshot

API principale :

Code: Select all

DWORD PssCaptureSnapshot(
    HANDLE ProcessHandle,
    PSS_CAPTURE_FLAGS CaptureFlags,
    DWORD ThreadContextFlags,
    HPSS* SnapshotHandle
);
Exemple logique :

Code: Select all

HPSS snapshot = NULL;

DWORD status = PssCaptureSnapshot(
    hProcess,
    PSS_CAPTURE_THREADS | PSS_CAPTURE_HANDLES,
    CONTEXT_ALL,
    &snapshot
);

if (status != ERROR_SUCCESS)
    return 1;
Le choix des flags détermine ce que l’on capture réellement.

Flags de capture importants

Il faut connaitre au minimum :
  • PSS_CAPTURE_NONE
  • PSS_CAPTURE_VA_CLONE
  • PSS_CAPTURE_VA_SPACE
  • PSS_CAPTURE_HANDLES
  • PSS_CAPTURE_HANDLE_NAME_INFORMATION
  • PSS_CAPTURE_HANDLE_BASIC_INFORMATION
  • PSS_CAPTURE_HANDLE_TYPE_SPECIFIC_INFORMATION
  • PSS_CAPTURE_HANDLE_TRACE
  • PSS_CAPTURE_THREADS
  • PSS_CAPTURE_THREAD_CONTEXT
  • PSS_CAPTURE_THREAD_CONTEXT_EXTENDED
  • PSS_CAPTURE_VA_SPACE_SECTION_INFORMATION
  • PSS_CAPTURE_IPT_TRACE
Le sens général :
  • PSS_CAPTURE_THREADS → capture les threads
  • PSS_CAPTURE_HANDLES → capture les handles
  • PSS_CAPTURE_VA_SPACE → capture l’espace virtuel
  • PSS_CAPTURE_VA_CLONE → clone mémoire, très puissant
  • PSS_CAPTURE_THREAD_CONTEXT → capture le contexte CPU des threads
Flags de création et comportement du clone

Autres flags utiles à connaitre :
  • PSS_CREATE_BREAKAWAY
  • PSS_CREATE_FORCE_BREAKAWAY
  • PSS_CREATE_BREAKAWAY_OPTIONAL
  • PSS_CREATE_USE_VM_ALLOCATIONS
  • PSS_CREATE_RELEASE_SECTION
Ils influencent la manière dont le snapshot ou le clone est effectivement produit.

Interroger un snapshot

APIs :
  • PssQuerySnapshot
  • PssWalkSnapshot
PssQuerySnapshot sert plutôt à récupérer des informations globales selon une classe donnée.

PssWalkSnapshot sert à parcourir des collections d’entrées, par exemple les threads ou les handles.

Classes importantes côté requêtes :
  • PSS_QUERY_PROCESS_INFORMATION
  • PSS_QUERY_VA_CLONE_INFORMATION
  • PSS_QUERY_AUXILIARY_PAGES_INFORMATION
  • PSS_QUERY_VA_SPACE_INFORMATION
  • PSS_QUERY_HANDLE_INFORMATION
  • PSS_QUERY_THREAD_INFORMATION
  • PSS_QUERY_HANDLE_TRACE_INFORMATION
  • PSS_QUERY_PERFORMANCE_COUNTERS
Structures importantes de PSS

Parmi les structures à reconnaitre :
  • PSS_PROCESS_INFORMATION
  • PSS_VA_CLONE_INFORMATION
  • PSS_AUXILIARY_PAGES_INFORMATION
  • PSS_VA_SPACE_INFORMATION
  • PSS_HANDLE_INFORMATION
  • PSS_THREAD_INFORMATION
  • PSS_PERFORMANCE_COUNTERS
  • PSS_HANDLE_ENTRY
  • PSS_THREAD_ENTRY
  • PSS_VA_SPACE_ENTRY
PSS_HANDLE_ENTRY représente une entrée de handle. PSS_THREAD_ENTRY représente une entrée de thread. PSS_VA_SPACE_ENTRY représente une région d’espace virtuel capturée.

Utilisation typique de PSS

Le flux classique ressemble à ceci :

Code: Select all

PssCaptureSnapshot
↓
PssQuerySnapshot
↓
PssWalkMarkerCreate
↓
PssWalkSnapshot dans une boucle
↓
PssWalkMarkerFree
↓
PssFreeSnapshot
C’est exactement le genre de mécanisme que l’on retrouve dans des outils de diagnostic, d’inspection, de sécurité ou de forensic.

MiniDump : à connaitre même si ce n’est pas PSS

Même si ce n’est pas exactement la meme chose que PSS, il faut au moins connaitre un autre grand mécanisme de capture d’état de processus dans le monde du debug et du diagnostic :
  • MiniDumpWriteDump
Il s’appuie sur DbgHelp et permet d’écrire un dump exploitable par un debugger. Ce n’est pas la meme philosophie qu’un snapshot PSS interrogeable en mémoire, mais c’est très complémentaire dans le monde réel.

Autres types utiles à connaitre de nom dans ce domaine :
  • MINIDUMP_TYPE
  • MINIDUMP_EXCEPTION_INFORMATION
  • MINIDUMP_USER_STREAM_INFORMATION
ETW : Event Tracing for Windows

On arrive maintenant au bloc le plus massif : ETW.

ETW est le système de tracing haute performance de Windows. C’est un mécanisme générique, très scalable, capable de tracer aussi bien des événements d’application que des événements noyau, avec des sessions dédiées, des consumers, des providers, des mots-clés, des niveaux, des métadonnées, etc.

Modèle mental général :

Code: Select all

Provider
↓
Event
↓
Session ETW
↓
Consumer
Le provider produit des événements. La session décide quoi écouter et où stocker ou diffuser les événements. Le consumer lit et interprète les traces.

Les composants d’ETW

Les grandes notions sont :
  • Provider
  • Event
  • Session
  • Consumer
  • Manifest ou metadata
  • TraceLogging dans les scénarios modernes
Provider :
  • source des événements
  • peut être une application, un service, un composant noyau, un provider TraceLogging, etc.
Event :
  • unité de trace
  • contient un provider, un descriptor, des données utilisateur, un timestamp, souvent PID/TID
Session :
  • capture en cours
  • choisit quels providers sont activés
  • détermine temps réel ou fichier
Consumer :
  • outil ou code qui lit les événements
  • WPA, logman, un parseur maison, un agent, etc.
Enumérer les providers ETW

Une première API importante est :
  • TdhEnumerateProviders
Elle permet de récupérer la liste des providers enregistrés.

Structures à connaitre :
  • PROVIDER_ENUMERATION_INFO
  • TRACE_PROVIDER_INFO
Le pattern classique Win32 s’applique ici aussi :

Code: Select all

1. appel avec buffer null / taille insuffisante
2. récupération de la taille
3. allocation
4. second appel
C’est un pattern très fréquent dans tout le monde TDH.

Enumérer les événements d’un provider

Une fois un provider identifié, on peut vouloir connaitre les événements qu’il expose.

API :
  • TdhEnumerateManifestProviderEvents
Structure importante :
  • PROVIDER_EVENT_INFO
Cela permet d’obtenir la liste des événements décrits dans son manifest.

Décrire un événement ETW

Deux APIs importantes :
  • TdhGetManifestEventInformation
  • TdhGetEventInformation
La première raisonne sur le manifest. La seconde interprète un événement réel capturé, de type EVENT_RECORD.

Structure vraiment centrale :
  • TRACE_EVENT_INFO
Elle contient notamment :
  • le GUID du provider
  • le descriptor de l’événement
  • des offsets vers les chaines de nom
  • le nombre de propriétés
  • les descriptions de propriétés
Autre structure centrale :
  • EVENT_PROPERTY_INFO
Elle décrit chaque propriété individuelle d’un événement.

Propriétés importantes :
  • nom
  • type d’entrée
  • type de sortie
  • taille
  • count
  • flags
Flags de propriété ETW à connaitre

Parmi les flags importants :

Code: Select all

PropertyStruct
PropertyParamLength
PropertyParamCount
PropertyWBEMXmlFragment
PropertyParamFixedLength
PropertyParamFixedCount
PropertyHasTags
PropertyHasCustomSchema
Ils servent à comprendre comment interpréter correctement la donnée utilisateur.

Parsing des propriétés ETW

C’est l’un des points les plus techniques du modèle ETW. Le champ UserData d’un EVENT_RECORD n’est qu’un buffer brut. Sans métadonnées, on ne sait pas où commencent les champs, quel type lire, quelle longueur utiliser, ni comment formatter la valeur.

Modèle mental :

Code: Select all

EVENT_RECORD
↓
EventHeader + UserData
↓
TdhGetEventInformation
↓
TRACE_EVENT_INFO + EVENT_PROPERTY_INFO[]
↓
TdhFormatProperty ou parsing manuel
API importantes :
  • TdhFormatProperty
  • TdhGetProperty
  • TdhGetPropertySize
  • TdhGetEventMapInformation
Structure associée aux maps :
  • EVENT_MAP_INFO
Les maps servent à convertir certaines valeurs entières en représentations textuelles lisibles.

Créer une session ETW

Une session est créée avec :
  • StartTrace
Structure critique :
  • EVENT_TRACE_PROPERTIES
Champs importants :
  • Wnode
  • LogFileMode
  • EnableFlags
  • BufferSize
  • MinimumBuffers
  • MaximumBuffers
  • FlushTimer
  • LoggerNameOffset
  • LogFileNameOffset
Il faut bien comprendre qu’en pratique, EVENT_TRACE_PROPERTIES est souvent suivie en mémoire par le nom de session et éventuellement le chemin de fichier. Il faut donc souvent allouer un buffer plus grand que la structure seule.

Autres APIs autour des sessions :
  • ControlTrace
  • StopTrace
  • FlushTrace
Modes de session ETW

Flags importants :

Code: Select all

EVENT_TRACE_REAL_TIME_MODE
EVENT_TRACE_FILE_MODE_SEQUENTIAL
EVENT_TRACE_FILE_MODE_CIRCULAR
EVENT_TRACE_FILE_MODE_APPEND
EVENT_TRACE_SYSTEM_LOGGER_MODE
Il faut distinguer au moins :
  • session vers fichier .etl
  • session temps réel
Temps réel :
  • les événements sont livrés au consumer en direct
Fichier :
  • les événements sont écrits dans un .etl pour analyse ultérieure
Activer un provider sur une session

Créer une session ne suffit pas. Il faut encore activer les providers à écouter.

APIs importantes :
  • EnableTraceEx
  • EnableTraceEx2
La version moderne et la plus souple est EnableTraceEx2.

On y configure notamment :
  • le GUID du provider
  • le niveau
  • les keywords match any
  • les keywords match all
  • des filtres éventuels
Structures utiles :
  • ENABLE_TRACE_PARAMETERS
  • EVENT_FILTER_DESCRIPTOR
Les filtres sont très importants pour réduire le bruit et se concentrer sur ce qui compte.

Lire une trace ETW

Une fois la trace produite, il faut la lire.

APIs centrales :
  • OpenTrace
  • ProcessTrace
  • CloseTrace
Structure critique :
  • EVENT_TRACE_LOGFILE
Champs importants :
  • LogFileName
  • LoggerName
  • ProcessTraceMode
  • EventCallback
  • EventRecordCallback
  • LogfileHeader
  • Context
Flags de lecture importants :

Code: Select all

PROCESS_TRACE_MODE_EVENT_RECORD
PROCESS_TRACE_MODE_REAL_TIME
PROCESS_TRACE_MODE_RAW_TIMESTAMP
En pratique moderne, on privilégie le modèle EVENT_RECORD.

EVENT_RECORD et structures liées

Quand ProcessTrace livre un événement moderne, on reçoit un :
  • EVENT_RECORD
Cette structure contient notamment :
  • EventHeader
  • BufferContext
  • ExtendedDataCount
  • UserDataLength
  • ExtendedData
  • UserData
  • UserContext
Structure très importante :
  • EVENT_HEADER
Elle contient notamment :
  • PID
  • TID
  • timestamp
  • provider GUID
  • event descriptor
  • ActivityId
Autre structure clé :
  • EVENT_DESCRIPTOR
Elle décrit le type d’événement avec :
  • Id
  • Version
  • Channel
  • Level
  • Opcode
  • Task
  • Keyword
Et pour les données étendues :
  • EVENT_HEADER_EXTENDED_DATA_ITEM
Cette dernière permet d’embarquer par exemple :
  • SID
  • stack trace
  • activity id
  • autres méta-infos étendues
Le provider noyau ETW

Le provider noyau, souvent associé à :

Code: Select all

SystemTraceControlGuid
est un cas un peu spécial. On ne l’active pas exactement comme un provider applicatif ordinaire. On utilise souvent des flags noyau dans EnableFlags.

Flags très importants à connaitre :

Code: Select all

EVENT_TRACE_FLAG_PROCESS
EVENT_TRACE_FLAG_THREAD
EVENT_TRACE_FLAG_IMAGE_LOAD
EVENT_TRACE_FLAG_DISK_IO
EVENT_TRACE_FLAG_DISK_FILE_IO
EVENT_TRACE_FLAG_REGISTRY
EVENT_TRACE_FLAG_FILE_IO
EVENT_TRACE_FLAG_FILE_IO_INIT
EVENT_TRACE_FLAG_NETWORK_TCPIP
EVENT_TRACE_FLAG_CSWITCH
EVENT_TRACE_FLAG_DPC
EVENT_TRACE_FLAG_INTERRUPT
EVENT_TRACE_FLAG_SYSTEMCALL
EVENT_TRACE_FLAG_VAMAP
EVENT_TRACE_FLAG_VIRTUAL_ALLOC
EVENT_TRACE_FLAG_PROFILE
Avec eux, on peut tracer :
  • créations et fins de processus
  • créations et fins de threads
  • chargements d’images
  • I/O disque
  • I/O fichiers
  • registre
  • réseau
  • context switches
  • interruptions
  • appels système
  • allocations virtuelles
C’est l’un des mécanismes les plus puissants de l’écosystème Windows pour l’observation système.

Interroger les sessions et providers actifs

Autres APIs importantes :
  • TraceQueryInformation
  • EnumerateTraceGuidsEx
  • QueryAllTraces
Enum importante :
  • TRACE_QUERY_INFO_CLASS
Exemples de classes :

Code: Select all

TraceGuidQueryList
TraceGuidQueryInfo
TraceGuidQueryProcess
TraceStackTracingInfo
TraceSystemTraceEnableFlagsInfo
TraceProfileSourceConfigInfo
TraceLastBranchConfigurationInfo
TraceMaxPmcCounterQuery
A quoi servent ces APIs :
  • QueryAllTraces → voir les sessions actives
  • EnumerateTraceGuidsEx → lister des GUID de providers
  • TraceQueryInformation → interroger des infos plus fines du sous-système ETW
TraceLogging : produire ETW plus simplement

Le monde ETW classique peut etre lourd, notamment avec les manifests. TraceLogging apporte une manière plus simple et moderne d’instrumenter son propre code.

Macros et APIs importantes :
  • TRACELOGGING_DEFINE_PROVIDER
  • TraceLoggingRegister
  • TraceLoggingUnregister
  • TraceLoggingWrite
  • TraceLoggingValue
  • TraceLoggingWideString
  • TraceLoggingLevel
  • TraceLoggingOpcode
Modèle mental :

Code: Select all

Ton code
↓
TraceLoggingWrite
↓
provider ETW maison
↓
session ETW
↓
consumer
Exemple d’idée :

Code: Select all

TraceLoggingWrite(
    g_Provider,
    "ProcessStarted",
    TraceLoggingValue(pid, "PID"),
    TraceLoggingWideString(path, "Path")
);
C’est une excellente manière d’instrumenter un service, un agent, un outil système ou une application interne complexe.

La Debug API : contrôler activement un processus

On arrive maintenant à un autre monde : la Debug API.

Ici, on ne se contente plus d’observer passivement. On devient le debugger du processus cible.

Il existe deux grands modes :
  • lancer un processus en mode debug
  • s’attacher à un processus existant
APIs importantes :
  • CreateProcess avec DEBUG_PROCESS
  • CreateProcess avec DEBUG_ONLY_THIS_PROCESS
  • DebugActiveProcess
  • DebugActiveProcessStop
La boucle du debugger

Le cœur d’un debugger Windows user-mode repose sur une boucle d’événements.

APIs centrales :
  • WaitForDebugEvent
  • WaitForDebugEventEx
  • ContinueDebugEvent
Modèle mental :

Code: Select all

process debuggué
↓
Windows suspend / notifie
↓
WaitForDebugEvent récupère un DEBUG_EVENT
↓
ton code traite l'événement
↓
ContinueDebugEvent relance le process / thread
Point absolument capital :
  • si tu oublies ContinueDebugEvent, le processus ou le thread concerné reste bloqué
DEBUG_EVENT : structure centrale

La structure critique est :
  • DEBUG_EVENT
Elle contient :
  • dwDebugEventCode
  • dwProcessId
  • dwThreadId
  • une union décrivant l’événement réel
Événements importants :

Code: Select all

CREATE_PROCESS_DEBUG_EVENT
CREATE_THREAD_DEBUG_EVENT
EXIT_PROCESS_DEBUG_EVENT
EXIT_THREAD_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
UNLOAD_DLL_DEBUG_EVENT
OUTPUT_DEBUG_STRING_EVENT
EXCEPTION_DEBUG_EVENT
RIP_EVENT
Structures importantes du debugger

Les structures à connaitre absolument sont :
  • EXCEPTION_DEBUG_INFO
  • CREATE_PROCESS_DEBUG_INFO
  • CREATE_THREAD_DEBUG_INFO
  • EXIT_PROCESS_DEBUG_INFO
  • EXIT_THREAD_DEBUG_INFO
  • LOAD_DLL_DEBUG_INFO
  • UNLOAD_DLL_DEBUG_INFO
  • OUTPUT_DEBUG_STRING_INFO
  • RIP_INFO
  • EXCEPTION_RECORD
  • CONTEXT
EXCEPTION_DEBUG_INFO contient notamment :
  • EXCEPTION_RECORD
  • dwFirstChance
CREATE_PROCESS_DEBUG_INFO contient par exemple :
  • hFile
  • hProcess
  • hThread
  • lpBaseOfImage
  • lpStartAddress
  • lpImageName
LOAD_DLL_DEBUG_INFO contient notamment :
  • hFile
  • lpBaseOfDll
  • lpImageName
Il faut bien comprendre que certains champs ne contiennent pas directement une chaine lisible dans ton propre processus, mais une adresse dans le processus cible. Il faut alors utiliser ReadProcessMemory.

Gestion des exceptions dans le debugger

Quand un événement d’exception arrive, le debugger doit choisir quoi faire.

Constantes importantes :

Code: Select all

DBG_CONTINUE
DBG_EXCEPTION_NOT_HANDLED
Sens :
  • DBG_CONTINUE → “j’ai géré l’événement”
  • DBG_EXCEPTION_NOT_HANDLED → “laisse le processus gérer cette exception”
C’est absolument fondamental pour comprendre :
  • les breakpoints
  • le single-step
  • les exceptions first chance / second chance
  • certains mécanismes anti-debug
Lire et écrire la mémoire du processus debuggué

APIs importantes :
  • ReadProcessMemory
  • WriteProcessMemory
Elles servent notamment à :
  • lire une chaine dans le processus cible
  • lire une structure PE
  • récupérer un nom d’image
  • inspecter une donnée à une adresse donnée
  • écrire un breakpoint logiciel
Autres APIs très importantes autour du debug mémoire et contexte :
  • GetThreadContext
  • SetThreadContext
  • SuspendThread
  • ResumeThread
  • FlushInstructionCache
  • VirtualProtectEx
  • VirtualQueryEx
GetThreadContext et SetThreadContext deviennent essentiels dès qu’on veut faire du single-step, regarder les registres, ou manipuler un breakpoint plus sérieusement.

Breakpoints et contrôle d’exécution : notions à connaitre

Même si ce cours reste introductif, il faut au moins connaitre quelques briques du debugger réel :
  • breakpoint logiciel classique via 0xCC
  • single-step via trap flag
  • lecture / écriture mémoire dans le processus cible
  • lecture / écriture du contexte de thread
APIs utiles de nom au moins :
  • ReadProcessMemory
  • WriteProcessMemory
  • GetThreadContext
  • SetThreadContext
  • FlushInstructionCache
Sans ces mécanismes, on a une boucle de debug, mais pas encore un vrai contrôle d’exécution sérieux.

Autres APIs de debugging utiles

Parmi les APIs importantes :
  • IsDebuggerPresent
  • CheckRemoteDebuggerPresent
  • DebugBreakProcess
  • DebugSetProcessKillOnExit
Elles servent respectivement à :
  • détecter un debugger sur le processus courant
  • détecter si un processus distant est debuggué
  • forcer un breakpoint dans un autre processus
  • choisir si les processus debuggués meurent quand le debugger se termine
Autres APIs très utiles à connaitre de nom :
  • DebugBreak
  • OutputDebugString
  • ContinueDebugEvent
  • WaitForDebugEventEx
Un debugger simple : modèle final

Le modèle final d’un debugger très simple est :

Code: Select all

création / attachement
↓
WaitForDebugEvent
↓
switch(evt.dwDebugEventCode)
↓
traitement spécifique
↓
ContinueDebugEvent
↓
boucle
Avec déjà cette base, on peut :
  • afficher les DLL chargées
  • afficher les threads créés ou terminés
  • voir les messages OutputDebugString
  • voir les exceptions
  • surveiller la vie entière du processus
ETW contre Debug API : ne pas confondre

C’est un point très important.

ETW :
  • observation plutôt passive
  • très scalable
  • très utilisé en production
  • très fort pour la performance et la supervision
Debug API :
  • contrôle actif
  • suspension / reprise
  • événements de debug
  • accès direct au cycle de vie du processus
Le résumé simple :

Code: Select all

ETW = j'observe le système
Debug API = je deviens le debugger du processus
Autres briques de diagnostic utiles à connaitre de nom

Même si le cœur du chapitre tourne autour de PDH, PSS, ETW et Debug API, il est bon de connaitre aussi quelques APIs souvent rencontrées dans les outils réels :
  • GetProcessMemoryInfo
  • GetProcessTimes
  • QueryFullProcessImageNameW
  • GetThreadTimes
  • MiniDumpWriteDump
  • SymInitialize
  • SymFromAddr
  • StackWalk64
Les trois dernières appartiennent plus au monde DbgHelp / symboles / stack walking, mais elles apparaissent très vite dès qu’on pousse le debugging plus loin.

Les APIs vraiment importantes à connaitre

Pour le debug texte :
  • OutputDebugStringA
  • OutputDebugStringW
  • OutputDebugString
Pour PDH :
  • PdhOpenQuery
  • PdhCloseQuery
  • PdhAddCounter
  • PdhAddEnglishCounter
  • PdhRemoveCounter
  • PdhCollectQueryData
  • PdhCollectQueryDataEx
  • PdhGetFormattedCounterValue
  • PdhGetFormattedCounterArray
  • PdhGetRawCounterValue
  • PdhGetCounterInfo
Pour PSS :
  • PssCaptureSnapshot
  • PssFreeSnapshot
  • PssQuerySnapshot
  • PssWalkMarkerCreate
  • PssWalkMarkerFree
  • PssWalkMarkerGetPosition
  • PssWalkMarkerSetPosition
  • PssWalkSnapshot
Pour ETW / TDH :
  • TdhEnumerateProviders
  • TdhEnumerateManifestProviderEvents
  • TdhGetManifestEventInformation
  • TdhGetEventInformation
  • TdhGetEventMapInformation
  • TdhFormatProperty
  • TdhGetProperty
  • TdhGetPropertySize
Pour les sessions ETW :
  • StartTrace
  • ControlTrace
  • EnableTraceEx
  • EnableTraceEx2
  • OpenTrace
  • ProcessTrace
  • CloseTrace
  • TraceQueryInformation
  • EnumerateTraceGuidsEx
  • QueryAllTraces
Pour TraceLogging :
  • TraceLoggingRegister
  • TraceLoggingUnregister
  • TraceLoggingWrite
Pour la Debug API :
  • CreateProcess
  • DebugActiveProcess
  • DebugActiveProcessStop
  • WaitForDebugEvent
  • WaitForDebugEventEx
  • ContinueDebugEvent
  • ReadProcessMemory
  • WriteProcessMemory
  • GetThreadContext
  • SetThreadContext
  • IsDebuggerPresent
  • CheckRemoteDebuggerPresent
  • DebugBreakProcess
  • DebugSetProcessKillOnExit
  • FlushInstructionCache
Les structures vraiment importantes à connaitre

Pour PDH :
  • PDH_FMT_COUNTERVALUE
  • PDH_FMT_COUNTERVALUE_ITEM
  • PDH_COUNTER_INFO
  • PDH_RAW_COUNTER
  • PDH_BROWSE_DLG_CONFIG
Pour PSS :
  • HPSS
  • HPSSWALK
  • PSS_PROCESS_INFORMATION
  • PSS_HANDLE_INFORMATION
  • PSS_HANDLE_ENTRY
  • PSS_THREAD_ENTRY
  • PSS_VA_SPACE_ENTRY
Pour ETW :
  • PROVIDER_ENUMERATION_INFO
  • TRACE_PROVIDER_INFO
  • PROVIDER_EVENT_INFO
  • TRACE_EVENT_INFO
  • EVENT_PROPERTY_INFO
  • EVENT_MAP_INFO
  • EVENT_TRACE_PROPERTIES
  • ENABLE_TRACE_PARAMETERS
  • EVENT_TRACE_LOGFILE
  • EVENT_RECORD
  • EVENT_HEADER
  • EVENT_DESCRIPTOR
  • EVENT_HEADER_EXTENDED_DATA_ITEM
  • EVENT_FILTER_DESCRIPTOR
  • TRACE_QUERY_INFO_CLASS
Pour le debugger :
  • DEBUG_EVENT
  • EXCEPTION_DEBUG_INFO
  • CREATE_PROCESS_DEBUG_INFO
  • CREATE_THREAD_DEBUG_INFO
  • EXIT_PROCESS_DEBUG_INFO
  • EXIT_THREAD_DEBUG_INFO
  • LOAD_DLL_DEBUG_INFO
  • UNLOAD_DLL_DEBUG_INFO
  • OUTPUT_DEBUG_STRING_INFO
  • RIP_INFO
  • EXCEPTION_RECORD
  • CONTEXT
Ce qu’il faut vraiment retenir

Si on résume ce chapitre en vision système, on obtient quelque chose comme :

Code: Select all

OutputDebugString
→ debug texte simple

PDH
→ métriques système

PSS
→ snapshot cohérent d’un processus

ETW
→ tracing haute performance du système

Debug API
→ contrôle actif d’un processus par un debugger
C’est un très bon résumé du terrain réel.

Vision terrain

Pour du développement applicatif simple, ce chapitre peut sembler exotique. Mais pour du développement système, il est énorme.

On retrouve directement ces concepts dans :
  • des outils de supervision
  • des Process Explorer / ProcMon-like
  • des antivirus / EDR
  • des anti-cheat
  • des outils internes de diagnostic
  • des debuggers
  • des composants de télémétrie
  • de l’instrumentation d’applications complexes
Erreurs classiques

Comme souvent en système, les erreurs viennent surtout d’un mauvais modèle mental.

Parmi les fautes fréquentes :
  • penser qu’OutputDebugString remplace un vrai logging
  • oublier le double collect en PDH
  • utiliser la mauvaise API PDH pour un compteur multi-instance
  • croire qu’un processus vivant peut être inspecté “cohérent” sans snapshot ni logique de capture
  • sous-estimer la complexité du parsing ETW
  • confondre observation ETW et contrôle via Debug API
  • oublier ContinueDebugEvent
  • croire que certaines adresses reçues dans un DEBUG_EVENT sont directement lisibles sans ReadProcessMemory
  • ignorer le cycle de vie des sessions ETW
Conclusion

Ce chapitre te fait passer d’une logique :

Code: Select all

"je code une appli Win32"
à une logique beaucoup plus système :

Code: Select all

"je sais observer, mesurer, capturer, tracer et contrôler ce que fait Windows"
Et ça, c’est un vrai cap.

Si tu maitrises déjà correctement :
  • OutputDebugString
  • PdhOpenQuery, PdhAddEnglishCounter, PdhCollectQueryData
  • PdhGetFormattedCounterValue et PdhGetFormattedCounterArray
  • PssCaptureSnapshot, PssQuerySnapshot, PssWalkSnapshot
  • TdhEnumerateProviders, TdhGetEventInformation, TdhFormatProperty
  • StartTrace, EnableTraceEx2, OpenTrace, ProcessTrace
  • TraceLoggingWrite
  • DebugActiveProcess, WaitForDebugEvent, ContinueDebugEvent
  • ReadProcessMemory, WriteProcessMemory, GetThreadContext
alors tu possèdes déjà une base très sérieuse sur tout le bloc debugging / diagnostics / ETW / debug.

Et surtout, tu commences à voir qu’en développement système Windows, savoir écrire du code ne suffit pas. Il faut aussi savoir regarder ce que fait le système, comprendre ce qu’il te montre, et parfois prendre le contrôle du processus pour l’analyser ou le guider.

A la prochaine pour un nouveau cours 8-)

Who is online

Users browsing this forum: No registered users and 0 guests