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
Code: Select all
SCM
↓
CreateProcess de service.exe
↓
service.exe démarre
↓
StartServiceCtrlDispatcher
↓
ServiceMain
↓
RegisterServiceCtrlHandlerEx
↓
SetServiceStatus
↓
worker thread / boucle du service
- 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
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
Code: Select all
int wmain()
{
SERVICE_TABLE_ENTRYW table[] =
{
{ (LPWSTR)L"MyService", ServiceMain },
{ NULL, NULL }
};
if (!StartServiceCtrlDispatcherW(table))
return 1;
return 0;
}
Code: Select all
main
→ StartServiceCtrlDispatcher
puis
SCM
→ appelle ServiceMain
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;
Code: Select all
SERVICE_TABLE_ENTRYW table[] =
{
{ (LPWSTR)L"MyService", ServiceMain },
{ NULL, NULL }
};
- 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
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
);
- 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
RegisterServiceCtrlHandlerEx : réception des contrôles
Un service doit s’enregistrer pour recevoir les contrôles envoyés par le SCM.
API importante :
- RegisterServiceCtrlHandlerExW
- RegisterServiceCtrlHandlerW
Exemple conceptuel :
Code: Select all
SERVICE_STATUS_HANDLE g_StatusHandle = RegisterServiceCtrlHandlerExW(
L"MyService",
ServiceCtrlHandlerEx,
NULL
);
if (!g_StatusHandle)
return;
- SERVICE_STATUS_HANDLE
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
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;
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
Les états les plus importants sont :
- SERVICE_START_PENDING
- SERVICE_RUNNING
- SERVICE_STOP_PENDING
- SERVICE_STOPPED
- SERVICE_PAUSE_PENDING
- SERVICE_PAUSED
- SERVICE_CONTINUE_PENDING
Règle importante :
Code: Select all
un service sérieux doit notifier ses transitions au SCM
- au démarrage : SERVICE_START_PENDING puis SERVICE_RUNNING
- à l’arrêt : SERVICE_STOP_PENDING puis SERVICE_STOPPED
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
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
- 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
C’est un piège classique.
SetServiceStatus : appel obligatoire
L’API fondamentale de notification est :
- SetServiceStatus
Code: Select all
SetServiceStatus(g_StatusHandle, &status);
- un service doit notifier correctement le SCM de ses changements d’état
- c’est un protocole, pas une simple option
Code: Select all
START_PENDING
↓
RUNNING
↓
STOP_PENDING
↓
STOPPED
- des timeouts SCM
- des comportements instables
- des arrêts forcés
- un service vu comme “planté” alors qu’il travaille encore
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;
- elle contient le PID du processus hébergeant le service
- elle donne une vue plus riche et plus utile en diagnostic
- debugging
- outillage d’admin
- corrélation avec Process Explorer
- inspection des services partagés
API importante :
- QueryServiceStatusEx
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;
}
- l’état courant réel
- les contrôles acceptés
- le PID
- les flags de service
- les codes de sortie
Enumération des services
Pour lister les services du système, l’API importante est :
- EnumServicesStatusExW
Code: Select all
EnumServicesStatusExW(
hSCM,
SC_ENUM_PROCESS_INFO,
SERVICE_WIN32,
SERVICE_STATE_ALL,
buffer,
size,
&bytesNeeded,
&servicesReturned,
NULL,
NULL
);
Code: Select all
typedef struct _ENUM_SERVICE_STATUS_PROCESS {
LPWSTR lpServiceName;
LPWSTR lpDisplayName;
SERVICE_STATUS_PROCESS ServiceStatusProcess;
} ENUM_SERVICE_STATUS_PROCESS;
- son nom logique
- son nom d’affichage
- son état
- son PID si applicable
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;
- QueryServiceConfigW
- 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
API clé :
- ChangeServiceConfigW
- le chemin binaire
- le type
- le type de démarrage
- le compte du service
- les dépendances
- le display name
Configuration avancée avec ChangeServiceConfig2
La version moderne pour beaucoup d’options avancées est :
- ChangeServiceConfig2W
- QueryServiceConfig2W
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
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
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;
- redémarrer le service
- redémarrer la machine
- lancer une commande
- ne rien faire
- watchdog système sans recoder tout un mécanisme de supervision
- résilience simple d’un service critique
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
- SERVICE_TRIGGER
- SERVICE_TRIGGER_INFO
- QueryServiceConfig2(... SERVICE_CONFIG_TRIGGER_INFO ...)
Code: Select all
service qui démarre seulement quand le réseau devient pertinent
Service SID : identité propre du service
Chaque service peut avoir un SID propre.
Exemple logique :
Code: Select all
NT SERVICE\MyService
- meilleure isolation
- permissions plus fines
- modèle de sécurité plus propre
- SERVICE_SID_INFO
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
- ACL
- permissions d’accès
- droits de lecture / modification / arrêt / suppression
- 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
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
- SeDebugPrivilege
- SeBackupPrivilege
- SeRestorePrivilege
- SeChangeNotifyPrivilege
- etc.
Code: Select all
principe du least privilege
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
- services liés à une session utilisateur
- applications modernes
- composants dont l’identité doit suivre l’utilisateur
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
- concept historique
- quasi obsolète
- à ne pas utiliser dans un design moderne
Code: Select all
un service ne doit jamais afficher de vraie UI
- le service en Session 0
- le client UI dans la session utilisateur
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
- OutputDebugString
- fichiers logs
- Event Log
- attacher un debugger
Code: Select all
if (IsDebuggerPresent())
Sleep(20000);
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
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)
Code: Select all
ServiceMain
→ boucle infinie bloquante
→ rien n’est notifié proprement
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
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
Pour le cycle de vie de base :
- StartServiceCtrlDispatcherW
- RegisterServiceCtrlHandlerW
- RegisterServiceCtrlHandlerExW
- SetServiceStatus
- OpenSCManagerW
- OpenServiceW
- CloseServiceHandle
- QueryServiceStatus
- QueryServiceStatusEx
- EnumServicesStatusExW
- CreateServiceW
- ChangeServiceConfigW
- ChangeServiceConfig2W
- QueryServiceConfigW
- QueryServiceConfig2W
- DeleteService
- QueryServiceObjectSecurity
- SetServiceObjectSecurity
- StartServiceW
- ControlService
- ControlServiceEx
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
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
A bientot pour un nouveau cours sur le dev Win32
