ViriMon

Rapport de projet synthèse

Eric Vachon
Roxanne Kouassi

Supervisé par Pr. Dr. Kamel Adi

Université du Québec en Outaouais

Les codes malicieux

Mise en situation

Avant les années 80, les ordinateurs étaient utilisés par des scientifiques pour effectuer les calculs complexes et longs, ce qui leur sauvait beaucoup de temps. Cependant la situation a changée lorsque les ordinateurs personnels ont fait leur entrée dans les résidences où les utilisateurs n'étaient pas nécessairement formés pour l'utilisation de ceux-ci. C'est alors que des programmes n'offrant pas les résultats escomptés font leur apparition. Ces logiciels sont des codes malicieux, conçus par des programmeurs voulant démontrer qu'ils peuvent dominer les ordinateurs de différentes manières.

Ces codes peuvent prendre plusieurs formes et sont plus souvent une nuisance qu'autre chose. Lors des débuts des ordinateurs personnels, les codes malicieux ont besoin d'une grande intervention humaine pour se reproduire. L'utilisateur doit envoyer les codes sur des dépôts de donnés (FTP), sur des babillards (BBS), avoir une disquette infectée et l'amener sur un autre ordinateur, etc. La propagation se fait donc très lentement et les concepteurs de logiciels de protection contre ces codes malicieux ont amplement le temps de trouver et de distribuer une solution contre ceux-ci avant qu'ils n'atteignent plusieurs ordinateurs.

Aujourd'hui l'Internet à changé ce paysage. Les codes malicieux peuvent se propager à une très grande vitesse. L'utilisateur n'est parfois impliqué que dans quelques cliques de la souris pour envoyer un courriel ou visiter une page web. Parfois il n'a même pas à lever le petit doigt, le seul fait d'être branché sur le réseau mondial le rend vulnérable aux codes malicieux. La seule limite imposée à ceux-ci est la vitesse des réseaux et de la puissance de calculs des différents ordinateurs. Les concepteurs de logiciels de protections doivent agir très rapidement afin d'éviter l'infection de millions d'ordinateurs lorsqu'une nouvelle menace voit le jour. Nous voyons rapidement que la réaction n'est plus la méthode à utiliser, nous devons trouver un autre moyen de défense.

Qu'est-ce qu'un code malicieux ?

Une définition large des codes malicieux est qu'ils englobent tous les logiciels ayant des actions négatives sur le système. Si nous prenons la totalité des logiciels conçus, nous pouvons voir que la majorité de ceux-ci n'ont pas été conçus dans l'intention de causer des problèmes aux systèmes. Nous pouvons retrouver des logiciels qui fonctionnent correctement et accomplissent leurs tâches sans jamais nuire au système. Toutefois, certains d'entre eux contiennent des bogues qui pourraient peut-être causer des dommages, sans que ceux-ci ne soient intentionnels, ils sont tout de même considérés comme des codes malicieux, cependant ce ne sont pas les pires.

Une certaine partie des logiciels sont conçus dans l'intention d'accomplir une tâche malicieuse. C'est donc ceux-ci que nous allons considérer comme étant les véritables codes malicieux. Afin de détecter ces logiciels indésirables, nous devons définir une propriété mesurable avec laquelle nous pourrons les identifier et mettre un terme à leurs actions négatives.

De tous les codes malicieux, les virus sont ceux qui détiennent la propriété la plus précisément définie et mesurable. Par contre si l'on prend les chevaux de Troie, nous pouvons difficilement trouver une propriété qui nous permet de définir avec exactitude si le code est malicieux ou non. C'est pourquoi nous devons faire une distinction entre les différentes catégories de codes malicieux et trouver la propriété qui définit exactement chacune des catégories. Il se peut que plus d'une propriété définisse ces différentes catégories, par exemple lorsqu'une catégorie comporte des sous classes. [15]

Étendue du problème

Les entreprises étant branchés sur Internet ont beaucoup de difficulté à combattre les codes malicieux. Le nombre de systèmes affectés ne cesse d'augmenter et ces attaques posent une grave menace pour les entreprises d'aujourd'hui. Selon l'ICSA, en 1997 un ordinateur sur trente en Amérique était infecté par un code malicieux et en 2001, 2.5 millions d'ordinateurs ont étés touchés par Nimda pour une période de seulement 24 heures. [13]

Les virus affectant les fichiers et l'amorce du système posent encore un sérieux problème, cependant ces virus ont été remplacés par ces codes beaucoup plus complexes. Ces nouveaux codes évitent les technologies de balayage en changeant leurs signatures à chaque infection. Ils peuvent cacher leurs actions malicieuses en faisant leurs manipulations sur le système lors de conditions très spécifiques et en espaçant le plus possible ces actions. Certains d'entre eux peuvent même infecter les anti-virus en utilisant les techniques de mises à jours de celui-ci ou en le désactivant tout simplement.

Autrefois, les entreprises croyaient que les virus ne pouvaient pas infecter les fichiers n'étant pas exécutables et plusieurs compagnie d'anti-virus ne balayaient même pas ces fichiers. En 1995, l'apparition des virus sous forme de macro a changé cette mentalité. Les vers ont connus une vitesse de propagation surprenante grâce à ces macros qui permettent d'écrire du code dans un langage plus facile à comprendre que les langages comme assembleur ou c++ dans lesquels les anciens virus étaient écrits. Le nombre de codes malicieux présents dans la communauté Internet est très difficile à évaluer, les variantes de codes déjà présents font en sorte que des centaines de nouveaux codes malicieux apparaissent chaque jour. Les concepts, les attaques qui ont étés démontrés, mais qui n'ont pas été relâchés dans la communauté, poussent le nombre de codes malicieux encore plus haut.

La création de codes malicieux n'est plus réservée à ceux ayant une connaissance technique très approfondie des systèmes d'exploitation. Il existe maintenant des modules de constructions qui permettent aux novices de produire des virus et des vers très facilement en suivant les indications à l'écran et en choisissant les effets désirés. Un enfant pourrait utiliser « [K]alamar's VBS Worm Generator 1.5 » pour créer un vers. En effet, l'auteur (OnTheFly) du vers VBS.SST@mm, aussi connus sous le nom de Anna Kournikova, s'est servis d'un de ces générateurs pour infecter des centaines de milliers d'ordinateurs à travers le monde. Ces générateurs ne sont pas nouveaux, « The Mutation Engine (MtE) », « Virus Creation Laboratory (VCL) » et « Phalcon/Skism Mass-Produced Code Generator » ont été conçus au début des années 90s. Il existe maintenant des centaines de générateurs sur Internet. [8]

Il est aussi intéressant de noter que les virus peuvent se combiner pour créer une autre sorte de virus qui peut lui aussi se répandre sur Internet. Sophos a identifié un incident où deux virus (WM97/Class-D et WM97/FF-A) ont interagi entre eux sur un système pour créer un nouveau virus (WM97/FF-H). Cet incident nous montre que les codes malicieux peuvent s'assembler et ainsi créer de nouvelles souches qui ne peuvent être détectées par les anti-virus de balayage traditionnels. [14]

En plus de l'attaque des « vrais » codes malicieux, les attrapes augmentent le temps nécessaire aux techniciens pour répondre aux usagers ayant été victime d'une « attaque » par ces codes malicieux inexistants. Même les systèmes sont touchés par les attrapes, les serveurs de courriels sont submergés par les chaînes de lettres que les utilisateurs s'envoient croyant avertir leur prochain d'une attaque probable. Les utilisateurs deviennent alors moins alertes à ces avertissements et baissent ainsi leurs défenses envers des attaques véritables.

Les pertes financières dues aux codes malicieux sont estimées à des milliards de dollars par années aux États-Unis seulement. Wired News indique qu'une recherche du Computer Economics que Nimda a fait perdre US$635 millions en coûts de réparation et en pertes de productivité. La somme totale pour les différentes versions de Code Red sont de US$2.62 milliards, SirCam est allé chercher US$1.15 milliards à lui seul et finalement Love Bug a coûté US$8.75 milliards pour l'éliminer. Ces chiffres proviennent des grandes entreprises et ne comptent pas les pertes subies par les petites entreprises et les usagers d'ordinateurs personnels. [1]

Il n'existe pas d'estimés pour quantifier les pertes secondaires dûes à une attaque majeure de code malicieux. Les coûts associés à la perte de donnés confidentielles résultant de l'activité d'un cheval de Troie sont variés : le temps de fermeture d'un réseau, la vérification des systèmes n'étant pas infectés, le stress subis par les utilisateurs et les techniciens du système, les interruptions dans les affaires de la compagnie, la détérioration de la valeur des actions, les relations publiques et la liste continue longtemps.

Il est clair que les codes malicieux sont d'une importance cruciale et que le coût engendré par la perte de productivité et d'informations n'est pas à négliger dans l'effort à fournir pour combattre ce fléau.

Objectif du projet

Notre objectif principal est de créer un prototype d'anti-virus basé sur le comportement. Il consiste dans le développement d'un module de surveillance des ressources critiques du système. Dans un premier temps, nous allons nous focaliser sur la surveillance des appels systèmes : le module devra être capable d'intercepter tous les appels systèmes et de les afficher dans une fenêtre Windows. Une fois ce module conçu, nous devrons concevoir un évaluateur de traces se basant sur un langage pouvant exprimer les comportements malicieux. Lorsqu'une règle est satisfaite, nous serons en présence d'un code pouvant potentiellement nuire au système.

Pour la surveillance des appels systèmes, un pilote devra être conçu pour se placer entre la couche noyau du système d'exploitation et la couche application de manière à ce que tous les appels systèmes lancés soient interceptés, enregistrés et ensuite redirigés vers l'appel système noyau correspondant. Ainsi, il sera possible d'exécuter une procédure à chaque appel système avant que le noyau effectue l'opération demandée. Cette procédure dans notre cas serait l'évaluation de la trace du processus appelant. Cette information pourra aussi être enregistrée dans un fichier texte afin de garder une trace de ce qui se produit.

Le module de surveillance devra s'exécuter sous le système d'exploitation Windows XP. Bien qu'une version sous Windows 9x ou Linux est envisageable, la plus grande difficulté est de produire une version pour le code propriétaire et très secret de Windows NT. Une recherche approfondie du fonctionnement des API et de la manière d'installer un pilote de surveillance sera nécessaire pour le développement de notre application.

Pour ce qui est de l'évaluation des traces d'appels systèmes, nous devrons concevoir un automate à partir d'une formule spécifiée par l'utilisateur. Il sera donc très important de bien maîtriser ce langage d'interprétation. Nous passerons donc les appels systèmes dans cet automate et l'état de celui-ci nous indiquera si une trace est malicieuse ou non.

Nous utiliserons les technologies de Microsoft pour le développement de notre application, soit Microsoft Visual Studio .NET pour la section en mode utilisateur et Microsoft Driver Development Kit for Windows XP pour la section en mode noyau. Les rapports seront développés à l'aide de LATEX.

Les travaux similaires

Les balayeurs de signature

Ce type de logiciel fonctionne à l'aide de la signature des codes malicieux. Pour simplifier l'explication, considérons qu'un code malicieux infecte les programmes hôtes avec une copie exacte ou presque exacte d'eux-mêmes. Nous ne prenons pas en compte les virus polymorphes qui changent leur code à chaque infection.

Supposons que nous voulons déterminer si un logiciel hôte $P$ est infecté par un virus $V$. La manière la plus simple serait de balayer le code machine de $P$ pour trouver une suite de bytes qui correspondent à $V$. Cependant il existe plusieurs problèmes à cette approche. Les virus typiques sont d'une taille de quelques milliers d'octets et l'existence de plusieurs milliers de codes malicieux ferait en sorte que la quantité de mémoire nécessaire pour contenir tout les codes serait de quelques mégaoctets. De plus, il serait dangereux pour les logiciels d'anti-virus de contenir une grande librairie de codes malicieux connus. Les concepteurs de ceux-ci seraient doublement reconnaissants s'ils pouvaient avoir une collection de codes malicieux si facilement disponible.

Au lieu d'avoir besoin d'une correspondance parfaite, les concepteurs d'anti-virus par balayage n'utilisent seulement qu'une petite partie du code malicieux pour l'identification. Ces petits fragments appelés signatures sont beaucoup plus facile à utiliser, entreposer et surtout ne révèlent rien d'utile aux concepteurs de codes malicieux. Un autre avantage d'utiliser des signatures est que même lorsque d'autres parties du code changent, l'anti-virus peut toujours les détecter.

Pour ce qui est des codes malicieux polymorphes, il existe deux sortes de changements possibles. Premièrement ces derniers peuvent être modifiés par des personnes qui veulent produire une nouvelle souche sans prendre le temps de créer un nouveau code malicieux. Ces nouvelles souches seront détectées si elles contiennent toujours la séquence d'octets utilisée dans la signature. La deuxième source de mutation est faite à l'aide de la programmation. Une petite minorité de codes malicieux sont faits pour changer de forme lorsqu'ils copient leur code dans une autre source. Cette procédure est normalement faite à l'aide d'une routine au début du code pour encrypter le corps du code malicieux. Une signature peut donc être trouvée en début de fichier. Par contre les nouveaux virus combattent cette technique en utilisant un très grand éventail d'entêtes de décryptions. Des algorithmes plus complexes doivent alors être utilisés pour détecter ces derniers.

Le balayage par signature comporte certains désavantages. Premièrement les signatures doivent être les plus courtes possibles pour capturer le plus grand nombre de changements possibles dans le corps du code malicieux. Ces séquences de codes sont donc beaucoup plus susceptibles de contenir des fragments utilisés légitimement par d'autres programmes. Surtout si le code malicieux à été fait à l'aide d'un langage de haut niveau, les routines utilisées se retrouvent fort probablement dans d'autres logiciels. Il en résulte alors de fausses alertes que l'utilisateur doit régler par lui-même. De plus, le temps que prend une compagnie d'anti-virus pour recevoir un nouveau code malicieux, en analyser son contenu, déterminer une signature et envoyer la mise à jour à tous les utilisateurs de leur logiciel prend souvent quelques heures. Il est toutefois connu que c'est dans les premières heures d'une attaque que le plus de dommages sont subis par les compagnies et les utilisateurs. Ces dommages se chiffrent parfois dans les milliards de dollars. [7]

Les balayeurs de CRC

Ce type d'anti-virus fonctionne en calculant la somme CRC des fichiers et des secteurs du système. Ces sommes sont sauvegardées dans la base de donnés du logiciel avec d'autres informations comme la taille des fichiers, la date de la dernière modification, etc. Lorsque le balayeur recommence sont processus, il compare sa base de donnés avec l'information qu'il calcule. Lorsqu'une entrée de la base de donnés et l'information perçue sur un fichier diffèrent, le logiciel avertit l'utilisateur d'une modification sur le fichier ou bien il indique qu'un code malicieux s'est infiltré.

Les balayeurs de CRC qui utilisent des algorithmes contre les camouflages de fichiers sont des outils très puissants contre les codes malicieux. Près de 100% de ces derniers sont détectés presque immédiatement après leur infiltration dans l'ordinateur. Cependant ce type d'anti-virus a un désavantage qui diminue de manière significative leur efficacité. Ils ne peuvent attraper un virus immédiatement après son entrée dans le système, mais seulement après un certain temps, lorsque le virus s'est propagé à travers l'ordinateur. Ils ne peuvent donc non plus détecter les virus sur les nouveaux fichiers qui viennent d'arriver par courriel, sur disquette, d'archives, etc. parce que leur base de donnés ne contient pas d'entrées sur ces fichiers. Donc il existe de nouveaux virus qui vont exploiter cette faiblesse et infecter que les nouveaux fichiers, ce qui les rend invisibles à ces anti-virus. [6]

Immunisations

Les immunisations sont divisées en deux types, ceux qui avertissent qu'une infection a eu lieu et ceux qui bloquent les tentatives de certains codes malicieux d'infiltrer le système. Les logiciels du premier type s'attachent à la fin des fichiers comme le font certains virus et à chaque lancement de ce fichier, ils vérifient s'il y a eu un changement dans le corps de ce dernier. Ils n'ont qu'une seule faille, c'est qu'ils ne peuvent pas détecter les infections faites par les codes malicieux utilisant des techniques de camouflages. C'est pour cette raison que les immunisations ne sont presque pas utilisées.

Les logiciels du deuxième type protègent le système de certaines infections. Les fichiers du disque sont modifiés d'une certaine manière à ce que le code malicieux les considèrent comme déjà infectés. Comme par exemple la ligne « MsDos » qui protège les fichiers contre l'ancien virus Jérusalem. Pour ce qui est des codes malicieux résidents en mémoire, un petit logiciel est installé dans la mémoire et lorsque le code malicieux le rencontre, il croit que le système est déjà infecté et il termine son exécution.

Les immunisations de ce type ne sont pas versatiles, il est impossible d'immuniser les fichiers contre tous les codes malicieux existants. Certains considèrent le fichier comme infecté lorsque le temps de la dernière modification contient « 62 » dans les secondes, tandis que d'autres serait « 60 ». Malgré cette faiblesse, les immunisations peuvent être utilisées pour protéger les fichiers d'un nouveau code malicieux jusqu'au moment où il devient détectable par d'autres types d'anti-virus. [6]

Architecture du prototype

L'architecture de notre prototype d'anti-virus basé sur la détection de comportements malicieux est la suivante :

Nous allons voir le fonctionnement interne des différentes parties de notre prototype dans les sections suivantes. L'installation du pilote qui est une procédure critique de notre application sera analysée en premier. Elle sera suivie de la modification de la table des appels systèmes de Windows. Cette procédure est complexe et très peu documentée, nous devons comprendre que Microsoft n'accepte pas que l'on modifie leur produit et qu'ils font tout en leur pouvoir pour rendre cette tâche difficile. La communication entre le pilote et l'application sera ensuite exposée. Il est important de comprendre comment une section en mode noyau peut transmettre de l'information vers une application de la manière la plus efficace possible. La création d'une trace des appels systèmes ne serait pas utile si on ne l'analyse pas par la suite. La logique linéaire temporelle nous permet de définir les comportements malicieux et nous l'utilisons pour créer notre évaluateur de trace d'appel système. Finalement l'interface utilisateur sera abordée, nous avons opté pour une interface conçue à l'aide des Microsoft Foundation Class afin d'avoir une plus grande liberté dans la conception de celle-ci.

Installation du pilote

Concept de base

Afin d'effectuer les interceptions d'appel système au niveau le plus bas possible, nous avons conçu un pilote qui se situe en mode noyau qui pourra faire le traitement nécessaire pour créer une trace unique de chaque processus en exécution dans le système.

La création d'un pilote est une tâche à la fois très complexe et délicate. La plus petite erreur précipite le système dans un état qui est plus souvent qu'autrement irrécupérable, ce qui nécessite un redémarrage (qui se fait d'ailleurs par lui-même dans 70% des cas). Il est donc important de bien maîtriser les concepts qui sous-tendent l'installation du pilote en mode noyau et les commandes qui nous sont disponibles dans cette couche du système.

Premièrement il faut savoir que les pilotes pour Windows NT doivent être conçus en C et qu'ils nécessitent une trousse d'outils provenant de Microsoft, le Driver Development Kit (DDK). Lorsque l'installation de ces outils est terminée, l'environnement nécessaire à la création des pilotes pour un environnement très spécifique (dans notre cas Windows XP) est présent.

La compilation requiert certains fichiers particuliers tel le makefile et le sources. Le fichier makefile contient les instructions d'environnement pour compiler le pilote, une seule ligne est nécessaire pour les pilotes simples : $!INCLUDE \ \$(NTMAKEENV)\backslash makefile.def$. Le fichier sources, par contre, contient les informations spécifiques pour la création du pilote : le nom, son type, les différents fichiers sources (sources, entêtes, ressources, etc.) et les instructions pour les codes spéciaux comme les caractères UNICODE par exemple.

Corps du pilote

Pour ce qui est du corps du pilote, nous pouvons le comparer à une librairie de liens dynamiques (DLL). Le pilote possède un point d'entrée qui ressemble au « DllMain » qui se nomme « DriverEntry ». Dans le cas des pilotes, la fonction « DriverEntry » est appelée une seule fois lorsque le système d'exploitation charge le pilote. Contrairement au « DriverEntry », le « DllMain » peut être appelé de nombreuses fois, tout dépendamment combien d'applications utilisent le DLL.

Un DLL définit ses interfaces qu'il exporte à travers un fichier librairie (.LIB) qu'une application peut utiliser pour faire le lien avec la librairie. Un pilote utilise un ensemble de points d'entrées standard qu'il définit dans la fonction « DriverEntry » en indiquant quelle procédure répond à quel appel. Il indique ceux-ci à travers une structure DRIVER_OBJECT qui a été crée par le système d'exploitation. Le système d'exploitation peut alors vérifier cette structure pour récupérer un pointeur vers la fonction approprié lorsqu'une requête est dirigée vers le pilote. Ces fonctions représentent soit le matériel physique ou virtuel que le pilote supporte.

Un DRIVER_OBJECT est un bloc de mémoire alloué et en partie initialisé par le système d'exploitation. Cet objet décrit à quel endroit le code du pilote est situé en mémoire, le nom du pilote et des pointeurs vers l'adresse des fonctions (Function Address Pointers) que le pilote doit remplir pour indiquer quelles tâches il supporte. Une des fonctions qui doit être remplis dans cette table est appelée la table d'envoie des appels (Function Dispatch Table). Cette table contient une entrée pour chaque code de fonction majeure (Major Function Code) que le système d'exploitation supporte. Il existe présentement 28 fonctions que le pilote peut décider de supporter, cependant la majorité des pilotes n'en supporte que huit ; IRP_MJ_CREATE, IRP_MJ_CLOSE, IRP_MJ_READ, IRP_MJ_WRITE, IRP_MJ_PNP, IRP_MJ_POWER, IRP_MJ_DEVICE_CONTROL, et IRP_MJ_SYSTEM_CONTROL. Lorsque le pilote reçoit cette table, toutes les entrées contiennent un pointeur vers une routine qui indique que cette fonction majeure n'est pas supportée. Donc chaque fois que l'utilisateur fait une requête au système d'exploitation pour qu'il performe une action spécifique au pilote, celui-ci détermine quelle code de fonction majeure correspond à cette action et envoie cette requête au pilote que l'utilisateur à spécifié. Dans notre cas, nous n'avons supporté que les fonctions IRP_MJ_CREATE, IRP_MJ_CLOSE et IRP_MJ_SYSTEM_CONTROL, car nous n'avons pas réellement du matériel à contrôler. Notre objectif étant d'être en mode noyau pour obtenir la liberté de modifier la structure interne du système d'exploitation, nous n'avions pas besoin d'en faire plus. [12]

Modification de la table des appels systèmes

Description de la table des appels systèmes

Lorsqu'un processus sous Windows NT fait une requête pour écrire sur un fichier, ouvrir un fichier, lire la base de registre, etc. le programme doit appeler une des fonctions fournies par l'API de Windows, comme WriteFile(). Ces fonctions API sont normalement composées de fonctions plus primitives se trouvant dans NTDLL.DLL, comme NtWriteFile(). Ces fonctions sont elles-mêmes de simples interfaces aux services que peut fournir le noyau du système d'exploitation (kernel). Ces fonctions noyaux ont le même nom que leur homonyme dans NTDLL.DLL, sauf qu'elles commencent par Zw à la place de Nt, comme par exemple, ZwWriteFile().

Afin de pouvoir faire une surveillance des accès aux fichiers ou bien à la base de registre au niveau le plus bas possible, il est nécessaire d'intercepter ces appels aux services du système. Il est avantageux pour nous d'intercepter ces appels en étant dans le noyau, et ce, pour un meilleur contrôle de la sécurité de notre processus et de sa confidentialité.

Chaque service du système est désigné par un identificateur unique qui est utilisé pour trouver la fonction pouvant faire l'action demandée. Ces identificateurs sont codés sur des entiers de 32 bits définis de la manière suivante (du bit ayant le poids le plus fort jusqu'au poids le plus faible.) :

Les tables de services sont utilisées pour grouper les fonctionnalités qui ont un liens entre elles. Les tables suivantes sont présentes dans Windows 2000 et XP :

Ces tables sont sauvegardés dans la table de services du système (System Service Table) KeServiceDescriptorTable qui est déclaré comme suit :

__declspec(dllimport) sst KeServiceDescriptorTable[4 ];


Chaque SDT contient les informations à propos des services de son groupe. Ils contiennent quatre éléments, les pointeurs vers les fonctions qui effectuent l'action demandée par l'utilisateur, un pointeurs vers des compteurs, le nombre de services disponibles et un pointeur vers une table qui contient le nombre de bytes de paramètres requis pour chaque fonction. Cette structure est codée de la manière suivante :

#pragma pack(push, 1)
typedef struct {
PVOID *rgpfnHandlerTable;
PULONG rgulCounterTable;
ULONG cServices;
PUCHAR rguchParamTable;
} sst; #pragma pack(pop, 1)

L'interception des appels systèmes

L'interception des appels systèmes en niveau noyau nécessite la localisation de l'information à propos du service que l'on désire intercepter et modifier l'information sur ce service pour appeler une de nos fonctions. Un pointeur vers la fonction originale est sauvegardé, ce qui permet à notre fonction d'appeler l'originale pour que le travail soit effectué. Nous devons donc trouver l'identificateur du service que l'on veut intercepter. Lorsque nous utilisons une procédure de désassemblage sur une fonction exécutant un appel système, nous constatons que chacun commence avec l'instruction MOV EAX, identificateur_de_service. Donc l'information hexadécimale sauvegardée à chaque début de fonction système est B8 xx xx xx xx. L'instruction B8 correspond à l'instruction i386 MOV EAX, imm32 opcode et le 32 bits suivant sont l'identificateur de service. Nous pouvons alors récupérer l'identificateur avec l'expression suivante : ulService = *(PULONG)((PUCHAR)pbHandler + 1);

Sous le système d'exploitation Windows XP, nous rencontrons une difficulté supplémentaire. Pour décourager les programmeurs d'utiliser ces interfaces non documentées, Microsoft à mis une protection en écriture sur cette table de services. Lorsqu'un logiciel essaie d'écrire dans cette table, le système arrête et donne un écran bleu d'erreur critique. À moins que l'on enlève la protection en écriture avant de lancer notre fonction. La méthode la plus simple pour désactiver la protection est de mettre le WP bit à zéro dans le registre CR0. Une autre méthode serait de changer les pointeurs vers les pages utilisées en mémoire pour la table de services afin que notre procédure puisse écrire sur ces pages et ensuite restaurer ces pointeurs. Cependant, nous utiliserons plutôt la procédure suivante :

MOV EAX, CR0
AND EAX, NOT 10000H
MOV CR0, EAX
; code pour la modification de la table
MOV EAX, CR0
OR EAX, 10000H
MOV CR0, EAX

Il est important de bien comprendre que ces méthodes utilisent des interfaces noyau qui ne sont pas documentées. Il est possible qu'elles cessent de fonctionner dans des versions plus récentes de Windows. Microsoft a démontré plus d'une fois qu'il n'aime pas que l'on utilise ces interfaces et feront probablement d'autres changements dans les versions ultérieurs de Windows pour que les fonctions utilisant ces interfaces cessent de fonctionner. [10] [9]  [3] [11]

Communication avec l'application

Les codes de contrôles d'entrées/sorties

Les codes de contrôles d'entrées/sorties (IOTCL) sont utilisés pour la communication entre une application en mode utilisateur et un pilote ou bien pour communiquer entre les pilotes à l'aide d'une pile. Ces codes de contrôles sont envoyés avec l'aide des IRPs (fonctions majeures). L'application utilisateur envoie son IOCTL au pilote en appelant la fonction DeviceIoControl, qui est décrite dans la documentation SDK de Microsoft. Un appel à cette fonction pousse le contrôleur d'entrées/sorties à appeler une requête IRP_MJ_DEVICE_CONTROL et l'envoie au pilote visé. De plus, les pilotes peuvent eux-mêmes envoyer des IOCTLs vers des pilotes de plus bas niveau (plus près de la machine) en créant et en envoyant une requête IRP_MJ_DEVICE_CONTROL ou IRP_MJ_INTERNAL_DEVICE_CONTROL. Les pilotes peuvent envoyer ces requêtes en invoquant les fonctions DispatchDeviceControl et DispatchInternalDeviceControl. (Les applications utilisateurs ne peuvent pas envoyer une requête de contrôle interne.)

Quelques IOTCLs sont publics et d'autres sont privés. Ceux publics sont normalement définis par le système et sont documentés par Microsoft, soit dans la DDK ou bien dans la SDK. Ils peuvent être envoyés du mode usagé à partir de la fonction DeviceIoControl ou bien d'un pilote à l'autre. Les privés d'un autre coté sont fait exclusivement par les concepteurs de logiciels pour communiquer entre eux. Les IOTCLs privés sont documentés dans un fichier entête du concepteur et ne sont normalement pas documentés publiquement. Ils peuvent, comme les IOTCLs publics, être envoyés à l'aide de la commande DeviceIoControl ou bien d'un pilote à l'autre. Il n'existe donc pas de différence entre les deux IOTCLs d'un point de vue utilisateur, cependant la différence se retrouve dans le code interne qui peut être utilisé dans ceux privé comparé à ceux publics. Si les IOTCLs fournis par Microsoft ne satisfont pas les besoins du concepteur, il peut donc en créer lui-même. C'est ce que nous avons fait pour notre prototype.

 [5]

Définir ses propres codes de contrôles d'entrées/sorties

Il est important de bien comprendre les règles suivantes lorsque l'on fait de nouveaux IOTCLs :

Un code de contrôle d'entrées/sorties est codé sur 32 bits et comprend plusieurs champs. La figure 1 montre le corps de celui-ci.

Corps d'un code de contrôle d'entrées/sorties.
Figure 1: Corps d'un code de contrôle d'entrées/sorties

Pour la création de nouveaux codes de contrôle d'entrées/sorties, nous pouvons utiliser le macro CTL_CODE qui est fournis avec les entêtes wdm.h et ntddk.h. La définition de ces nouveaux codes, qu'ils soit conçus pour une utilisation avec IRP_MJ_DEVICE_CONTROL ou bien IRP_MJ_INTERNAL_DEVICE_CONTROL doivent utiliser la commande suivante : #define IOCTL_Device_Function CTL_CODE(DeviceType, FunctionCode, TransferType, RequiredAccess) Il est important de choisir des noms descriptifs pour la constante qui est ainsi définie, Device doit être le nom du pilote qui utilise le code et Function est l'opération effectuée. De cette manière le code source d'une application sera beaucoup plus intuitif. Nous devons fournir les paramètres suivants au macro CTL_CODE :

DeviceType
Identifie le type de pilote. Cette valeur doit correspondre à celle qui a été définie dans la structure DEVICE_OBJECT du pilote. Les valeurs plus basses que 0x8000 sont réservées à Microsoft et celles qui sont plus hautes peuvent être utilisées par les concepteurs. Lorsqu'une valeur plus haute est utilisée, le bit Common est activé.
FunctionCode
Identifie la fonction à accomplir par le pilote. Les valeurs plus basses que 0x800 sont réservés à Microsoft et celles qui sont plus hautes peuvent être utilisés par les concepteurs. Lorsqu'une valeur plus haute est utilisée, le bit Custom est activé.
TransferType
Indique comment le pilote va transférer les données entre l'application appelante (soit en mode utilisateur ou un pilote) et le pilote qui répond à l'appel. Nous devons utiliser une des constantes définies par le système.
METHOD_BUFFERED
Spécifie que le l'information sera transféré à l'aide de tampons. Cette méthode est normalement utilisée pour transmettre de petites quantités de données sur demande. La majorité des codes de contrôle pour les pilotes intermédiaires utilisent cette méthode. C'est cette méthode que nous utilisons dans notre application.
METHOD_IN_DIRECT ou METHOD_OUT_DIRECT
Spécifie un mode de transfert direct qui est normalement utilisé pour écrire et lire de grande quantités d'informations en utilisant les techniques d'adressage de mémoire directe (DMA) ou tout autres méthode permettant un transfère rapide des données. L'appelant de la méthode doit spécifier METHOD_IN_DIRECT lorsqu'il veut transférer des données vers le pilote et METHOD_OUT_DIRECT lorsqu'il veut recevoir des données du pilote.
METHOD_NEITHER
Spécifie que le transfère de données ne sera pas fait à l'aide de tampons ou de transfert direct à la mémoire. Les IRPs vont fournir les adresses virtuelles des tampons d'entrée et de sortie du mode utilisateur qui seront spécifiés par l'appel de la fonction DeviceIoControl ou IoBuildDeviceIoControlRequest. Ces adresses virtuelles ne seront pas validées ni paginées par le pilote. Cette méthode ne peut être utilisée que si le pilote peut garantir qu'elle sera exécutée dans le même contexte que l'application ou le thread qui envoie la requête d'entrée/sortie. Seul les pilotes de haut niveau dans le mode noyau peuvent garantir qu'ils rencontreront cette condition. Donc, cette méthode n'est pas souvent utilisée pour les codes de contrôles passés aux pilotes de bas niveau. Les pilotes doivent déterminer s'ils doivent donner un accès avec tampon ou direct aux données lorsqu'ils reçoivent cette requête. Il devra possiblement mettre sous verrou les tampons lorsqu'il transfèrera de l'information à ceux-ci. Tout ceci dans le but de prévenir que le logiciel appelant accède aux tampons lorsqu'un transfert de données est effectué.
RequiredAccess
Indique le type d'accès que l'application appelante doit demander lorsqu'il ouvre le fichier d'objet du pilote (lorsqu'il se connecte au pilote). Le contrôleur d'entrées/sorties va créer le IRP et appeler le pilote seulement si l'application à les droits d'accès nécessaires. Nous pouvons utiliser les constantes suivantes pour déterminer l'accès de notre IOTCL :
FILE_ANY_ACCESS
Le contrôleur envoie des requêtes seulement pour les applications ayant le handle du pilote correspondant.
FILE_READ_DATA
Le contrôleur envoie des requêtes seulement pour les applications ayant un droit de lecture. Ce qui alloue au pilote de transférer des données du pilote vers la mémoire système.
FILE_WRITE_DATA
Le contrôleur envoie des requêtes seulement pour les applications ayant un droit d'écriture. Ce qui alloue au pilote de transférer des données de la mémoire système vers sa mémoire interne.
Nous pouvons utiliser un OU logique entre FILE_READ_DATA et FILE_WRITE_DATA ce qui demande à l'application appelante d'avoir un accès en lecture et en écriture. Avant de spécifier un accès FILE_WRITE_DATA pour un nouveau IOTCL, nous devons être absolument certain qu'en allouant un accès non restrictif à notre pilote nous ne créons pas des manières possibles pour les utilisateurs malicieux de compromettre le système. Étant donné que notre application n'est qu'en un stade de prototype, nous avons choisis ce type d'accès. [5]

Description des tampons pour les différents codes de contrôles d'entrées/sorties

Les codes de contrôles sont contenus dans les requêtes IRP_MJ_DEVICE_CONTROL et IRP_MJ_INTERNAL_DEVICE_CONTROL. Le contrôleur d'entrées/sorties crée ces requêtes lorsque les fonctions DeviceIoControl et IoBuildDeviceIoControlRequest sont appelées. Ces deux fonctions acceptent comme paramètre un tampon d'entrée et un tampon de sortie. Donc, toutes les requêtes lancées vont fournir un tampon d'entrée et un tampon de sortie. La manière qu'il sont traités par le système dépend du type de transfert définis par le IOTCL. Nous pouvons donc utiliser les tampons de la manière suivante, dépendamment quel type de transfert à été définis :

METHOD_BUFFERED
Pour ce type de transfère, l'IRP offre un pointeur vers un tampon à Irp->AssociatedIrp.SystemBuffer. Ce tampon représente le tampon d'entrée et le tampon de sortie qui sont spécifiés lors de l'appel de DeviceIoControl et IoBuildDeviceIoControlRequest. Le pilote transfert les données hors de ce tampon et ensuite le rempli avec les données qui doivent sortir du pilote. La taille du tampon d'entrée est spécifiée par Parameters.DeviceIoControl.InputBufferLength et celle du tampon de sortie par Parameters.DeviceIoControl.OutputBufferLength dans la structure IO_STACK_LOCATION. La taille du tampon que le système alloue pour l'entrée/sortie est la plus grande des deux valeurs.
METHOD_IN_DIRECT ou METHOD_OUT_DIRECT
Pour ce type de transfère, l'IRP offre un pointeur vers un tampon à Irp->AssociatedIrp.SystemBuffer. Il représente le tampon d'entrée qui est spécifié lors de l'appel de DeviceIoControl et IoBuildDeviceIoControlRequest. La taille du tampon est spécifiée par Parameters.DeviceIoControl.InputBufferLength dans la structure IO_STACK_LOCATION. Pour ces types de transfert, l'IRP fournis aussi un pointeur vers une liste de descripteur de mémoire (MDL) à Irp->MdlAddress. Ce MDL est le tampon qui a été défini comme celui de sortie lors de l'appel des fonctions. La taille de tampon est donc définie par Parameters.DeviceIoControl.OutputBufferLength dans la structure IO_STACK_LOCATION. Cependant, ce dernier peut être utilisé soit comme un tampon d'entrée, soit comme un tampon de sortie, dépendamment de la méthode de transfert utilisé :
METHOD_NEITHER
Le contrôleur d'entrées/sorties n'offre aucun tampons ou MDLs. L'IRP fournis les adresses virtuelles des tampons d'entrées et de sorties en mode utilisateur qui ont été spécifiées lors de l'appel de DeviceIoControl et IoBuildDeviceIoControlRequest sans les valider ou bien les paginer. L'adresse du tampon d'entrée est fournie par Irp->UserBuffer Parameters.DeviceIoControl.Type3InputBuffer dans la structure IO_STACK_LOCATION et l'adresse du tampon de sortie est à Irp->UserBuffer. La taille du tampon d'entrée et de sortie sont respectivement à Parameters.DeviceIoControl.InputBufferLength et à Parameters.DeviceIoControl.OutputBufferLength dans la structure IO_STACK_LOCATION du pilote. [5]

Création et envoie de la trace à l'aide de tampons

Dans la structure interne de notre application, nous avons deux tampons qui définissent chacun une trace. Nous plaçons donc l'information de chaque appel système dans une des traces en prenant soin d'indiquer le processus appelant et le temps de l'appel (nous pourrions placer les informations supplémentaires concernant chaque appel, mais ce n'est pas dans les objectifs de notre prototype). Un évènement est créé pour avertir l'application lorsque l'un des tampons interne du pilote est plein. L'application peut donc utiliser un thread séparé pour faire une requête à une intervalle régulière ou bien lorsqu'un tampon interne est plein, afin de récupérer la trace des appels systèmes. Cette trace est alors soit filtrée par processus et passée à l'évaluateur de modèle ou bien elle est tout simplement affichée à l'écran.

Évaluation de modèle

L'autre technologie qui a été utilisée pour mettre au point notre prototype d'anti-virus basé sur la détection de comportements malicieux est l'Évaluation de modèle dit « Model Checking » dans la littérature anglophone. L'évaluation de modèle sert à faire de la vérification formelle de système et quelques-uns de ses domaines d'applications sont :

Nous allons tout d'abord définir et expliquer les bases nécessaires pour implanter l'évaluateur de modèle. Ces bases sont : la logique linéaire temporelle (LTL), l'automate de Büchi et la méthode de conversion de la formule LTL en un automate de Büchi. Ensuite nous expliquerons la méthode d'évaluation de modèle.

La logique linéaire temporelle

Pour implanter notre évaluateur de modèle, il a fallu maîtriser tout d'abord la logique linéaire temporelle. En effet, cette logique sera utilisée plus tard pour exprimer le comportement malicieux que le système ne devrait pas vérifier. La logique linéaire temporelle englobe la logique propositionnelle. Elle est utilisée pour exprimer certaines propriétés caractéristiques des systèmes réactifs. Ces types de systèmes réagissent et interagissent continuellement avec l'environnement et les propriétés que ces systèmes doivent vérifier sont la sécurité et la durabilité. Donc, comme mentionné précédemment, la logique linéaire temporelle recouvre la logique propositionnelle et c'est la raison pour laquelle la logique temporelle utilise les mêmes opérateurs que la logique propositionnelle enrichie des opérateurs temporels. La syntaxe de la logique LTL est donc la suivante :

Soit $\rho$ une proposition atomique, l'ensemble des formules LTL est défini par :

$\varphi$ ::= $\rho \ \vert \ \neg \varphi \ \vert \ \varphi \vee \varphi \ \vert \ X \varphi \ \vert \ \varphi \cup \varphi$

Les opérateurs booléens habituels : la conjonction ($\wedge$), l'implication ($\Rightarrow$) et l'équivalence ( $\Leftrightarrow$) ainsi que les formules true et false se définissent comme suit :

Quant aux opérateurs temporels, leur signification respective est la suivante :

$ \mathbf{X \varphi} $ (next) :

Il fait référence à l'état suivant. Cette formule est vraie si $\varphi$ est vraie dans cet état suivant.
$ \mathbf{\gamma \cup \varphi} $ (until) :

Il fait référence à tous les états futurs jusqu'à ce qu'une certaine condition soit vraie. Cette formule est vraie si $\gamma$ est vraie jusqu'à ce que $\varphi$ le devienne.
$ \mathbf{F \varphi \equiv \emph{true} \cup \varphi} $ (eventually) :

Il fait référence à un certain état dans le futur. Autrement dit la formule est vraie si $\varphi$ est vraie à un instant dans le futur.
$ \mathbf{G \varphi \equiv \neg F \neg \varphi} $ (globally) :

Il fait référence à tous les états futurs et la formule est vraie que si et seulement si $\varphi$ est vraie dans tous les états futurs, c'est-à-dire que $\neg \varphi$ ne soit jamais vraie dans le futur.

Dans notre application :

Ainsi, la logique linéaire temporelle sera utilisée pour formuler le comportement non désiré du système, ensuite la formule sera convertie en un automate de Büchi, de même que le modèle du système à vérifier.

L'automate de Büchi

Le but est de construire un automate qui génère toutes les séquences d'exécution satisfaisant une formule $\varphi$ dans la logique temporelle donnée. [2] L'automate qui sera construit est appelé automate de Büchi généralisé car il comporte plusieurs ensembles d'états finaux contrairement à l'automate de Büchi simple qui n'en comporte qu'un seul. Un automate de Büchi généralisé est la donnée d'un quadruplet $A =
(Q, I, \rightarrow, F)$, où :

Une exécution de $A$ est une séquence infinie $ \sigma =
q_{0}q_{1}q_{2}… $ telle que $q_{0} \in I$ et, pour chaque $i \geq
0$, $q_{i} \rightarrow q_{i} + 1$. Une exécution acceptée $\sigma$ est une exécution tel que pour chaque ensemble d'états finaux, il existe au moins un état $q$ appartenant à cette ensemble qui apparaît infiniment souvent dans $\sigma$.

Il faut à présent définir l'alphabet de cet automate :

Soit $D$, un domaine fini ou l'alphabet et $L : Q \rightarrow
2^{D}$ la fonction associant à chaque état $q$ un ensemble d'éléments de cet alphabet. Le mot $ \xi = x_{0}x_{1}x_{2}…$ est accepté par l'automate si et seulement si il existe une exécution acceptée $\sigma$ tel que pour chaque $i \geq 0, \ x_{i} \in L
(q_{i})$.

L'algorithme permettant de convertir une propriété LTL en un automate de Büchi

L'automate de Büchi sera représenté par un graphe et la structure de chaque noeud est la suivante :

L'algorithme [2], pour convertir une formule $\varphi$ dans la logique LTL en un automate de Büchi, commence avec un noeud unique. Au départ, le champ m_new ne contient que la seule formule $\varphi$ à convertir et tous les autres ensembles sont vides. Avec le nud courant $N$, l'algorithme vérifie s'il reste des formules non traitées dans l'ensemble m_new. Si c'est le cas, le nud $N$ est étendu et une formule $\eta$ est retirée de l'ensemble m_new. Dans le cas où $\eta$ est une proposition ou la négation d'une proposition et que $\neg \eta$ est dans l'ensemble m_old, donc le nud est supprimé, sinon il est rajouté à m_atomic. Si $\eta$ n'est pas un littéral, le nud $N$ peut être divisé ou non et de nouvelles formules peuvent être rajoutées aux ensembles m_new et m_old.

Dépendamment de $\eta$ on aura :

Lorsque toutes les formules étant dans l'ensemble m_new du noeud $N$ ont été traitées, il faut ensuite vérifier si ce noeud sera ajouté au graphe. S'il existe déjà un nud dans le graphe qui a les mêmes contenus pour les ensembles m_next et m_old, ce noeud doit être mis à jour en fusionnant son ensemble m_incoming à celui du nud $N$. Cependant si le nud $N$ est un nouveau nud, il est rajouté tout simplement au graphe et un autre nud est créé de la manière suivante :

Implantation de l'évaluation de modèle

Comme il est dit plus haut, l'évaluation de modèle sert à faire de la vérification de système afin de voir si le système n'a pas un comportement indésirable. Ainsi, supposons que l'on veuille vérifier que le propriété $\eta$ est toujours vérifiée par le système. Les étapes à suivre sont les suivantes :

L'algorithme le plus souvent utilisé pour implanter l'évaluation de modèle est le « Nested Depth-First Search », autrement dit la recherche en profondeur itérative. [4] Pour améliorer les performances de cet algorithme et réduire considérablement l'espace d'états générés par le produit synchrone, la construction de ce produit peut être fait « sur demande ». Cela veut dire qu'un nouveau noeud est considéré dans le graphe de la formule que si aucun cycle acceptant n'a été trouvé. Ainsi il est possible de trouver un résultat sans avoir à construire tout le graphe de la formule et, donc par la même occasion tout le graphe du produit. [2]

Illustration de l'évaluation de modèle.
Figure 2: Illustration de l'évaluation de modèle

Interface utilisateur

Fonctionnement interne

L'application en mode utilisateur contient un thread qui, lorsque démarr/, communique avec le pilote pour obtenir l'information sur les interceptions de processus. Ce thread est activé lorsque l'utilisateur lance la fonction démarrer les interceptions d'appel système. À ce moment, il envoie un message au pilote pour qu'il commence à intercepter les appels systèmes et remplisse ses tampons. Le thread se met en attente d'un événement de tampon plein, mais avec un délai d'attente d'une centaine de millisecondes. Lorsque l'un des deux arrive à échéance, il va chercher le contenu du tampon dans le pilote et place cette information dans une trace interne à l'application.

Lorsque l'application reçoit les appels système, il n'a qu'un numéro d'appel qu'il doit référer à l'appel système correspondant, ce qui est fait à l'aide d'une information partagée entre les deux applications. Les autres informations présentes sont le temps, c'est-à-dire le nombre de tics qui ont eu lieu depuis le premier janvier 1970 et l'identificateur de processus. Nous convertissons donc le temps sous un format que l'utilisateur peut comprendre et mettons cette information avec le numéro d'appel système. L'utilisateur ne peut pas faire un lien direct entre l'identificateur de processus et le nom de l'application. Nous devons donc faire une recherche pour retrouver ce nom d'application. La technique utilisée comprend plusieurs étapes. Nous débutons par prendre une photo des processus présents en mémoire et nous parcourons cette liste. Nous tentons ensuite de trouver une combinaison entre l'identificateur du processus appelant et l'image en mémoire. De cette manière nous avons toute l'information nécessaire pour notre prototype. Nous plaçons le tout dans une structure qui est contenue dans une liste chaînée.

Cette liste chaînée est notre trace, nous utilisons une classe de la librairie MFC qui nous permet de placer différents éléments et les référer lorsque nécessaire. Cette liste contient plusieurs outils que nous pourrons utilisés pour l'affichage et l'enregistrement de la dite trace.

Affichage des données

Notre fenêtre principale contient des menus, des boutons et un écran pour l'affichage des données. Nous avons fait en sorte que l'affichage des données représente l'aspect de la liste chaînée, nous utilisons un comptage des éléments à la gauche, suivit du nom de l'image, du numéro de processus, du nom de l'appel système lancé et du temps de l'appel. Il est à noter qu'ici on affiche le nom de l'appel de haut niveau, tandis que l'appel intercepté était de très bas niveau. Par exemple nous devrions avoir ZwReadFile au lieu de ReadFile. Cependant nous avons cru que pour simplifier la lecture de l'utilisateur, il serait mieux d'utiliser des noms qu'il comprend. Cet affichage était très simple à concevoir, car il est en lien direct avec la liste chaînée utilisée, donc lorsque la liste est modifiée, l'affichage change automatiquement.

Nous utilisons aussi des options d'affichage pour rendre la vue plus facile à l'utilisateur. Par exemple, nous avons une option qui empêche l'écran de dérouler automatiquement lorsqu'un nouvel appel système est enregistré, ce qui permet de lire ce qui est afficher et de ne pas en être étourdis. Il est à noter que l'affichage de plusieurs centaines d'appels à la seconde est demandant sur des processeurs plus anciens et il est recommandé de ne pas mettre à jour l'écran automatiquement (déroulement automatique) afin d'optimiser les performance de l'application. Nous avons aussi une commande pour effacer la trace en mémoire lorsque l'on désire analyser un processus différent et lorsque nous voulons avoir les appels systèmes fait par celui-ci. Finalement, une option pour sauvegarder la trace est disponible si l'utilisateur veut l'analyser ultérieurement.

Fenêtre de vérification de code malicieux

L'option la plus importante de notre application est la fenêtre où l'on peut déterminer quel comportement est malicieux et indiquer l'application à vérifier. Cette fenêtre comporte deux champs, le premier doit contenir le nom de l'application à vérifier. Ce nom doit être exactement le même que celui de l'application et doit respecter la case. Autrement, les appels systèmes ne seront pas interceptés pour cette application. Le deuxième champ est utilisé pour entrer la formule à vérifier. Cette formule doit être une formule valide LTL et elle doit être entrée en mode préfixé.

L'application comprend des boutons qui permettent d'entrer les codes spécifiques au langage LTL et les appels systèmes doivent être entrés en lettres majuscules. Par exemple l'appel ReadFile() doit être entré de la manière suivante : READFILE. Il existe présentement 29 appels systèmes pouvant être interceptés et analysés dans notre application : OPENFILE, QUERYINFORMATIONFILE, SETINFORMATIONFILE, READFILE, WRITEFILE, CREATEDIRECTORYOBJECT, MAKETEMPORARYOBJECT, OPENSECTION, MAPVIEWOFSECTION, UNMAPVIEWOFSECTION, SETINFORMATIONTHREAD, CREATEKEY, OPENKEY, DELETEKEY, ENUMERATEKEY, ENUMERATEVALUEKEY, FLUSHKEY, QUERYKEY, QUERYVALUEKEY, SETVALUEKEY, OPENSYMBOLICLINKOBJECT, QUERYSYMBOLICLINKOBJECT, CREATETIMER, OPENTIMER, CANCELTIMER, SETTIMER, QUERYDIRECTORYFILE, QUERYSYSTEMINFORMATION et CREATEFILE.

Lorsque l'utilisateur clique sur 'OK', l'application commence automatiquement à n'intercepter que les appels systèmes étant faits par l'application donnée. Chaque appel est passé à l'évaluateur de modèle qui fait changer l'automate d'état et détermine s'il est dans un état final. Lorsque l'automate entre dans un état final, un message est affiché à l'écran indiquant qu'un comportement malicieux a été découvert et indique la formule correspondante. Les appels systèmes s'arrêtent à ce moment. Il se peut que la formule écrite par l'utilisateur ne soit pas correcte, dans ce cas un message sera affiché à l'écran indiquant que la formule est incorrecte et les appels systèmes de l'application seront interceptés, mais pas analysés.

Conclusions

Résultats de nos travaux

Nous avons fait plusieurs tests sur notre prototype avec différentes applications et de nombreuses formules logiques. Présentement, tout semble fonctionner correctement. Pour ce qui est de l'analyse des comportements, l'application détecte une suite d'appels systèmes malicieux qui ont étés définis par une formule LTL. Cependant, il ne peut analyser qu'un seul processus à la fois et lorsque deux processus du même nom sont activés, il prend la trace des deux à la fois.

L'application reste tout de même fonctionnelle pour les objectifs que nous avions fixés au départ. Nous avons un prototype qui démontre qu'il est possible d'intercepter les appels systèmes en mode noyau, de passer ceux-ci à un interpréteur en mode utilisateur et de déterminer si un enchaînement spécifique des processus nous donne un comportement malicieux.

Un autre objectif sous tendant à ce prototype est l'apprentissage que nous avons fait au cours du développement de notre projet synthèse. En effet, nous savons maintenant comment développer un pilote qui se place en mode noyau, chose que nous ne savions pas avant de commencer. Cette connaissance nous offre une vue plus approfondie sur le fonctionnement même du système d'exploitation et de l'importance d'avoir du code très fonctionnel dans la couche noyau. Nous avons aussi assimilé d'importantes connaissances pour ce qui est de la logique linéaire temporelle. Cet acquis nous permet d'analyser non seulement les appels systèmes comme dans notre application, mais aussi les événements pouvant survenir lors de l'exécution d'une application en temps réel. Nous pourrons facilement transférer ces connaissances à d'autres domaines d'applications.

Objectifs futurs

Ce prototype nous a placé devant un grand défi, celui de concevoir un anti-virus viable dans un environnement réel. Nous devons comprendre ce qu'il reste à faire pour rendre ce logiciel performant et aussi ce qui doit être changé. L'optimisation du code en mode noyau est une tâche très importante, chaque appel système qui est lancé est dérouté vers notre application et est analysé. Ce traitement doit être le plus rapide possible pour ne pas que l'utilisateur puisse s'apercevoir du changement dans le temps de réponse du système. Nous pourrions avoir des objectif comme une utilisation du temps de processeur par notre application ne pouvant dépasser un certain nombre de cycles par appels.

L'analyse comportementale devrait avoir lieu en mode noyau afin d'améliorer la vitesse de l'application en générale et afin de permettre au pilote de suspendre l'application fautive en cas de comportement malicieux. De cette manière, nous pourrions aussi garder une trace et une image des appels faits par l'application fautive et tenter de faire un retour en arrière pour réparer les dommages potentiels sur les fichiers.

Une recherche approfondie sur les codes malicieux, ainsi qu'une analyse des différents types nous permettrais d'acquérir les connaissances nécessaires à l'élaboration de formules logiques pouvant détecter les codes malicieux avec finesse, et laisser passer les applications légitimes. Il est important d'avoir la formule la plus concise possible afin d'éviter les fausses alarmes et ainsi blaser l'utilisateur qui laisserait passer toutes les applications sans se soucier d'observer quel est le comportement détecté.

Notre application devrait aussi permettre à l'utilisateur de spécifier différentes formules LTL qui déterminent des comportements qu'il ne veut pas dans son système. Ces formules seraient sauvegardées avec les autres qui seraient fournies par l'application. De plus, l'utilisateur devrait pouvoir éliminer des comportements qu'il considère comme souhaitables dans son système, afin de permettre certaines actions qui seraient bloquées en temps normal.

Une autre application serait aussi de mettre notre évaluateur de modèle dans un système d'exploitation virtuel et d'analyser les comportements des logiciels entrant dans le réseau d'une entreprise, afin de déterminer s'ils ont un comportement malicieux. Cette technique appelée Sandbox est en voie de développement et est très prometteuse dans le domaine des anti-virus. Il est cependant important d'avoir une machine virtuelle la plus complète possible afin que le code malicieux ne puisse déterminer s'il est dans un vrai environnement ou non.

Comme nous l'avons vu, beaucoup de choses peuvent être ajoutées à notre prototype pour le rendre au produit final. Il serait intéressant de poursuivre la recherche dans ce domaine pour combattre les codes malicieux à la source, avant même qu'ils soient conçus

Bibliographie

1
Michelle Delio.
Wired news: Find the cost of (virus) freedom.
http://www.wired.com/news/infostructure/0,1377,49681,00.html, January 2002.

2
Rob Gerth, Doron Peled, Moshe Y. Vardi, and Pierre Wolper.
Simple on-the-fly automatic verification of linear temporal logic.
In Protocol Specification Testing and Verification, pages 3-18, Warsaw, Poland, 1995. Chapman & Hall.

3
Nishad P. Herath.
Adding native apis.
microsoft.public.win32.programmer.kernel, October 1998.

4
Gerard J. Holzmann.
The model checker SPIN.
Software Engineering, 23(5):279-295, 1997.

5
http://msdn.microsoft.com.
Microsoft Solution Development Network (MSDN).

6
Eugene Kaspersky.
Computer viruses-what are they and how to fight them?
http://www.avp.ch/avpve, January 2002.

7
Jeffrey O. Kephart and William C. Arnold.
Automatic extraction of computer virus signatures.
In R. Ford, editor, 4th Virus Bulletin International Conference, pages 178-184, Thomas J. Watson Research Center, Yorktown Heights, NY 10598, 1994. High Integrity Computing Laboratory, Virus Bulletin Ltd.

8
James Middleton.
Anna virus generator back in business.
http://www.vnunet.com/News/1118930, March 2001.

9
Milind Borate Prasad Dabak, Sandeep Phadke.
Undocumented Windows NT.
M&T Books, October 1999.

10
Tim Robbins.
Windows nt system service table hooking.
http://www.wiretapped.net/~fyre/sst.html, May 2003.

11
Mark Russinovich and Bryce Cogswell.
Windows nt system-call hooking.
Dr. Dobb's Journal, January 1997.

12
Sven B. Schreiber.
Undocumented Windows 2000 Secrets.
Addison-Wesley, 2001.

13
Pete Simpson.
An infectious year: Looking back on malware in 2001.
http://www.itsecurity.com/papers/mime4.htm, February 2002.

14
Sophos.
Sophos virus analysis: Wm97/ff-h.
www.sophos.com/virusinfo/analyses/wm97ffh.html, February 2001.

15
Morton Swimmer.
Malware.
http://www.swimmer.org/morton/malware.html, 2002.