Trois vulnérabilités d'exécution de code à distance dans l'exécution RPC
Synthèse
Le chercheur Ben Barnea d'Akamai a découvert trois vulnérabilités importantes dans l'exécution RPC de Microsoft Windows auxquelles ont été attribués les codes (CVE-2023-24869), (CVE-2023-24908)et (CVE-2023-23405), tous avec un score de base de 8,1.
Ces vulnérabilités peuvent entraîner l'exécution de code à distance. Étant donné que la bibliothèque d'exécution RPC est chargée dans tous les serveurs RPC et qu'ils sont couramment utilisés par les services Windows, toutes les versions de Windows (bureau ou serveur) sont affectées.
Les vulnérabilités sont des dépassements d'entiers dans trois structures de données utilisées par l'exécution RPC.
Ces vulnérabilités ont été révélées de manière responsable à Microsoft et corrigées dans le Patch Tuesday de mars 2023.
Introduction
MS-RPC est un protocole très utilisé dans les réseaux Windows, sur lequel s'appuient bon nombre de services et d'applications. En tant que telles, les vulnérabilités dans MS-RPC peuvent avoir de graves conséquences. L'année dernière, le groupe Security Intelligence d'Akamai a entrepris des recherches sur MS-RPC. Nous avons trouvé et exploité des vulnérabilités, élaboré des outils de recherche et rédigé certains des éléments internes non documentés du protocole.
Alors que les précédents articles de blog portaient sur les vulnérabilités dans les services, cet article examinera les vulnérabilités dans l'exécution RPC, le "moteur" de MS-RPC. Ces vulnérabilités sont similaires à une vulnérabilité découverte en mai 2022.
Modèle de dépassement d'entier
Les trois nouvelles vulnérabilités ont un point commun, elles sont toutes dues à un dépassement d'entier dans l'insertion de trois structures de données :
SIMPLE_DICT (un dictionnaire qui enregistre uniquement les valeurs)
SIMPLE_DICT2 (un dictionnaire qui enregistre à la fois les clés et les valeurs)
File d'attente
Toutes ces structures de données sont implémentées à l'aide d'un tableau dynamique qui s'accroit chaque fois que le tableau est plein. Cela se produit en allouant deux fois la mémoire allouée au tableau actuel. Cette allocation est susceptible d'entraîner un dépassement d'entier.
La figure 1 présente le code décompilé de l'exécution RPC. Elle montre le processus d'insertion dans la structure SIMPLE_DICT et la ligne de code vulnérable (mise en évidence) où le dépassement d'entier peut être déclenché.
Analyse d'une vulnérabilité
Pour déclencher une vulnérabilité, nous devons comprendre sa cause sous-jacente, déterminer si un flux vers la fonction vulnérable existe et évaluer le temps qu'il faut pour qu'elle se déclenche.
Pour plus de concision, nous ne décrirons qu'une seule des trois vulnérabilités : celle de qui se trouve dans la structure de données de la file d'attente. Les autres dépassements d'entier étant de nature similaire, l'analyse effectuée dans les sections suivantes peut être menée de manière interchangeable.
Compréhension du dépassement d'entier
Une file d'attente est une structure de données FIFO (premier arrivé, premier sorti) simple. Une file d'attente dans l'exécution RPC est implémentée à l'aide d'une structure qui contient un tableau d'entrées de file d'attente, la capacité actuelle et la position du dernier élément de la file d'attente.
Lorsqu'une nouvelle entrée est ajoutée à la file d'attente (dans la mesure où un emplacement est disponible), tous les éléments sont avancés dans le tableau et le nouvel élément est ajouté au début du tableau. La position du dernier élément de la file d'attente est alors incrémentée.
Lorsqu'une entrée est enlevée de la file d'attente, le dernier élément est retiré et la position du dernier élément est décrémentée (Figure 2).
Comme nous l'avons mentionné précédemment, la vulnérabilité survient lors de l'insertion d'une nouvelle entrée. Si le tableau dynamique est plein, le code effectue les opérations suivantes :
Il alloue un nouveau tableau de la taille suivante :
CapacitéActuelle * 2 * taillede(EntréeFileAttente)Il copie les anciens éléments dans le nouveau tableau
Il libère le tableau des anciens éléments
Il double la capacité
Pour un système 32 bits, le dépassement se produit lors du calcul de la nouvelle taille du tableau :
Nous remplissons la file d'attente avec 0x10000000 (!) éléments.
Une extension se produit. La taille de la nouvelle allocation est calculée : 0x10000000 * 16. En cas de dépassement, la nouvelle taille d'allocation est égale à 0.
Un tableau de longueur nulle est alloué.
Le code copie les éléments de l'ancien tableau dans le nouveau petit tableau. Cela entraîne la création d'une copie générique (une grande copie linéaire).
Dans un système 64 bits, cette vulnérabilité n'est pas exploitable en raison de l'échec d'une importante allocation. Cela permet au code de sortir normalement sans déclencher d'écritures hors limites. Bien que les systèmes 64 bits ne soient pas vulnérables à ce problème, ils le sont aux autres dépassements d'entiers (dans SIMPLE_DICT et SIMPLE_DICT2).
Flux de code
Une connexion RPC est représentée à l'aide de la classe OSF_SCONNECTION. Chaque connexion peut traiter plusieurs appels client (OSF_SCALL), mais à chaque fois, un seul appel est autorisé à s'exécuter sur la connexion, alors que les autres sont en file d'attente.
Ainsi, la fonction OSF_SCONNECTION::MaybeQueueThisCall qui utilise une file d'attente est intéressante. Elle est appelée dans le cadre de la distribution d'un nouvel appel qui est arrivé sur la connexion. Dans ce cas, la file d'attente est utilisée pour "mettre en attente" les appels entrants pendant le traitement d'un autre appel.
Nous disposons ainsi d'un moyen de mise en file d'attente contrôlé par l'utilisateur (en envoyant les appels client les uns après les autres), mais cette fonction impose une exigence : un appel est en cours de traitement par la connexion. Cela signifie que si nous voulons mettre un appel en file d'attente, nous devons en avoir un qui dure un certain temps. Pendant le traitement de l'appel, nous enverrons les nouveaux appels qui rempliront la file d'attente de répartition.
Quel type d'appel de fonction prend le plus de temps ?
Le premier type d'appel concerne la fonction qui nous permet d'entraîner une boucle infinie.
Le deuxième concerne une vulnérabilité de coercition à l'authentification car le serveur se connecte alors à nous, ce qui nous permet d'avoir le contrôle sur le temps de réponse.
Un dernier recours serait une fonction complexe avec une logique compliquée ou une fonction qui traite un gros volume de données et prend donc beaucoup de temps à s'exécuter.
Nous avons décidé d'utiliser notre propre vulnérabilité de coercition à l'authentification.
Le temps nécessaire au déclenchement
Jusqu'à présent, nous avons compris ce qu'il faut faire pour remplir la file d'attente et comment nous pouvons le faire. Mais une question importante se pose, est-ce réalisable ?
Nous contrôlons un minimum la variable qui entraîne le dépassement d'entier, nous ne pouvons l'incrémenter qu'un par un, comme les dépassements de refcount (nombre de référence). Ce type de dépassement d'entier est légèrement plus grave que les dépassements d'entier où deux variables que nous contrôlons entièrement sont ajoutées ou multipliées, ou lorsque la taille ajoutée peut être quelque peu contrôlée (par exemple, la taille de paquet).
Comme nous l'avons mentionné précédemment, nous devons allouer 0x10000000 (~268M) éléments. Cela fait beaucoup.
La tentative de déclenchement de la vulnérabilité sur mon ordinateur a donné un taux d'environ 15 à 20 appels mis en file d'attente par seconde. Cela signifie qu'il faudrait environ 155 jours pour le déclencher sur une machine moyenne ! Nous nous attendions à ce que le nombre d'appels mis en file d'attente par seconde soit plus élevé. Existe-t-il une raison pour que l'exécution RPC soit si lente ? N'est-elle pas multithread ?
Nous avons supposé que plusieurs threads traitaient et mettaient en file d'attente simultanément différents appels pour la même connexion. Après avoir effectué quelques inversions de code, nous avons constaté que dans la pratique, le flux est un peu différent.
Gestion des paquets MS-RPC
Juste avant la distribution d'un appel, le code lance un nouveau thread (si nécessaire) et appelle la fonction OSF_SCONNECTION::TransAsyncReceive. TransAsyncReceive tente de recevoir une requête sur la même connexion. Elle soumet ensuite la demande au nouveau thread (en appelant la fonction CO_SubmitRead).
L'autre thread reprend la requête de TppWorkerThread et aboutit finalement à ProcessReceiveComplete, qui appelle la fonction MaybeQueueThisCall pour mettre l'appel SCALL dans la file d'attente de distribution. Ensuite, il se propage et tente de recevoir une nouvelle requête pour cette connexion.
Par conséquent, bien que nous ayons plusieurs threads en cours d'exécution, en pratique, un seul est utilisé pour la connexion. Cela signifie que nous ne pouvons pas ajouter simultanément d'appels à la file d'attente à partir de plusieurs threads.
Paquet « restant »
Nous avons essayé de trouver des moyens d'effectuer plus d'appels par seconde pour réduire le temps nécessaire au déclenchement de la vulnérabilité. Lors de l'inversion du code de réception, nous avons remarqué que si la longueur d'un paquet est supérieure à la requête RPC réelle contenue dans le paquet, l'exécution RPC enregistre ce qui reste. Plus tard, lorsqu'il recherche de nouvelles requêtes, il n'utilise pas immédiatement le socket. Il vérifie d'abord s'il dispose d'un paquet « restant » et, si tel est le cas, il répond à la nouvelle requête à partir du restant.
Cela nous a permis d'envoyer beaucoup moins de paquets, avec un nombre maximum de requêtes dans chacun d'entre eux. Le nombre d'appels mis en file d'attente par seconde est resté relativement inchangé lorsque nous avons fait cela, donc ce ne fut pas d'une grande aide.
Résumé
Malgré la faible probabilité d'exploitation de ces vulnérabilités, nous les avons ajoutées à la liste des vulnérabilités importantes que nous avons identifiées au cours de notre dernière année de recherche sur MS-RPC. Il convient de rappeler que même les vulnérabilités difficiles à exploiter constituent une opportunité pour un attaquant compétent (et patient).
Bien que le MS-RPC existe depuis plusieurs décennies, il reste encore des vulnérabilités à découvrir.
Nous espérons que cette recherche encouragera d'autres chercheurs à s'intéresser à MS-RPC et à la surface d'attaque qu'il présente. Nous tenons à remercier Microsoft d'avoir répondu rapidement et d'avoir résolu les problèmes.
Notre référentiel GitHub contient un grand nombre d'outils et de techniques qui vous aideront à démarrer.