Cet article se veut être le prolongement de mon premier sur ilearned, un blog communautaire. Dans celui-ci j’abordais dans les grandes lignes le fonctionnement du modèle sécurité de Windows (je ne voulais rentrer autant en profondeur pour que l’article soit compréhensible de vraiment tout le monde même les plus linuxiens). Ici, je vais apporter une vision plus technique et plus profonde sur certains passages.
Durant tout l’article, chaque valeur (que ce soit un droit ou une structure relative au descripteur de sécurité) sera indiquée sous son format alphanumérique, et SDDL (pour ceux ne sachant pas ce qu’est SDDL, ne soyez pas effrayez !).
SecurableObject
Je souhaite ici introduire plus de détail sur le SecurityDescriptor
dont la structure est la suivante:
typedef struct _SECURITY_DESCRIPTOR { BYTE Revision; BYTE Sbz1; SECURITY_DESCRIPTOR_CONTROL Control; PSID Owner; PSID Group; PACL Sacl; PACL Dacl; } SECURITY_DESCRIPTOR, *PISECURITY_DESCRIPTOR;
Ils sont donc composés de plusieurs choses: le SID du propriétaire de l’objet (O:
), ainsi que le SID du groupe propriétaire de l’objet (G:
); une System Access Control List
(SACL, S:
) qui permet de garder une trace des objets qui ont accédé au propriétaire du SecurityDescriptor
, nous ne nous y intéresserons pas; une Discretionary Access Control List
(DACL, D:
). Les descripteurs de sécurité sont usuellement au format SDDL (Security Descriptor Definition Langage) bien que peu reluisant, il est en réalité très pratique car simple d’utilisation (non pas de compréhension). L’accès à chaque objet est donc définit grâce à cette structure.
Token d’accès
Le token d’accès contient précisément le SID de l’utilisateur, le SID de son groupe, des privilèges d’accès (nous y reviendrons plus tard), un SID pour la session, une DACL par défaut (utilisée lorsqu’il créer un objet). Il y a de plus certain nombre de statistiques, le type d’Access Token, une liste éventuel qui restreint les SIDs auxquels nous pouvons accéder et le niveau “d’impersonnation”, la source du token. Ces informations sont présente sous forme de structure:
La DACL par défaut:
typedef struct _TOKEN_DEFAULT_DACL { PACL DefaultDacl; } TOKEN_DEFAULT_DACL, *PTOKEN_DEFAULT_DACL;
Les groupes d’appartenance:
typedef struct _TOKEN_GROUPS { DWORD GroupCount; #if ... SID_AND_ATTRIBUTES *Groups[]; #else SID_AND_ATTRIBUTES Groups[ANYSIZE_ARRAY]; #endif } TOKEN_GROUPS, *PTOKEN_GROUPS;
Le possesseur du token:
typedef struct _TOKEN_OWNER { PSID Owner; } TOKEN_OWNER, *PTOKEN_OWNER;
Le groupe primaire:
typedef struct _TOKEN_PRIMARY_GROUP { PSID PrimaryGroup; } TOKEN_PRIMARY_GROUP, *PTOKEN_PRIMARY_GROUP;
La liste des privilèges:
typedef struct _TOKEN_PRIVILEGES { DWORD PrivilegeCount; LUID_AND_ATTRIBUTES Privileges[ANYSIZE_ARRAY]; } TOKEN_PRIVILEGES, *PTOKEN_PRIVILEGES;
etc. Vous pouvez trouver toutes ces structures (et celles que je n’ai cité) à cette page https://docs.microsoft.com/en-us/windows/win32/secauthz/access-tokens. Il existe 2 types de token, ceux dit primaires, c’est à dire créés par le kernel de windows, et ceux “usurpés”, c’est à dire créés par un utilisateur possédant un privilège le permettant. Le token d’accès est alors gardé dans un processus un peu particulier appelé LSASS.exe
pour “Local Security Autority SubSystem Service”. Ce dernier est donc vital et très sensible. Lorsqu’un utilisateur se connecte il fournit ses informations de connexion dans le processus Winlogon.exe
qui se chargera de l’authentification et la création du token. Pour cela il fait appel à la fonction LsaLogonUser()
pour obtenir un token d’accès, et ce dernier sera réutilisé par la suite. Attention, contrairement à ce que j’ai pu peut-être faire comprendre, le condensat du mot de passe n’est pas contenu dans le token d’accès !
Lorsque l’on créer un nouveau processus (à l’aide de la fonction CreateProcess()
par exemple) c’est un token hérité de l’utilisateur qui est utilisé. Pour utiliser un autre token d’accès il faudra appeler plutôt CreateProcessWithToken()
. Cependant, ce genre de fonction nécessite un certain nombre de privilèges dans le token d’accès du programme qui utilise l’une de ces dernières.
Privilèges d’accès
Pour information, il existe un autre nom pour les privilèges que l’on peut retrouver dans winnt.h
:
#define SE_CREATE_TOKEN_NAME TEXT("SeCreateTokenPrivilege") #define SE_ASSIGNPRIMARYTOKEN_NAME TEXT("SeAssignPrimaryTokenPrivilege") #define SE_LOCK_MEMORY_NAME TEXT("SeLockMemoryPrivilege") #define SE_INCREASE_QUOTA_NAME TEXT("SeIncreaseQuotaPrivilege") #define SE_UNSOLICITED_INPUT_NAME TEXT("SeUnsolicitedInputPrivilege") #define SE_MACHINE_ACCOUNT_NAME TEXT("SeMachineAccountPrivilege") #define SE_TCB_NAME TEXT("SeTcbPrivilege") #define SE_SECURITY_NAME TEXT("SeSecurityPrivilege") #define SE_TAKE_OWNERSHIP_NAME TEXT("SeTakeOwnershipPrivilege") #define SE_LOAD_DRIVER_NAME TEXT("SeLoadDriverPrivilege") #define SE_SYSTEM_PROFILE_NAME TEXT("SeSystemProfilePrivilege") #define SE_SYSTEMTIME_NAME TEXT("SeSystemtimePrivilege") #define SE_PROF_SINGLE_PROCESS_NAME TEXT("SeProfileSingleProcessPrivilege") #define SE_INC_BASE_PRIORITY_NAME TEXT("SeIncreaseBasePriorityPrivilege") #define SE_CREATE_PAGEFILE_NAME TEXT("SeCreatePagefilePrivilege") #define SE_CREATE_PERMANENT_NAME TEXT("SeCreatePermanentPrivilege") #define SE_BACKUP_NAME TEXT("SeBackupPrivilege") #define SE_RESTORE_NAME TEXT("SeRestorePrivilege") #define SE_SHUTDOWN_NAME TEXT("SeShutdownPrivilege") #define SE_DEBUG_NAME TEXT("SeDebugPrivilege") #define SE_AUDIT_NAME TEXT("SeAuditPrivilege") #define SE_SYSTEM_ENVIRONMENT_NAME TEXT("SeSystemEnvironmentPrivilege") #define SE_CHANGE_NOTIFY_NAME TEXT("SeChangeNotifyPrivilege") #define SE_REMOTE_SHUTDOWN_NAME TEXT("SeRemoteShutdownPrivilege") #define SE_UNDOCK_NAME TEXT("SeUndockPrivilege") #define SE_ENABLE_DELEGATION_NAME TEXT("SeEnableDelegationPrivilege") #define SE_MANAGE_VOLUME_NAME TEXT("SeManageVolumePrivilege") #define SE_IMPERSONATE_NAME TEXT("SeImpersonatePrivilege") #define SE_CREATE_GLOBAL_NAME TEXT("SeCreateGlobalPrivilege")
Pour savoir où sont précisément renseignés les privilèges d’accès, il faut étudier plus en profondeur la structure _TOKEN_PRIVILEGES
. Le second objet de la structure est une liste de LUID_AND_ATTRIBUTES
qui est une structure dont voici la définition:
typedef struct _LUID_AND_ATTRIBUTES { LUID Luid; DWORD Attributes; } LUID_AND_ATTRIBUTES, *PLUID_AND_ATTRIBUTES;
Le champ Attributes
nous renseigne sur l’activation où nom du LUID
. Le LUID
est une structure qui définit le privilège en question:
typedef struct _LUID { DWORD LowPart; LONG HighPart; } LUID, *PLUID;
Pour obtenir un LUID
en accord avec un droit précis, on utilise la fonction LookupPrivilegeValueA()
(un article arrive bientôt sur la manipulation des tokens d’accès en C++).
Les listes d’accès
Comme je l’ai dis, qu’importe le type d’ACL, elles porteront un header commun que voici:
typedef struct _ACL { BYTE AclRevision; BYTE Sbz1; WORD AclSize; WORD AceCount; WORD Sbz2; } ACL;
où AceCount
est le nombre d’ACE, AclSize
la taille de l’ACL (qui est au maximum de 64 kb), Sbz1/Sbz2
un complément permettant d’aligner les éléments de l’en-tête. On peut aisément en déduire qu’elle contient une liste d’ACE. Pour définir une DACL il faut utiliser le SDDL lors de la création du SecurityDescriptor
avec la partie D:
dans laquelle les différentes ACEs sont renseignées.
Les Access Control Entry
sont les éléments individuels d’une ACL qui permettent de définir les accès. Il existe deux types d’ACE différentes: génériques et objets. Dans le premier cas, elles sont utilisées pour décrire les permissions sur les fichiers notamment:
typedef struct _ACE_HEADER { BYTE AceType; BYTE AceFlags; WORD AceSize; } ACE_HEADER;
Où AceType
désigne comme son nom l’indique le type d’ACE, les principales étant ACCESS_ALLOWED
(A
, en SDDL), ACCESS_DENIED
(D
, en SDDL). AceSize
indique la taille de l’ACE, AceFlags
contrôle l’héritage du droit accordé par l’ACE. On retrouve CONTAINER_INHERIT_ACE
(CI
) qui signifie que l’accès est accordé dans tous les objets enfants (que peuvent être les dossiers et fichiers, les groupes etc), INHERIT_ONLY_ACE
(IO
) est dans la continuité du précédent puisqu’il indique que l’accès conféré est le même que celui du parent (on peut donc remonter comme ceci jusqu’à l’ACE CONTAINER_INHERIT_ACE
), INHERITED_ACE
(ID
) indique que l’ACE, a été hériter. La structure d’une ACE est définie en fonction du type d’accès, il existe logiquement ACESS_DENIED
et ACCESS_ALLOWED
:
typedef struct _ACCESS_ALLOWED_ACE { ACE_HEADER Header; ACCESS_MASK Mask; DWORD SidStart; } ACCESS_ALLOWED_ACE; typedef struct _ACCESS_DENIED_ACE { ACE_HEADER Header; ACCESS_MASK Mask; DWORD SidStart; } ACCESS_DENIED_ACE;
Où ACE_HEADER
est la structure vue précédemment, ACCESS_MASK
est une structure permettant de définir le type d’accès accordé. SidStart
est le Security Identifier (SID) de l’objet auquel on confère l’accès qui peut être présenté en tant que “well known identifier” (i.e AN
=Anonymous, BA
=Builtin Administrators, BU
=Builtin Users, DA
=Domain Administrators …), ce dernier est dénommé “trustee”. En SDDL on déclare une ACE avec des parenthèses et est structurée comme ceci (ace_type;ace_flags;rights;object_guid;inherit_object_guid;account_sid;resource_attribute
) lorsqu’une information n’est pas précisé il faut toujours garder les points virgules; si on souhaite ajouter plusieurs ACEs il suffit d’ajouter plusieurs structures à la suite.
Le masque d’accès est essentiel pour comprendre le système de permission, car c’est lui qui contient les droits accordés. Il possède la structure suivante:
typedef DWORD ACCESS_MASK; typedef ACCESS_MASK* PACCESS_MASK;
Il contient une unique valeur de type DWORD
qui représente l’ensemble des droits. Ces derniers peuvent être des droits standard qui sont issues du fichier winnt.h
et sont originellement définis comme ceci:
#define DELETE (0x00010000L) #define READ_CONTROL (0x00020000L) #define WRITE_DAC (0x00040000L) #define WRITE_OWNER (0x00080000L) #define SYNCHRONIZE (0x00100000L) #define STANDARD_RIGHTS_REQUIRED (0x000F0000L) #define STANDARD_RIGHTS_READ (READ_CONTROL) #define STANDARD_RIGHTS_WRITE (READ_CONTROL) #define STANDARD_RIGHTS_EXECUTE (READ_CONTROL) #define STANDARD_RIGHTS_ALL (0x001F0000L) #define SPECIFIC_RIGHTS_ALL (0x0000FFFFL)
Ces droits standards permettent alors de construire ce que l’on appelle les droits génériques. Une manière définir ces droits pour un objet est d’utiliser la structure _GENERIC_MAPPING
(qui explique pourquoi, parfois GENERIC_ALL
n’est pas la combinaison des autres accès):
typedef struct _GENERIC_MAPPING { ACCESS_MASK GenericRead; ACCESS_MASK GenericWrite; ACCESS_MASK GenericExecute; ACCESS_MASK GenericAll; } GENERIC_MAPPING;
Cependant, ils sont également définit de manière générale dans winnt.h
:
#define GENERIC_READ 0x80000000 #define GENERIC_WRITE 0x40000000 #define GENERIC_EXECUTE 0x20000000 #define GENERIC_ALL 0x10000000
Pour que ces droits soient effectifs, nous devons les lier à des droits spécifique d’objet, propre donc à chaque objet. Ainsi pour un fichier, ou pour un processus, ce ne seront pas les mêmes. Pour les fichiers par exemple:
#define FILE_READ_DATA 0x0001 /* file & pipe */ #define FILE_LIST_DIRECTORY 0x0001 /* directory */ #define FILE_WRITE_DATA 0x0002 /* file & pipe */ #define FILE_ADD_FILE 0x0002 /* directory */ #define FILE_APPEND_DATA 0x0004 /* file */ #define FILE_ADD_SUBDIRECTORY 0x0004 /* directory */ #define FILE_CREATE_PIPE_INSTANCE 0x0004 /* named pipe */ #define FILE_READ_EA 0x0008 /* file & directory */ #define FILE_READ_PROPERTIES FILE_READ_EA #define FILE_WRITE_EA 0x0010 /* file & directory */ #define FILE_WRITE_PROPERTIES FILE_WRITE_EA #define FILE_EXECUTE 0x0020 /* file */ #define FILE_TRAVERSE 0x0020 /* directory */ #define FILE_DELETE_CHILD 0x0040 /* directory */ #define FILE_READ_ATTRIBUTES 0x0080 /* all */ #define FILE_WRITE_ATTRIBUTES 0x0100 /* all */ #define FILE_ALL_ACCESS (STANDARD_RIGHTS_REQUIRED|SYNCHRONIZE|0x1ff) #define FILE_GENERIC_READ (STANDARD_RIGHTS_READ | FILE_READ_DATA | FILE_READ_ATTRIBUTES | FILE_READ_EA | SYNCHRONIZE) #define FILE_GENERIC_WRITE (STANDARD_RIGHTS_WRITE | FILE_WRITE_DATA | FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA | FILE_APPEND_DATA | SYNCHRONIZE) #define FILE_GENERIC_EXECUTE (STANDARD_RIGHTS_EXECUTE | FILE_EXECUTE | FILE_READ_ATTRIBUTES | SYNCHRONIZE)
Pour les processus:
#define PROCESS_TERMINATE 0x0001 #define PROCESS_CREATE_THREAD 0x0002 #define PROCESS_VM_OPERATION 0x0008 #define PROCESS_VM_READ 0x0010 #define PROCESS_VM_WRITE 0x0020 #define PROCESS_DUP_HANDLE 0x0040 #define PROCESS_CREATE_PROCESS 0x0080 #define PROCESS_SET_QUOTA 0x0100 #define PROCESS_SET_INFORMATION 0x0200 #define PROCESS_QUERY_INFORMATION 0x0400 #define PROCESS_SUSPEND_RESUME 0x0800 #define PROCESS_QUERY_LIMITED_INFORMATION 0x1000 #define PROCESS_ALL_ACCESS (STANDARD_RIGHTS_REQUIRED|SYNCHRONIZE|0xfff)
Lors de la création d’un masque d’accès, ces valeurs sont combinées dans la structure. Lorsqu’un programme souhaite accéder à un objet, il doit préciser les droits qu’il utilisera, plus exactement, il doit préciser un masque d’accès avec les droits qu’il voudra obtenir sur l’objet (le processus d’accès lui verra ou non accorder ce qu’il demande). La fonction CreateFileA()
demande simplement ce droit, seulement, beaucoup de fonctions font appel à des fonctions de ntdll.dll
ce qui est le cas ici. Plus précisément elle fait appelle à NtCreateFile()
:
__kernel_entry NTSTATUS NtCreateFile( PHANDLE FileHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PIO_STATUS_BLOCK IoStatusBlock, PLARGE_INTEGER AllocationSize, ULONG FileAttributes, ULONG ShareAccess, ULONG CreateDisposition, ULONG CreateOptions, PVOID EaBuffer, ULONG EaLength );
Et effectivement, le masque d’accès apparaît et la documentation nous confirme qu’il faut ajouter des droits spécifiques aux fichiers. Pour mieux comprendre la manière dont s’agencent les droits dans un masque d’accès, je vous propose le schéma suivant (merci Microsoft):
J’espère que les petites précisions apportées ici vous auront permis de comprendre plus en profondeur le contenu de mon précédent article, si vous voulez savoir comment on applique ce modèle sur les objets Active Directory, je vous invite à consulter mon article sur l’abus des ACLs !