Les Services Partie Avancée

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:

Les Services Partie Avancée

Post by Hydraxx »

Salut, :D

dans ce cours on va continuer sur les services Windows, mais cette fois avec une vision plus avancée, plus proche de ce qu’on rencontre dans un vrai code système, dans un service de prod, ou dans un outil d’administration un peu sérieux.

Le but ici n’est plus seulement de savoir qu’un service existe, ou de connaitre deux ou trois APIs de base comme OpenSCManagerW ou CreateServiceW. Le but est de comprendre le vrai modèle d’exécution, les structures importantes, le protocole imposé par le SCM, les états, les handlers, l’énumération, la configuration, la sécurité, les triggers, les SID de service, les failure actions, et les points de debug qui comptent vraiment.

Vision générale : un service n’est pas lancé comme une application classique

Le point de départ à bien fixer est le suivant :

Code: Select all

un service n’est pas lancé comme un EXE classique par un utilisateur
Le flux réel est plus proche de ceci :

Code: Select all

SCM
↓
CreateProcess de service.exe
↓
service.exe démarre
↓
StartServiceCtrlDispatcher
↓
ServiceMain
↓
RegisterServiceCtrlHandlerEx
↓
SetServiceStatus
↓
worker thread / boucle du service
Donc, ce qu’il faut comprendre immédiatement, c’est :
  • le processus du service est créé par le Service Control Manager
  • le SCM attend un protocole précis de la part du binaire service
  • un meme processus peut héberger un ou plusieurs services
  • chaque service possède son propre ServiceMain
  • le service ne vit pas “librement” comme une appli console ordinaire
Ce point change complètement la manière de raisonner sur l’entrée dans le programme.

Le vrai point d’entrée logique d’un service

Le binaire service possède toujours un point d’entrée classique, par exemple main ou wmain. Mais pour un vrai service, ce point d’entrée n’est pas la logique métier du service. Son rôle réel consiste surtout à se connecter au SCM.

L’API centrale ici est :
  • StartServiceCtrlDispatcherW
Exemple conceptuel :

Code: Select all

int wmain()
{
    SERVICE_TABLE_ENTRYW table[] =
    {
        { (LPWSTR)L"MyService", ServiceMain },
        { NULL, NULL }
    };

    if (!StartServiceCtrlDispatcherW(table))
        return 1;

    return 0;
}
Autrement dit :

Code: Select all

main
→ StartServiceCtrlDispatcher

puis
SCM
→ appelle ServiceMain
C’est fondamental. Si on rate cette idée, on comprend mal tout le reste.

SERVICE_TABLE_ENTRY : structure clé

La structure qui relie le nom logique du service à son point d’entrée est :

Code: Select all

typedef struct _SERVICE_TABLE_ENTRY {
    LPWSTR lpServiceName;
    LPSERVICE_MAIN_FUNCTION lpServiceProc;
} SERVICE_TABLE_ENTRY;
Exemple :

Code: Select all

SERVICE_TABLE_ENTRYW table[] =
{
    { (LPWSTR)L"MyService", ServiceMain },
    { NULL, NULL }
};
Ce qu’il faut retenir :
  • le tableau est terminé par { NULL, NULL }
  • chaque entrée mappe un nom de service vers une fonction ServiceMain
  • dans un processus partagé, il peut y avoir plusieurs entrées réelles
  • c’est cette table que StartServiceCtrlDispatcherW passe au SCM
Cela permet au SCM de savoir quelle fonction appeler selon le service concerné.

ServiceMain : vrai point d’entrée du service

Le SCM appelle ensuite la fonction de service de type :

Code: Select all

VOID WINAPI ServiceMain(
    DWORD argc,
    LPWSTR* argv
);
C’est ici que le service doit :
  • s’initialiser
  • s’enregistrer auprès du SCM avec un handler de contrôle
  • signaler SERVICE_START_PENDING
  • créer ses ressources internes
  • lancer un worker thread ou sa logique principale
  • passer à SERVICE_RUNNING
Une erreur fréquente consiste à mettre toute la logique lourde directement dans ServiceMain sans organisation propre. En pratique, une bonne architecture consiste presque toujours à laisser ServiceMain faire l’initialisation protocolaire, puis à lancer la vraie logique ailleurs, souvent dans un thread worker.

RegisterServiceCtrlHandlerEx : réception des contrôles

Un service doit s’enregistrer pour recevoir les contrôles envoyés par le SCM.

API importante :
  • RegisterServiceCtrlHandlerExW
Il existe aussi :
  • RegisterServiceCtrlHandlerW
mais la version Ex est plus riche.

Exemple conceptuel :

Code: Select all

SERVICE_STATUS_HANDLE g_StatusHandle = RegisterServiceCtrlHandlerExW(
    L"MyService",
    ServiceCtrlHandlerEx,
    NULL
);

if (!g_StatusHandle)
    return;
Le type retourné est :
  • SERVICE_STATUS_HANDLE
C’est un handle logique servant au dialogue avec le SCM pour les mises à jour d’état.

Les contrôles que peut recevoir un service

Le handler peut recevoir différents codes de contrôle, parmi lesquels :
  • SERVICE_CONTROL_STOP
  • SERVICE_CONTROL_PAUSE
  • SERVICE_CONTROL_CONTINUE
  • SERVICE_CONTROL_INTERROGATE
  • SERVICE_CONTROL_SHUTDOWN
  • SERVICE_CONTROL_PARAMCHANGE
  • SERVICE_CONTROL_SESSIONCHANGE
  • SERVICE_CONTROL_POWEREVENT
  • SERVICE_CONTROL_DEVICEEVENT
  • SERVICE_CONTROL_PRESHUTDOWN
Tous les services ne gèrent pas tout. Mais il faut au moins connaitre le modèle. Le service annonce ce qu’il accepte via dwControlsAccepted.

SERVICE_STATUS : cœur du cycle de vie

La structure centrale du protocole service/SCM est :

Code: Select all

typedef struct _SERVICE_STATUS {
    DWORD dwServiceType;
    DWORD dwCurrentState;
    DWORD dwControlsAccepted;
    DWORD dwWin32ExitCode;
    DWORD dwServiceSpecificExitCode;
    DWORD dwCheckPoint;
    DWORD dwWaitHint;
} SERVICE_STATUS;
C’est l’une des structures les plus importantes du monde des services.

Les champs critiques sont les suivants.
  • dwServiceType : type du service
  • dwCurrentState : état courant du service
  • dwControlsAccepted : contrôles acceptés
  • dwWin32ExitCode : code de sortie générique Win32
  • dwServiceSpecificExitCode : code propre au service si nécessaire
  • dwCheckPoint : progression durant une phase pending
  • dwWaitHint : délai estimé pour continuer la transition
dwCurrentState : états essentiels

Les états les plus importants sont :
  • SERVICE_START_PENDING
  • SERVICE_RUNNING
  • SERVICE_STOP_PENDING
  • SERVICE_STOPPED
  • SERVICE_PAUSE_PENDING
  • SERVICE_PAUSED
  • SERVICE_CONTINUE_PENDING
Le service doit passer par ces états proprement. Un vrai service stable ne “saute pas” cette logique.

Règle importante :

Code: Select all

un service sérieux doit notifier ses transitions au SCM
Par exemple :
  • au démarrage : SERVICE_START_PENDING puis SERVICE_RUNNING
  • à l’arrêt : SERVICE_STOP_PENDING puis SERVICE_STOPPED
dwControlsAccepted : ce que le service sait gérer

C’est ici que le service indique les contrôles qu’il accepte. Les flags fréquents sont :
  • SERVICE_ACCEPT_STOP
  • SERVICE_ACCEPT_SHUTDOWN
  • SERVICE_ACCEPT_PAUSE_CONTINUE
  • SERVICE_ACCEPT_PARAMCHANGE
  • SERVICE_ACCEPT_SESSIONCHANGE
  • SERVICE_ACCEPT_POWEREVENT
  • SERVICE_ACCEPT_PRESHUTDOWN
Si ton service n’accepte pas STOP, il ne doit pas annoncer SERVICE_ACCEPT_STOP. Le SCM se base sur cette information.

dwWaitHint et dwCheckPoint : champs trop souvent mal compris

Ces deux champs sont très importants pendant les phases pending.

Le sens général :
  • dwWaitHint indique au SCM combien de temps approximatif la transition peut prendre
  • dwCheckPoint indique que le service progresse réellement
Exemple mental :
  • le service démarre
  • il met SERVICE_START_PENDING
  • il renseigne dwWaitHint
  • il augmente régulièrement dwCheckPoint s’il a encore besoin de temps
Si tu ne mets jamais à jour ces champs pendant une transition longue, le SCM peut considérer que le service est bloqué ou défaillant.

C’est un piège classique.

SetServiceStatus : appel obligatoire

L’API fondamentale de notification est :
  • SetServiceStatus
Exemple :

Code: Select all

SetServiceStatus(g_StatusHandle, &status);
Règle critique :
  • un service doit notifier correctement le SCM de ses changements d’état
  • c’est un protocole, pas une simple option
Schéma type :

Code: Select all

START_PENDING
↓
RUNNING
↓
STOP_PENDING
↓
STOPPED
Si ce protocole est mal respecté, tu peux avoir :
  • des timeouts SCM
  • des comportements instables
  • des arrêts forcés
  • un service vu comme “planté” alors qu’il travaille encore
SERVICE_STATUS_PROCESS : version étendue

Quand on veut interroger plus finement l’état d’un service depuis l’extérieur, on utilise souvent la structure étendue :

Code: Select all

typedef struct _SERVICE_STATUS_PROCESS {
    DWORD dwServiceType;
    DWORD dwCurrentState;
    DWORD dwControlsAccepted;
    DWORD dwWin32ExitCode;
    DWORD dwServiceSpecificExitCode;
    DWORD dwCheckPoint;
    DWORD dwWaitHint;
    DWORD dwProcessId;
    DWORD dwServiceFlags;
} SERVICE_STATUS_PROCESS;
Différence clé :
  • elle contient le PID du processus hébergeant le service
  • elle donne une vue plus riche et plus utile en diagnostic
Très utile pour :
  • debugging
  • outillage d’admin
  • corrélation avec Process Explorer
  • inspection des services partagés
QueryServiceStatusEx : lecture précise de l’état

API importante :
  • QueryServiceStatusEx
Exemple :

Code: Select all

SERVICE_STATUS_PROCESS status = { 0 };
DWORD bytesNeeded = 0;

if (!QueryServiceStatusEx(
    hService,
    SC_STATUS_PROCESS_INFO,
    (LPBYTE)&status,
    sizeof(status),
    &bytesNeeded))
{
    return 1;
}
Cela permet de lire :
  • l’état courant réel
  • les contrôles acceptés
  • le PID
  • les flags de service
  • les codes de sortie
C’est l’API qu’il faut privilégier en outillage moderne plutôt qu’une lecture trop simpliste.

Enumération des services

Pour lister les services du système, l’API importante est :
  • EnumServicesStatusExW
Exemple de principe :

Code: Select all

EnumServicesStatusExW(
    hSCM,
    SC_ENUM_PROCESS_INFO,
    SERVICE_WIN32,
    SERVICE_STATE_ALL,
    buffer,
    size,
    &bytesNeeded,
    &servicesReturned,
    NULL,
    NULL
);
Structure utilisée :

Code: Select all

typedef struct _ENUM_SERVICE_STATUS_PROCESS {
    LPWSTR lpServiceName;
    LPWSTR lpDisplayName;
    SERVICE_STATUS_PROCESS ServiceStatusProcess;
} ENUM_SERVICE_STATUS_PROCESS;
Cela permet de récupérer pour chaque service :
  • son nom logique
  • son nom d’affichage
  • son état
  • son PID si applicable
C’est une API de base pour un outil type gestionnaire de services, Process Explorer-like, ou outil de supervision.

La configuration d’un service

Un service ne se résume pas à “un nom + un EXE”. Il a une vraie configuration persistante dans le SCM, largement stockée via le registre.

La structure classique pour lire la configuration de base est :

Code: Select all

typedef struct _QUERY_SERVICE_CONFIG {
    DWORD dwServiceType;
    DWORD dwStartType;
    DWORD dwErrorControl;
    LPWSTR lpBinaryPathName;
    LPWSTR lpLoadOrderGroup;
    DWORD dwTagId;
    LPWSTR lpDependencies;
    LPWSTR lpServiceStartName;
    LPWSTR lpDisplayName;
} QUERY_SERVICE_CONFIG;
API importante :
  • QueryServiceConfigW
Elle permet de lire notamment :
  • le chemin binaire
  • le type du service
  • le type de démarrage
  • le contrôle d’erreur
  • les dépendances
  • le compte du service
  • le display name
Modifier la configuration d’un service

API clé :
  • ChangeServiceConfigW
Elle permet de modifier :
  • le chemin binaire
  • le type
  • le type de démarrage
  • le compte du service
  • les dépendances
  • le display name
Il faut bien comprendre que certaines modifications demandent ensuite un redémarrage du service pour être réellement prises en compte.

Configuration avancée avec ChangeServiceConfig2

La version moderne pour beaucoup d’options avancées est :
  • ChangeServiceConfig2W
  • QueryServiceConfig2W
C’est ici qu’on retrouve des blocs de configuration plus riches.

Parmi les structures importantes :
  • SERVICE_DESCRIPTIONW
  • SERVICE_DELAYED_AUTO_START_INFO
  • SERVICE_FAILURE_ACTIONSW
  • SC_ACTION
  • SERVICE_REQUIRED_PRIVILEGES_INFOW
  • SERVICE_SID_INFO
  • SERVICE_TRIGGER_INFO
  • SERVICE_PRESHUTDOWN_INFO
  • SERVICE_LAUNCH_PROTECTED_INFO
C’est un point très important : le monde des services modernes est bien plus riche que le simple CreateService initial.

Description, Delayed Auto Start, Failure Actions

Quelques exemples essentiels.

SERVICE_DESCRIPTIONW

Permet d’associer une description lisible au service.

SERVICE_DELAYED_AUTO_START_INFO

Permet le delayed auto-start, c’est-à-dire un démarrage automatique différé après le boot.

Pourquoi c’est utile :
  • éviter de surcharger le démarrage trop tôt
  • retarder des services non critiques
  • lisser la charge après boot
SERVICE_FAILURE_ACTIONSW et SC_ACTION

Très important en production.

Structure :

Code: Select all

typedef struct _SERVICE_FAILURE_ACTIONS {
    DWORD dwResetPeriod;
    LPWSTR lpRebootMsg;
    LPWSTR lpCommand;
    DWORD cActions;
    SC_ACTION* lpsaActions;
} SERVICE_FAILURE_ACTIONS;
Actions possibles :
  • redémarrer le service
  • redémarrer la machine
  • lancer une commande
  • ne rien faire
Utilisation réelle :
  • watchdog système sans recoder tout un mécanisme de supervision
  • résilience simple d’un service critique
C’est très utilisé dans les vrais environnements d’entreprise.

Triggers : services démarrés par événement

Concept avancé très important : un service peut démarrer non pas uniquement via auto-start ou demande manuelle, mais via un trigger.

Idée :
  • connexion réseau
  • arrivée d’un périphérique
  • événement firewall
  • changement système précis
Structure importante :
  • SERVICE_TRIGGER
Et plus globalement :
  • SERVICE_TRIGGER_INFO
Lecture typique via :
  • QueryServiceConfig2(... SERVICE_CONFIG_TRIGGER_INFO ...)
Exemple réel :

Code: Select all

service qui démarre seulement quand le réseau devient pertinent
Cela permet des services plus intelligents et moins gourmands.

Service SID : identité propre du service

Chaque service peut avoir un SID propre.

Exemple logique :

Code: Select all

NT SERVICE\MyService
Pourquoi c’est utile :
  • meilleure isolation
  • permissions plus fines
  • modèle de sécurité plus propre
Structure importante :
  • SERVICE_SID_INFO
Le service SID permet d’accorder des droits à une identité spécifique du service, plutot qu’à un compte plus large ou à tout un groupe.

C’est très important en sécurité.

Sécurité d’un service

Les services sont des objets sécurisés. On peut interroger ou modifier leur sécurité via :
  • QueryServiceObjectSecurity
  • SetServiceObjectSecurity
Cela permet de manipuler :
  • ACL
  • permissions d’accès
  • droits de lecture / modification / arrêt / suppression
Très important en pratique :
  • un service peut être protégé contre l’arrêt
  • un service peut être protégé contre certaines modifications
  • la sécurité du service compte autant que celle du processus qu’il lance
Il faut donc voir le service aussi comme un objet sécurisé du SCM.

Comptes et privilèges avancés

Un service s’exécute sous un compte, mais cela ne suffit pas à tout décrire. On peut aussi définir plus précisément les privilèges requis.

Structure importante :
  • SERVICE_REQUIRED_PRIVILEGES_INFOW
Cela permet de déclarer explicitement des privilèges comme :
  • SeDebugPrivilege
  • SeBackupPrivilege
  • SeRestorePrivilege
  • SeChangeNotifyPrivilege
  • etc.
Point très important en sécurité :

Code: Select all

principe du least privilege
Il ne faut pas donner plus de privilèges que nécessaire.

C’est un point de qualité et de sécurité énorme dans un vrai service.

Services par utilisateur : per-user services

Windows moderne introduit aussi la notion de services instanciés par utilisateur.

Types à connaitre :
  • SERVICE_USER_OWN_PROCESS
  • SERVICE_USER_SHARE_PROCESS
Utilisation typique :
  • services liés à une session utilisateur
  • applications modernes
  • composants dont l’identité doit suivre l’utilisateur
Cela montre bien que le monde des services modernes va plus loin que le vieux modèle “service machine global en Session 0”.

Session 0, UI et services interactifs

Il faut aussi rappeler un point critique : les services modernes sont isolés en Session 0.

Conséquence :
  • pas d’interface graphique normale
  • pas de vraie interaction UI avec l’utilisateur comme avant
  • le modèle “service interactif” est legacy et pratiquement à éviter
Services interactifs :
  • concept historique
  • quasi obsolète
  • à ne pas utiliser dans un design moderne
Règle pratique :

Code: Select all

un service ne doit jamais afficher de vraie UI
Si tu as besoin d’une interface, il faut séparer :
  • le service en Session 0
  • le client UI dans la session utilisateur
Debug d’un service

Déboguer un service est plus délicat qu’une simple appli console.

Problèmes classiques :
  • pas de console visible
  • pas de printf pratique
  • cycle piloté par le SCM
  • timings sensibles au démarrage
Solutions réalistes :
  • OutputDebugString
  • fichiers logs
  • Event Log
  • attacher un debugger
Petite astuce utile en debug local :

Code: Select all

if (IsDebuggerPresent())
    Sleep(20000);
Cela peut laisser le temps d’attacher un debugger. Ce n’est pas une solution de prod, mais c’est parfois pratique en développement.

Autres techniques utiles :
  • lancer le service en mode debug spécial si le code le prévoit
  • utiliser un mode console alternatif pour la logique métier
  • attacher WinDbg ou Visual Studio au bon moment
Architecture saine d’un service : modèle mental pro

Un bon modèle mental de service ressemble à ceci :

Code: Select all

main
→ StartServiceCtrlDispatcher

SCM
→ appelle ServiceMain

ServiceMain
→ RegisterServiceCtrlHandlerEx
→ SetServiceStatus(START_PENDING)
→ création stopEvent
→ création worker thread
→ SetServiceStatus(RUNNING)

HandlerEx
→ reçoit STOP
→ SetServiceStatus(STOP_PENDING)
→ SetEvent(stopEvent)

Worker
→ travaille
→ surveille stopEvent
→ nettoie
→ fin

ServiceMain
→ attend worker si besoin
→ SetServiceStatus(STOPPED)
C’est beaucoup plus sain que :

Code: Select all

ServiceMain
→ boucle infinie bloquante
→ rien n’est notifié proprement
Bonnes pratiques niveau pro

Quelques règles simples mais très importantes :
  • ne jamais bloquer ServiceMain inutilement
  • notifier le SCM correctement à chaque étape
  • créer un ou plusieurs workers si la logique est longue
  • gérer proprement STOP et SHUTDOWN
  • utiliser un compte le moins privilégié possible
  • configurer les failure actions si le service est important
  • éviter toute UI
  • faire attention aux dépendances et au timing de démarrage
  • journaliser proprement les erreurs réelles
Structures à connaitre absolument

Pour ce niveau avancé, il faut reconnaitre au minimum :
  • SERVICE_TABLE_ENTRYW
  • SERVICE_STATUS
  • SERVICE_STATUS_PROCESS
  • ENUM_SERVICE_STATUS_PROCESSW
  • QUERY_SERVICE_CONFIGW
  • SERVICE_DESCRIPTIONW
  • SERVICE_DELAYED_AUTO_START_INFO
  • SERVICE_FAILURE_ACTIONSW
  • SC_ACTION
  • SERVICE_TRIGGER
  • SERVICE_TRIGGER_INFO
  • SERVICE_SID_INFO
  • SERVICE_REQUIRED_PRIVILEGES_INFOW
  • SERVICE_PRESHUTDOWN_INFO
  • SERVICE_STATUS_HANDLE
APIs à connaitre absolument

Pour le cycle de vie de base :
  • StartServiceCtrlDispatcherW
  • RegisterServiceCtrlHandlerW
  • RegisterServiceCtrlHandlerExW
  • SetServiceStatus
Pour l’ouverture et l’admin SCM :
  • OpenSCManagerW
  • OpenServiceW
  • CloseServiceHandle
Pour l’état et l’énumération :
  • QueryServiceStatus
  • QueryServiceStatusEx
  • EnumServicesStatusExW
Pour la configuration :
  • CreateServiceW
  • ChangeServiceConfigW
  • ChangeServiceConfig2W
  • QueryServiceConfigW
  • QueryServiceConfig2W
  • DeleteService
Pour la sécurité :
  • QueryServiceObjectSecurity
  • SetServiceObjectSecurity
Pour le contrôle :
  • StartServiceW
  • ControlService
  • ControlServiceEx
Ce qu’il faut retenir

Un service avancé, ce n’est pas juste un EXE “qui tourne en fond”. C’est une entité pilotée par le SCM, avec un protocole précis, des structures dédiées, un cycle de vie strict, un compte d’exécution, des paramètres avancés, une vraie sécurité, et parfois des mécanismes modernes comme les triggers, les service SID ou les failure actions.

Les idées essentielles à garder sont les suivantes :
  • le SCM crée le processus et pilote la vie du service
  • SERVICE_TABLE_ENTRY relie nom logique et ServiceMain
  • SERVICE_STATUS et SetServiceStatus sont le cœur du protocole
  • SERVICE_STATUS_PROCESS et EnumServicesStatusExW sont cruciaux pour le tooling
  • la configuration d’un service dépasse largement le simple chemin binaire
  • failure actions, triggers, SID de service et privilèges requis font partie du vrai monde des services modernes
  • un service ne doit jamais être conçu comme une appli GUI cachée
Conclusion

Comprendre les services à ce niveau, c’est commencer à voir le vrai modèle système Windows derrière le simple mot “service”. C’est aussi comprendre qu’un service n’est pas seulement un binaire enregistré dans le SCM, mais une entité durable, fortement encadrée, liée à la sécurité, au démarrage, aux dépendances, aux politiques de récupération, et à des structures très précises.

Si tu maitrises déjà correctement :
  • StartServiceCtrlDispatcherW
  • SERVICE_TABLE_ENTRYW
  • SERVICE_STATUS et SetServiceStatus
  • RegisterServiceCtrlHandlerExW
  • SERVICE_STATUS_PROCESS et QueryServiceStatusEx
  • EnumServicesStatusExW
  • QUERY_SERVICE_CONFIGW et ChangeServiceConfigW
  • ChangeServiceConfig2W avec SERVICE_FAILURE_ACTIONSW, SERVICE_TRIGGER_INFO et SERVICE_SID_INFO
  • la sécurité d’un service via QueryServiceObjectSecurity et SetServiceObjectSecurity
alors tu es déjà très au-dessus du niveau “service basique”, et tu commences à avoir une vraie vision système du sujet.

A bientot pour un nouveau cours sur le dev Win32 8-)

Who is online

Users browsing this forum: No registered users and 0 guests