Pourquoi s’embarrasser des chaînes de caractères ?
Lorsqu’on analyse un nouvel échantillon trouvé « dans la nature », il peut être judicieux d’extraire les chaînes de caractères qui s’y trouvent pour identifier des adresses IP, des domaines, des fichiers de log ou des signatures des serveurs C&C. Par exemple, si un modèle d’Intelligence Artificielle tel qu’HL-AI identifie comme malveillant un binaire nommé « calc.exe », et que l’on constate la présence de Mimikatz dans ses chaînes de caractères, l’exécutable n’a probablement pas été conçu pour calculer quoi que ce soit.
Trop de Lj&&lZ66~A?<
Le fait est que la plupart des chaînes de caractères extraites ne sont que des données aléatoires correspondant à des caractères ASCII valides. Rien de grave si on traite manuellement un échantillon, mais alors comment automatiser le processus d’extraction pour gérer des milliers de binaires ? Un stockage aveugle de toutes les chaînes extraites reviendrait à gaspiller de l’espace de stockage, sans oublier les ressources impliquées dans le filtrage et dans l’extraction d’informations utiles. C’est pourquoi il nous faut un système capable de dire si une chaîne de caractères s’avère être du non-sens ou si elle est pertinente (dans le contexte de la cybersécurité, POFXCALLBACKENUMERATEUNMASKEDINTERRUPTS et iisncizn.dll sont des chaînes pertinentes). Le système doit se montrer suffisamment efficace pour filtrer rapidement des milliers de chaînes de caractères, tout en présentant de très faibles taux de faux négatif pour éviter de passer à côté de chaînes importantes, et un taux relativement bas de faux positifs pour éviter du stockage inutile.
Choisir un plan de bataille
Deux approches viennent à l’esprit pour la conduite de ce chantier :
- L’approche déterministe où des expressions rationnelles servent à extraire des adresses IP, noms de fichiers, noms de domaines, etc.
- L’approche non déterministe qui exploite le Machine Learning soit directement sur les chaînes de caractères, soit sur des caractéristiques qui en sont extraites.
La première méthode est utile pour l’extraction d’Indicateurs de Compromission (IoC), mais peu évidente à généraliser à des chaînes sans motifs (patterns) comme des noms de fonctions. La seconde méthode présente une capacité de généralisation plus importante, mais nous ne pouvons pas être sûrs qu’elle ne passera pas à côté de chaînes de grande importance. Dans ce cas, pourquoi ne pas combiner ces deux approches ? Un premier filtre détecterait les IoC sur la base d’un ensemble de motifs afin de s’assurer d’identifier le serveur de l’attaquant, puis la méthode reposant sur du Machine Learning filtrerait le charabia.
Construire la base de données : trier les chaînes de caractères, ou le Tinder des chaînes
Nous avons donc une idée plus claire de ce que nous souhaitons faire, et il nous faut une base de données où figureront des chaînes identifiées comme pertinentes ou sans intérêt pour entraîner et tester notre système de filtrage. Nous avons ainsi créé un outil qui prend en entrée les binaires, en extrait les chaînes, et demande à l’utilisateur de les rejeter ou de les approuver en les faisant glisser vers la gauche ou la droite au moyen des flèches de son clavier. Heureusement, les chaînes sont généralement extraites par ensembles de « bonnes » ou « mauvaises » chaînes, et nous avons donc pu créer une base de données de 15 000 chaînes étiquetées en une heure.
Choisir le meilleur modèle de Machine Learning
Comme indiqué précédemment, nous voulons que notre modèle soit aussi performant que possible et filtre rapidement les chaînes vides de sens de notre fichier. De nombreux modèles conventionnels de Machine Learning viennent à l’esprit : Random Forest, XGBoost, SVMs, voire même quelque chose d’aussi simple qu’une régression logistique ou une classification naïve bayésienne. Chacun de ces modèles est relativement rapide à entraîner tout en permettant des inférences extrêmement rapides, ce qui correspond exactement à ce que nous recherchons.
Maintenant que nous disposons d’un ensemble de modèles de classification, il nous faut convertir les chaînes en quelque chose qu’un modèle pourra comprendre. En d’autres mots, il nous faut trouver une représentation numérique des chaînes avec suffisamment d’informations pour que le modèle puisse faire la différence entre les « bonnes » et les « mauvaises » chaînes. La manière la plus simple de convertir les chaînes de caractères en valeurs numériques est de calculer les caractéristiques que nous jugeons intéressantes. L’entropie de Shannon en est un bon exemple : elle évalue la répartition des caractères ou symboles au sein d’une chaîne de caractères et détermine s’ils sont agencés de façon imprévisible. Les chaînes présentant des niveaux d’entropie plus élevés montrent une répartition plus variée et désordonnée des caractères, insinuant un plus haut niveau d’aléa. Les chaînes présentent des valeurs moins élevées d’entropie montrent un agencement plus structuré des caractères, indiquant plus de régularité et de prévisibilité.
Outre l’entropie de Shannon, les features suivants sont utilisés :
string_length
: Longueur de la chaîneproportion_numbers
: Proportion d’entiers trouvésproportion_letters
: Proportion de lettres alphabétiques trouvéesproportion_space
: Proportion d’espaces blancs trouvésproportion_special
: Proportion de caractères spéciaux trouvésproportion_lower
: Proportion de caractères minuscules trouvés
Nous disposons à présent des modèles et des représentations numériques. Nous sommes donc prêts pour entraîner et tester la performance des modèles de Machine Learning que nous avons choisis.
Choisir quel modèle utiliser
Étant donné que plusieurs algorithmes de Machine Learning peuvent accomplir cette tâche, nous devons tenter de déterminer lequel donne les meilleurs résultats. En Machine Learning, il est d’usage de répartir les données en un ensemble d’entraînement et un ensemble de test afin d’évaluer la performance du modèle sur des éléments qu’il n’a jamais vus au cours de son entraînement. La performance du modèle est souvent évaluée de cette manière. Néanmoins, ce procédé peut donner des résultats trompeurs, car il est toujours possible que l’ensemble dédié au test ne soit pas représentatif de la répartition des données d’entraînement.
Imaginons, par exemple, que les « mauvaises » chaînes incluent fréquemment des caractères spéciaux tels que %, !, #, alors que les « bonnes » chaînes montrent une répartition des caractères plus uniforme. Dans une seule division aléatoire entre l’ensemble d’entraînement et l’ensemble de test, si l’ensemble de test contient principalement de « bonnes » chaînes et l’ensemble d’entraînement compte plus de « mauvaises » chaînes où figurent ces caractères spéciaux, le modèle peut à tort associer ces caractères à la notion de malveillance. L’évaluation du modèle sur la base de cet ensemble de test peut se révéler d’une grande exactitude. Cependant, le modèle n’a de bonnes performances que par l’apprentissage de ces caractères spécifiques (%, !, #) à partir des « mauvaises » chaînes de l’ensemble d’entraînement.
Une approche courante et assez efficace pour éviter ce piège consiste en l’utilisation d’une validation croisée. Cette technique nous aide à choisir l’algorithme de Machine Learning le plus adapté à la tâche, en effectuant une série de mini-tests à l’aide de chacun des algorithmes. Au lieu de ne s’appuyer que sur un test, la validation croisée répartit les données en différents groupes représentant mieux la répartition globale des données.
Le bon algorithme pour limiter les faux négatifs
En calculant un score de précision (accuracy), Random Forest se présente comme le meilleur classificateur avec un score moyen de 0,969 sur 5 plis, suivi de près par XGBoost (0,967), puis la régression logistique (0,968), et enfin la classification naïve bayésienne (0,886).
Random Forest peut encore être amélioré en ajustant certains paramètres, appelés des hyperparamètres. Les hyperparamètres sont comme des réglages sur un modèle de Machine Learning que vous pouvez ajuster pour améliorer le fonctionnement du modèle pour une tâche spécifique. Pour les ajuster, nous agissons sur un ensemble prédéfini de valeurs d’hyperparamètres, et créons une grille de combinaisons à étudier. Chaque combinaison est évaluée par validation croisée. Le score moyen de l’algorithme Random Forest augmente alors de 1 %, avec l’aide d’hyperparamètres mieux sélectionnés. La précision atteint alors le score de 0,98.
Maintenant que l’algorithme est optimal, notre objectif est de réduire le nombre de chaînes que notre classificateur marque à tort comme non importantes. En d’autres mots, nous devons minimiser autant que possible les faux négatifs. Pour y parvenir, nous pourrions ajuster les seuils du classificateur binaire jusqu’à ce que le ratio de faux négatifs soit suffisamment bas. Une autre approche, et celle que nous préférons finalement, consiste à établir des règles qui nous aident à repérer rapidement les chaînes qui pourraient être importantes. Ces chaînes spéciales n’ont pas besoin de passer par notre algorithme d’apprentissage automatique et sont directement classifiées comme étant malveillantes.
Ces règles sont conçues pour identifier rapidement des chaînes qui semblent importantes selon des critères spécifiques. En l’occurrence, voici ce que nous cherchons :
- File Extension Detection (détection des extensions de fichiers) : si une chaîne se termine par une extension de fichier connue (par exemple, « random.dll » ou « evenmorerandom.pdf »), elle est automatiquement catégorisée comme d’intérêt.
- Domain Name Matching (correspondance des noms de domaines) : les chaînes qui correspondent à des noms de domaine sont jugées comme présentant de l’intérêt.
- URL Validation (validation d’URL) : si une chaîne se plie à un format d’URL valide, elle est automatiquement catégorisée comme d’intérêt.
- IP Address Validation (validation d’adresses IP) : les chaînes ressemblant à des adresses IP valides sont considérées comme d’intérêt.
- Base64 Validation (validation de l’encodage en Base64) : les chaînes remplissant les critères d’un encodage en Base64 valide sont considérées comme d’intérêt.
- Windows Object Detection (détection des objets Windows) : si une chaîne commence par « », indiquant un objet Windows, elle est automatiquement catégorisée comme d’intérêt.
- Bitcoin Address Recognition (reconnaissance des adresses Bitcoin) : les chaînes ressemblant à des adresses Bitcoin sont jugées comme présentant de l’intérêt.
- Email Detection (détection d’adresses e-mail) : si une chaîne semble correspondre à une adresse e-mail, elle est considérée comme présentant de l’intérêt.
Et qu’en est-il des embeddings et des réseaux neuronaux ?
Malgré les résultats appréciables obtenus au moyen d’algorithmes classiques de Machine Learning, nous avons tout de même essayé quelques alternatives, à savoir des embeddings de chaînes. L’embedding de chaînes sous-entend de représenter les chaînes comme des vecteurs, de façon à permettre aux algorithmes de comprendre les relations sémantiques entre les caractères et sous-chaînes qui peuvent ne pas être évidentes à repérer sous leurs formes brutes. Deux algorithmes d’embedding ont été testés : n-grams et CANINE. N-grams implique la décomposition des chaînes en plus petites sous-unités (des sous-séquences de caractères de longueur n) et de créer des features basés sur leurs fréquences. CANINE, pour sa part, a recours à un modèle de langage préentrainé conçu pour comprendre et représenter efficacement les chaînes.
Les réseaux de neurones sont naturellement adaptés à la classification de ces embeddings, en particulier les architectures de type RNN (réseaux de neurones récurrents). Les RNN sont efficaces dans le traitement de séquences et aptes à la gestion des données issues des chaînes. Les RNN traitent les séquences étape par étape, en conservant une mémoire interne des éléments passés pour influencer le traitement des éléments futurs.
Malheureusement, le RNN (ici un LSTM bidirectionnel) n’a pas fait mieux que les algorithmes de Machine Learning et nos features sur-mesure. La précision sur l’ensemble de test se situait à 0,85 pour n-gram et 0,83 pour CANINE, malgré une convergence satisfaisante de la perte (loss) pendant l’entraînement. Ce n’est pas surprenant, compte tenu de la nature spécifique des chaînes (qui ne représentent pas des mots réels) et du fait que nos caractéristiques (features) sont adaptées au contexte des chaînes malveillantes et bénignes.
Il semble que Random Forest (comme les autres algorithmes de Machine Learning) peut efficacement exploiter nos features en présentant l’avantage de l’interprétabilité. Il est crucial de choisir des méthodes qui correspondent à la complexité de la tâche à accomplir, démontrant que les méthodes les plus sophistiquées ne sont pas toujours les plus adaptées.
Conclusion
Pour notre mission qui consiste à détecter des chaînes de caractères malveillantes, nous avons opté pour une approche pratique en combinant de simples règles et du Machine Learning. Notre principal objectif était d’entraîner un algorithme rapide et efficace, et de porter une attention particulière à la minimisation des faux négatifs.
Nous avons créé une base de données de chaînes labélisées, testé plusieurs modèles de Machine Learning, et étudié des techniques de pointe, dont l’embedding de chaînes et les réseaux de neurones. Finalement, nous avons constaté que le modèle de Random Forest, associé à des features spécialement adaptées à cette tâche, ainsi qu’à des règles d’identification, s’est avéré être le plus efficace.
L’algorithme final est désormais utilisé par l’équipe CTI chez Harfanglab, rendant l’analyse des chaînes beaucoup plus facile et moins chronophage qu’auparavant : sur nos échantillons de test, le modèle a éliminé plus de 80 % des chaînes, ne laissant que celles qui sont pertinentes. L’algorithme sera également intégré au produit lui-même pour stocker en mémoire les chaînes pertinentes trouvées par les agents, facilitant ainsi les investigations des analystes.
Article co-écrit avec Axel Rousselot de l’équipe CTI d’HarfangLab.