• Nenhum resultado encontrado

Construction par Analyse Statique

No documento Philippe Beaucamps (páginas 34-48)

Dans le cas d’une approche statique, l’automate de traces est construit selon la procédure suivante :

Programme =⇒ Code du

programme =⇒ Machine à

états finis =⇒ Automate de traces 1. Le programme est désassemblé et son code dans le langage source ou dans

un langage intermédiaire de plus haut niveau est construit (Section3.2).

2. Une représentation par une machine à états finis est construite (Sec- tion3.2).

3. Finalement, un automate de traces est construit (Section3.2).

Décompilation du Code

Dans le cas de programmes dans des langages intermédiaires comme ceux des technologies .NET et Java, particulièrement présentes sur les appareils mobiles Windows Phone et Android respectivement, le code est facilement décompilé du fait de la spécification très restrictive de ces langages : des ou- tils de décompilation existent d’ailleurs, permettant d’obtenir un code source quasiment identique au code source initial.

De même, dans le cas d’un script (en Javascript, VBScript, etc.), on a faci- lement accès au code source. En outre, les techniques de protection éventuel- lement mises en œuvre sont souvent facilement contournables. Par exemple, un script Javascript malicieux se protège typiquement en déchiffrant son code au moment de l’exécution puis en l’évaluant avec la fonction eval. Le code malicieux en clair s’obtient alors en capturant le premier appel à la fonction eval.

Enfin, dans le cas de programmes x86, le code est désassemblé par ana- lyse statique, en suivant le flot de contrôle [31, 103], comme le fait l’outil de désassemblage IDA. C’est l’approche adoptée par Bergeron et al. [20], Christo- dorescu et al. [30], Singh et Lakhotia [99] ou encore Kirda et al. [72]. Lorsque le programme est protégé par des techniques de packing, des techniques d’un- packing sont utilisées, consistant à laisser le programme s’unpacker avant de l’analyser [92, 90, 16]. Notons que dans certains scénarios d’attaques ciblées, contrairement aux codes malicieux usuels, le code est peu ou pas protégé, afin de ne pas éveiller les soupçons d’un antivirus [80]. De plus, lors d’une analyse comportementale au cours d’une exécution, on peut ne commencer à analy- ser un programme packé que lors de sa première opération critique, opération après laquelle le programme s’est en général intégralement unpacké.

Un problème de l’analyse statique de programmes x86 réside dans l’exis- tence de sauts et d’appels indirects : certaines parties du code du programme et certains chemins dans le code peuvent alors ne pas être découverts. Toute- fois, certaines techniques d’analyse statique permettent de calculer les valeurs des adresses de saut ou d’appels [12,71,43]. Des techniques d’exécution sym- bolique permettent également de calculer ces valeurs ou de construire auto- matiquement de nouvelles entrées afin de découvrir de nouveaux chemins du code. C’est le cas des outils Dart [50], Sage [51] et BitScope [26] et d’un outil de Kruegel et Kirda [88], ainsi que d’une approche de McMillan [85].

Ces différents outils ont pour objectif d’extraire une vue plus exhaustive d’un programme lorsque son exécution dépend de l’environnement, en interprétant les conditions dirigeant le flot de contrôle.

Modélisation par une Machine à États Finis

On modélise ensuite le programme par une machine à états finis, dont les états correspondent aux états du programme à l’exécution. Les données considérées pour caractériser un état du programme dépendent des propriétés à vérifier sur le modèle. Par exemple, un état peut être caractérisé par le pointeur d’instruction, l’état de la mémoire (ou des variables du programme), etc. De plus, les transitions entre états peuvent être étiquetées, par exemple par l’instruction exécutée ou, lors d’un branchement conditionnel, par la condition vérifiée.

La construction d’un modèle fini exact (correct et complet) repose alors sur la finitude de l’ensemble des états du programme, finitude qui n’est pas garantie en général du fait des comportements dynamiques du programme comme l’allocation non-bornée de mémoire, la récursion non-bornée de fonc- tions ou la création dynamique de threads. Et lorsqu’une représentation exacte du programme par une machine à états finis existe, la taille du modèle est sou- vent prohibitive (par exemple, lorsque doit être représentée l’entrelacement de 10 threads complexes). Une simplification est alors cruciale et habituellement pratiquée [35,39], l’objectif étant d’obtenir une représentation compacte d’un système en éliminant les informations superflues qui ne seront pas utilisées lors de la vérification d’une propriété sur le modèle et en approximant les données ou comportements dynamiques de façon à assurer la décidabilité et l’efficacité de cette vérification.

Ces approximations sont encore plus pertinentes en analyse comportemen- tale. Ainsi, il semble inutile de considérer de la récursion non-bornée, de la création de threads non-bornée, des allocations de mémoire non-bornées, etc.

car les comportements que nous cherchons représentent en général une sé-

quence finie de fonctionnalités. Par exemple, une fuite d’informations peut être modélisée par une capture de données suivi de l’envoi de ces données vers un emplacement extérieur : les simplifications précédentes ne compromettent pas l’observation de ces deux fonctionnalités. On décide donc plus générale- ment de limiter tous les paramètres dynamiques à un seuil fixé et de travailler sur le modèle fini associé. Notons que le choix de ce seuil (pour chaque pa- ramètre dynamique) peut être affiné en fonction du comportement recherché.

Par exemple, plutôt que d’interdire la récursion, on peut l’autoriser jusqu’à une profondeur donnée.

Dans ce chapitre, on s’intéresse plus particulièrement à des programmes Java et C. La construction d’un modèle fini du programme est réalisée à l’aide d’outils existants, qui permettent de modéliser un programme dans un lan- gage de modélisation commePromela (utilisé parSpin [59]) ouBir (utilisé par Bandera [35, 95]). Les fondements d’une telle transformation pour des programmes Java sont étudiés dans [101] et [63].

Dans le cas de programmes Java, les outils suivants permettent de construire un modèlePromela:Bandera [64], qui génère un modèle intermédiaire en Bir, et Java Path Finder[57,6] (dans sa première version, puisqu’il s’agit désormais d’un outil de vérification dynamique). Dans le cas de programmes C, les outils Blast [22, 2] et C to Promela [67] permettent par exemple d’obtenir un modèlePromela.

Dans la suite du chapitre, nous considérons plus spécifiquement le langage de modélisationPromela, qui est le langage utilisé par ces différents outils.

Notons que ces outils appliquent diverses techniques d’optimisation et de simplification :

– Program slicing (élimination du code n’affectant pas la vérification) [61, 56,39] ;

– Abstraction [15] ;

– Spécialisation pour un environnement d’exécution particulier [55] ; – Limitation de la profondeur de récursion et inlining de fonctions [32,3] ; – Limitation du nombre de threads ;

– Identification des mécanismes de synchronisation dans le code et repré- sentation par des constructions spécifiques du langage Promela; – Partial Order Reduction [49,40] et minimisation, pour représenter effi-

cacement l’entrelacement de threads (source d’une explosion du nombre d’états).

– Identification des opérations locales à un thread, mise en œuvre dans l’outil Indus [39] : ces actions peuvent alors être exécutées de façon ato- mique et contribuer ainsi à réduire le nombre d’états induit par l’entre-

lacement de threads.

Enfin, dans le cas de programmes C, les outils Blast et C to Promela attendent en entrée un code C, qui doit donc être inféré du code désassemblé du programme, obtenu à l’étape précédente. Dans notre cas, ce code n’a pas besoin de représenter précisément le programme mais seulement ses séquences d’appels de librairies et leurs paramètres : les calculs intermédiaires peuvent donc être ignorés. Or, lorsque le programme n’est pas obfusqué1, les appels de librairies sont facilement identifiables dans le code désassemblé, IDA les reconnaissant automatiquement. De plus, les appels de librairies attendent des paramètres de types fixés, ce qui permet de déduire partiellement la structure de la mémoire du programme. C’est en fait la technique appliquée par IDA pour interpréter la pile d’une fonction de façon intelligible. Les variables res- tant inconnues sont typiquement celles qui ne sont pas liées directement aux paramètres des appels de librairies et qui n’entrent donc pas en compte dans les traces représentées. Ainsi, on peut construire une approximation du code C du programme, permettant de générer un modèlePromelaà partir de l’un des outils précédents.

Construction de l’Automate de Traces

Lorsque l’on travaille sur des mots (autrement dit, lorsque l’on ignore les ar- guments des appels de librairies), on construit l’automate de traces directement depuis le modèlePromelagénéré, en utilisantSpinqui permet de construire l’automate fini représentant le modèle2: les transitions sont étiquetées par des instructions, ou par des conditions dans le cas de branchements conditionnels.

Il suffit alors, pour nos besoins, de restreindre l’automate aux instructions re- présentant des appels de librairies et de remplacer par des �-transitions les transitions étiquetées par des conditions (ce qui consiste à transformer les branchements conditionnels en branchements non déterministes).

Lorsque par contre on souhaite représenter les arguments des appels de librairies (i.e. on travaille sur des termes), ces arguments sont représentés sur l’alphabet des données Fd et le problème est alors de déterminer à quelles constantes deFdassocier les arguments. Nous avons, dans ce cas, besoin d’une procédure permettant d’associer de manière automatique un argument d’un appel de librairie à un symbole de Fd. Nous nommons cette procédure abs- traction du flux de données.

1. Rappelons que bien que les programmes malicieux soient packés la plupart du temps, on suppose que l’analyse statique est effectuée une fois le code unpacké en mémoire.

2. Commandepan -D.

Une solution intuitive pour l’abstraction du flux de données est d’indexer les variables du programme sur l’alphabet Fd. En effet, le modèle Promela est obtenu par analyse statique, en associant à chaque variable du programme une variable dans le modèle ; la transformation d’un argument d’un appel de librairie dans le modèle en un symbole deFdest donc immédiate.

Mais cette approche a un défaut car les dépendances dans le flux de données doivent alors être traitées de façon explicite : si deux appels de librairies se font sur des variables différentes, cela ne garantit pas pour autant que ces variables représentent des objets distincts. Par exemple, dans le code suivant, il faudra tenir compte du fait que les actionsload_data etuse_data, bien qu’opérant sur des variables différentes, utilisent en réalité le même objet :

void *a, **b;

a = malloc(1024);

b = &a;

/* load data in a */

load_data(a, 1024);

/* use data in b */

use_data(*b, 1024);

Plus généralement, le problème de déterminer si deux variables référencent le même objet, appelé dans la littérature problème d’analyse des aliases de pointeurs, est NP-complet lorsqu’un programme utilise une profondeur non bornée d’indirections de pointeurs [78], comme on peut le rencontrer en C.

Pour s’affranchir de l’analyse coûteuse des pointeurs lors de l’abstraction et de la détection de comportements, nous représentons par des éléments de Fd les objets manipulés par le programme, de telle sorte que deux appels de librairies utilisent le même objet ssi ils sont paramétrés par la même constante de Fd. Les traces obtenues par analyse statique opérant sur les variables du programme, il faut alors les transformer de façon à exprimer leurs paramètres surFd.

Pour ce faire, nous construisons une représentation abstraite de la mé- moire du programme, qui est une approximation de la forme de la mémoire du programme, composée de sa pile (variables locales) et de son tas (variables dynamiques).

Construction d’une représentation abstraite de la mémoire du pro- gramme Nous utilisons des techniques d’analyse de forme [69,60,28,94,68]

qui consistent à modéliser la mémoire du programme par un graphe, dont les nœuds représentent des positions dans la mémoire et les arcs représentent des références de pointeurs entre deux positions de la mémoire. Par exemple, les

Figure 3.2: Exemple d’état de la représentation abstraite de la mémoire.

model checkers Blast [23, 2] et Slam2 [13, 14] utilisent l’analyse de forme pour traiter avec plus de précision les pointeurs et les structures de données récursives.

Des techniques existantes sont donc appliquées afin de construire une re- présentation abstraite de la mémoire, dont les cases mémoire seront indexées par Fd et telle qu’en tout point du programme, on puisse déterminer quel ensemble de cases mémoire désigne une variable.

La construction de ce type de représentation abstraite est étudiée dans [60, 68,94] et repose sur l’abstraction du flux de données selon une méthode conser- vatrice, permettant de calculer, en chaque point du programme, l’ensemble des états possibles de la mémoire du programme, avec les dépendances de données sous-jacentes. Une telle abstraction est courante en analyse du flux de données (cf. Aho et Ullman [9, Chapitre 9]). Dans [60, 68,94], un état de la mémoire est un ensemble de cases mémoire tel que chaque variable du programme est représentée sur ces cases et tel que certaines cases référencent d’autres cases.

Lorsque l’allocation dynamique de mémoire est prise en compte, l’espace des états est potentiellement non borné et une approximation appelée k-limiting est appliquée, consistant à limiter à une profondeurkla profondeur des struc- tures de données récursives.

Exemple 17. Pour l’exemple de programme suivant, avant l’exécution de la dernière instruction, l’algorithme d’Horwitz, Pfeiffer et Reps [60] construit l’état de la mémoire représenté en Figure3.2.

struct LinkedList { int hd;

LinkedList *tl;

}

x := new LinkedList(0, NULL);

y := x;

y := y.hd;

Une instruction d’allocation dynamique (malloc,new, etc.), lorsqu’elle peut être exécutée plus d’une fois, peut allouer plusieurs objets et la représentation

abstraite de la mémoire du programme doit donc a priori représenter l’ensemble de ces objets. On décide de borner par 1 le nombre de ces objets, autrement dit une telle instruction ne peut être exécutée plusieurs fois qu’à la condition que l’objet précédemment créé ait été libéré entre temps (parfree,delete, etc.).

Cela nous permet de considérer que la représentation abstraite de la mémoire du programme reste la même au cours de l’exécution, c’est-à-dire qu’entre deux états de la mémoire, seules varient l’application associant une variable à un ensemble de cases mémoire et les dépendances de données entre ces cases mémoire.

Ainsi, une représentation abstraite de la mémoire d’un programme est dé- finie de façon unique à partir de l’ensemble des objets du programmes, qui se décomposent en :

– les objets locaux, i.e. alloués statiquement (dans la pile) : x et y dans l’Exemple 17;

– les objets alloués dynamiquement (dans le tas) : l’objet alloué par new LinkedList(0, NULL) dans l’Exemple17.

De plus, on tient compte des constantes (nombres, chaînes de caractères, etc.) utilisées comme paramètres des appels de librairies en les assimilant à des variables du programme, autrement dit on les représente explicitement dans la représentation abstraite de la mémoire.

Pour terminer, notons qu’on peut approximer certaines structures de don- nées afin de simplifier la représentation abstraite de la mémoire du programme (et par extension l’analyse du flux de données lors de l’analyse comportemen- tale) :

– On traite les collections (tableaux, listes, etc.) comme des ensembles pré- définis d’éléments associés à des variables distinctes, ce qui est une ap- proche usuelle (par exemple, le model checker Spin simule des tableaux de taille fixe en les remplaçant le moment venu par un ensemble fini de variables). On peut même encore simplifier le modèle, comme dans [93], en considérant qu’une collection contient au plus un élément car ses diffé- rents éléments ont en général un rôle similaire (par exemple la collection contient un ensemble de fichiers à infecter, un ensemble d’adresses mail destinataires d’un spam, etc.).3

– Dans le cadre de l’analyse comportementale, certains champs des objets ne nous intéressent jamais et une flexibilité peut être introduite en alté- rant les structures de données, de façon à ce que certains champs soient

3. Notons que lorsqu’on représente une collection par un ensemble prédéfini d’éléments, il faut alors simuler les champs associés à la collection (par exemple le champlengthd’un tableau) par des constantes ou par des accesseursgetetset.

ignorés ou ne deviennent accessibles que par des pseudo-accesseurs get ou set.

L’exemple suivant illustre sur un cas concret la construction d’une repré- sentation abstraite de la mémoire d’un programme.

Exemple 18. Considérons le code suivant extrait d’une application mobile réelle, SMS_Replicator_Secret, pour systèmes Android, qui fait suivre vers un numéro tiers tous les SMS reçus ou envoyés. Cette application définit une classe SMSReceiver avec une méthode particulière onReceive(Context context, Intent intent) et demande à Android, via son fichier de méta- données, d’exécuter cette méthode à chaque SMS reçu ou envoyé. Lorsqu’un SMS est reçu ou envoyé, le système exécute alors cette méthode en initialisant le champ extras[“pdus”] du paramètre intent par l’objet représentant le SMS concerné.

Le code ci-dessous a été obtenu en utilisant les outilsdex2jar(qui fournit un fichier exécutable .jar contenant le code compilé de l’application) et jad (décompilation des classes Java contenues dans le fichier.jar).

Bien que le code ait été simplifié et certaines erreurs de décompilation corrigées, il est présenté sous une forme relativement brute, correspondant à la sortie de jad : par exemple, les concaténations de chaînes de caractères sont implantées par la classeStringBuilderet les appels de typef(g())sont développés à l’aide d’une variable temporaire ent = g() ; f(t).

1 public class SMSReceiver extends BroadcastReceiver

2 {

3 String number;

4

5 public SMSReceiver()

6 {

7 this.number = "0XXX";

8 }

9 10

11 public void onReceive(Context context, Intent intent)

12 {

13 Bundle bundle;

14 Object pdus[];

15

16 String from = null;

17 String msg = "";

18 String str = "";

19

20 bundle = intent.getExtras();

21 pdus = (Object[])bundle.get("pdus");

22

23 // Pour chaque message envoye

24 int pdus_len = pdus.length;

25 for(int i = 0; i < pdus_len; i++)

26 {

27 Object pdu = pdus[i];

28 SmsMessage smsmessage = SmsMessage.createFromPdu((byte[])pdu);

29

30 // from = "From:" + smsmessage.getDisplayOriginatingAddress() + ":";

31 StringBuilder sb1 = new StringBuilder("From:");

32 String s1 = smsmessage.getDisplayOriginatingAddress();

33 sb1.append(s1);

34 sb1.append(":");

35 from = sb1.toString();

36

37 // msg = msg + smsmessage.getMessageBody();

38 StringBuilder sb2 = new StringBuilder(msg);

39 String s2 = smsmessage.getMessageBody();

40 sb2.append(s2);

41 msg = sb2.toString();

42 }

43

44 // str = from + msg;

45 StringBuilder sb3 = new StringBuilder(from);

46 sb3.append(msg);

47 str = sb3.toString();

48

49 SmsManager sms_manager = SmsManager.getDefault();

50 for (int i = 0; i < str.length(); i += 160)

51 {

52 String s3 = str.substring(i, i + 160);

53 sms_manager.sendTextMessage( this.number, null, s3, null, null );

54 }

55 }

56 }

On s’intéresse spécifiquement à l’exécution de la fonctiononReceive. Lorsque cette fonction est appelée, la représentation abstraite de la mémoire contient une représentation des objets définis par les variables locales et paramètres de la fonction et des objets alloués dynamiquement lors de l’exécution.

On simplifie tout d’abord les classes en les représentant par des structures où seuls les champs pertinents pour la détection comportementale sont pris en compte. Ainsi, la classeIntentest représentée par une structure contenant un

unique champextras, les classesContext,Bundle,String,StringBuilderet SmsManagersont représentées par des structures vides et la classeSmsMessage est représentée par une structure contenant deux champs,display_origina- ting_address etmessage_body:

class Intent {

Bundle extras;

}class Context {

}class SmsMessage {

String display_originating_address;

String message_body;

}

Dans un second temps, on prend en compte l’ensemble des allocations dynamiques. Ces dernières sont soit explicites (avec l’opérateur new, comme à la ligne 31), soit implicites (induites par des appels de fonctions, comme à la ligne21). Ainsi, on associe des objets dans la représentation abstraite de la mémoire pour4 :

– l’allocation de l’objet pdus, ligne21;

– l’allocation de l’objet smsmessage, ligne 28; – l’allocation de l’objet sb1, ligne 31;

– l’allocation de l’objet from, ligne35; – etc.

Cet exemple montre par ailleurs la pertinence, dans le contexte de l’analyse comportementale, de considérer qu’un point d’allocation dynamique est associé à au plus un objet dans la représentation abstraite de la mémoire, même lorsque ce point d’allocation se situe au sein d’une boucle.

La Figure 3.3 décrit la représentation abstraite résultante de la mémoire associée au programme. On suppose que le champ number et les paramètres contextetintentont été alloués indépendamment.

Construction de l’Ensemble des États Possibles de la Représentation Abstraite en chaque Point du Programme On peut dès lors appliquer les techniques précédentes d’abstraction de la mémoire du programme [60,68, 94], qui permettent de calculer, pour un état initial de la mémoire, l’ensemble

4. L’initialisation de l’objetBundleà la ligne20n’est pas considérée comme une allocation mais comme un accès au champ (déjà alloué)extrasde l’objetintent.

No documento Philippe Beaucamps (páginas 34-48)