Fabrice Desclaux
Commissariat à l’énergie atomique et aux énergies alternatives Direction des applications militaires
fabrice.desclaux(@)cea.fr, https://code.google.com/p/smiasm
Résumé Miasmest unframework dereverse engineering open-source.
Son but est de permettre l’analyse, la modification et la regénération de programmes binaires. Il embarque plusieurs sous-composants, comme un manipulateur de (PE/ELF/CLASS) et un assembleur/désassembleur.
Miasm définit également un langage intermédiaire sur lequel il s’appuie pour faciliter l’analyse de programmes.
1 Introduction
Miasm est un framework de reverse engineering écrit en python et open-source. Son but est de permettre l’analyse, la modification et la regénération de programmes binaires (PE/ELF/CLASS)
Pour cela, il embarque son propre assembleur/désassembleur ainsi qu’un langage intermédiaire.
Ce document décrira l’architecture de Miasm. Nous nous attarderons alors sur la définition de son langage intermédiaire. Ce dernier est la base pour les manipulations d’analyse de code, de désobscurcissement ou même de recherche de vulnérabilités. Il permet en effet de s’affranchir de la disparité des différents assembleurs natifs (x86, ARM, MIPS, . . .) et du fait qu’ils sont peu adaptés à cet usage.
Enfin, des exemples illustreront l’utilisation de ce langage dans l’ému- lation de programmes, ainsi que dans l’analyse statique de binaires.
2 Architecture et principaux composants de Miasm
Cette partie décrit les divers modules de Miasm. Des exemples de manipulations du format PE ainsi que de l’assembleur sont donnés plus loin.
2.1 Architecture
Miasm est un framework de reverse enginnering. Il est composé de plusieurs modules :
– Elfesteem : un module de manipulation de binaire : ce module est utilisé pour disséquer des fichiers PE/ELF/CLASS, les modifier puis les regénérer. Ce module tente de s’occuper automatiquement de tout ce qui peut l’être lors des regénérations de programmes, comme les entrées des relocations, des imports ou des ressources d’un bi- naire.
– Miasm ASM/DISASM : un module pour assembler et désassembler supportant pour l’instant les architectures x86/arm/ppc/java.
– Mi-asm IL : un module de langage intermédiaire ainsi qu’un tra- ducteur des langages assembleurs natifs vers ce langage. Le but est ici d’appliquer divers algorithmes sur un programme binaire. Tra- vailler sur l’assembleur directement est assez fastidieux et non por- table d’un assembleur à un autre : l’idée est de définir un langage intermédiaire dans lequel les assembleurs peuvent être traduits, et d’appliquer des algorithmes sur ce nouveau langage, plus simple et plus adapté.
– un module de simplification et d’exécution symbolique de ce lan- gage intermédiaire. Il utilise le langage intermédiaire et permet de résoudre des problèmes simples posés par l’obscurcissement de pro- gramme en faisant par exemple de la propagation de constante.
– Miasm JiT : un module d’émulation, faisant de la traduction de code à la volée en utilisant la sémantique des instructions décrite dans le langage intermédiaire.
– Grandalf : un placeur de graphe, permettant d’afficher des graphes de flots d’exécution.
La figure 1 présente un schéma qui récapitule l’ensemble des modules de Miasm
2.2 Manipulation PE/ELF/Class
Elfesteem1 permet de faire des manipulations sur les ELF, PE et CLASS à la manière de [4], c’est-à-dire à tenter de faire automatique- ment tout ce qui peut l’être : les entrées des relocations, des imports ou des ressources d’un binaire.
Premièrement, le module Elfesteem permet la manipulation de PE/ELF/CLASS. Par exemple nous allons écrire un programme qui prend un fichier contenant unshellcode et crée un exécutable windows contenant une section (le shellcode) qui sera exécutable.
# ! / usr / bin / env p y t h o n
1. développé avec Philippe Biondi
Smiasm
Miasm Miasm IL Symbolic
execution
C Backend
Emulator
Simplification engine
...
Asm/Disasm
x86/ppc/arm/java Elfesteem
Parse/Build
pe/elf/class Grandalf
Frida
Figure1. Composants deMiasm
i m p o r t sys
f r o m e l f e s t e e m . p e _ i n i t i m p o r t PE
m y s h e l l c o d e = o p e n ( sys . a r g v [ 1 ] ) . r e a d () e = PE ()
s _ t e x t = e . S H L i s t . a d d _ s e c t i o n ( n a m e = " t e x t ", a d d r = 0 x1000 , d a t a = m y s h e l l c o d e )
e . O p t h d r . A d d r e s s O f E n t r y P o i n t = s _ t e x t . a d d r o p e n (’ m y s h e l l c o d e . exe ’, ’ wb ’) . w r i t e ( str ( e ) )
Le code suivant illustre la modification d’entrées du répertoire des res- sources dans un binaire Windows. Ces modifications sont prises en compte parMiasm lors de la génération du programme, qui calcule automatique- ment leurs tailles et leurs positions finales.
Ici, nous allons changer le nom du menu Fichier de la calculatrice.
# ! / usr / bin / env p y t h o n i m p o r t sys
f r o m e l f e s t e e m . p e _ i n i t i m p o r t PE i m p o r t s t r u c t
e = PE ( o p e n ( sys . a r g v [1] , ’ rb ’) . r e a d () ) n a m e = " \ x00 ". j o i n (" c o c o l a s t i c o t ") +" \ x00 "
for i in x r a n g e (2) :
m e n u = e . D i r R e s . r e s d e s c . r e s e n t r i e s [ 1 ] . s u b d i r . r e s e n t r i e s [ i ].
s u b d i r
# r e a d m e n u len & d a t a
off = 6 + s t r u c t . u n p a c k (’ H ’, m e n u . r e s e n t r i e s [ 0 ] . d a t a . s [ 4 : 6 ] ) [0]
end = m e n u . r e s e n t r i e s [ 0 ] . d a t a . s [ off :]
# m o d i f y
m e n u . r e s e n t r i e s [ 0 ] . d a t a . s = m e n u . r e s e n t r i e s [ 0 ] . d a t a . s [ : 4 ] m e n u . r e s e n t r i e s [ 0 ] . d a t a . s += s t r u c t . p a c k (’ H ’, len ( n a m e ) ) + n a m e
+ end
p r i n t r e p r ( m e n u . r e s e n t r i e s [ 0 ] . d a t a . s ) o p e n (’ c a l c _ m e n u _ m o d . exe ’, ’ wb ’) . w r i t e ( str ( e ) )
Si on exécute le binaire généré, on peut se rendre compte que le nom du menu est effectivement modifié.
2.3 Assembleur/Désassembleur
Miasm est également composé d’un assembleur/désassembleur. Ceux- ci sont écrits dans le but de pouvoir aussi bien travailler sur des instruc- tions que sur un petit code source assembleur complet. Un mécanisme de multiplexeur permet également de désassembler une source pouvant être une chaîne de caractères, un binaire PE/ELF/CLASS ou même la machine d’émulation deMiasm.
Voici quelques manipulations de bases de l’assembleur/désassembleur :
f r o m m i a s m . a r c h . i a 3 2 _ a r c h i m p o r t x 8 6 _ m n
> > > f r o m m i a s m . a r c h . i a 3 2 _ a r c h i m p o r t x 8 6 _ m n
> > > # d i s a s m nop
... l = x 8 6 _ m n . dis (’ \ x90 ’)
> > > p r i n t str ( l ) nop
> > > # asm nop
... p r i n t x 8 6 _ m n . asm (’ nop ’) [’ \ x90 ’]
> > > # asm inc eax
... p r i n t x 8 6 _ m n . asm (’ inc eax ’) [’ @ ’, ’ \ xff \ xc0 ’]
> > > # d i s a s m int eax ( b o t h f o r m s ) ... p r i n t str ( x 8 6 _ m n . dis (’ @ ’) )
inc eax
> > > p r i n t str ( x 8 6 _ m n . dis (’ \ xff \ xC0 ’) )
inc eax
Miasm inclut également une gestion des symboles, ce qui lui permet d’assembler des codes complets. Il est possible d’écrire directement des codes assembleurs qui seront par la suite injectés dans un binaire, servir de shellcode, . . . Des exemples sont disponibles dans le fichier example/
asm_x86.pydans le code deMiasm2.
Pour terminer, Miasm embarque également un mini désassembleur Frida, offrant une vue graphique d’un listing assembleur.
2. http://code.google.com/p/miasm
3 Langage intermédiaire
Cette partie décrit le langage intermédiaireMi-asm ainsi que ses API de manipulations.
3.1 Description
État de l’art Miasm utilise un langage intermédiaire pour représenter la sémantique des instructions d’un architecture donnée. Le but premier est de représenter les instructions dans une forme indépendante de l’ar- chitecture. Son faible nombre de mots évite les instructions redondantes et conserve une certaine simplicité dans les algorithmes qui l’utilisent.
L’écriture de Miasm3 a été motivée par le constat de l’absence de langage simple pouvant représenter la sémantique des instructions assem- bleur. Aujourd’hui de nombreux autres langages permettant cette repré- sentation. Par exemple :
– [6] qui est un assembleurRISC composé de 17 instructions, et dont le point fort est de permettre la traduction x86 vers REIL et inver- sement. Le point faible est peut être le nombre encore un peu im- portant d’instructions, déplaçant une partie de la complexité dans les algorithmes de traitement (ce point peut se discuter).
3. aux alentour de 2007
– [7], dont le langage intermédiaire est assez proche de celui de Mi- asm. Il a été utilisé avec succès aussi bien dans l’analyse de code que la décompilation semi-automatique. Il n’a toutefois pas encore debackendpermettant de faire de l’émulation à partir de ce langage.
– [2] est le langage intermédiaire de BAP. Ce langage est également proche de celui de Miasm. Une des différences se situe dans le fait que la représentation des instructions dans ce langage utilise des variables temporaires, alors queMi-asm tente de les décrire par des opérations réalisées en parallèle (ceci sera décrit en détail par la suite).
– [3] utilisé principalement pour la décompilation. Il n’est pas open- source.
– [1] qui implémente une représentation simple des effets de bord d’une instruction servant surtout à l’émulation simple d’instructions.
Définition Le langage intermédiaire deMiasm tente de répondre à plu- sieurs problématiques : il a été conçu pour pouvoir être utilisé dans le désobscurcissement, l’analyse de code, la recherche de vulnérabilités dans des programmes binaires. C’est un langage d’expressions, défini comme suit :
– ExprAff(dst, src) c’est la seule expression qui modifie l’environne- ment d’exécution symbolique (ou d’émulation). Elle affecte l’expres- sionsrcà l’expressiondst.
– ExprInt(valeur, taille) cette expression représente un entier dont la taille en bits est passée en paramètre. Si la taille est omise, la variable est considérée sur 32 bits.
– ExprId(nom, taille) c’est une variable dont le nom et la taille en bits sont passés en paramètre. Si la taille est omise, la variable est considérée sur 32 bits.
– ExprCond(condition, valeur_si_vrai, valeur_si_faux)cette expres- sion est équivalente à l’opérateur ternaire du langage C. Elle prend en paramètre 3 sous-expressions représentant respectivement la condition à tester, l’expression renvoyée si cette condition est vraie et l’expression renvoyée si cette condition est fausse.
– ExprMem(adresse, taille) cette expression représente un déréféren- cement mémoire, pointé par l’adresse donnée par une expression en paramètre, ainsi qu’une taille passée en paramètre. Si la taille est omise, la zone mémoire est considérée sur 32 bits.
– ExprOp(nom_de_l_opérateur, opérande1, opérande2, ...) cette ex- pression représente l’application de l’opérateur dont le nom est passé
en paramètre sur les opérandes représentés par les expressions sui- vantes. Le nom de l’opérateur est de type : [’+’, ’-’, ’*’, ’/’, ’∧’,
’|’, ’&’, ’<<’, ’>>’, ’<<<’, ’a>>’, ’parity’, ...]. ’<<<’ représente une rotation à gauche, ’a>>’ le décalage à droite arithmétique. Par exemple, ExprOp(’+’, ExprInt(1), ExprInt(2)) est l’addition entre les entiers 1 et 2 (codés sur 32 bits).
– ExprSlice(src, bit_start, bit_stop) : extrait une tranche de bits de l’expression passée en paramètre. Cela peut par exemple extraire la portionah du registre eax en x86, ou lezero flag du eflag.
– ExprCompose(expr1, expr2, ...) crée une expression composée de la concaténation des bits de sous-expressions. Ces sous-expressions sont desExprSliceTo.
– ExprSliceTo(expression, start, stop)cette expression est utilisée uni- quement dans leExprCompose. Elle permet de donner l’information de placement de l’expression dans la concaténation d’expressions.
Dans le futur, cette expression devrait être supprimée, car ces infor- mations seront stockées dans leExprCompose.
La simplicité du langage tente de représenter l’état d’esprit de l’au- teur4. Les tailles des expressions sont incluses lors de leur définition : Cela offre la possibilité de faire des vérifications de compatibilité lors de la création des expressions.
Le choix a été fait de ne pas créer autant d’expressions opérateur que d’opérateurs. La nature de l’opérateur reste un paramètre de l’expression opérateur.
Cela a certains avantages : les algorithmes qui traitent le langage in- termédiaire restent simples et ne manipulent que ces 9 entités. Même si on rajoute un opérateur qui n’est pas connu de l’algorithme, les traitements de flots de données restent valables.
Exemple de manipulation du langage intermédiaire :
> > > f r o m m i a s m . e x p r e s s i o n . e x p r e s s i o n i m p o r t *
> > >
> > > # d e f i n e 2 ID
... a = E x p r I d (’ eax ’, 32)
> > > b = E x p r I d (’ ebx ’, 32)
> > > p r i n t a , b eax ebx
> > > # add t h o s e ID
... c = E x p r O p (’ + ’, a , b )
> > > p r i n t c ( eax + ebx )
> > > # + a u t o m a t i c a l y g e n e r a t e s E x p r O p ( ’+ ’ , a , b ) ... c = a + b
4. Le langage a été défini avec la participation d’Axel “Capitaine Igloo” Tillequin.
> > > p r i n t c ( eax + ebx )
> > > # ax is a s l i c e of eax ... ax = a [ : 1 6 ]
> > > p r i n t ax eax [ 0 : 1 6 ]
> > > # m e m o r y d e r e f ... d = E x p r M e m ( c , 32)
> > > p r i n t d
@32 [( eax + ebx ) ]
À partir de là, on peut définir les instructions de l’assembleur x86. Pour cela, on traduit une instruction par une liste d’expressions écrites dans le langage précédent. Chacune de ces expressions représente une entité (un registre, un flag, une case mémoire) modifiée par l’instruction. Par exemple, l’instructionxchg qui échange le contenu de ses deux arguments est représentée par :
def x c h g ( info , a , b ) : e = []
e . a p p e n d ( E x p r A f f ( a , b ) ) e . a p p e n d ( E x p r A f f ( b , a ) ) r e t u r n e
Cette instruction est décomposée en deux affectations : – on place le contenu de la variableb dans la variablea – puis on place le contenu de adans la variableb
La première remarque pouvant être émise est que si l’on exécute ces affectations séquentiellement, le résultat est erroné. Il faudrait introduire une variable temporaire qui conserve la valeur deaavant la première affec- tation. Ici, une autre solution est adoptée : on considère que les expressions de cette liste sont exécutées simultanément.
Notez également que des informations sur le contexte de l’instruction sont passées en paramètre. Ceci est nécessaire car dans certains cas comme dans l’exécution 16 bits, les arguments n’apportent pas assez d’informa- tion, et on ne sait pas s’il faut soustraire au pointeur de pile 16 ou 32 bits.
Pour se représenter cette idée, on peut considérer une instruction comme une fonction de transfert entre un état d’entrée in et un état de sortieout. L’instructionxchg peut alors être écrit :
def x c h g ( info , a_in , b _ i n ) : e = []
e . a p p e n d ( E x p r A f f ( a_out , b _ i n ) ) e . a p p e n d ( E x p r A f f ( b_out , a _ i n ) ) r e t u r n e
Maintenant, si on effectue ces affectations séquentiellement on obtient :
a _ o u t = b _ i n b _ o u t = a _ i n
On a bien les valeurs a_out etb_out qui valent respectivement b_in eta_in, et ainsi obtenir le comportement attendu. Pour finir, à la fin de l’évaluation d’une instruction, son état de sortie devient l’état d’entrée de l’instruction suivante.
a _ i n = a _ o u t b _ i n = b _ o u t
On peut alors passer à l’évaluation de la prochaine instruction en uti- lisant le même principe.
Dans la représentation sémantique d’une instruction, chaque variable ne sera écrite qu’une fois (pour une instruction donnée). Pour que cette idée soit totalement applicable, quelques manipulations sont toutefois né- cessaires, notamment pour l’affectation dans desSlices. Par exemple, l’af- fectation à ax est normalement représentée par :
eax [ 0 : 1 6 ] = E x p r I n t ( u i n t 1 6 ( 4 2 ) )
Lors de la création de cette expression, une transformation est appli- quée et donnera :
eax = E x p r C o m p o s e ( E x p r S l i c e T o ( E x p r I n t ( u i n t 1 6 ( 4 2 ) ) , 0 , 16) ,
E x p r S l i c e T o ( eax [ 1 6 : 3 2 ] , 16 , 32) )
Ici, on voit que les bits de 0 à 15 sont composés de l’entier 42, et les bits 16 à 31 composés des bits originaux deeax.
Voilà quelques exemples de représentation d’instructions du langage x86 :
def mov ( info , a , b ) : r e t u r n [ E x p r A f f ( a , b ) ] def x c h g ( info , a , b ) :
e = []
e . a p p e n d ( E x p r A f f ( a , b ) ) e . a p p e n d ( E x p r A f f ( b , a ) ) r e t u r n e
def p u s h ( info , a ) : e = []
s = a . g e t _ s i z e ()
if not s in [ 1 6 , 3 2 ] :
r a i s e ’ bad s i z e s t a c k e r ! ’
c = E x p r O p (’ - ’, esp , E x p r I n t ( u i n t 3 2 ( s /8) ) ) e . a p p e n d ( E x p r A f f ( esp , c ) )
e . a p p e n d ( E x p r A f f ( E x p r M e m ( c , s ) , a ) ) r e t u r n e
Le mov devient une simple affectation. Le xchg est représenté par deux affectations. Le push est représenté par le stockage en mémoire de l’argument, ainsi que la mise à jour du pointeur de pile.
Mi-asm permet également la rédaction de macro-instructions pour simplifier l’implémentation d’instructions comportant beaucoup d’effets de bord :
def add ( info , a , b ) : e = []
c = E x p r O p (’ + ’, a , b ) e += u p d a t e _ f l a g _ a r i t h ( c ) e += u p d a t e _ f l a g _ a f ( c )
e += u p d a t e _ f l a g _ a d d ( a , b , c ) e . a p p e n d ( E x p r A f f ( a , c ) ) r e t u r n e
Ces macro-instructions sont implémentées avec les mêmes manipula- teurs ; Par exemple, pourupdate_flag_zf :
def u p d a t e _ f l a g _ z f ( a ) :
c a s t _ i n t = t a b _ u i n t s i z e [ a . g e t _ s i z e () ]
r e t u r n [ E x p r A f f ( zf , E x p r O p (’ == ’, a , E x p r I n t ( c a s t _ i n t (0) ) ) ) ]
Utilisation simple On peut alors très simplement retrouver pour une instruction donnée les registres lus/écrits :
f r o m m i a s m . a r c h . i a 3 2 _ s e m i m p o r t * def g e t _ r w ( e x p r s ) :
o_r = set () o_w = set () for e in e x p r s :
o_r . u p d a t e ( e . g e t _ r () ) for e in e x p r s :
o_w . u p d a t e ( e . g e t _ w () ) r e t u r n o_r , o_w
a = E x p r I d (’ eax ’)
b = E x p r M e m ( E x p r I d (’ ebx ’) , 32) e x p r s = add ((’ u32 ’, ’ u32 ’) , a , b ) o_r , o_w = g e t _ r w ( e x p r s )
# r e a d ID
p r i n t [ str ( x ) for x in o_r ]
# [ ’ eax ’ , ’ @32 [ ebx ] ’]
# w r i t t e n ID
p r i n t [ str ( x ) for x in o_w ]
# [ ’ eax ’ , ’ pf ’ , ’ af ’ , ’ of ’ , ’ zf ’ , ’ cf ’ , ’ nf ’]
Un algorithme simple peut permettre alors de retrouver le flot de don- née d’unbasic bloc5. Par exemple pour :
0 mov d w o r d ptr [0 x F F F F F F E 4 + ebp ] , eax 1 lea eax , d w o r d ptr [ eax +0 x 0 0 0 0 0 0 0 C +4* edi ] 2 mov d w o r d ptr [0 x F F F F F F E C + ebp ] , eax 3 c a l l l o c _ 0 0 0 0 0 0 0 0 0 1 0 0 7 B C 1
On obtient le graphe suivant :
1_ds:@32[(0xFFFFFFE4 + ebp)]
0 _ e a x
0 mov dword ptr [0xFFFFFFE4+ebp], eax 1 l e a e a x , d w o r d p t r [ e a x + 0 x 0 0 0 0 0 0 0 C + 4 * e d i ]
1 _ e a x
2 mov dword ptr [0xFFFFFFEC+ebp], eax
1_@32[(esp + 0xFFFFFFFC)]
0 _ e d i 0 _ e b p
1 _ e i p
1_ds:@32[(0xFFFFFFEC + ebp)]
0 _ e s p
3 call loc_0000000001007BC1
1 _ e s p
D’autres algorithmes peuvent être utilisés ici, comme l’analyse de re- gistres morts, la détection des variables locales, la dépendance de don- nées inter-blocs, la détermination de la nature des arguments d’une fonc- tion, . . ..
3.2 API de manipulations du langage intermédiaire
Évaluation symbolique L’exécution symbolique deMiasm peut servir à trouver la fonction de transfert d’un basic bloc. Cette fonction donne l’équation qui lie les registres et la mémoire après l’exécution d’un basic bloc à l’état des registres et de la mémoire en entrée de ce bloc.
Pour cela,Miasmpart d’une machine dont l’état est représenté par un dictionnaire. Les clefs de ce dictionnaire peuvent être soit des identifiants (ExprId), soit des déréférencements mémoire (ExprMem). On associe à chaque clef l’expression représentant sa valeur.
Par exemple, dans le dictionnaire suivant :
5. Un basic bloc est une suite de lignes assembleurs dont aucun flot d’exécution n’arrive entre ces instructions, et dont le seul flot d’exécution sortant ne peut être que de sa dernière instruction
E x p r I d (’ eax ’) : E x p r I d (’ i n i t \ _ e a x ’) E x p r I d (’ ebx ’) : E x p r I n t (4)
E x p r M e m (’ e a x _ i n i t ’+ E x p r I n t ( 4 2 ) ) : E x p r I n t ( 1 3 3 7 )
Le registre eax vaut eax_init; le registreebx vaut l’entier 4 et la case mémoire pointée par eax_init+42 vaut l’entier 1337.
Pour émuler une instruction, Miasm applique les transformations dé- crites par la sémantique de cette instruction à l’état de la machine, et le met à jour. L’émulation de la prochaine instruction se fait de même en partant de ce nouvel état.
L’exécution symbolique d’un basic bloc complet est l’exécution sym- bolique répétée sur les instructions qui le composent. On obtient l’état de la machine en sortie de bloc, qui lie les registres et les cases mémoire à leurs valeurs en entrée de bloc.
Miasm implémente la sémantique de la plupart des instructions x86, ainsi qu’une petite partie de l’architecture ARM.
Voilà un code qui va désassembler un bloc d’un binaire de type PE, et l’exécuter symboliquement :
i m p o r t sys
f r o m m i a s m . a r c h . i a 3 2 _ a r c h i m p o r t * f r o m m i a s m . t o o l s . e m u l _ h e l p e r i m p o r t *
f r o m m i a s m . c o r e . b i n _ s t r e a m i m p o r t b i n _ s t r e a m e = p e _ i n i t . PE ( o p e n ( sys . a r g v [ 1 ] ) . r e a d () ) i n _ s t r = b i n _ s t r e a m ( e . v i r t )
j o b _ d o n e = set ()
s y m b o l _ p o o l = a s m b l o c . a s m _ s y m b o l _ p o o l () l = a s m b l o c . a s m _ l a b e l (’ t o t o ’)
b = a s m b l o c . a s m _ b l o c ( l ) ad = 0 x 1 0 1 2 0 f a
a s m b l o c . d i s _ b l o c ( x86_mn , in_str , b , ad , j o b _ d o n e , s y m b o l _ p o o l ) p r i n t b
m a c h i n e = x 8 6 _ m a c h i n e () e m u l _ b l o c ( machine , b )
p r i n t d u m p _ r e g ( m a c h i n e . p o o l ) p r i n t d u m p _ m e m ( m a c h i n e . p o o l )
et voilà le résultat :
# c o d e a s s e m b l e u r
"""
xor ecx , ecx
mov ebx , 0 x 0 0 0 0 1 3 3 7
p u s h eax
add eax , 0 x 0 0 0 0 0 0 0 5
pop eax
"""
# R e s u l t a t eax = i n i t _ e a x ebx = 0 0 0 0 1 3 3 7 ecx = 0 0 0 0 0 0 0 0 edx = i n i t _ e d x ...
zf = (( i n i t _ e a x + 0 x5 ) == 0 x0 )
L’exemple choisi met en œuvre plusieurs mécanismes :
– le registreedx n’est pas touché par ce basic bloc, il conserve donc sa valeur d’origineinit_edx
– le registre ebx est bien positionné à la constante 0x1337
– comme le registre ecx est XORé avec lui-même, son équation devrait être ecx =ecx_init ∧ ecx_init. Le moteur de simplification réduit cette expression à 0. La valeur finale deecx est donc 0.
– pour finir, le registre eax est poussé sur la pile, puis on lui ajoute 5 ; Son équation est alors eax = eax_init + 5. Mais plus loin, le pop eax repositionne sa valeur à sa valeur d’origine. Son équation finale est donceax =eax_init. On retrouve donc bien automatiquement le fait que lepush eax/pop eax revient à ne rien faire, modulo les effets de bords générés par des instructions encadrées par lespush/pop – on note donc que le zero flag positionné par le add prend bien en
compte la nullité de eax_init + 5
Moteur de simplification Miasm inclut également un moteur de sim- plification d’expressions. Ce dernier est une suite de règles de réductions simples, qui seront appliquées si leurs conditions d’applications sont res- pectées. Par exemple :
# A + 0 = > A
if op in [’ + ’, ’ - ’, ’ | ’, " ^ ", " < < ", " > > "]:
if i s i n s t a n c e ( a r g s [1] , E x p r I n t ) and a r g s [ 1 ] . arg == 0:
r e t u r n e x p r _ s i m p ( a r g s [ 0 ] )
# (( a > > > b ) < < < b ) = > a
if op in [’ < < < ’, ’ > > > ’] and i s i n s t a n c e ( a r g s [0] , E x p r O p ) and a r g s [ 0 ] . op in [’ < < < ’, ’ > > > ’] and a r g s [1] == a r g s [ 0 ] . a r g s [ 1 ] :
if ( op , a r g s [ 0 ] . op ) in [(’ < < < ’, ’ > > > ’) , (’ > > > ’, ’ < < < ’) ]:
e = e x p r _ s i m p ( a r g s [ 0 ] . a r g s [ 0 ] ) r e t u r n e
La description de ces simplifications se fait pour le moment en python, mais l’utilisation d’un module spécialisé tiers ou bien l’utilisation d’un langage permettant des desciptions simples est à l’étude.
Voilà une illustration d’utilisation6 :
> > > f r o m m i a s m . a r c h . i a 3 2 _ s e m i m p o r t *
> > > f r o m m i a s m . e x p r e s s i o n . e x p r e s s i o n _ h e l p e r i m p o r t *
> > > a = E x p r I d (’ eax ’)
> > > b = E x p r I d (’ ebx ’)
> > > c = a + b
> > > p r i n t c ( eax + ebx )
> > > d = c - a
> > > p r i n t d
(( eax + ebx ) - eax )
> > > p r i n t e x p r _ s i m p ( d ) ebx
> > > e = E x p r I n t ( u i n t 3 2 (0 x12 ) ) + E x p r I n t ( u i n t 3 2 (0 x30 ) ) - a
> > > p r i n t e
((0 x12 + 0 x30 ) - eax )
> > > p r i n t e x p r _ s i m p ( e ) (0 x42 - eax )
À ce mécanisme de simplification, est ajouté (si on le désire) un mo- dule permettant d’utiliser certaines heuristiques pour simplifier le code résultant d’une émulation symbolique. Par exemple, en x86, tout objet situé au delà du pointeur de pile n’a normalement plus de raison d’être7. Pour détecter ces variables, le moteur recherche après chaque émula- tion symbolique d’une instruction les variables mémoires référencées par un pointeur de pile, et soustrait le pointeur de pile courant à cette adresse :
def d e l _ a b o v e _ s t a c k ( state , e s p _ v a l = N o n e ) : if e s p _ v a l == N o n e :
e s p _ v a l = s t a t e [ esp ] kk = s t a t e . k e y s ()
for k in kk :
if not i s i n s t a n c e ( k , E x p r M e m ) : c o n t i n u e
e _ d i f f = e x p r _ s i m p ( E x p r O p (’ - ’, e x p r _ s i m p ( k . arg ) , e x p r _ s i m p ( e s p _ v a l ) ) )
m a c h i n e = e v a l _ a b s ( state ,
m e m _ r e a d _ w r a p , m e m _ w r i t e _ w r a p , )
ee = m a c h i n e . e v a l _ e x p r ( e_diff , {}) ee = e x p r _ s i m p ( ee )
if not i s i n s t a n c e ( ee , E x p r I n t ) : c o n t i n u e
if i n t 3 2 ( ee . arg ) < 0:
del ( s t a t e [ k ])
6. Le lecteur d’un certain âge pourra faire le rapprochement avec les calculs sym- boliques desTI-92
7. sauf dans certains obscurcissements où leur utilisation déroute des analyseurs automatiques
Par exemple, si après l’exécution symbolique d’une instruction l’état de la machine est :
esp = E x p r O p (’ + ’, E x p r I d ( i n i t _ e s p ) , E x p r I n t (0 x4 ) )
c a s e _ m e m o i r e = E x p r M e m ( E x p r O p (’ - ’, E x p r I d ( i n i t _ e s p ) , E x p r I n t (0 x10 ) ) )
alors le code tentera d’évaluer l’expression représentée par la soustrac- tion du pointeur mémoire et de esp :
E x p r O p (’ - ’, E x p r I d ( i n i t _ e s p ) , E x p r I n t (0 x10 ) ) - E x p r O p (’ + ’, ( E x p r I d ( i n i t _ e s p ) , E x p r I n t (0 x4 ) ) )
Ce qui donnera après simplification :
-0 xC
Ceci permet d’ordonner deux expressions symboliques, et voir que la case mémoire est située au dessus du pointeur de pile et doit donc être supprimée.
Bonus : moteur deJust-in-time compilation Pour valider la correc- tion de l’implémentation de la sémantique du x86, ainsi que sa couverture, l’idée d’implémenter un émulateur s’est imposée d’elle même. Pour cela, Miasm opère les opérations suivantes :
– il désassemble le x86.
– il le traduit en langage intermédiaire en utilisant la description de la sémantique de chaque instruction.
– le langage intermédiaire est alors passé à un Backend générant du C à la volée
– ce code C est ensuite compilé à l’aide de TCC.
le code C généré applique les effets de bord qu’aurait eu l’instruc- tion sur les registres et la mémoire, émulant donc son comportement. Ce mécanisme est effectué sur desbasic blocs.
Si le résultat donné par un programme exécuté sur un vrai CPU et le résultat d’un programme émulé par ce mécanisme sont identiques, on sait que l’émulation s’est bien passée, et donc que la sémantique décrite dans Miasm est valide.
Pour le x86, ceci a été testé sur des programmes joués, ainsi que sur de vrais virus. Certains de ces virus étaient également packés/obscurcis (upx/aspack/expression/...). Ces tests ont permis de vérifier une large palette d’instructions du x86.
La figure 2 est un graphique représentant cette mécanique interne.
Exécute un bloc
désassemble
langage intermédiaire
Code C
module Python Suppr bloc
lib émulée
bloc inconnu bloc connu
code automo- difiant bibliothèque
Figure2. Mécanisme d’émulation concrète
Cet émulateur est également associé àElfesteemqui permet de mapper un ELF/PE en mémoire. Les fonctions simples des bibliothèques utilisées par les programmes visés sont émulées en python. Ce mécanisme est dé- taillé plus tard et notamment son utilisation dans l’unpacking/analyse de malware.
L’émulateur est au final un émulateur de CPU scriptable en python.
Néanmoins, certains mécanismes de l’OS peuvent et ont été implémentés, par exemple le mécanismeSEH de rattrapage d’erreurs sous Windows, un bout du mécanisme de chaînage des bibliothèques chargées en mémoire, . . . 4 Exemples d’utilisation de Miasm
Cette partie décrit le mécanisme de traduction de code à la volée en utilisant le langage intermédiaire Mi-asm. Ceci permet par exemple l’exécution cloisonnée de code.
4.1 Exemple 1 : émulation d’un malware à partir du langage intermédiaire
On va utiliser ici le module d’émulation de Miasm dans le but de faire de l’étude de malwares.Miasm intègre un gestionnaire de mémoire virtuelle qui permet de manipuler les plages mémoires du programme visé.
La mémoire du programme émulé est réservée sur le système hôte. Un mécanisme de traduction d’adresses est mis en place pour transformer
les adressages du programme émulé vers la mémoire de l’hôte. Les pages mémoire émulées ont exactement les mêmes propriétés que celles d’un système d’exploitation (Read, Write, Execute). Si le programme émulé sousMiasm écrit dans une page en lecture seule,Miasm déclenchera une faute.
Miasmremplit le même travail que leloaderdu système d’exploitation.
Pour charger le binaire cible, il parse les sections du binaire (contenant le code, les données, . . .) et va créer des pages mémoire sur le système hôte représentant ces sections.
Un binaire a en général besoin de bibliothèques pour assurer son bon déroulement.Miasm peut opérer de façon identique pour charger ces bi- bliothèques dans la mémoire émulée du programme cible.
L’émulation des bibliothèques pouvant être lourde (allant parfois jus- qu’aukernel), on peut utiliser un autre mécanisme : un programme qui a besoin de bibliothèques embarque une liste des noms et des fonctions de ces bibliothèques (c’est cette liste qui est utilisée par le loader pour sa- voir quoi charger). Il décrit également où placer les adresses des fonctions nécessaires lorsqu’elles seront résolues par le linker. Miasm note toutes ces informations. L’astuce est de conserver la correspondance entre ces adresses et les noms des fonctions qu’elles représentent, ce qui servira plus tard lors de l’émulation : quand le pointeur d’instruction vaudra une de ces adresses, l’émulateur aura le choix entre émuler cette fonction ou appeler uncallback d’une fonction python qui simulera le comportement de cette fonction. Elle doit prendre les arguments sur la pile, faire son traitement faire pointer l’adresse de la prochaine instruction à exécuter sur l’adresse de retour de la fonction.
Voilà un exemple de fonction simulée en python :
def k e r n e l 3 2 _ G e t V e r s i o n () : r e t _ a d = v m _ p o p _ u i n t 3 2 _ t () r e g s = v m _ g e t _ g p r e g () r e g s [’ eip ’] = r e t _ a d
r e g s [’ eax ’] = w i n _ a p i . g e t v e r s i o n v m _ s e t _ g p r e g ( r e g s )
L’exemple suivant est unschellcodeMetasploit. On peut observer dans les logs les appels des API Windows de ceshellcode :
– le chargement de la bibliothèque ws2_32.dll
– la création d’unesocket (appel à ws2_32_WSASocketA) – la connexion vers le site malveillant (appel à ws2_32_connect)
Dans le code d’exemple, on simule les fonctions de connexion et d’envoi et réception de données. On arrive ainsi à simuler le shellcode jusqu’au téléchargement et l’exécution du secondstage.
s t a r t e m u l a t i o n 0 x 4 0 1 0 a 2
k e r n e l 3 2 _ L o a d L i b r a r y A 0 x 4 0 1 0 a 2 0 x 1 2 3 f f e c
’ w s 2 _ 3 2 ’
w s 2 _ 3 2 _ W S A S t a r t u p 0 x 4 0 1 0 b 2
w s 2 _ 3 2 _ W S A S o c k e t A 0 x 4 0 1 0 c 1 0 x2 0 x1 0 x0 0 x0 0 x0 0 x0 w s 2 _ 3 2 _ c o n n e c t 0 x 4 0 1 0 d b 0 x 1 3 3 7 0 x 1 2 3 f e 4 c 0 x10
’ \ x02 \ x00 \ x11 \\\ xc0 \ xa8 \ x01 \ x01 \ x05 \ x00 \ x00 \ x00 \\\ xfe #\ x01 ... ’ 0 x 4 0 1 0 f 8
w s 2 _ 3 2 _ r e c v 0 x 4 0 1 0 f 8 0 x 1 3 3 7 0 x 1 2 3 f e 4 c 0 x4 0 x0 ...
4.2 Exemple 2 : émulation d’un Bootloader
Le but est ici d’analyser le déroulement d’un bootloader, pour valider l’absence de malwares pouvant infecter leMBR. Nous allons utiliser uti- liser le module d’émulation de Miasm, comme dans la partie précédente.
Quelques différences sont notables : il n’y a pas ici de fonctions de bibliothèques à simuler mais les services du BIOS de la machine ; L’émula- tion se déroulera en 16 bits et il faut également supporter la segmentation.
Lebootloader n’a pas non plus de descripteurs de section comme dans un binaire PE/ELF. Pour savoir comment charger lebootloader en mémoire, on peut se référer au comportement d’unPC, à savoir charger le premier secteur du disque dur à l’adresse 0x7C00.
Voilà la petite partie de code responsable de la simulation de l’inter- ruption 0x13, et notamment le service 0x41, qui permet de demander au BIOS les informations de géométrie du disque dur de la machine.
Dans cette fonction, il faut entre autre récupérer les paramètres tel que le numéro du disque dur physique dans les registres du programme émulé.
Il faut alors positionner ces mêmes registres comme le feraient les services du BIOS de façon à renvoyer une information sur un disque factice.
def d e a l _ d i s k _ c a l l () : s e c t o r _ s i z e = 512 c y l _ n u m = 1
r e g s = v m _ g e t _ g p r e g () s e g m s = v m _ g e t _ s e g m ()
bx = ( r e g s [’ ebx ’] ) & 0 x F F f f f u n c = ( r e g s [’ eax ’] > > 8) & 0 xFF d r v _ i n d e x = ( r e g s [’ edx ’] ) & 0 xFF
p r i n t ’ D I S K A C C E S S ’, ’ f u n c ’, hex ( f u n c ) , ’ drv ’, d r v _ i n d e x , hex ( f u n c ) ,
p r i n t ’ ds : si ’, hex ( s e g m s [’ ds ’]) , hex ( r e g s [’ esi ’]) , ’ es ’, hex ( s e g m s [’ es ’])
if f u n c == 0 x41 :
p r i n t " D I S K GET EXT "
if bx != 0 x 5 5 A A :
r a i s e V a l u e E r r o r (’ bad f u n c f o r m a t ’) r e g s [’ cf ’] = 0
r e g s [’ ebx ’] = 0 x a a 5 5 r e g s [’ ecx ’] = 0 x1 r e g s [’ eax ’] = 0 x 1 0 0 r e g s [’ eip ’] +=2 v m _ s e t _ g p r e g ( r e g s ) ...
Après avoir simulé la partie du BIOS qui charge le premier secteur du disque en mémoire et place le pointeur d’instruction en 0x7C00, l’émula- tion peut commencer, on a alors les logs :
D I S K A C C E S S f u n c 0 x42 drv 128 0 x42 ds : si 0 x0 0 x 7 b d e es 0 x0 D I S K R E A D s i z e 0 x10 rez 0 x0 n u m _ s e c t _ t o _ r e a d 0 x1 ptr 0 x 7 c 0 0
s e e k _ s e c t 0 x3f r e a d 0 x 2 0 0
0 x 7 c 0 0
’ \ x e b R \ x 9 0 N T F S \ x00 \ x02 \ x08 \ x00 \ x00 \ x00 \ x00 \ x00 \ x00 \ x00 \ xf8 ...
...
D I S K A C C E S S f u n c 0 x42 drv 128 0 x42 ds : si 0 x0 0 x 7 b c 8 es 0 x d 2 0 D I S K R E A D s i z e 0 x10 rez 0 x0 n u m _ s e c t _ t o _ r e a d 0 x1 ptr 0 x d 2 0 0 0 0 0
s e e k _ s e c t 0 x40 r e a d 0 x 2 0 0
0 x d 2 0 0
’\ x05 \ x 0 0 N \ x 0 0 T \ x 0 0 L \ x 0 0 D \ x 0 0 R \ x00 \ x04 \ x 0 0 $ \ x 0 0 I \ x 0 0 3 ...
D I S K A C C E S S f u n c 0 x42 drv 128 0 x42 ds : si 0 x0 0 x c f a 2 es 0 x d 0 0 D I S K R E A D s i z e 0 x10 rez 0 x0 n u m _ s e c t _ t o _ r e a d 0 x1 ptr 0 x d 0 0 3 0 0 0
s e e k _ s e c t 0 x 6 0 0 0 3 f r e a d 0 x 2 0 0
0 x 1 0 0 0 0
’ F I L E 0 \ x00 \ x03 \ x 0 0 Z \ x14 \ x95 \ x08 \ x00 \ x00 \ x00 \ x00 \ x01 \ x00 \ x01 \ x 0 0 8
Bien évidement, l’émulation complète d’un système d’exploitation est difficilement envisageable : cela nécessiterait l’implémentation de tous les périphériques d’un PC.8
4.3 Exemple 3 : analyse statique utilisant le langage intermédiaire
Cette partie illustre l’utilisation du langage intermédiaire dans le désobscurcissement de programmes malveillants.
8. De plus des outils plus adaptés comme Ramooflax permettent le débuggage de ces systèmes à tous les niveaux
Pour cela, on va utiliser l’exécution symbolique pour extraire des pro- priétés sur le code obscurci : des informations comme les registres morts, la hauteur de pile, et l’utilisation de calculs menant toujours à des constantes permettront de simplifier et de comprendre le code original. L’exécution symbolique utilise le mécanisme décrit en3.2.
Quelques heuristiques sont toutefois apportées à ce principe : la pile est naturellement représentée dans l’état mémoire par des cases mémoires (indexées par une expression basée sur esp), auxquelles sont associées les valeurs poussées. Cependant, après l’exécution de chaque instruction, une procédure s’assure qu’aucun objet mémoire adressé avec esp n’est au dessus de la valeur courante de esp, auquel cas il est détruit. Cette heuristique modélise le comportement d’une pile.
L’exemple suivant concerne l’étude d’une machine virtuelle. Des tra- vaux ont déjà été faits dans ce domaine en utilisant un langage intermé- diaire. Un très bon article est celui de [5]. L’exemple décrit suit les pistes de ce document en utilisant les capacités deMiasm.
L’utilisation du langage intermédiaire se fera à travers l’étude d’un malware protégé avec une couche de code virtualisé. On peut remarquer que le code est obscurci, ce qui rend son analyse manuelle fastidieuse :
...
0 x 9 5 1 c 6 2 l o d s b b y t e ptr ds :[ esi ] 0 x 9 5 1 c 6 3 xor al , bl
0 x 9 5 1 c 6 5 p u s h dx
0 x 9 5 1 c 6 7 mov w o r d ptr [ esp ] , cx 0 x 9 5 1 c 6 b mov ch , 0 x 0 0 0 0 0 0 A 4 0 x 9 5 1 c 6 d xor al , ch
0 x 9 5 1 c 6 f jmp l o c _ 0 0 0 0 0 0 0 0 0 0 9 5 6 1 6 9 0 x 9 5 6 1 6 9 mov cx , w o r d ptr [ esp ] 0 x 9 5 6 1 6 d p u s h esi
0 x 9 5 6 1 6 e p u s h 0 x 0 0 0 0 5 5 8 9
0 x 9 5 6 1 7 3 mov d w o r d ptr [ esp ] , ebp 0 x 9 5 6 1 7 6 mov ebp , esp
0 x 9 5 6 1 7 8 add ebp , 0 x 0 0 0 0 0 0 0 4 0 x 9 5 6 1 7 e sub ebp , 0 x 0 0 0 0 0 0 0 4 ...
0 x 9 6 5 2 b 0 x c h g ebp , d w o r d ptr [ esp ]
0 x 9 6 5 2 b 3 pop esp
0 x 9 6 5 2 b 4 m o v z x eax , al
0 x 9 6 5 2 b 7 jmp d w o r d ptr [4* eax + edi ]
En combinant l’émulation symbolique et le moteur de simplification, on obtient la fonction de transfert du bloc. Voilà les valeurs des registres ainsi que les cases mémoires touchées :
eax ( ( ( ( @8 [( @32 [ i n i t _ e s p ] - 0 x 9 A A F F E C ) ] ^ @8 [ i n i t _ e s p ]) ^ 0 xA4 ) + 0 x15 ) _to [0:8] , 0 x 0 _ t o [ 8 : 3 2 ] )
ebx ((( @8 [ i n i t _ e s p ] - ((( @8 [( @32 [ i n i t _ e s p ] - 0 x 9 A A F F E C ) ] ^ @8 [ i n i t _ e s p ]) ^ 0 xA4 ) + 0 x62 ) ) + 0 x4D ) _to [0:8] , @32 [ i n i t _ e s p ] [ 8 : 3 2 ] _to [ 8 : 3 2 ] )
ecx 0 0 0 0 0 0 0 1 edx i n i t _ e d x
esi ( @32 [ i n i t _ e s p ] - 0 x 9 A A F F E B ) edi 0 0 9 5 0 E8C
esp ( i n i t _ e s p - 0 x24 ) ebp i n i t _ e b p
eip @32 [ ( ( ( ( ( ( @8 [( @32 [ i n i t _ e s p ] - 0 x 9 A A F F E C ) ] ^ @8 [ i n i t _ e s p ]) ^ 0 xA4 ) + 0 x15 ) _to [0:8] , 0 x 0 _ t o [ 8 : 3 2 ] ) * 0 x4 ) + 0 x 9 5 0 E 8 C ) ]
On voit sur la sortie un empilement de tous les registres x86 qui est ca- ractéristique du passage du monde x86 au monde de la machine virtuelle.
Ils seront modifiés plus tard par l’exécution successive des mnémoniques virtuelles :
@32 [( i n i t _ e s p - 0 x8 ) ] i n i t _ e a x
@32 [( i n i t _ e s p - 0 x24 ) ] i n i t _ e d i
@32 [( i n i t _ e s p - 0 x1C ) ] i n i t _ e b p
@32 [( i n i t _ e s p - 0 x14 ) ] i n i t _ e b x
@32 [( i n i t _ e s p - 0 x4 ) ] 0 x 2 0 2
@32 [( i n i t _ e s p - 0 xC ) ] i n i t _ e c x
@32 [( i n i t _ e s p - 0 x10 ) ] i n i t _ e d x
@32 [( i n i t _ e s p - 0 x20 ) ] i n i t _ e s i
@32 [( i n i t _ e s p - 0 x28 ) ] i n i t _ e d x
@32 [( i n i t _ e s p - 0 x30 ) ] ( i n i t _ e s p - 0 x28 )
@32 [( i n i t _ e s p - 0 x18 ) ] ( i n i t _ e s p - 0 x14 )
@32 [( i n i t _ e s p - 0 x2C ) ] ( i n i t _ e s p - 0 x24 )
@32 [( i n i t _ e s p - 0 x38 ) ] 0 x1
@32 [( i n i t _ e s p - 0 x3C ) ] 0 x E 0 4 0 8 8 2 3
@32 [( i n i t _ e s p - 0 x34 ) ] ( i n i t _ e s p - 0 x30 )
Ici la condition de terminaison de l’analyseur est conditionnée par la résolution du pointeur d’instruction. Si un obscurcissement introduit un faux saut conditionnel, l’analyseur9 saura résoudre sa condition (par exemple par propagation de constante) et trouvera l’adresse du prochain pointeur d’instruction. Dans le cas contraire, il s’arrête et affiche son équa- tion.
Ici, une rapide analyse du pointeur d’instruction montre qu’il est de la formeeip= @32[base_address+ 4∗X]. Il s’agit de la fonction quiparse les opcodes des mnémoniques de la machine virtuelle et qui va utiliser ceci comme une table de saut pour exécuter le code responsable de l’émulation de cette mnémonique : on pourra alors extraire ce tableau pour analyser chaque mnémonique individuellement.
La partie de code responsable du parsing des instructions a les effets de bords suivant :
9. s’il est assez performant
eax = ( ( ( ( @8 [ i n i t _ e s i ] ^ i n i t _ e b x [ 0 : 8 ] ) ^ 0 xA4 ) + 0 x15 ) _to [0:8] , 0 x 0 _ t o [ 8 : 3 2 ] )
ebx = ((( i n i t _ e b x [ 0 : 8 ] - ((( @8 [ i n i t _ e s i ] ^ i n i t _ e b x [ 0 : 8 ] ) ^ 0 xA4 ) + 0 x62 ) ) + 0 x4D ) _to [0:8] , i n i t _ e b x [ 8 : 3 2 ] _to [ 8 : 3 2 ] )
ecx = i n i t _ e c x edx = i n i t _ e d x
esi = ( i n i t _ e s i + 0 x1 ) edi = i n i t _ e d i
esp = i n i t _ e s p ebp = i n i t _ e b p
zf = ( i n i t _ e s p == 0 x0 )
@32 [( i n i t _ e s p - 0 xC ) ] ( i n i t _ e s p - 0 x4 )
@32 [( i n i t _ e s p - 0 x8 ) ] i n i t _ e s p
@32 [( i n i t _ e s p - 0 x14 ) ] i n i t _ e c x
@32 [( i n i t _ e s p - 0 x4 ) ] i n i t _ e d x
@32 [( i n i t _ e s p - 0 x10 ) ] ( i n i t _ e s p - 0 xC )
eip = @32 [ ( ( ( ( ( ( @8 [ i n i t _ e s i ] ^ i n i t _ e b x [ 0 : 8 ] ) ^ 0 xA4 ) + 0 x15 ) _to [0:8] , 0 x 0 _ t o [ 8 : 3 2 ] ) * 0 x4 ) + i n i t _ e d i ) ]
Par des raisons de clarté, on supprimera les registres qui ont une valeur en sortie de bloc égale à leur valeur en entrée de bloc, par exempleeax = init_eax. De plus, on supprimera les cases mémoire obsolètes de la pile en utilisant l’heuristique décrite précédemment, ce qui donne ici :
eax = ( ( ( ( @8 [ i n i t _ e s i ] ^ i n i t _ e b x [ 0 : 8 ] ) ^ 0 xA4 ) + 0 x15 ) _to [0:8] , 0 x 0 _ t o [ 8 : 3 2 ] )
ebx = ((( i n i t _ e b x [ 0 : 8 ] - ((( @8 [ i n i t _ e s i ] ^ i n i t _ e b x [ 0 : 8 ] ) ^ 0 xA4 ) + 0 x62 ) ) + 0 x4D ) _to [0:8] , i n i t _ e b x [ 8 : 3 2 ] _to [ 8 : 3 2 ] )
esi = ( i n i t _ e s i + 0 x1 )
eip = @32 [ ( ( ( ( ( ( @8 [ i n i t _ e s i ] ^ i n i t _ e b x [ 0 : 8 ] ) ^ 0 xA4 ) + 0 x15 ) _to [0:8] , 0 x 0 _ t o [ 8 : 3 2 ] ) * 0 x4 ) + i n i t _ e d i ) ]
On peut noter plusieurs choses : – edi n’est pas modifié
– eax est mort, c’est-à-dire qu’il est écrit sans être lu
– esi est incrémenté, et utilisé après déréférencement dans le calcul du prochain eip. Ceci pourrait être le program counter de la machine virtuelle
– ebx est une clef qui évolue, permettant de déchiffrer l’instruction courante.
Connaissant ces informations, on peut appliquer le même mécanisme pour retrouver la sémantique des mnémoniques de la machine virtuelle.
Ici, la mnémonique :
0 x 9 5 b 2 c e mov ecx , d w o r d ptr [ esp ] 0 x 9 5 b 2 d 1 p u s h 0 x 0 0 0 0 1 C B 5
0 x 9 5 b 2 d 6 mov d w o r d ptr [ esp ] , ebp 0 x 9 5 b 2 d 9 p u s h 0 x 0 0 0 0 6 E C 5
0 x 9 5 b 2 d e mov d w o r d ptr [ esp ] , esp 0 x 9 5 b 2 e 1 p u s h edx
0 x 9 5 b 2 e 2 mov edx , 0 x 0 0 0 0 0 0 0 4
0 x 9 5 f 3 9 c add d w o r d ptr [ esp +0 x 0 0 0 0 0 0 0 4 ] , edx
0 x 9 6 1 6 9 5 pop edx
...
0 x 9 5 7 d d 2 x c h g ebp , d w o r d ptr [ esp ]
0 x 9 5 7 d d 5 pop esp
0 x 9 5 7 d d 6 mov d w o r d ptr [ esp ] , edi 0 x 9 5 7 d d 9 p u s h d w o r d ptr [ esp +0 x 0 0 0 0 0 0 0 4 ] 0 x 9 5 7 d d d mov edi , d w o r d ptr [ esp ] 0 x 9 5 d 4 4 e add esp , 0 x 0 0 0 0 0 0 0 4 0 x 9 6 1 5 f 5 pop d w o r d ptr [ esp ] 0 x 9 6 1 5 f 8 mov esp , d w o r d ptr [ esp ] 0 x 9 6 1 5 f b mov d w o r d ptr [ esp ] , edx 0 x 9 5 3 b d 6 p u s h eax
0 x 9 5 3 b d 7 p u s h f d
0 x 9 5 3 b d 8 jmp l o c _ 0 0 0 0 0 0 0 0 0 0 9 5 1 C 6 2
se simplifie en :
ecx = @32 [ i n i t _ e s p ]
edx = ( @32 [( i n i t _ e s p + 0 x4 ) ] u m u l 3 2 _ h i @32 [ i n i t _ e s p ]) esp = ( i n i t _ e s p - 0 x4 )
@32 [( i n i t _ e s p - 0 x4 ) ] = ( ( ( 0 x1 == ( ( ( ( i n i t _ e s p - 0 x4 ) ^ 0 x4 ) ^ i n i t _ e s p ) > > ...
@32 [( i n i t _ e s p + 0 x4 ) ] = ( @32 [( i n i t _ e s p + 0 x4 ) ] u m u l 3 2 _ h i @32 [ i n i t _ e s p ])
@32 [ i n i t _ e s p ] = ( @32 [( i n i t _ e s p + 0 x4 ) ] u m u l 3 2 _ l o @32 [ i n i t _ e s p ])
En analysant les effets de bord de cette mnémonique, on peut voir : – la pile a augmenté de 4 octets ;
– les deux avant-derniers éléments sur la pile correspondent aux par- ties haute et basse d’une multiplication 32 bits ;
– le dernier élément de la pile représente le eflags, c’est-à-dire les bits représentant les états du dernier calcul (négatif, nul, ...).
Les opérandes de la multiplication (@32[init_esp] et@32[(init_esp + 0x4)]) proviennent des deux éléments de tête de la pile en entrée de bloc.
Nous pouvons en déduire que la machine virtuelle est une machine à pile.
Tous les calculs ont le même schéma :
– empilement des opérandes par l’instruction précédente ;
– dépilement, calcul de l’opération, empilement des résultats, ainsi que dueflag résultant.
Pour cette mnémonique, on a donc le pseudo-code suivant :
pop a pop b c = a * b
p u s h h i p a r t ( c ) p u s h l o w p a r t ( c ) p u s h e f l a g s
Voilà une deuxième mnémonique :
esp = ( i n i t _ e s p - 0 x2 )
@32 [( i n i t _ e s p - 0 x2 ) ] = (0 x 2 _ t o [0:2] , ( p a r i t y ( @8 [( i n i t _ e s p + 0 x2 ) ]
^ @8 [ i n i t ...
@8 [( i n i t _ e s p + 0 x2 ) ] = ( @8 [( i n i t _ e s p + 0 x2 ) ] ^ @8 [ i n i t _ e s p ])
Ici, il calcule un XOR sur deux éléments 16 bits. Voilà le pseudo-code :
p o p 1 6 a p o p 1 6 b p u s h 1 6 a ^ b p u s h 1 6 e f l a g s
Le dernier travail serait, comme souligné dans le document [5], de retraduire les mnémoniques simplifiées dans le langage x8610.
5 Conclusion
Après avoir succinctement décrit les divers modules de Miasm, nous nous sommes attardés sur la description de son langage intermédiaire ainsi que les divers composants permettant de l’exploiter. Celui-ci permet de représenter un code assembleur de façon plus simple et plus utilisable pour un analyseur de code.
Les exemples fournis ici illustrent le moteur d’émulation basé sur la traduction à la volée des instruction x86 en utilisant leurs sémantiques, ainsi que l’utilisation du langage intermédiaire et le désobscurcissement de code.
L’utilisation du langage intermédiaire promet de grandes possibilités pour automatiser l’analyse de code dans ces domaines, mais aussi dans la recherche de vulnérabilités.
Les futures directions de recherche s’orienteront surtout dans des al- gorithmes répondant à des questions simples, comme la découverte des variables locales d’une fonction et la distinction entre entier et pointeur.
Ces algorithmes pourront s’appuyer sur les briques de base décrites dans ce document.
Références
1. . Radare. radare.org, 2007.
2. David brumley and Ivan Jager. BAP intermediate langage.bap.ece.cmu.edu/doc/
bap.pdf, 2011.
10. Et permettant ainsi de retomber dans le langage maternel de l’analyste.
3. Ilfak Guilfanov. Hex Ray Microcode. hex-rays.com, 2009.
4. Philippe Biondi. scapy. blabla, 2012.
5. Rolf Rolles. Unpacking Virtualization Obfuscators. http://www.usenix.org/
event/woot09/tech/full_papers/rolles.pdf, 2009.
6. Thomas Dullien, Sebastian Porst. REIL.www.zynamics.com/download/csw09.pdf, 2009.
7. Yoann Guillot. Metasm. metasm.cr0.org, 2007.