Métamorphisme et apprentissage automatique

lab-logo

Les programmes métamorphiques mutent lors de leur exécution.

Il est important de protéger tout logiciel commercial contre les techniques de rétro-ingénierie qui ouvriraient la porte au piratage ou au vol de technologie.

Une personne qualifiée peut utiliser un désassembleur pour convertir votre exécutable binaire en langage assembleur (1). Il peut s’agir d’un pirate qui désire le modifier afin de contourner des protocoles de sécurité, ou d’un concurrent qui souhaite comprendre la logique sous-jacente pour réutiliser des parties de votre code.

Un désassembleur en action
Fig. 1: Un désassembleur en action

Programmes métamorphiques

Les programmes métamorphiques peuvent gêner voir empêcher les processus de rétro-ingénierie, car ils changeront de code machine au moment de l’exécution. Ils modifient leur structure, sans modifier leurs fonctionnalités.

Le programme lit son propre code compilé et effectue des modifications soit à la volée dans le code machine chargé dans la RAM, soit dans son fichier binaire. Les techniques couramment utilisées en métamorphisme sont le remplacement de code caves, l’insertion de code superflu, l’échange de registres et la réorganisation d’instructions (2).

Register swapping is a common technique of low-level metamorphismFig.2: L’échange de registres est une technique courante utilisée en métamorphisme

Toutes ces solutions consistent à échanger un court morceau de code ou insérer un remplaçant connu à l’avance. Par conséquent, un agent heuristique bien entraîné pourrait prédire de tels changements.

Idéalement, le framework métamorphique parfait recevrait n’importe quel morceau de code de longueur variable et produirait un code fonctionnellement équivalent mais structurellement différent.

Concept

Le code machine est exécuté séquentiellement. Chaque instruction est la combinaison d’un OP Code et de plusieurs arguments.

Une instruction modifie l’état en mémoire du programme. Ce dernier est composé des registres du processeur, de la pile (stack), du tas (heap) et d’autres sections de mémoire virtuelle qui sont mappées par notre programme.

Si nous observons l’état du programme avant et après avoir exécuté une instruction inconnue, nous sommes en mesure de deviner de quelle instruction il s’agit. 

Ainsi, en entraînant un modèle à déduire une séquence d’instructions depuis la transition de deux états, nous pouvons reconstruire une séquence différente mais de fonction identique. Celle-ci nous permettrait de faire muter notre code machine sans modifier son comportement, ce qui ferait obstacle au travail des pirates.

Par soucis de concision, les détails théoriques et techniques ont été omis, mais restent disponibles dans ce billet ou dans cette publication.  

Données d’entraînement

Pour construire notre jeu de données, nous lançons un programme compilé à travers un désassembleur, nous l’exécutons étape par étape et capturons l’état du programme lors de chaque instruction. Ce processus a été automatisé à l’aide de gdb et de ses APIs python (3).

The scrapping process with gdbFig. 3: Aggrégation d’un jeu de données

Cette technique a permis de récupérer 200 000 séquences d’instructions pour l’entraînement.

Les données doivent être ensuite analysées, puis préparées afin d’être servies à un réseau de neurones.

La figure suivante (4) présente une réduction des données récoltées : chaque carré représente un bit de l’état du programme, et l’intensité de la couleur représente la fréquence à laquelle ce bit change de valeur entre deux instructions.

En réalisant cette figure pour des instructions connues, nous avons pu nous assurer de l’intégrité des données récoltées.


Fig. 5: Visualisation des données récoltées

Modèle d’apprentissage

Le but du projet était de construire un modèle seq2seq simple afin d’inférer les instructions utilisées pour passer d’un état connu à un autre.

L’ensemble du modèle est constitué d’un encodeur et d’un décodeur d’inférence avec des cellules LSTM, et a été construit à l’aide de l’API fonctionnelle Keras. Les modèles ont été entraînés avec un optimiseur Adam en mini batches. 

Fig. 6: Evolution de la précision et de la fonction de coût lors de l’entraînement

 

Le score F1 du modèle sur les données de test est de 0,872.

Après 70 époques d’entraînement, le modèle a commencé à tendre vers un sur-apprentissage (6). Pour le moment, un jeu de données plus large serait la meilleure façon d’améliorer la précision du modèle.

L’intégralité du projet et les modèles pré-entraînés peuvent être consultés sur le dépôt github.

Conclusion

Cette étude offre une approche prometteuse, avec de bons résultats, mais des recherches supplémentaires doivent être menées sur la création de jeux de données plus vastes et sur l’expérimentation de différents modèles.

En l’état, le modèle n’est pas utilisable dans un environnement de production. Néanmoins nous serions à même de construire un agent heuristique utilisant ce réseau de neurones afin de faire muter votre programme, garantissant sa protection.

Une telle approche d’apprentissage automatique nécessite cependant beaucoup de puissance de calcul pour une utilisation en temps réel. L’agent heuristique pourrait utiliser des mutations précalculées de diverses parties du programme qui seraient mélangées pour produire un résultat unique. Ces mutations pourraient être stockées dans un cluster data sécurisé pour empêcher leur exposition à l’utilisateur final.