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 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) :

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 :

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 :

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 :

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 :

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.


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 :

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 :

Pour une plus grande facilité, voir comment déployer son lab avec Ludus ;)
Ces trois projets pourront porter les noms :
rpc_interface_defrpc_clientrpc_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 interfaceInterface_c.c: le stub clientInterface_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 interfacerpc_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 RPCSetSecurityDescriptorDacl: définit la liste de contrôle d’accès (DACL), ici ouverte à tous avec une NULL DACLRpcServerUseProtseq: enregistre une séquence de protocole RPC (ncacn_ip_tcp) avec allocation dynamique du port par l’OSRpcServerRegisterIf2: enregistre l’interface RPC (UUID, version, stubs) et la rend appelable par des clientsRpcServerInqBindings: 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/135RpcServerListen: démarre l’écoute RPC et accepte les BIND et REQUEST entrantsRpcEpUnregister: 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 ! 🙂