DCE/RPC (Distributed Computing Environment / Remote Procedure Call) est un protocole qu’on retrouve beaucoup dans les entreprises. Et pour cause : celui-ci est au cœur du fonctionnement d’Active Directory et des environnements Microsoft. Les extensions ajoutées par Microsoft forment MSRPC.

Fonctionnement de DCE/RPC

À noter qu’on retrouve d’autres systèmes d’appels de procédures à distance connus, tels que gRPC (implémentation Google) - qui repose sur une pile moderne (HTTP/2 + Protobuf).

Le “livret de famille” de MSRPC (beaucoup d’oncles ont été omis)

Le RPC définit le principe d’appeler une fonction distante telle que ServezMoiUneBièreSvp() comme si elle était sur l’environnement dans lequel elle s’exécute, alors que celle-ci peut être prise en charge par un système distant. RPC abstrait le réseau pour le client afin qu’il puisse commander sa bière depuis chez lui, sans même se soucier de comment son ordre est transporté. De la même façon pour le serveur, dont les procédures sont appelées sans qu’aucune notion de réseau ne soit implémentée.

Quelques concepts pour bien comprendre de quoi on parle :

  • Procédure : une fonction qu’on veut appeler dans un autre environnement (la plupart du temps sur une machine distante).
  • Interface : un ensemble de procédures qui vont ensemble (ex. : interface barman). Une interface se reconnaît par son UUID, qui est un identifiant unique.
  • Syntaxe de transfert : manière de sérialiser les paramètres, c’est-à-dire de les transformer dans une représentation commune pour les deux parties de la connexion réseau. Le plus souvent, nous retrouverons NDR ou NDR64.

DCE/RPC et son abstraction du réseau

À bien noter que DCE/RPC ne définit pas qu’une seule manière de faire les appels de procédures à distance : plusieurs choix sont possibles, tels que :

  • NCACN_IP_TCP : RPC sur TCP
  • NCADG_IP_UDP : RPC sur UDP
  • NCACN_NP : RPC sur named pipes (souvent via SMB côté Windows)

Le transport est choisi via le binding (par exemple ncacn_ip_tcp ou ncacn_np). Ensuite, lors de l’établissement de la connexion, une phase de négociation (au moment du BIND) permet notamment de s’accorder sur la transfer syntax (NDR/NDR64) et l’authentification si nécessaire.

Exemples d’utilisation concrète

Concrètement, dans un environnement Windows, les machines jointes à un domaine et les utilisateurs authentifiés vont pouvoir appeler tout un ensemble de procédures utiles, telles que :

  • Netlogon RPC : une interface qui fournit des procédures pour permettre aux utilisateurs et aux machines du domaine de s’identifier auprès du contrôleur de domaine.
  • LSA RPC : permet de gérer et de récupérer les politiques sur la gestion des divers objets du contrôleur de domaine.
  • SCM : Service Control Manager, utile pour gérer des processus à distance sur une autre machine (particulièrement utile pour un attaquant également, pour se latéraliser sur une autre machine).

Enfin, on peut citer une interface particulièrement importante : l’Endpoint Mapper.

Endpoint Mapper

L’Endpoint Mapper est le registre qui va nous permettre de retrouver toutes les interfaces enregistrées sur une machine. Sous Windows, celui-ci sera toujours en écoute sur le port 135/TCP pour RPC/TCP (et il peut aussi être accessible via \pipe\epmapper en ncacn_np). En lui demandant gentiment, celui-ci nous listera toutes les interfaces disponibles, et aussi comment y accéder.

Flow classique DCE/RPC

Schématiquement, voici comment se déroule la résolution d’un appel DCE/RPC (notamment sur RPC/TCP) :

Flow classique DCE/RPC : résolution puis appel de l’interface

Ici, le client résout auprès de l’Endpoint Mapper (EPM) l’interface qu’il souhaite appeler, et une fois trouvée, il appelle la procédure qu’il souhaitait.

Implémentation d’une backdoor

Maintenant, l’idée est la suivante : une fois un attaquant passé sur un système, pourrait-il enregistrer une nouvelle interface pour lui permettre de revenir plus tard ? De plus, le flux DCE/RPC se noyant dans la masse, celui-ci pourrait passer incognito.

On pourrait imaginer un scénario ainsi :

Scénario : ajout d’une interface RPC malveillante sur DC01

L’attaquant ayant réussi à compromettre le contrôleur de domaine DC01 depuis la machine user01, il pourra vouloir laisser un accès distant pour pouvoir y revenir avec peu d’efforts.

Le code de la backdoor est disponible ici : https://github.com/theophane-droid/blog_content/tree/main/rpc_backdoor/. Une fois le programme du serveur démarré, on pourra appeler sans aucune authentification toutes les commandes souhaitées sur le serveur distant.

Les prérequis sont d’avoir un accès administrateur sur la machine que l’on souhaite backdoorer (ici : le contrôleur de domaine) et de modifier les règles de pare-feu local de la machine pour autoriser les connexions.

Démo

Une fois la backdoor démarrée sur le serveur, on peut s’y connecter depuis notre client à distance :

Démo : client RPC se connectant à la backdoor

Et ainsi, on peut exécuter n’importe quelle commande sur notre serveur distant sans authentification aucune. Le plus redoutable est qu’au niveau réseau cela semble relativement anodin, étant donné que le trafic ressemble à un échange RPC classique au premier abord. Sur RPC/TCP, on retrouve typiquement le pattern 135/TCP → port dynamique élevé (résolu via l’EPM), ce qui peut malgré tout servir de signal lors d’une analyse.

Détection de la backdoor côté réseau

On va maintenant voir comment on pourrait détecter la backdoor d’un point de vue réseau.

On pourrait commencer par chercher la backdoor grâce à des interfaces UUID inhabituelles. Vous pouvez trouver le PCAP qui concerne l’exploitation ici : https://github.com/theophane-droid/blog_content/raw/refs/heads/main/rpc_backdoor/backdoor_exploitation.pcap

Dans la requête map ci-dessous :

Wireshark : requête EPM map mettant en évidence l’UUID

On retrouve bien l’UUID de notre backdoor, qu’on a défini plus tôt. Du point de vue d’un défenseur, cela peut être étonnant de trouver des UUID non standards, même si beaucoup d’applications type EDR ou monitoring pourraient définir leurs propres interfaces.

Ce qui peut être le plus étonnant, c’est l’absence d’authentification. Si l’on compare avec des appels DCE/RPC légitimes : en utilisant compmgmt.msc, par exemple, on peut accéder à une interface de gestion pour un ordinateur distant. Cette application Microsoft utilise RPC pour cela :

compmgmt.msc : gestion à distance via RPC

Vous pourrez trouver ici les échanges RPC légitimes correspondants : https://github.com/theophane-droid/blog_content/raw/refs/heads/main/rpc_backdoor/standard_rpc_call.pcap

En comparant les deux, et notamment la phase de BIND des interfaces cibles, on peut remarquer quelque chose d’intéressant : aucune authentification n’est proposée lors de l’appel de la backdoor, ce qui est assez suspect.

BIND de la backdoor : pas d’authentification (Auth Length = 0)

BIND d’une interface légitime : authentification (ex. SPNEGO)

Ainsi, en combinant le signal faible d’un UUID inconnu avec l’absence totale d’authentification, ces appels pourraient mettre la puce à l’oreille au défenseur (même si un UUID “inconnu” seul reste un indicateur faible).

Détection côté système

Du côté du système sur lequel la backdoor a été déployée, avec Sysmon, on peut observer la création du processus cmd.exe avec les arguments de l’attaquant pour chaque commande qu’il passe :

Sysmon : création de cmd.exe avec la commande de l’attaquant

Pour le coup, c’est extrêmement bruyant, d’autant plus que le processus parent est un exécutable d’origine inconnue.

Implémentation de la backdoor

Maintenant, voyons comment ça se passe côté dev. Dans Visual Studio, on va créer trois projets console ainsi :

Visual Studio : création des projets console

Pour une plus grande facilité, voir comment déployer son lab avec Ludus ;)

Ces trois projets pourront porter les noms :

  • rpc_interface_def
  • rpc_client
  • rpc_server

Définition de l’interface

Dans un nouveau projet Visual Studio, on pourrait définir ainsi Interface.idl :

[
    uuid(cd99b06b-0c7b-422a-80da-1cb5758adbec),
    version(1.0)
]
interface MyInterface
{
    // Lance la commande de manière asynchrone et renvoie son ID
    int LaunchCommand(
        [in] handle_t hBinding,
        [in, string] char* command
    );

    // Renvoie l’output d’une commande par son ID
    int GetCommandOutput(
        [in] handle_t hBinding,
        [in] int command_id,
        [out, string] char** output,
        [out] int* is_finished
    );

    // Arrête une commande en cours
    int StopCommand(
        [in] handle_t hBinding,
        [in] int command_id
    );
}

IDL étant le langage descriptif d’interfaces, il permet de définir toutes ses procédures et leurs arguments. On pourra compiler cette interface avec midl, le compilateur Microsoft pour IDL :

midl Interface.idl /env x64

De cette compilation, vous obtiendrez :

  • Interface.h : le header de définition de notre interface
  • Interface_c.c : le stub client
  • Interface_s.c : le stub serveur

Implémentation du serveur

Le serveur va s’écrire en deux parties :

  • server_impl.c : implémente les procédures de notre interface
  • rpc_server.c : enregistre notre interface auprès de l’Endpoint Mapper

Synthétiquement, voici les appels systèmes réalisés pour cela dans rpc_server.c :

  • InitializeSecurityDescriptor : initialise un descripteur de sécurité Windows utilisé pour contrôler l’accès aux objets RPC
  • SetSecurityDescriptorDacl : définit la liste de contrôle d’accès (DACL), ici ouverte à tous avec une NULL DACL
  • RpcServerUseProtseq : enregistre une séquence de protocole RPC (ncacn_ip_tcp) avec allocation dynamique du port par l’OS
  • RpcServerRegisterIf2 : enregistre l’interface RPC (UUID, version, stubs) et la rend appelable par des clients
  • RpcServerInqBindings : récupère les bindings effectifs du serveur RPC (transport et endpoint attribué)
  • RpcEpRegister : publie l’interface et ses bindings dans l’Endpoint Mapper pour découverte via TCP/135
  • RpcServerListen : démarre l’écoute RPC et accepte les BIND et REQUEST entrants
  • RpcEpUnregister : retire l’interface de l’Endpoint Mapper lors de l’arrêt du serveur

Côté server_impl.c, on utilise des pipes anonymes pour écrire et lire la sortie des commandes. On utilise CreateProcessA pour lancer cmd.exe /c avec, comme argument, la commande de l’utilisateur. Ainsi, on peut lire les sorties de manière asynchrone.

Implémentation du client

Côté client, toute la logique est dans rpc_client.c. On peut décomposer le programme ainsi :

  • RpcStringBindingCompose : construit une chaîne de binding RPC à partir du transport, de l’adresse et éventuellement du port.
  • RpcBindingFromStringBinding : crée un handle RPC exploitable à partir de la chaîne de binding.
  • RpcStringFree : libère la mémoire associée à une chaîne de binding RPC.
  • RpcEpResolveBinding : interroge l’Endpoint Mapper pour résoudre dynamiquement l’endpoint de l’interface RPC.
  • RpcBindingSetAuthInfo : configure (ou désactive ici) l’authentification et le niveau de sécurité du binding RPC.
  • LaunchCommand : invoque à distance la procédure RPC pour exécuter une commande sur le serveur.
  • GetCommandOutput : invoque à distance la procédure RPC pour récupérer la sortie d’une commande exécutée.
  • StopCommand : invoque à distance la procédure RPC pour arrêter une commande en cours.

Ce qui est particulièrement intéressant est ici :

current_cmd_id = LaunchCommand(hBinding, command);

Simplement avec notre handle de binding, initialisé précédemment, on peut appeler notre commande distante sur le serveur sans se soucier du réseau : c’est la magie de RPC !

Mot de la fin

Dans la petite démonstration présentée ici, il y a plusieurs limites :

Tout d’abord, on aurait pu ajouter des techniques plus avancées pour cacher l’exécution de nos commandes, ou bien montrer comment l’attaquant établirait la persistance de sa backdoor à chaque redémarrage de la machine victime. Enfin, une approche encore plus discrète serait de détourner des processus légitimes en exploitant leurs interfaces déjà enregistrées pour les backdoorer.

J’espère que cet article vous a plu, et à bientôt ! 🙂

Références