• Aucun résultat trouvé

Modélisation de la rétro-ingénierie par interprétation abstraite

5.2.2 Autres travau

Plusieurs outils d’analyse statique et dynamique implémentent déjà des algorithmes d’unpacking, mais ils requièrent souvent une intervention humaine et ne sont pas adaptés à l’unpacking automatique d’un programme aussi dange- reux qu’un virus.

IDA 4.9 propose un plug-in appelé uunp (universal PE unpacker debugger plug-in module [Dat05, Dat06]) qui automatise l’analyse et l’unpacking de binaires packés. Ce plug-in utilise le débogueur pour laisser le programme se déchiffrer lui-même en mémoire et dès que l’exécution atteint le point d’entrée original, il suspend l’exécution du programme. L’utilisateur peut alors prendre un instantané de la mémoire.

L’algorithme de ce plug-in est le suivant :

1. démarrer le processus et observer l’exécution jusqu’à ce que le point d’entrée du programme packé soit atteint ; 2. ajouter un point d’arrêt à l’adresse kernel32.GetProcAddress, reprendre l’exécution et attendre jusqu’à ce que le packer appelle la fonction GetProcAddress. Si le nom de la fonction passée à GetProcAddress n’est pas dans la liste des fonctions à ignorer, alors passer en mode trace.

Un appel à la fonction GetProcAddress() signifie vraisemblablement que le programme a été unpacké en mémoire et est à présent en train de renseigner sa table des imports (IAT) ;

3. tracer le programme en mode pas-à-pas jusqu’à ce qu’une instruction de branchement mène à la zone du point d’entrée original ;

4. dès que le pointeur de l’instruction courante appartient à la zone de l’OEP2, suspendre l’exécution et informer

l’utilisateur.

Donc, en résumé, nous permettons au code de la protection de faire son travail à pleine vitesse jusqu’à ce qu’il commence à renseigner la table des imports. À ce moment, nous passons en mode pas-à-pas et essayons de trouver le point d’entrée original. Bien que cet algorithme fonctionne avec UPX, ASPack et un certain nombre d’autres packers, il peut échouer et perdre le contrôle de l’exécution du programme packé. Par conséquent ce plug-in doit être utilisé avec précaution.

1Une telle approche est à de nombreux égards comparable aux techniques de fuzzing, utilisées pour valider la sûreté de fonctionnement

des logiciels, mais également leurs sécurité. Le principe du fuzzing est d’injecter des données aléatoires dans les entrées d’un programme. Si le programme échoue, alors il y a des défauts à corriger. La différence de notre approche avec le fuzzing est que les fautes ne sont pas injectées uniquement au niveau des données d’entrées du programme, mais également au niveau des données manipulées et transformées par le programme lors de son exécution et au niveau du code du programme. L’objectif n’est plus de détecter une erreur de programmation mais de récupérer de l’information sur le fonctionnement du programme (en particulier concernant les protections qu’il implémente ou les secrets qu’il manipule) en vue de faciliter sa rétro-conception.

Les plug-ins d’Ollydbg [Yus07,Ope07] (FindCrypt, DeJunk, Ollybone, OllyDump, OllySnake, Polymorphic Break- point Manager, PE Dumper, Universal Hooker, Virtual2Physical et OllyScript) sont très utiles pour unpacker ma- nuellement un exécutable PE protégé.

Le plug-in OllyScript permet à un analyste de sécurité d’automatiser Ollydbg en écrivant des scripts dans un langage proche de l’assembleur. Ce plug-in est particulièrement pratique car lorsque l’on analyse manuellement un binaire protégé, de nombreuses tâches très répétitives doivent être effectuées simplement pour trouver l’OEP au sein de l’application protégée. En utilisant ce plug-in, c’est possible en un seul script. À titre d’exemple, le script suivant atteint automatiquement l’OEP d’un exécutable packé avec UPX :

eob Break findop eip, #61# bphws $RESULT, "x" run Break: sto sto bphwc $RESULT ret

De nombreux scripts OSC3 ont déjà été écrits et permettent de trouver l’OEP d’un exécutable packé, de réparer

l’IAT (Import Address Table), de supprimer le junk code et de trouver la table des relocations et le code déplacé (stolen code).

Tous ces outils d’analyse dynamique doivent être utilisés avec précaution, tout particulièrement lorsque l’on a affaire à des infections informatiques hostiles. Parce que le programme cible s’exécute sur le système hôte, il peut échapper au contrôle du débogueur (qu’il s’exécute en mode utilisateur ou en mode noyau) et se propager.

Force est de constater que les désassembleur statiques sont faciles à induire en erreur (cf. chapitre 3 l’utilisa- tion de sauts à des adresses variables, ou [LD03] par exemple) et que les débogueurs sont faciles à détecter et éventuellement à contourner (cf. chapitre3section3.2.2). L’exécution au sein d’un environnement contrôlé, comme une machine virtuelle, peut résoudre les problèmes d’isolation et de furtivité. Nous allons à présent présenter dans cette section plusieurs approches basées sur l’émulation ou la virtualisation permettant d’unpacker des programmes protégés de manière sécurisée et fiable.

Plusieurs algorithmes ont déjà été proposés, fondés sur l’analyse de la mémoire virtuelle et sur des hypothèses faites à propos des mécanismes de protection sous-jacents. Ces approches sont proches de la notre. Nous pouvons citer de nombreux projets qui focalisent sur des algorithmes alternatifs et utiles pour tracer l’utilisation mémoire d’un processus cible et éventuellement d’unpacker un programme auto-protégé. Parmi ceux-ci, nous allons focaliser sur trois approches intéressantes :

– Algorithme d’analyse de la mémoire corrompue : le projet Argos [PSB06b,PSB06c,PSB06a] utilise l’analyse de la mémoire corrompue [Bos06] de manière à détecter un programme hostile (et potentiellement auto-protégé) en mémoire. Argos est un émulateur du système conçu pour être utilisé dans les Honeypots [Por04]. Il utilise une version modifiée de QEMU [Bel05,Bal06] pour tracer les accès mémoire.

Les auteurs d’Argos ont étendu l’émulateur QEMU pour lui permettre de détecter les tentatives d’accès à distance visant à compromettre le système invité émulé (comme par exemple le shellcode d’un ver). En utilisant l’analyse dynamique de la mémoire corrompue, Argos trace les données du réseau durant l’exécution du processeur et détecte toute tentative de les utiliser de manière malicieuse. Lorsqu’une attaque est détectée, un instantané de la mémoire est journalisé et l’émulateur s’arrête.

Le principe de l’analyse de la mémoire corrompue est le suivant : les données provenant d’une source non sûre sont marquées comme étant corrompues (par exemple, les données provenant du réseau). Les données corrompues sont tracées durant l’exécution (lorsqu’elles sont copiées en mémoire ou dans un registre, le nouvel emplacement est également marqué comme étant corrompu). De manière à identifier et prévenir tout usage

non sûr d’une donnée corrompue, une alarme est levée (lorsqu’elle est utilisée par exemple comme cible d’une instruction de branchement).

– Algorithme de normalisation : le projet WiSA utilise un algorithme de normalisation [CKJ+05] de manière

à unpacker les virus avant d’appliquer un algorithme d’extraction de signature ou des transformations d’ob- fuscation. Ce projet utilise également une version modifiée de l’émulateur QEMU de manière à appliquer l’algorithme de normalisation.

Leur approche est très intéressante mais est plus intrusive et complexe que la notre. L’émulateur est modifié de façon à surveiller la mémoire et à collecter toutes les écritures. Si une tentative d’exécuter du code depuis une zone mémoire qui a été précédemment écrite est faite par le programme cible, l’instruction de déclenche- ment est capturée et l’exécution est terminée. En utilisant les données capturées, un programme équivalent est reconstruit. Cet algorithme est appliqué itérativement jusqu’à ce que tout le code de la protection ait été supprimé.

Pour qui souhaite implémenter et tester l’algorithme de normalisation du projet WiZA, un bon point de départ est la version modifiée de l’émulateur QEMU du projet Argos qui implémente tout ce qui est requis pour pouvoir tracer les accès mémoire.

Malheureusement, le papier du projet WiZA n’explique pas comment reconstruire l’exécutable après unpa- cking. Nous présentons dans ce chapitre au moins une méthode qui peut être appliquée pour reconstruire un exécutable déprotégé après unpacking, basée sur le hook des fonctions d’API Win32 et natives.

– Algorithme forensique : la procédure d’unpacking couvre souvent deux problèmes : – premièrement, dumper la mémoire,

– deuxièmement, partant de cet instantané de la mémoire, trouver les zones qui appartiennent au programme cible et reconstruire l’exécutable.

Lorsque l’on fait un dump mémoire en aveugle, il est difficile de recouvrer un exécutable complètement fonc- tionnel. Cependant, des heuristiques forensiques peuvent donner de bons résultats et peuvent être appliquées au problème qui consiste à retrouver un processus dans un instantané mémoire et à retrouver une image binaire du processus cible.

Parmi les outils d’analyse forensique les plus connus, nous trouvons les suivants intéressants : MemParser [Bet06], idetect, ProcEnum, WMFT [Bur06a, Bur06b, Bur05, Bur06c, Bur06d], Ramdump, lsproc, lspm, ReadPE [Car06b, Car06a], Dd, md5lib, md5sum, VolumeDump, Wipe, ZlibU, nc, GetOpt [Gar06], Kntlist [GM06], Dd for Windows [New06], Minidumps [Pen02b, Pen02a], les scripts PERL MemDump et PTFinder [Sch06f,Sch06h,Sch06i,Sch06d,Sch06a,Sch06b,Sch06c, Sch06g,Sch06e], Mdump [Wea03b,Wea03a], etc.

Avant de présenter la méthode que j’ai développée pour supprimer le loader de protection d’un exécutable packé puis reconstruire l’exécutable, après projection de son image mémoire, je signale que d’autres méthodes ont été proposées depuis la publication de mes résultats dans les actes de la conférence AAVAR, en 2006 [Jos06b]. Parmi celles-ci, la solution décrite dans [KPY07] (publiée dans les actes de la conférence WORM’07) me paraît la plus per- tinente. Fondée sur l’utilisation de l’émulateur TEMU (nouveau nom donné à TTAnalyze [BKK06]), leur méthode est très similaire à celle proposée dans [CKJ+05], dans la mesure où elle se propose de détecter l’exécution d’un

code nouvellement généré en surveillant les écritures en mémoire après démarrage du programme. À la différence du papier [CKJ+05], leur papier décrit complètement les choix de conception et d’implémentation : l’outil Renovo

créé une structure de donnée comparable à celle des page tables pour maintenir une mémoire secondaire permettant de reproduire l’espace mémoire du processus observé. À chaque octet de cette mémoire est associé un bit, indiquant s’il l’octet correspondant est ou non corrompu. Cette mémoire est initialement considérée comme non corrompue. Durant l’exécution du programme, ses opérations d’écritures sont reproduites en mémoire secondaire, en marquant les zones correspondantes (les instruction d’écriture en mémoire sont instrumentées par l’émulateur TEMU). Avant l’exécution d’un bloc de code, un contrôle de la mémoire correspondante (occupée par ce bloc de code) est effectué. Si la mémoire est marquée comme corrompue, c’est qu’il s’agit d’une portion de code nouvellement générée. Dans ce cas, les pages contenant les octets marqués comme corrompus sont sauvegardées sur le système de fichier. De façon à éviter les fausses alertes dues au chargement/déchargement dynamique de DLL dans l’espace d’adressage du processus, les écritures correspondantes ne sont pas marquées.

Pour extraire le code d’un programme packé par utilisation de plusieurs couches de chiffrement, la mémoire se- condaire est remise à zéro avant de reprendre le processus de surveillance de la mémoire. Remarquons que la solution décrite dans [KPY07] ne propose pas de méthode pour recouvrer un exécutable fonctionnel. Leur objectif est de récupérer automatiquement le code chiffré sous forme déchiffrée.

Je pense que Renovo offre, concernant la fonction de restauration, des fonctionnalités équivalentes à celles que je décris dans la section suivante. Si elle peut à certains égards paraître plus élégante (notamment l’utilisation d’un masque associé aux octets de la mémoire secondaire quand ma solution impose une comparaison octet par octet de deux basics blocs), je pense qu’elle implémente une approche très similaire et se heurte donc aux mêmes limitations : lorsque le code protégé comporte du code auto-modifiable, nos deux approches se heurtent à un écueil incontournable, dans la mesure où nous ne sommes pas capables de distinguer le flot de contrôle de la protection de celui du programme protégé.

C’est malheureusement souvent le cas des programmes viraux.

Le tableau5.2présente de manière synthétique les différentes solutions, en terme de couverture des mécanismes de protection (X signifie que le produit est capable de contourner le mécanisme de protection). Ces mécanismes de pro- tection sont décrits au chapitre3 (section3.2.2). Nous pouvons remarquer que les modules d’unpacking fondés sur

Technique de protection IDA Pro UUnP OllyBone WiZa Renovo VxStripper

Mesure des temps d’exécution X X X

Contrôle d’intégrité du code X X X

Mise à zéro des registres de déboguage X X X

Auto-déboguage X X X

Chiffrement simple X X X X X

Chiffrement par couches X X X

Détournement des interruptions X X X

Redirection des appels aux fonctions importées par l’application

X

Redirection avec émulation partielle des imports X Redirection avec émulation totale des imports

Altération de la section code Virtualisation de code

Dissimulation du processus X X X

Protection du processus X X X

Altération de l’entête X

Tab. 5.2 – Récapitulatif des différentes solutions

l’utilisation d’un débogueur ring3 ne permettent pas de contourner systématiquement les mécanismes de protection implémentés dans les packers, i.e. qu’ils ne permettent généralement pas de supprimer le loader de protection de manière automatique. Une interaction humaine est donc le plus souvent indispensable.

Concernant les modules d’unpacking fondés sur l’utilisation d’un CPU logiciel, la résistance vis-à-vis des protection anti-déboguage, chiffrement et contrôle d’intégrité est identique. La solution VxStripper est la seule à proposer une fonction de reconstruction de l’exécutable, après projection de l’image mémoire. Pour les autres solutions, l’utilisa- tion d’outils tiers est donc nécessaire. L’avantage d’intégrer les deux types d’outils (unpacker et reconstruction de l’exécutable) est que cela permet à la fonction de reconstruction d’utiliser une partie de l’information récupérée lors de la première phase de l’analyse (interaction avec les fonctions d’API en particulier).

Nous pouvons remarquer que VxStripper n’est pas capable de contourner efficacement les mécanismes de redirec- tion avec émulation totale des imports et d’altération de la section code. Pour contrer efficacement ce mécanisme, il serait nécessaire d’implémenter quelques heuristiques supplémentaires, impliquant de mener l’analyse conjointe de l’exécutable protégé et de l’exécutable déprotégé, afin de recouvrer les portions de code manquantes (embarquées dans le code de la protection). Une solution algorithmique permettant de lever automatiquement ce type de pro-

tection reste un problème ouvert.

Nous avons présenté au chapitre 3 le mécanisme de protection par virtualisation de code et des travaux récents [GG09] menés pour lever presque automatiquement ce type de protection. Les auteurs utilisent une méthode d’ana- lyse hybride statique/dynamique, fondée sur l’utilisation et l’extension de l’outil Metasm [Gui07]. VxStripper n’est pas capable aujourd’hui de contourner efficacement ce type de mécanisme.

Concernant le mécanisme de chiffrement par couches, il convient de remarquer que les algorithmes implémentés dans WiZa, Renovo et VxStripper souffrent de la même limitation lorsque le programme protégé comporte lui même du code auto-modifiable. Une analyse manuelle du programme est alors nécessaire pour identifier le point d’entrée original. Remarquons que cette phase d’analyse manuelle est considérablement simplifiée dans le cas des CPU logiciels Renovo et VxStripper, dans la mesure où ces outils tracent les interactions avec les API du système.