aujourd’hui on va voir un des mécanismes les plus importants de Windows : les tokens de sécurité.
C’est un sujet central, parce qu’en pratique énormément de comportements qui paraissent “bizarres” en Win32 deviennent immédiatement logiques dès qu’on comprend le token. Pourquoi OpenProcess marche sur une machine mais pas sur une autre. Pourquoi CreateProcessAsUserW échoue. Pourquoi un processus administrateur n’a pas toujours tous les droits. Pourquoi un thread peut accéder à une ressource alors que le processus qui l’héberge n’aurait pas pu. Dans énormément de cas, la réponse se trouve dans le contexte de sécurité, et donc dans le token.
Pourquoi les tokens sont fondamentaux
Comprendre les tokens permet de mieux voir :
- comment Windows représente l’identité de sécurité d’un processus ou d’un thread
- comment sont décidés les droits réels au moment d’un accès
- pourquoi certains privilèges doivent etre activés explicitement
- comment fonctionne l’impersonation
- comment lancer un processus sous une autre identité
- comment UAC, intégrité et privilèges interagissent
- pourquoi “etre admin” ne veut pas dire “tout peut etre fait automatiquement”
Ce qu’est réellement un token
Un token de sécurité est un objet noyau qui représente une identité de sécurité et un ensemble d’attributs associés. Quand Windows doit décider si du code a le droit d’effectuer une action, il ne se base pas sur le nom du programme, son icône, ou le dossier dans lequel il se trouve. Il se base sur le token utilisé pour l’opération.
Un token contient notamment :
- l’identité de l’utilisateur
- les groupes auxquels cet utilisateur appartient
- les privilèges présents
- l’état activé ou non de certains privilèges
- le niveau d’intégrité
- le type de token
- des informations d’élévation
- le session id
- des restrictions éventuelles
- le propriétaire par défaut et le DACL par défaut
Code: Select all
Est-ce que ce contexte de sécurité a le droit d’effectuer cette action ?
A quoi sert concrètement un token
Windows ne valide pas les accès à partir du “nom du programme”. Il valide les accès en comparant le contexte de sécurité du demandeur avec le descripteur de sécurité de la ressource ciblée.
Cela concerne par exemple :
- les fichiers
- les processus
- les threads
- les services
- les clés de registre
- les objets noyau
- les pipes nommés
- les sections mémoire
- les stations de fenêtre et desktops
Processus, thread et ordre de priorité du contexte de sécurité
Un processus possède normalement un token primaire. C’est le token principal associé à son identité de sécurité.
Un thread, lui, peut éventuellement posséder un token d’impersonation. Dans ce cas, pour certaines opérations d’accès, c’est ce token de thread qui est utilisé à la place du token primaire du processus.
La logique générale est la suivante :
- si le thread impersonne, le système utilise le token du thread
- sinon, le système retombe sur le token primaire du processus
Token primaire et token d’impersonation
Il faut bien distinguer les deux grandes catégories.
Le token primaire :
- est celui qui représente l’identité de base d’un processus
- sert notamment à la création d’un nouveau processus
- est le token “normal” attaché au processus
- est attaché à un thread
- sert à accéder à des ressources au nom d’une autre identité
- peut représenter différents niveaux d’impersonation
- SecurityAnonymous
- SecurityIdentification
- SecurityImpersonation
- SecurityDelegation
Ouvrir le token d’un processus
Pour manipuler un token, il faut d’abord obtenir un handle vers lui. L’API la plus classique est OpenProcessToken.
Exemple :
Code: Select all
HANDLE hToken = NULL;
if (!OpenProcessToken(
GetCurrentProcess(),
TOKEN_QUERY,
&hToken))
{
return 1;
}
Exemple :
Code: Select all
CloseHandle(hToken);
- OpenThreadToken
- SetThreadToken
- GetCurrentThread
- GetCurrentProcess
- OpenProcessToken
Comme pour les processus, les threads ou les fichiers, l’ouverture d’un token dépend des droits demandés.
Parmi les droits importants à connaitre :
- TOKEN_QUERY
- TOKEN_QUERY_SOURCE
- TOKEN_ADJUST_PRIVILEGES
- TOKEN_ADJUST_GROUPS
- TOKEN_ADJUST_DEFAULT
- TOKEN_ADJUST_SESSIONID
- TOKEN_DUPLICATE
- TOKEN_IMPERSONATE
- TOKEN_ASSIGN_PRIMARY
- TOKEN_ALL_ACCESS
Lire les informations d’un token avec GetTokenInformation
L’API principale pour interroger un token est :
- GetTokenInformation
Code: Select all
DWORD retSize = 0;
GetTokenInformation(hToken, TokenUser, NULL, 0, &retSize);
BYTE* buffer = new BYTE[retSize];
if (!buffer)
return 1;
if (!GetTokenInformation(
hToken,
TokenUser,
buffer,
retSize,
&retSize))
{
delete[] buffer;
return 1;
}
/* exploitation du buffer */
delete[] buffer;
Les classes TOKEN_INFORMATION_CLASS à connaitre
Le deuxième paramètre de GetTokenInformation est de type TOKEN_INFORMATION_CLASS. Il détermine quel type d’information on veut obtenir.
Parmi les classes les plus utiles à connaitre :
- TokenUser
- TokenGroups
- TokenPrivileges
- TokenOwner
- TokenPrimaryGroup
- TokenDefaultDacl
- TokenSource
- TokenType
- TokenImpersonationLevel
- TokenStatistics
- TokenSessionId
- TokenIntegrityLevel
- TokenElevationType
- TokenElevation
- TokenLinkedToken
- TokenRestrictedSids
- TokenMandatoryPolicy
- TokenOrigin
Les structures importantes du monde des tokens
Pour lire ou manipuler correctement un token, il faut reconnaitre plusieurs structures Win32 essentielles.
Les plus importantes à connaitre au début sont :
- TOKEN_USER
- TOKEN_GROUPS
- TOKEN_PRIVILEGES
- TOKEN_OWNER
- TOKEN_PRIMARY_GROUP
- TOKEN_DEFAULT_DACL
- TOKEN_STATISTICS
- TOKEN_ELEVATION
- TOKEN_MANDATORY_LABEL
- SID_AND_ATTRIBUTES
- LUID
- LUID_AND_ATTRIBUTES
- ACL
- SID
L’utilisateur et les groupes
Quand on demande TokenUser, on reçoit une structure TOKEN_USER, qui contient essentiellement un SID_AND_ATTRIBUTES. Le SID représente l’identité de sécurité de l’utilisateur.
Quand on demande TokenGroups, on reçoit une structure TOKEN_GROUPS contenant une liste de SID_AND_ATTRIBUTES.
Le type SID_AND_ATTRIBUTES est important, car il combine :
- un SID
- des attributs indiquant le rôle ou l’état de cette identité
- GetLengthSid
- CopySid
- EqualSid
- IsValidSid
- AllocateAndInitializeSid
- FreeSid
- ConvertSidToStringSidW
- ConvertStringSidToSidW
- LookupAccountSidW
Les privilèges
Un privilège n’est pas la meme chose qu’un droit d’accès sur un objet. C’est une capacité spéciale reconnue par le système pour certaines opérations sensibles.
Exemples de privilèges très connus :
- SeDebugPrivilege
- SeShutdownPrivilege
- SeBackupPrivilege
- SeRestorePrivilege
- SeTakeOwnershipPrivilege
- SeIncreaseQuotaPrivilege
- SeAssignPrimaryTokenPrivilege
- SeImpersonatePrivilege
- SeLoadDriverPrivilege
- SeSecurityPrivilege
- présent dans le token
- absent du token
- présent mais désactivé
- présent et activé
- présent mais supprimé ou restreint selon le contexte
Activer un privilège avec AdjustTokenPrivileges
Le schéma classique pour activer un privilège consiste à :
- ouvrir le token avec TOKEN_ADJUST_PRIVILEGES et souvent TOKEN_QUERY
- résoudre le LUID du privilège avec LookupPrivilegeValueW
- remplir une structure TOKEN_PRIVILEGES
- appeler AdjustTokenPrivileges
- vérifier GetLastError
Code: Select all
TOKEN_PRIVILEGES tp = { 0 };
if (!LookupPrivilegeValueW(
NULL,
L"SeDebugPrivilege",
&tp.Privileges[0].Luid))
{
return 1;
}
tp.PrivilegeCount = 1;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if (!AdjustTokenPrivileges(
hToken,
FALSE,
&tp,
sizeof(tp),
NULL,
NULL))
{
return 1;
}
if (GetLastError() != ERROR_SUCCESS)
{
return 1;
}
Les attributs importants d’un privilège incluent notamment :
- SE_PRIVILEGE_ENABLED
- SE_PRIVILEGE_ENABLED_BY_DEFAULT
- SE_PRIVILEGE_REMOVED
- SE_PRIVILEGE_USED_FOR_ACCESS
Le niveau d’intégrité est un autre élément fondamental du token. Il ne remplace pas les ACL ni les privilèges, mais ajoute une couche supplémentaire de contrôle dans le modèle MIC, Mandatory Integrity Control.
Les niveaux courants sont :
- Low
- Medium
- High
- System
- TokenIntegrityLevel
- TOKEN_MANDATORY_LABEL
Ce point est très important avec UAC. Un utilisateur membre du groupe Administrateurs n’exécute pas forcément ses processus avec un token élevé en permanence. Très souvent, il démarre avec un contexte medium, puis un contexte high après élévation explicite.
Autrement dit :
- etre membre du groupe Administrateurs ne veut pas dire que le processus courant tourne déjà avec tous les effets d’un token élevé
Pour raisonner proprement sur l’élévation, plusieurs classes d’information sont très utiles :
- TokenElevation
- TokenElevationType
- TokenLinkedToken
- TOKEN_ELEVATION
C’est une partie importante pour comprendre le comportement admin moderne sous Windows.
Token primaire contre token d’impersonation en pratique
Il faut toujours garder une règle simple :
- pour créer un processus sous une identité, on a besoin d’un token primaire
- pour accéder à une ressource au nom d’un autre, un token d’impersonation peut suffire selon le contexte
Dupliquer un token
L’API clé ici est :
- DuplicateTokenEx
Code: Select all
HANDLE hNewToken = NULL;
if (!DuplicateTokenEx(
hToken,
TOKEN_ALL_ACCESS,
NULL,
SecurityImpersonation,
TokenPrimary,
&hNewToken))
{
return 1;
}
- obtenir un token primaire à partir d’un autre token
- préparer un contexte pour CreateProcessAsUserW
- construire des scénarios d’impersonation ou de sandbox
- manipuler des tokens dans les services Windows
- DuplicateToken
Impersonation : agir temporairement comme un autre
L’impersonation est un mécanisme majeur de Windows. Elle permet à un thread d’agir temporairement sous une autre identité de sécurité.
API importantes à connaitre :
- ImpersonateLoggedOnUser
- ImpersonateSelf
- SetThreadToken
- RevertToSelf
- OpenThreadToken
Lancer un processus avec un token
Pour lancer un processus sous une autre identité de sécurité, il faut généralement utiliser des API comme :
- CreateProcessAsUserW
- CreateProcessWithTokenW
- CreateProcessWithLogonW
Code: Select all
STARTUPINFOW si = { 0 };
PROCESS_INFORMATION pi = { 0 };
si.cb = sizeof(si);
if (!CreateProcessAsUserW(
hNewToken,
NULL,
L"cmd.exe",
NULL,
NULL,
FALSE,
0,
NULL,
NULL,
&si,
&pi))
{
return 1;
}
- SeAssignPrimaryTokenPrivilege
- SeIncreaseQuotaPrivilege
CreateProcessWithLogonW, elle, permet un autre modèle fondé sur des identifiants explicites, souvent utile en applicatif.
Restreindre un token
Windows permet aussi de fabriquer des tokens restreints, ce qui est important pour certains scénarios de confinement ou de sécurité défensive.
API utiles à connaitre :
- CreateRestrictedToken
- CheckTokenMembership
- IsTokenRestricted
Décrire un accès : AccessCheck
Quand on veut comprendre ou simuler une décision d’accès, l’API très importante est :
- AccessCheck
Les structures utiles autour de ça incluent souvent :
- GENERIC_MAPPING
- PRIVILEGE_SET
- SECURITY_DESCRIPTOR
Descripteurs de sécurité, ACL et relation avec le token
Le token n’agit pas seul dans le vide. Il est toujours comparé à une politique de sécurité portée par l’objet cible.
Il faut donc au moins connaitre de nom :
- SECURITY_DESCRIPTOR
- ACL
- ACE
- DACL
- SACL
- le token décrit qui demande l’accès
- le security descriptor décrit qui est autorisé ou auditée
- Windows compare les deux
- GetSecurityInfo
- SetSecurityInfo
- InitializeSecurityDescriptor
- SetSecurityDescriptorDacl
- GetKernelObjectSecurity
- SetKernelObjectSecurity
Session, logon et provenance du token
Un token n’est pas juste “user + groupes + privilèges”. Il porte aussi des informations de session et d’origine.
Classes utiles à connaitre :
- TokenSessionId
- TokenOrigin
- TokenSource
- TOKEN_SOURCE
- TOKEN_STATISTICS
Admin, SeDebugPrivilege et faux raccourcis
Une erreur fréquente consiste à croire que “etre admin” suffit. En réalité, la sécurité Windows est plus subtile.
Quelques règles simples à retenir :
- etre dans le groupe Administrateurs ne garantit pas un token élevé en permanence
- un privilège présent dans le token n’est pas forcément activé
- SeDebugPrivilege n’ouvre pas magiquement toutes les portes si le reste du contexte ne suit pas
- le niveau d’intégrité et les ACL continuent à compter
- certaines opérations demandent une combinaison précise de droits, privilèges et type de token
Quelques API très utiles autour des tokens
En plus des grandes API déjà vues, il vaut la peine de connaitre aussi :
- GetTokenInformation
- SetTokenInformation
- LookupPrivilegeNameW
- LookupPrivilegeDisplayNameW
- LookupAccountNameW
- LookupAccountSidW
- CheckTokenMembership
- GetCurrentThreadEffectiveToken
- GetCurrentThreadToken
- GetCurrentThreadToken
- GetCurrentProcessToken
Les structures à connaitre absolument
Pour avoir déjà une base solide, il faut reconnaitre rapidement les structures suivantes :
- TOKEN_USER
- TOKEN_GROUPS
- TOKEN_PRIVILEGES
- TOKEN_OWNER
- TOKEN_PRIMARY_GROUP
- TOKEN_DEFAULT_DACL
- TOKEN_STATISTICS
- TOKEN_ELEVATION
- TOKEN_MANDATORY_LABEL
- SID_AND_ATTRIBUTES
- LUID
- LUID_AND_ATTRIBUTES
- TOKEN_SOURCE
- PRIVILEGE_SET
- GENERIC_MAPPING
- SECURITY_DESCRIPTOR
- ACL
- SID
Pour ouvrir et manipuler un token :
- OpenProcessToken
- OpenThreadToken
- DuplicateToken
- DuplicateTokenEx
- SetThreadToken
- CloseHandle
- GetTokenInformation
- SetTokenInformation
- CheckTokenMembership
- IsTokenRestricted
- LookupPrivilegeValueW
- LookupPrivilegeNameW
- LookupPrivilegeDisplayNameW
- AdjustTokenPrivileges
- ImpersonateLoggedOnUser
- ImpersonateSelf
- RevertToSelf
- CreateProcessAsUserW
- CreateProcessWithTokenW
- CreateProcessWithLogonW
- LookupAccountSidW
- LookupAccountNameW
- ConvertSidToStringSidW
- ConvertStringSidToSidW
- EqualSid
- IsValidSid
- AccessCheck
- GetSecurityInfo
- SetSecurityInfo
- GetKernelObjectSecurity
- SetKernelObjectSecurity
- InitializeSecurityDescriptor
- SetSecurityDescriptorDacl
- CreateRestrictedToken
Comme souvent en sécurité Windows, les erreurs les plus fréquentes viennent d’un raisonnement trop simplifié.
Parmi les fautes classiques :
- confondre appartenance au groupe Administrateurs et élévation effective
- penser qu’un privilège est automatiquement activé parce qu’il est présent
- oublier CloseHandle sur le token
- demander trop de droits d’accès au token
- ne pas vérifier GetLastError après AdjustTokenPrivileges
- confondre token primaire et token d’impersonation
- croire que l’impersonation transforme magiquement tout le processus alors qu’elle touche souvent le thread
- ignorer le niveau d’intégrité
- oublier que le token est comparé à un security descriptor et pas utilisé seul
Le token de sécurité définit l’identité réelle utilisée par Windows pour autoriser ou refuser une action. Il contient l’utilisateur, les groupes, les privilèges, le niveau d’intégrité, et d’autres informations qui déterminent le contexte de sécurité réel du code.
Les idées importantes à garder sont les suivantes :
- Windows vérifie les accès à partir du token, pas du nom du programme
- un processus possède un token primaire
- un thread peut posséder un token d’impersonation
- les privilèges doivent souvent etre activés explicitement
- le niveau d’intégrité joue un rôle très important avec UAC
- la différence entre token primaire et token d’impersonation est fondamentale
- la sécurité dépend toujours de la rencontre entre le token et le security descriptor de l’objet ciblé
Comprendre les tokens, c’est comprendre une grande partie de la sécurité Windows réelle. Sans eux, les notions de privilèges, d’impersonation, d’élévation, d’accès aux processus, de création de processus sous une autre identité ou d’autorisation sur les objets système restent floues.
Si tu maitrises déjà correctement :
- OpenProcessToken
- GetTokenInformation
- TOKEN_USER, TOKEN_GROUPS et TOKEN_PRIVILEGES
- LookupPrivilegeValueW et AdjustTokenPrivileges
- TokenIntegrityLevel
- DuplicateTokenEx
- CreateProcessAsUserW
- ImpersonateLoggedOnUser et RevertToSelf
- AccessCheck et la logique security descriptor + token
Et surtout, tu commences à voir que le token n’est pas une simple structure parmi d’autres, mais le coeur réel de la question : “qu’est-ce que ce code a le droit de faire ?”
A bientot pour le prochain cours
