Produit

Comment et pourquoi nous avons adopté Rust pour développer notre EDR

Comment nous sommes passés de Python à Rust, et pourquoi ? Au-delà de l’optimisation de la consommation de RAM et de CPU, en quoi ce langage de développement permet de réduire le risque d’erreurs et améliore la sécurité ?
10 min

Rust : définition 

Rust est un langage de programmation né en 2006 à l’initiative de Graydon Hoare, soutenu par Mozilla depuis 2009. 
Il est adopté par une large communauté de développeurs, et par de nombreuses entreprises telles qu’Amazon, Google, Dropbox, Meta, Discord ou encore Microsoft... et HarfangLab, donc. 

C’est un langage bas niveau proche de C et C++, compilé, et limitant la charge sur le processeur. Son créateur décrit Rust comme une réponse aux frustrations que pouvaient connaître les développeurs avec C++, et il est axé sur l’amélioration des performances, la sécurité et la capacité à exécuter différentes tâches en parallèle. 

Alors, en pratique, comment sommes-nous passés d’un agent développé en Python à Rust ? Avant de plonger dans les détails de nos péripéties, et ce que nous et nos utilisateurs avons gagné au change, revenons rapidement sur le fonctionnement de notre solution. 

HarfangLab EDR : fonctionnement des agents et prérequis techniques

HarfangLab repose sur deux composantes :  

  • Les agents, soit une brique logicielle déployée sur un parc informatique ; 
  • Le backend, empilement de couches logicielles dans lequel on retrouve un puit de données pour stocker des informations, des briques pour piloter les agents, des briques de détection, et une API pour visualiser les données. 

Le rôle de l’agent est de collecter un très grand nombre d’informations. En effet, il voit tout ce qui se passe sur un endpoint : création de processus, connexions réseau, accès à des fichiers... et il doit faire de la détection sur ces événements pour savoir s’il s’agit d’une activité légitime ou malveillante, et répondre en cas de besoin. 

Pour ce faire, à partir de la collecte d’informations, l’agent décide s’il laisse un programme s’exécuter, ou s’il le bloque voire le termine, s’il met éventuellement l’exécutable en quarantaine... et il remonte ce qu’il a détecté au backend.  

EDR - Endpoint Detection and Response - Engines and Rules


Ce procédé intervient pour chaque événement, ce qui soulève forcément la question de la performance des endpoints. En effet, si l’agent met trop de temps à répondre, le programme est bloqué pour l’utilisateur final, et le système se retrouve fortement ralenti. 

Ainsi, en tant qu’éditeur de solution de cybersécurité, notre objectif est triple : 

  • Être léger, en offrant un temps de réponse inférieur à 50ms, ce qui implique d’avoir toute la logique de détection sur la machine pour qu’il n’y ait pas de latence ;  
  • Être invisible, c’est-à-dire ne pas interférer avec le fonctionnement de la machine pour préserver l’expérience utilisateur ; 
  • Pouvoir absorber des pics de charge, parfois jusqu’à 500k événements par minute. 

Performance et sécurité au centre des priorités 

Au démarrage projet en 2018, nous avions une contrainte de temps pour lancer un produit viable, avec des effectifs limités - rien de très original pour une start-up. 

Nous avons donc fait le choix de démarrer en Python, pour les agents comme pour le backend, en sachant que nous devrions porter notre travail sur une autre technologie à moyen terme. 

Au cours des deux premières années d’existence de notre EDR, l’équipe technique a déployé différentes versions, assuré des évolutions en réponse aux demandes des clients... et cet empilement de fonctionnalités dans l’agent a fini par atteindre une limite en termes de performance, surtout sur les machines peu puissantes ou anciennes.  

En plus des performances liées à Python, nous étions face à d’autres limites : le déploiement de nouvelles fonctionnalités était complexe en raison des limites des outils de refactoring et de vérification de code de Python. Par ailleurs, la maintenance était également pénible, notamment en raison du support peu développé pour des OS obsolètes. 

Enfin, il faut savoir que l’EDR doit s’exécuter en tant que service (c’est-à-dire que sur Windows, par exemple, un fichier .exe doit "discuter” avec le service manager de Windows). En outre, avec Python, le manque de contrôle sur le binaire final ne permet pas de faire du hardening pour se prémunir contre certaines attaques - sujet que nous allons détailler un peu plus tard. 

Arrivés à cette étape de la vie de notre solution, nous avons comparé les différentes options qui se présentaient en fonction de nos prérequis, à savoir :  

  • La performance,
  • La sécurité,
  • La compatibilité avec tous les OS, mêmes obsolètes,
  • Le fait que le langage soit bien implanté pour faciliter le support. 

Le premier critère étant la performance, seuls quelques langages compilés semblaient pouvoir répondre à nos besoins : C/C++, Go et Rust. 

Comment faire un choix ? Allons plus loin dans les prérequis : nous avions besoin d’écrire du code haut niveau et d’interagir avec des primitives système bas niveau via des API système, souvent développées en C, réduisant le nombre de langages pertinents. 

Malgré le fait que de nombreux logiciels et systèmes d’exploitation soient développés en C/C++, le très grand nombre de vulnérabilités de type corruption mémoire (buffer overflow, use after free, ...) exploitées depuis des années nous ont fait passer notre chemin. 

Il faut savoir par exemple qu’en 2019, Microsoft rapportait déjà que 70% des vulnérabilités pour lesquelles ils déclaraient une CVE étaient dues à des corruptions mémoire et préconisaient Rust pour écrire du code sûr 

Finalement, la balance a oscillé entre Go et Rust, et nous avons choisi Rust pour les raisons suivantes : 

  • Meilleures performances (pas de Garbage Collector par exemple), 
  • Meilleure sécurité (garanties offertes par le compilateur que le code est sûr, disparition de certaines classes de vulnérabilités liées à la corruption mémoire...), 
  • Ecosystème sécurité plus étoffé, 
  • Meilleure dynamique de la communauté pour le développement et les évolutions futures du langage. 

Pour résumer, voici les critères que nous avons évalués : 

  • C/C++ : Performance +++ / Sécurité - / Compatibilité ++ / Implantation +++
  • Go : Performance ++ / Sécurité ++ / Compatibilité + / Implantation ++
  • Rust : Performance +++ / Sécurité +++ / Compatibilité ++ / Implantation ++

Bien que le développement en Rust soit décrit comme plus difficile qu’en C ou Go (la marche initiale est plus haute), ce que le langage nous a apporté par la suite nous a confortés dans ce choix. 

Python vs. Rust : le match 

Nous avions besoin de dépasser les limites que nous avons vues, mais aussi de disposer d’un langage à la fois Memory safe et Thread safe, avec un accès bas niveau aux fonctionnalités du système d’exploitation.  
Si vous connaissez Python, vous allez peut-être vous dire que ce langage répond à ces critères...  

Python vs. Rust comparison


Mais nous en avions d’autres à remplir ! 
 

Notamment, Python ne permet pas de faire de vrai multithreading (en raison du GIL), et ne permet pas non plus de contrôler les allocations mémoire - entre autres à cause du Garbage Collector - ce qui conduit à une utilisation plus importante de la RAM. 
 
Rust est beaucoup plus flexible : il permet de choisir les allocations mémoire et les structures des données appropriées, il propose un Type System puissant pour faciliter la maintenance et vérifier que les refactoring sont corrects. 
 
Dans le registre de la sécurité, le hardening est aussi une priorité car nous avons besoin de protéger l’agent contre des menace peu communes telles que le DLL hijacking, et un EDR doit en effet se prémunir contre un attaquant qui serait parvenu à augmenter ses privilèges en vue de s’octroyer des droits administrateur d’un système. 
Pour répondre à ce besoin, Rust offre une sécurité plus poussée grâce à sa gestion par Static linking (alors que Python force à avoir des dépendances externes), tout en laissant la porte ouverte au Dynamic linking, ainsi que la possibilité de différer le chargement des dépendances indirectes (par exemple via le mécanisme de delay loading de Windows). 

Python and Rust pros and cons

Enfin, au-delà de l‘optimisation du développement, nous pensions évidemment à nos utilisateurs, avec l’ambition de mener ces différents projets en vue de leur offrir la meilleure expérience possible :  

  • Amélioration des performances, quelle que soit la puissance des machines, 
  • Maintien de la rétrocompatibilité, notamment pour nos clients dépendants du format de données de l’API,  
  • Renforcement de la sécurité (hardening),  
  • Intégration du support de nouveaux OS (macOS, Linux), 
  • Révision du design de la plateforme. 

A la lumière de notre contexte et de nos objectifs, Rust était un choix évident. 
 

Python vs. Rust : résultats et bilan chiffré 

Sans conteste, il y a un avant et un après Rust : nous avons pu enrichir nos fonctionnalités, affiner la gestion des différents OS, et disposer d’une documentation bien plus complète. 

Nous avons commencé avec l’agent et le backend en Python, ce qui permettait beaucoup de partage de code. 

Dorénavant, le backend est toujours en Python, et l’agent en Rust. Comment assurons-nous la communication entre les deux ? 
Nous utilisons du gRPC pour partager l’interface de communication et abstraire les problèmes de protocole, et des bindings Python pour utiliser le code Rust et améliorer les performances également côté backend. 

En ce qui concerne l’ajout de règles de détection Sigma, par exemple, elles sont maintenant validées directement dans le backend via le même moteur Sigma que l’agent. Pour ce faire, PyO3 permet de partager le même code de validation entre le backend et l’agent, et de gagner encore en performance par rapport à un validateur écrit en Python. 

Et en pratique, quels sont les résultats observés depuis que l’agent est développé en Rust ? 

D’une part, notre agent est beaucoup plus stable, et nous intégrons bien plus facilement de nouvelles fonctionnalités grâce au système de refactoring et au Type System de Rust, qui permettent de vérifier les erreurs avant de déployer le code. 
Concrètement, la première version d'agent pour un nouveau système d’exploitation nous a demandé environ 3 mois de développement, facilité par une base de fonctionnalités que nous avons pu mutualiser pour tous les OS ! 

Enfin, la consommation de ressources est fortement diminuée, avec 3 fois moins de RAM et 3 fois moins de CPU utilisés lors du passage de Python à Rust, à fonctionnalités équivalentes.   

A partir de cette évolution, nous avons pu, de manière sereine, ajouter de nombreuses fonctionnalités supplémentaires à notre agent sans provoquer de surconsommation sur la machine (moteurs d’IA, EPP, moteurs de corrélation...). 

Ils témoignent

“Nous avons été les premiers à manifester notre intérêt pour Rust, langage de développement adopté par l'EDR d'HarfangLab, et à en mesurer les gains significatifs sur les performances de nos endpoints." 
Responsable Cybersécurité - Grand groupe industriel 

Vous voulez en savoir plus sur tous les autres avantages de notre EDR ?