L'obfuscation en .net

L'obfuscation en .net

Introduction

L'obfuscation vous permet de protéger votre code en le rendant totalement incompréhensible pour une personne ou un décompilateur.

Après avoir expliqué à quelle problématique répond l'obfuscation, nous détaillerons les techniques standards qu'utilise un obfuscateur afin de protéger votre code. Puis, nous verrons un exemple concret avec l'obfuscateur .Net DotFuscator.

Qu'est-ce que l'obfuscation ?

L'obfuscation est une technique de masquage qui permet l'attribution transparente de nouveaux noms pour les symboles de vos assemblys dans le but de rendre incompréhensible (d'un point de vue sémantique) le code qui pourrait être récupéré à l'aide d'un décompilateur.

Lorsqu'elle est appliquée correctement, l'obfuscation permet d'accroître sensiblement la protection contre la décompilation, tout en laissant l'application intacte. Il est important de comprendre que l'obfuscation en .NET se fait sur du code MSIL compilé (dll ou exécutable) et non pas sur le code source. Le code obfusqué est fonctionnellement identique au code classique et il va s'exécuter dans le Common Langage Runtime (CLR) avec des résultats similaires.

Une obfuscation profonde crée une multitude de possibilités de décompilation, dont certaines peuvent générer une logique incorrecte lors de la recompilation.

Un obfuscateur évolué peut aussi permettre de faire échouer les décompilateurs.

Techniques d'obfuscation

Attribution d'un nouveau nom aux identificateurs

L'attribution d'un nouveau nom est un moyen de rendre la sortie décompilée plus difficile à comprendre. Le remplacement des noms par des caractères non imprimables (ou des noms illégaux dans la langue cible) est inutile, car les décompilateurs présentent des options permettant de renommer de tels identificateurs.

La méthode Overload Induction

L'Overload Induction est une technologie brevetée pour la méthode d'attribution d'un nouveau nom.

Tandis que la plupart des systèmes d'attribution d'un nouveau nom, attribuent simplement un nouveau nom en remplacement chaque ancien nom (c'est-à-dire que getX() devient a() et getY() devient b()), la méthode Overload Induction impose une surcharge maximale. L'algorithme tente de renommer autant de méthodes que possible avec exactement le même nom. Il parvient ainsi plus lentement à des noms plus longs (tels que aa, aaa, etc.). Cela permet également de gagner de l'espace.

En plus de rendre le code difficilement compréhensible, l'attribution d'un nouveau nom présente un effet secondaire positif, à savoir la réduction de la taille du code. Cela permet également de gagner de l'espace en conservant des entrées de tas de chaînes (string heap). Le fait de tout remplacer par "a" signifie que "a" est stocké une seule fois et que chaque méthode ou champ renommé en "a" peut pointer sur cette entrée. La méthode Overload Induction renforce cet effet, car les identificateurs les plus courts sont réutilisés en permanence. Lorsque l'on sait que la méthode Overload Induction peut donner lieu à trois méthodes nommées a(), la compréhension du résultat décompilé sera pour le moins difficile.

L'algorithme Overload Induction breveté détermine toutes les collisions possibles de l'attribution d'un nouveau nom et ne provoque la surcharge des méthodes que lorsque cela peut être fait en toute sécurité.

La méthode Overload Induction améliorée

Une amélioration de l'"Overload Induction" consiste à utiliser le type de retour de la méthode comme critère afin de déterminer l'unicité de la méthode. Cette fonctionnalité vous permet un gain de 15% supplémentaire de redondance dans le renommage des méthodes. De plus, comme la surcharge sur les types retournés n'est pas autorisée dans certains langages (comme C# et VB), cela entrave encore plus la décompilation.

Obfuscation du flux de contrôle

L'obfuscation du flux de contrôle traditionnelle, introduit de fausses instructions conditionnelles et d'autres structures trompeuses afin de semer la confusion et, si possible, de faire échouer les décompilateurs.

Dotfuscator utilise l'obfuscation du flux de contrôle avancée :
Plutôt que d'ajouter des structures de code, il détruit les structures de code utilisées par les décompilateurs pour recréer le code source. Au final, le code est équivalent à l'original du point de vue sémantique, mais il ne contient aucun indice sur la façon dont le code d'origine était écrit. Même si des décompilateurs avancés sont utilisés, leur résultat consistera au mieux à deviner les structures.

Cryptage de chaînes utilisateur

Dans la mesure où le cryptage de chaînes engendre une légère dégradation de la vitesse d'exécution (pour le décryptage à la volée lors de l'utilisation de la chaîne), le cryptage n'est à utiliser que pour les sections sensibles de votre code.

Réduction du code

En général, plus l'application est petite et plus le temps de chargement et d'exécution est rapide. De plus, pour les environnements embarqués, comme le compact framework .NET, les économies en taille sont cruciales.

Qu'est-ce que l'élagage ("Pruning") ?

Les développeurs utilisent souvent des librairies et des types qui ont été écrits pour être réutilisés mais au final, une petite partie seulement du code est exploitée. L'élagage va servir à enlever tout ce code non utilisé et aura pour conséquence bénéfique de réduire la taille de l'exécutable ce qui améliorera les performances au niveau des temps de réponse et de la mémoire.

L'analyse statique parcourt le code, en commençant par un ensemble de méthodes appelées"déclencheurs". Il s'agit des points d'entrée de votre application. En général, toute méthode qui doit appeler des applications externes doit être définie en tant que déclencheur. Par exemple, dans une application autonome simple, la méthode "Main" doit être définie en tant que déclencheur.

Lorsque l'obfuscateur parcourt le code de la méthode de chaque déclencheur, il note quels champs, méthodes et types sont utilisés. Il analyse ensuite de façon similaire toutes les méthodes appelées. Le processus se poursuit jusqu'à ce que toutes les méthodes appelées aient été analysées.

Une fois l'opération terminée, l'obfuscateur peut déterminer un ensemble minimum de types et les membres nécessaires à l'exécution de l'application. Seuls ces types seront inclus dans l'assembly de sortie.

Exemple

Voici maintenant un exemple concret d'obfuscation d'une dll .Net grâce à l'outil Dotfuscator.
L'objectif final est d'obtenir une dll dont la majeure partie du code sera obfusqué, tout en laissant à disposition les méthodes principales susceptibles d'être appelées.

  • Visualisation de l'assembly dans Reflector avant l'obfuscation :

L'analyse de la dll au travers de Reflector permet de révéler la façon dont elle est construite ainsi que les classes qui la composent avec leurs méthodes et propriétés associées.

A voir également
Ce document intitulé « L'obfuscation en .net » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous