Présentation du MS-RPC et de ses mécanismes de sécurité
Qu'est-ce que le RPC ?
Un appel de procédure à distance (RPC) est une forme de communication interprocessus (IPC). Il permet à un client d'invoquer une procédure exposée par un serveur RPC. Le client appelle la fonction de la même façon que s'il s'agissait d'un appel de procédure normal. Pour une interaction à distance, il n'a (presque) pas besoin de coder les détails. Le serveur peut être hébergé sur une machine distante ou bien sur la même machine, mais dans un processus différent.
Dans cet article, nous allons analyser la mise en œuvre du RPC (MS-RPC) de Microsoft. MS-RPC a été obtenu à partir de la mise en œuvre de référence (V1.1) du protocole de RPC au cœur même de l'environnement informatique distribué.
Planification de tâches, création de service, paramètres d'impression et de partage, gestion des données chiffrées conservées à distance… Le RPC est largement utilisé par Windows pour de nombreux services différents. Comme le RPC constitue un vecteur d'attaque à distance, ce protocole fait l'objet d'une attention particulière en matière de sécurité. Cet article de blog vise à essayer de vous faire comprendre le fonctionnement de base du MS-RPC, tout en mettant l'accent sur ses mécanismes de sécurité intégrés.
Comment fonctionne le MS-RPC ?
Les procédures et leurs paramètres utilisent un langage descriptif appelé Interface Definition Language (IDL). Un identifiant unique appelé UUID est attribué à chaque interface. Il est utilisé par le serveur, mais aussi par le client pour pouvoir accéder à l'interface exacte exposée par le serveur.
Le fichier en IDL est ensuite compilé à l'aide du compilateur IDL de Microsoft. Une fois passé dans cet outil, il se transforme en fichiers de codes sources et de codes d'en-tête contenant la fonctionnalité d'exécution. Techniquement parlant, ces stubs utilisés par le serveur et par le client donnent les rênes à l'exécution RPC mise en œuvre dans rpcrt4.dll. Chez le client, l'exécution RPC permet de rassembler les données et de les transmettre à l'exécution RPC de l'autre partie (Figure 1).
Figure 1 : Exécution RPC
Il se peut que vous vous demandiez comment le serveur et le client communiquent en réseau ou en local. Le serveur écoute les connexions RPC entrantes en enregistrant une séquence de protocole et un point de terminaison. Une séquence de protocole peut être ncacn_ip_tcp (TCP), ncacn_np (canal nommé) ou ncalrpc (LPC). Le point de terminaison peut être un port comme un port TCP 5555. Si vous utilisez un canal nommé, le terminal peut être \\pipe\\example. Les canaux nommés sont transmis par le transport SMB du port TCP 445 à l'aide du partage masqué d'IPC$ . Consultez une liste exhaustive des séquences de protocole sur le site Web de Microsoft.
Le serveur écoute les connexions de certains points de terminaison. Mais comment le client sait-il où se connecter ? La réponse dépend du type de point de terminaison utilisé : est-il dynamique ou bien connu (statique) ?
Un point de terminaison dynamique est enregistré via le mappeur de points de terminaison du serveur. Aussi appelé epmapper, le mappeur de points de terminaison est un service RPC qui mappe un service sur le point de terminaison réel. L'epmapper utilise HTTP pour exploiter les ports TCP 135 et TCP 593 pour RPC. Par conséquent, le client peut énumérer tous les serveurs RPCenregistrés dynamiquement sur une machine distante à l'aide de l'epmapper grâce aux API spécifiques.
Un point de terminaison connu est un point qui n'a pas été enregistré via l'epmapper. Le client doit connaître le point de terminaison enregistré à l'avance par le serveur. Pour connaître le point de terminaison à l'avance, ce dernier doit être codé en dur dans le code du client et du serveur ou inclus dans le fichier IDL. Pour illustrer cela, voici un exemple tiré de l'IDL du service Print Spooler.
[
uuid(12345678-1234-ABCD-EF00-0123456789AB),
version(1.0),
ms_union,
endpoint("ncacn_np:[\\pipe\\spoolss]"),
pointer_default(unique)
]
RPC représente une connexion logique entre un client et un serveur utilisant un handle de liaison. Le serveur et le client utilisent les fonctions pour manipuler les données de liaison, et ce notamment lors de la définition des informations d'authentification. Lorsque le client définit l'authentification sur la liaison, cette dernière est considérée comme authentifiée. Lorsque le programme client appelle la fonction RPC, la liaison réseau se met en place. Du point de vue du trafic réseau, l'interaction RPC du client RPC commence dès l'envoi d'une demande de liaison contenant les informations d'authentification vers le paquet. Le serveur peut répondre par bind_ack (reconnue) ou bind_nak (une erreur s'est produite).
Figure 2 : Extrait de Wireshark montrant une résolution dynamique au niveau d'un point de terminaison
Dans l'extrait de la Figure 2, nous pouvons voir la résolution dynamique des points de terminaison pour l'interface Planificateur de tâches. Dès que le client reçoit les informations du point de terminaison du Planificateur de tâches, une nouvelle connexion se crée. Ensuite, nous pouvons créer une nouvelle connexion à l'aide d'un autre processus de liaison tel que le paquet AUTH3 pour l'authentification de liaison.
À l'instar des points de terminaison, les liaisons ont plusieurs types : automatique, implicite et explicite (Figure 3). Leur seule différence, c'est que l'application n'a pas le même contrôle sur le processus de liaison.
- Liaison automatique (obsolète) — Les applications du client et du serveur ne gèrent pas le processus de liaison. Elles laissent l'exécution RPC contrôler complètement ce processus.
- Liaison implicite — Le client a la possibilité de configurer le handle de liaison avant que la liaison ait lieu. Une fois que la liaison a été établie par le client, la bibliothèque d'exécution RPC se charge du reste.
- Liaison explicite — Le client doit configurer le handle de liaison. Ensuite, l'exécution RPC le transmet au serveur.
Sécurité RPC pour les requêtes à distance
Maintenant que vous connaissez le fonctionnement de base du RPC, nous allons évoquer les actions, stratégies et mécanismes qui peuvent empêcher un client d'accéder à une fonction. Que ce soit d'un point de vue offensif ou défensif, ces éléments sont intéressants.
Authentification de transport
Pour certains transports, tels que les canaux nommés ou le HTTP, l'authentification fait partie de leur protocole. C'est notamment le cas des canaux nommés transportés par SMB qui disposent d'une authentification. Cela signifie que lorsqu'un serveur enregistre le point de terminaison d'un canal nommé, seuls les clients qui possèdent les identifiants de connexion d'un utilisateur valide peuvent se connecter audit point de terminaison. Dans un environnement de domaine, le fait d'avoir un utilisateur de domaine dans le même domaine suffit à éviter la vérification d'authentification. Si les machines ne font pas partie d'un domaine, le client doit disposer des identifiants de connexion d'un utilisateur local au serveur distant (Figure 4). Pour en savoir plus sur les exceptions, poursuivez votre lecture.
Figure 4 : Exemple d'erreur système refusant l'accès
Authentification de liaison
Le serveur peut utiliser l'authentification de liaison pour disposer d'un mécanisme d'authentification. Pour ce faire, le serveur doit enregistrer les informations d'authentification en appelant RpcServerRegisterAuthInfo. Le client doit utiliser le nom principal du service correct et la méthode du fournisseur SSP utilisée par le serveur pour ne pas voir le message « RPC_S_UNKNOWN_AUTHN_SERVICE » du serveur apparaître.
Un client peut se connecter à un serveur en définissant les données d'authentification et d'autorisation sur la liaison à l'aide d'API telles que RpcBindingSetAuthInfo et RpcBindingSetAuthInfoEx. Le client spécifie la méthode d'authentification (NTLM, Kerberos, Negotiate, SCHANNEL, etc.) et le nom principal du service.
Vous devez garder à l'esprit deux choses importantes :
Si un client définit l'authentification sur la liaison et que le serveur n'a pas enregistré d'informations d'authentification, le serveur renvoie « RPC_S_UNKNOWN_AUTHN_SERVICE ».
Ce n'est pas parce que le serveur a enregistré une liaison d'authentification que le client doit utiliser une liaison authentifiée. De plus, si les identifiants de connexion sont erronés, l'exécution RPC ne transmettra pas l'authentification client. Poursuivez votre lecture pour découvrir comment le serveur peut refuser l'accès à des liaisons non authentifiées.
Il convient de noter que l'exécution RPC enregistre les informations d'authentification à l'échelle mondiale. Cela signifie que si deux serveurs RPC partagent le même processus et que l'un d'eux enregistre les informations d'authentification, l'autre serveur aura également des informations d'authentification. Un client peut désormais authentifier la liaison lors de l'accès à chacun des serveurs. Ce comportement est appelé multiplexage SSPI.
Sécurité des points de terminaison
Un serveur peut définir un descripteur de sécurité sur le point de terminaison. Le descripteur de sécurité est un mécanisme de sécurité de vérification d'accès général dans Windows. Il permet la création de « règles » pour les personnes autorisées à accéder à un objet et pour les personnes auxquelles cet accès est interdit. Lorsqu'une personne essaye d'accéder à l'objet, le système d'exploitation compare le jeton d'accès de l'appelant au descripteur de sécurité pour voir si l'accès lui est autorisé. Le cas échéant, l'objet est le point de terminaison et le jeton d'accès est le jeton d'accès du client dérivé de l'authentification du protocole de transport. Lorsque l'authentification est utilisée, ce contrôle s'applique uniquement aux transports authentifiés tels que les canaux nommés, l'ALPC et le HTTP. Le TCP est un protocole de transport non authentifié qui, par conséquent, ne pourrait pas bénéficier de ce contrôle d'accès.
Comme pour le multiplexage SSPI, les points de terminaison sont multiplexés. L'interface et le point de terminaison ne sont donc pas liés. Un serveur enregistre les interfaces et les points de terminaison séparément. Si un processus enregistre plusieurs points de terminaison, chaque interface enregistrée dans ce processus est accessible via chacun de ces points de terminaison.
Imaginez-vous enregistrer votre interface et créer un point de terminaison qui écoute via \\pipe\mypipe. Et là, un autre serveur RPC hébergé dans le même processus enregistre son propre point de terminaison sur le port TCP 7777. Votre interface sera également accessible par TCP. Ce protocole contournera les restrictions de sécurité imposées aux points de terminaison telles qu'un descripteur de sécurité (Figure 5). Par conséquent, nous vous recommandons de ne pas vous fier à la sécurité des points de terminaison ou de vérifier que le client a bien utilisé le protocole de transport attendu.
Figure 5 : Exemple de contournement du descripteur de sécurité du point de terminaison en raison du multiplexage dudit point de terminaison
Sécurité des interfaces
Pour sécuriser une interface, il existe trois méthodes : la définition d'un rappel de sécurité, la définition d'un descripteur de sécurité sur l'interface et l'utilisation d'indicateurs d'interface.
Définir un rappel de sécurité
Le rappel de sécurité est le premier mécanisme de sécurité d'interface. C'est un rappel personnalisé mis en œuvre par le développeur du serveur. La logique du rappel de sécurité obéit au développeur et permet au serveur de limiter l'accès à l'interface. Si le rappel renvoie RPC_S_OK, l'accès du client est autorisé.
Si un rappel de sécurité est enregistré, les clients non authentifiés seront automatiquement refusés par l'exécution RPC. Ils seront cependant acceptés si l'indicateur RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH est défini.
Définir un rappel de sécurité et un indicateur RPC_IF_ALLOW_SECURE_ONLY ne signifie pas que le client dispose de privilèges élevés. Le rappel de sécurité permet au serveur de se renseigner sur le niveau de privilège du client. Pour effectuer cette vérification, il appelle RpcBindingInqAuthClient. La vérification de la méthode de transport est tout aussi fréquente. Elle est utilisée par le client pour mettre en œuvre une connexion par le biais d'un transport authentifié sécurisé tel que les canaux nommés. Pour effectuer cette vérification, le serveur appelle RpcBindingServerFromClient → RpcBindingToStringBinding → RpcStringBindingParse avant de comparer le paramètre Protseq (séquence de protocole). Cette action prévient l'abus de multiplexage du point de terminaison.
Mettre en cache des rappels de sécurité
Si un rappel de sécurité est enregistré, il sera appelé pendant les contrôles de sécurité. Si la mise en cache est activée et que le rappel de sécurité réussit (et qu'il renvoie RPC_S_OK), le résultat sera mis en cache. Si le même client rappelle une fonction sur l'interface et que le résultat du rappel de sécurité est mis en cache, le rappel de sécurité ne sera pas invoqué et l'entrée mise en cache sera utilisée à la place.
L'exécution de la mise en cache n'a rien de compliqué, mais elle dépend de nombreux facteurs.
- La mise en cache est liée au contexte de sécurité du client qui est basé sur la liaison. Par conséquent, si le serveur (ou tout autre serveur du processus) n'enregistrait aucune liaison d'authentification, ou si le client n'avait pas défini de liaison d'authentification, la mise en cache serait désactivée pour cet appel.
- Si le serveur enregistre l'interface avec l'indicateur RPC_IF_SEC_NO_CACHE, l'exécution RPC force l'appel du rappel de sécurité pour chaque appel. Ainsi, elle désactive le mécanisme de mise en cache.
- L'indicateur d'interface RPC_IF_SEC_CACHE_PER_PROC non documenté affecte également le mécanisme de mise en cache. Si le serveur a spécifié cet indicateur, la mise en cache sera basée sur un appel et non sur une interface . Cela signifie que si le cache contient une valeur positive pour la fonction X de l'interface TEST, un appel à la fonction Y sur l'interface TEST déclenchera un nouveau rappel de sécurité.
Le mécanisme de mise en cache peut provoquer des vulnérabilités logiques pour tout rappel de sécurité dépendant de la fonction appelée par le client. Si vous êtes développeur RPC ou auditeur, vous devez impérativement maîtriser le mécanisme de mise en cache. Nous avons publié des recherches approfondies sur l'exécution de la mise en cache et sur les vulnérabilités que nous avons détectées grâce à ce mécanisme.
Définir un descripteur de sécurité
Le deuxième mécanisme de sécurisation d'une interface consiste à définir un descripteur de sécurité sur ladite interface. Seule la fonction RpcServerRegisterIf3 a la possibilité d'enregistrer un descripteur de sécurité dans l'interface. Si aucun descripteur de sécurité n'est défini, le descripteur de sécurité par défaut est le suivant :
- NT AUTHORITY\ANONYMOUS LOGON
- Tout le monde
- NT AUTHORITY\RESTRICTED
- BUILTIN\Administrators
- SELF
Utiliser les indicateurs d'interface
La troisième méthode utilisée pour sécuriser une interface implique des indicateurs d'interface définis lors de la création de l'interface via les fonctions RpcServerRegisterIf*. En ce qui concerne la sécurité, les indicateurs les plus intéressants sont les suivants :
RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH — Le rappel de sécurité sera appelé pour chaque invocation, quel que soit la méthode de transport ou le niveau d'authentification utilisés.
RPC_IF_ALLOW_LOCAL_ONLY — Seules les requêtes locales seront autorisées.
RPC_IF_ALLOW_SECURE_ONLY — Les connexions sont limitées aux clients dont le niveau d'autorisation est supérieur à RPC_C_AUTHN_LEVEL_NONE. La spécification de cet indicateur refuse l'accès aux clients qui se connectent à partir d'une session NULL. Cet indicateur ne garantit pas le niveau de privilège d'un utilisateur appelant. Il ne veille qu'à la validité des identifiants de connexion fournis par le client.
RPC_IF_SEC_NO_CACHE — Cet indicateur désactive complètement la mise en cache pour le rappel de sécurité de l'interface.
RPC_IF_SEC_CACHE_PER_PROC — Au lieu de désactiver l'ensemble du mécanisme de mise en cache, cet indicateur modifie le comportement par défaut pour qu'il soit basé sur un appel plutôt que sur une interface.
Règles à l'échelle du système
À l'échelle du système, plusieurs stratégies sont définies en fonction du type de machine : client, serveur ou contrôleur de domaine (DC).
La règle « Restreindre la règle des clients RPC non authentifiés » est une autre règle de système intéressante qui est, elle aussi, liée à la sécurité des points de terminaison. Trois valeurs peuvent être définies.
- « Authentifié » — L'exécution RPC bloque l'accès aux clients TCP qui ne se sont pas authentifiés à l'avance si l'indicateur RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH n'est pas défini sur l'interface.
- « Authentifié uniquement » — Toutes les connexions non authentifiées sont bloquées.
- "Pas de restrictions" — Tous les clients RPC sont autorisés à se connecter aux serveurs RPC exécutés sur la machine.
Les interfaces accessibles via TCP qui enregistrent l'indicateur RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH représentent une surface d'attaque intéressante pour les clients non authentifiés. Pour appeler les fonctions des interfaces, le client devra évidemment réussir l'étape de contrôle du rappel de sécurité.
Comme mentionné précédemment, les connexions via des canaux nommés transportés par SMB ont leur propre authentification dans le cadre du protocole SMB. Un client peut utiliser « l'identification anonyme » (aussi appelée session NULL) lorsqu'il se connecte via SMB. Les règles de système « Restreindre l'accès anonyme aux canaux et partages nommés » et « Accès réseau : canaux nommés accessibles de manière anonyme » sont liées. Si la première règle est activée, seuls les canaux nommés définis avec la seconde option peuvent être accessibles de manière anonyme. Pour les postes de travail, la deuxième règle est vide. Cela signifie donc que vous ne pouvez pas utiliser une session NULL pour les postes de travail du domaine. Pour une machine DC, la liste des canaux nommés dans la règle inclut « \pipe\netlogon », « \pipe\samr » et « \pipe\lsarpc ». Pour les cybercriminels, cette liste est intéressante puisqu'elle contient les points de terminaison auxquels une machine qui n'est pas dans le domaine peut se connecter.
En ce qui concerne le contrôle du descripteur de sécurité du point de terminaison, il existe une règle à l'échelle du système qu'il suffit d'activer : « Accès réseau : permettre aux autorisations « Tout le monde » d'être appliquées aux utilisateurs anonymes ». Lorsque cette option est activée, l'identificateur de sécurité « Tout le monde » est ajouté au jeton créé pour les connexions anonymes.
Vérifications de procédure de la sécurité
Le programmeur de serveur peut mettre en place des contrôles de sécurité dans le cadre des fonctions exposées sur l'interface. Il a, par conséquent, la possibilité de modifier ou de personnaliser la logique du contrôle selon la fonction appelée. Comme on peut le voir dans la Figure 6 (extraite du srvsvc), le contrôle d'accès est l'un des contrôles de sécurité fréquemment effectués.
Figure 6 : Le contrôle d'accès, un exemple de contrôle de sécurité fréquent
Dans cet exemple, LocalrSessionGetInfo appelle SsAccessCheck. SsAccessCheck usurpe l'identité du client en appelant RpcImpersonateClient, avant d'appeler la fonction NtAccessCheckAndAuditAlarmqui se charge du contrôle d'accès. Ce contrôle est suivi d'un appel du RpcRevertToSelf pour revenir au jeton du serveur. Il est important de vérifier la valeur renvoyée de la fonction RpcImpersonateClient, car en cas d'échec, le serveur continue à s'exécuter dans le jeton de sécurité du processus du serveur, et non pas dans le processus du client.
Synthèse
Cet article de blog était riche en informations portant sur le MS-RPC et sur ses mécanismes de sécurité. Nous encourageons d'autres chercheurs à examiner différents serveurs MS-RPC, car ces derniers sont exposés à une surface d'attaque importante avec de nouvelles vulnérabilités potentielles. Nous espérons que cet article aidera d'autres chercheurs à faire leurs premiers pas dans la recherche MS-RPC.
Nous continuons à créer et à recueillir toujours plus de ressources sur le RPC pour alimenter notre référentiel GitHub. Nous remercions toutes les personnes qui ont apporté leur pierre à l'édifice en nous partageant leurs connaissances et leurs expériences. Nous pensons notamment à James Forshaw et Carsten Sandker.