Modèle de sécurité Windows8 minute(s) de lecture

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;

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;

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;

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.

Vision schématique d’une ACE

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 !

Leave a Reply