Introspection et analyse de malware via LibVMI

Magazine
Marque
MISC
Numéro
102
|
Mois de parution
mars 2019
|
Domaines


Résumé

Dans cet article, nous allons illustrer ce qu’est l’introspection des machines virtuelles et comment on peut l’utiliser dans le but d’analyser des malwares.


Body

1. Qu’est-ce que l’introspection ?

De façon générale, le terme introspection désigne l’observation et l’examen de son propre état mental et émotionnel. Il est considéré comme l’acte de se regarder soi-même. Le meilleur exemple pour illustrer cela est l’œil de judas qui permet d’observer le monde extérieur sans avoir besoin d’en faire partie. Toutefois, l’introspection des machines virtuelles est l’art de monitorer les machines virtuelles depuis l’hyperviseur et y accéder sans être dedans (aucun agent n’est installé sur la machine virtuelle).

2. Pourquoi l’introspection ?

Grâce à la technologie d’introspection, il n’y a nul besoin de faire partie de l’environnement du malware pour l’analyser. Tout simplement parce que le comportement des processus à surveiller sera réalisé en dehors de la machine virtuelle, depuis l’hyperviseur. De plus, un malware employant des techniques de détection de débogueurne marchera pas étant donné que le système d’introspection interagit uniquement avec la mémoire de la machine virtuelle et ne s’attache en aucun cas aux processus lancés sur la machine. De même, il est possible de tromper un malware employant des techniques de détection d’environnement d’analyse (modifier à la volée l’accès à une clé de registre VirtualBox par exemple). De ce fait, on ne peut nier que l’application de l’introspection à l’analyse de malware est bien meilleure que les technologies traditionnelles d’analyse automatisée.

3. Architecture de l’introspection

3.1 Types d’hyperviseurs

On considère généralement deux types d’hyperviseurs :

  • Hyperviseur natif : connu aussi sous le nom de l’hyperviseur type-1 ou encore « Bare Metal ». Ce type d’hyperviseur fonctionne directement sur le matériel de la machine afin de contrôler et gérer les machines virtuelles. Exemples d’hyperviseurs de type-1 : Xen, KVM...
  • Hyperviseur hébergé : connu aussi sous le nom de l’hyperviseur de type-2 ou encore « Hosted ». Ce type d’hyperviseur s’exécute à l’intérieur d’un autre système d’exploitation. Exemples d’hyperviseurs de type-2 : VirtualBox, VMware...

La figure suivante illustre le fonctionnement de l’introspection sur l’hyperviseur de type-1. Les outils d’introspection installés sur l’hyperviseur permettent de surveiller et de contrôler tout ce qui se passe sur la machine virtuelle (activité système, réseau, applicative…).

 

introspection_dans_analyse_de_malware_figure_01

 

Fig 1 : Exemple d’hyperviseur natif et d’hyperviseur hébergé.

On peut bien imaginer le même fonctionnement pour l’hyperviseur de type-2.

3.2 Mappage mémoire

De façon générale, il existe deux niveaux de mémoire ; mémoire virtuelle et mémoire physique de la machine physique. Et trois niveaux de mémoire quand on parle d’hyperviseur (mémoire virtuelle et mémoire physique de la machine virtuelle, et mémoire physique de la machine hôte. La mémoire virtuelle de la machine hôte est abstraite dans ce cas). Il faut garder en tête que les hyperviseurs allouent uniquement de la mémoire à la machine virtuelle. Les hyperviseurs par défaut n’ont aucune connaissance de ce qui se passe dans la mémoire virtuelle de la machine virtuelle. Pour ce faire, des outils additionnels doivent être installés. Ci-dessous un exemple simplifié du partage mémoire avec la machine virtuelle.

 

introspection_dans_analyse_de_malware_figure_02

 

Fig 2 : Exemple des trois niveaux d’adressage mémoire sous hyperviseur.

L’un des objectifs des outils d’introspection est de réaliser la traduction des adresses mémoire de la mémoire virtuelle de la machine virtuelle en mémoire physique de la machine virtuelle, et ensuite de la mémoire physique de la machine virtuelle en mémoire physique de la machine physique, dans le but d’aider l’hyperviseur à accéder à la bonne zone mémoire pendant son introspection.

4. LibVMI

4.1 Qu’est-ce que LibVMI ?

LibVMI est une librairie en C permettant de mettre en place un système d’introspection des machines virtuelles sous Linux ou Windows. Elle permet d’accéder à la mémoire d’une machine virtuelle en cours d’exécution. Ceci tout en offrant une panoplie de fonctions permettant d’accéder à la mémoire grâce à l’adressage physique ou virtuel et des symboles Kernel. LibVMI offre aussi un accès à la mémoire à partir d’un Snapshot de mémoire physique, ce qui peut s’avérer intéressant lors de débogage ou d’investigation numérique.

En plus de l’accès mémoire, LibVMI supporte les événements mémoire. Ces événements déclenchent des notifications quand une région mémoire est accédée en lecture, écriture ou en exécution.

Plusieurs niveaux d’abstraction complexes existent quand on parle d’introspection. Ces niveaux sont heureusement gérés par LibVMI [1] et nous sont complètement transparents.

4.2 Les API LibVMI

LibVMI met à disposition une variété de fonctions et d’API intuitives permettant au développeur d’interagir plus facilement avec la mémoire. Il existe des API basiques et d’autres, plus avancées.

Toutes les API peuvent être retrouvées sur le site officiel de LibVMI.

4.3 Exemples d’utilisation

LibVMI fournit par défaut un ensemble d’exemples afin de tester ce concept, voire même reposer sur ces exemples comme base dans le but de créer son propre système d’introspection.

 

Tous les exemples listés ci-après sont à retrouver au complet sur le GitHub de LibVMI [2].

4.3.1 Listing de processus

Le code suivant permet de lister les processus lancés sur la machine virtuelle depuis l’hyperviseur.

On commence par initialiser les contextes de fonctionnement de LibVMI et donc l’instance LibVMI qui correspondra à la machine virtuelle sur laquelle nous lancerons notre programme.

...
vmi_init_complete(&vmi, name, VMI_INIT_DOMAINNAME, NULL, VMI_CONFIG_GLOBAL_FILE_ENTRY, NULL, NULL)

...

Ensuite, suivant le système d’exploitation sur lequel nous souhaitons lister les processus, l’initialisation des champs sera différente (dans l’exemple fourni, on considère trois systèmes d’exploitation différents : Windows, Linux et FreeBSD).

...

if (VMI_OS_LINUX == vmi_get_ostype(vmi)) {
   if (VMI_FAILURE == vmi_get_offset(vmi, "linux_tasks", &tasks_offset))
      goto error_exit;

   if (VMI_FAILURE == vmi_get_offset(vmi, "linux_name", &name_offset))

      goto error_exit;

   if (VMI_FAILURE == vmi_get_offset(vmi, "linux_pid", &pid_offset))

      goto error_exit;

} else if (VMI_OS_WINDOWS == vmi_get_ostype(vmi)) {

   if (VMI_FAILURE == vmi_get_offset(vmi, "win_tasks", &tasks_offset))

      goto error_exit;

   if (VMI_FAILURE == vmi_get_offset(vmi, "win_pname", &name_offset))

      goto error_exit;

   if (VMI_FAILURE == vmi_get_offset(vmi, "win_pid", &pid_offset))

      goto error_exit;

} else if (VMI_OS_FREEBSD == vmi_get_ostype(vmi)) {

   tasks_offset = 0;

   if (VMI_FAILURE == vmi_get_offset(vmi, "freebsd_name", &name_offset))

      goto error_exit;

   if (VMI_FAILURE == vmi_get_offset(vmi, "freebsd_pid", &pid_offset))

      goto error_exit;

}

...

Après avoir récupéré les données nous intéressant, nous allons ensuite boucler sur la liste des processus et les afficher (PID, nom du processus et son adresse mémoire) un par un, jusqu’à la fin de la chaîne des processus.

...

while (1) {

   current_process = cur_list_entry – tasks_offset;

   vmi_read_32_va(vmi, current_process + pid_offset, 0, (uint32_t*)&pid);

   procname = vmi_read_str_va(vmi, current_process + name_offset, 0);

 

   if (!procname) {

      printf("Failed to find procname\n");

      goto error_exit;

   }

 

   printf("[%5d] %s (struct addr:%"PRIx64")\n", pid, procname, current_process);

 

   if (procname) {

      free(procname);

      procname = NULL;

   }

 

   if (VMI_OS_FREEBSD == os && next_list_entry == list_head) {

      break;

   }

 

   cur_list_entry = next_list_entry;

   status = vmi_read_addr_va(vmi, cur_list_entry, 0, &next_list_entry);

 

   if (status == VMI_FAILURE) {

      printf("Failed to read next pointer in loop at %"PRIx64"\n", cur_list_entry);

      goto error_exit;

   }

 

   if (VMI_OS_WINDOWS == os && next_list_entry == list_head) {

      break;

   } else if (VMI_OS_LINUX == os && cur_list_entry == list_head) {

      break;

   }

};

...

Et enfin, nous allons appeler la fonction qui va détruire l’instance LibVMI créée sur la machine virtuelle et libérer la mémoire concernée.

...

vmi_destroy(vmi);
...

4.3.2 Monitoring des événements

Mis à part son utilisation classique comme démontré dans l’exemple précédent, LibVMI permet aussi de monitorer des « événements » à la volée. Cela en notifiant l’hyperviseur de tout accès mémoire qui peut avoir lieu (lecture, écriture, exécution).

Ci-dessous, un exemple tiré aussi des exemples LibVMI permettant de créer des événements d’interruption.

Contrairement à la phase d’initialisation dans l’exemple du listing de processus, une petite modification est nécessaire. Le flag VMI_INIT_EVENTS doit être ajouté pour permettre à l’instance de monitorer les événements à la volée.

...
vmi_init(&vmi, VMI_XEN, (void*)name, VMI_INIT_DOMAINNAME | VMI_INIT_EVENTS, NULL, NULL)

...

Après avoir initialisé l’instance LibVMI, il est possible de créer un callback afin de traquer le type d’événement souhaité. Dans notre exemple, le choix a été fait sur l’événement d’interruption INT3 (breakpoint).

...
memset(&interrupt_event, 0, sizeof(vmi_event_t));
interrupt_event.version = VMI_EVENTS_VERSION;
interrupt_event.type = VMI_EVENT_INTERRUPT;
interrupt_event.interrupt_event.intr = INT3;

vmi_register_event(vmi, &interrupt_event);

...

On déclare ensuite une fonction de callback, qui sera exécutée à chaque fois qu’une interruption a lieu. Cette fonction de callback suppose que tous les événements INT3 sont causés par un débogueur par exemple, et va tout simplement les réinjecter sans rien faire.

event_response_t int3_cb(vmi_instance_t vmi, vmi_event_t *event)

{

   event->interrupt_event.reinject = 1;

 

   if (!event->interrupt_event.insn_length)

      event->interrupt_event.insn_length = 1;

 

   return 0;

}

5. Analyse de malware avec LibVMI

LibVMI permet de manipuler la mémoire, mais en aucun cas cela ne permet de faire de l’analyse de malware tel qu’il est. Afin de profiter du système d’introspection de LibVMI, il faudra développer son propre système pour analyser les malwares en reposant sur les API fournis par LibVMI.

Dans cette partie, nous allons nous focaliser sur le monitoring des API Kernel Windows, afin de lister les différents comportements du malware. Ci-dessous, quelques possibilités fournies par LibVMI pour monitorer la mémoire d’un exécutable.

5.1 Monitoring via des breakpoints

L’idée est donc de placer des breakpoints (points d’arrêt d’exécution, souvent utilisés dans le reverse engineering) sur les API Kernel qu’on souhaite monitorer sur la machine virtuelle lors de l’initialisation du système d’introspection. Ceci, tout en maintenant une table de correspondance des offsets des breakpoints placés et les octets modifiés. Après la mise en place et le lancement du système d’introspection, un malware sollicitant une API Kernel en passant par une API Usermode (l’appel à l’API Usermode CreateFile requiert l’appel à l’API Kernel NtCreateFile) sera interrompu si l’API Kernel figure parmi les API monitorées. Une notification est donc remontée au niveau de l’hyperviseur, nous permettant de retrouver quel exécutable dans l’espace utilisateur a atteint le breakpoint dans l’espace Kernel. Une fois que les informations souhaitées (la fonction, ses arguments…) sont récupérées, l’exécution du malware continue (surtout pour ne pas modifier son comportement) jusqu’au prochain breakpoint. Grâce à ces informations, il nous est possible de comprendre le comportement du malware analysé.

5.2 Monitoring via des pages mémoire

Avec cette technique, il n’y a nul besoin de placer des breakpoints et donc d’altérer la mémoire. L’idée consiste à placer une surveillance avec des événements mémoire sur chaque page mémoire exécutable et de calculer l’adresse des API qu’on souhaite monitorer. À chaque fois qu’une région de la page marquée en exécution est accédée, l’information est remontée et une comparaison est faite avec l’adresse de l’API monitorée.

5.3 Monitoring via des breakpoints + altp2m

Comme décrit précédemment, l’utilisation des breakpoints seule ne fonctionnera pas, c’est pourquoi les développeurs de Xen ont élaboré une technique intitulée altp2m. Cette technique a pour but de créer un shadow copy des pages mémoire souhaitées. L’idée est donc de créer une vue altérée (contenant les breakpoints sur les API Kernel qu’on souhaite monitorer) et garder une vue intacte qui sera retournée à chaque fois qu’il y a un accès en lecture ou écriture à ces pages mémoire.

Grâce à cette méthode, on résout les deux problèmes cités dans 5.1 Monitoring via des breakpoints.

6. Étude de cas

6.1 Préparation de l’environnement

Après avoir choisi le nom du domaine XEN de la machine, créé le fichier contenant le dictionnaire et déterminé le nom de fichier de malware à analyser, on lance depuis l’hyperviseur la commande suivante :

./monitor_api w6164-1 /tmp/w6164-1.json malware.exe

Notre script va prendre en argument plusieurs entrées :

  • monitor_api : un script que nous avons développé et qui repose sur les fonctions de LibVMI ;
  • w6164-1 : le nom du domaine XEN correspondant à de la machine virtuelle ;
  • /tmp/w6164-1.json : le fichier contenant les fonctions/structures et les offsets correspondants ;
  • malware.exe : le malware que nous souhaitons analyser.

Ci-dessous, le code permettant l’initialisation de notre système d’introspection :

...

vmi_init(&vmi, VMI_XEN | VMI_INIT_COMPLETE | VMI_INIT_EVENTS, argv[1]);

...

On crée ensuite la vue altp2m qui sera altérée :

...

xc_altp2m_set_domain_state(xch, domain_id, 1);

...
xc_altp2m_create_view(xch, domain_id, 0, &shadow_view);

...

6.2 Insertion des breakpoints

On définit dans un premier temps les API que nous souhaitons monitorer. Prenons par exemple les trois API suivantes :

  • NtCreateFile : permet la création/ouverture de fichier ;
  • NtSetValueKey: permet de créer ou remplacer la valeur d’une clé de registre ;
  • NtDelayExecution : permet de retarder l’exécution (cette API est exécutée quand l’appel à l’API Sleep est effectué). Elle peut être utilisée à des fins d’évasion.

Pour plus d’informations sur ces API, vous pouvez vous référer à la documentation MSDN [3].

Ensuite, on insère nos breakpoints dans la vue altp2m pour chacune des API.

...
uint8_t trap = 0xcc;

vmi_write_8_pa(vmi, (shadow << 12) + shadow_offset, &trap)

...

6.3 Déclaration des callbacks

6.3.1 Callback des événements d’interruption

Ce callback est déclenché à chaque fois qu’un breakpoint est atteint. Il va permettre de récupérer les informations souhaitées grâce à la fonction process_event(). Cette fonction va nous permettre d’identifier le nom de l’API qui a atteint le breakpoint ainsi que ses arguments.

...
SETUP_INTERRUPT_EVENT(&trap_event, 0, interrupt_event_cb);

vmi_register_event(vmi, &trap_event);
...
event_response_t interrupt_event_cb(vmi_instance_t vmi, vmi_event_t *event)

{

   ...

   process_event(...);

   event->slat_id = 0;

   event->interrupt_event.reinject = 0;

   return VMI_EVENT_RESPONSE_TOGGLE_SINGLESTEP | VMI_EVENT_RESPONSE_VMM_PAGETABLE_ID;

}

6.3.2 Callback des événements de pas-à-pas

Ce callback est déclenché directement après l’exécution du callback d’interruption. Cela permet d’exécuter en pas-à-pas (comme dans un débugueur) l’instruction du breakpoint depuis la vue non altérée. Ensuite, on reprend l’exécution depuis la vue altérée. Dans le cas où plusieurs vcpus sont attribués à la machine virtuelle, il est nécessaire de boucler sur le nombre de vcpus et de créer un callback pour chaque vcpu.

...

int vcpus = vmi_get_num_vcpus(vmi);

for (i = 0; i < vcpus; i++)

{

   SETUP_SINGLESTEP_EVENT(&singlestep_event[i], 1u << i, singlestep_event_cb, 0);

   vmi_register_event(vmi, &singlestep_event[i]);

}

...

event_response_t singlestep_event_cb(vmi_instance_t vmi, vmi_event_t *event)

{

   event->slat_id = shadow_view;

   return VMI_EVENT_RESPONSE_TOGGLE_SINGLESTEP | VMI_EVENT_RESPONSE_VMM_PAGETABLE_ID;

}

6.3.3 Callback des événements lecture/écriture mémoire

Ce callback est déclenché quand une lecture ou écriture est faite sur l’une des pages mémoire du shadow copy. Si tel est le cas, un changement de vue est instantanément fait sur la vue non altérée. Ceci permet d’éviter un BSOD lorsque le système ou un malware effectue une vérification d’intégrité.

...

SETUP_MEM_EVENT(&mem_event, ~0ULL, VMI_MEMACCESS_RW, memory_event_cb, 1);

vmi_register_event(vmi, &mem_event);

...

event_response_t memory_event_cb(vmi_instance_t vmi, vmi_event_t *event)

{

   event->slat_id = 0;

   return VMI_EVENT_RESPONSE_TOGGLE_SINGLESTEP | VMI_EVENT_RESPONSE_VMM_PAGETABLE_ID;

}

Pour plus d’informations sur le fonctionnement de ces callbacks, veuillez consulter le site de LibVMI.

6.4 Récupération des informations

Quand un breakpoint est atteint, on le confronte au dictionnaire d’offsets et d’API, on récupère les informations souhaitées (arguments de l’API) et on poursuit l’exécution.

Prenons exemple d’un breakpoint sur NtCreateFile (ci-dessous son prototype).

NTSTATUS NtCreateFile(

   OUT PHANDLE                       FileHandle,

   IN ACCESS_MASK                    DesiredAccess,

   IN POBJECT_ATTRIBUTES             ObjectAttributes,

   OUT PIO_STATUS_BLOCK              IoStatusBlock,

   IN PLARGE_INTEGER AllocationSize  OPTIONAL,

   IN ULONG                          FileAttributes,

   IN ULONG                          ShareAccess,

   IN ULONG                          CreateDisposition,

   IN ULONG                          CreateOptions,

   IN PVOID                          EaBuffer OPTIONAL,

   IN ULONG                          EaLength

);

Le nom du fichier et les droits d’accès sont principalement les informations intéressantes de cette API. Le nom de fichier se trouve dans la structure OBJECT_ATTRIBUTES. Afin de parser cette structure, on utilisera le dictionnaire généré précédemment et qui contient les offsets des différentes entrées de la structure :

"_OBJECT_ATTRIBUTES": [48, {

   "Attributes": [24, ["unsigned long", {}]],

   "Length": [0, ["unsigned long", {}]],

   "ObjectName": [16, ["Pointer", { "target": "_UNICODE_STRING"}]]

On fera de même pour les autres structures.

Il n’y a nul besoin de modifier les arguments de l’API, même s’il est possible de le faire. Le but est de laisser exécuter le malware sans l’interrompre, récupérer les informations intéressantes à la volée, puis ensuite à la fin de son exécution, analyser les informations remontées et déterminer son comportement.

Ci-dessous un extrait de log des événements générés lors d’analyse de malware. Seules les informations faisant référence à un potentiel comportement malveillant sont gardées.

...

{"target":"C:\\Users\\Francis\\AppData\\Roaming\\Windows\\conhost.exe","process":"C:\\Users\\Francis\\Desktop\\sample.exe","pid":2616,"api":"NtCreateFile","access_mask":1074790528},

...

...

{"target":"\\REGISTRY\\USER\\S-1-5-21-336141597-709016518-532797093-1001\\SOFTWARE\\MICROSOFT\\WINDOWS\\CURRENTVERSION\\RUN\\Persistence","process" : "C:\\Users\\Francis\\Desktop\\sample.exe","pid":2616,"value":"C:\\Users\\Francis\\AppData\\Roaming\\Windows\\conhost.exe","api":"NtSetValueKey"},

...

...

{"value":3600,"process":"C:\\Users\\Francis\\Desktop\\sample.exe","pid":2616,"api":"NtDelayExecution"},

...

Conclusion

Les cyberattaques à base de malware ne cessent d’augmenter et sont de plus en plus sophistiquées, ce qui rend leur détection difficile. L’utilisation de l’introspection peut s’avérer très efficace. Comme démontré dans cet article, elle permet d’aller profondément dans l’analyse de malware et de tirer un maximum d’information sur son fonctionnement, sans avoir besoin d’interrompre son exécution normale.

Références

[1] http://libvmi.com

[2] https://github.com/libvmi/

[3] https://docs.microsoft.com/

 

Sur le même sujet

Introduction à Zero Trust 

Magazine
Marque
MISC
Numéro
110
|
Mois de parution
juillet 2020
|
Domaines
Résumé

La sécurité informatique adore les modes. Le « Zero Trust » fait partie de ces concepts qui sont devenus populaires du jour au lendemain. Et comme le sexe chez les adolescents, « tout le monde en parle, personne ne sait réellement comment le faire, tout le monde pense que tous les autres le font, alors tout le monde prétend le faire* ».

Pré-authentification Kerberos : de la découverte à l’exploitation offensive

Magazine
Marque
MISC
Numéro
110
|
Mois de parution
juillet 2020
|
Domaines
Résumé

Les opérations relatives à l’authentification Kerberos ne sont pas toujours remontées dans les journaux des contrôleurs de domaine, ce qui fait de ce protocole une arme de choix pour mener des attaques furtives en environnement Active Directory. Le mécanisme de pré-authentification de ce protocole offre par exemple des possibilités intéressantes pour attaquer les comptes d’un domaine.

Les enjeux de sécurité autour d’Ethernet

Magazine
Marque
MISC
HS n°
Numéro
21
|
Mois de parution
juillet 2020
|
Domaines
Résumé

Quand nous parlons attaque, cela nous évoque souvent exploit, faille logicielle, ou même déni de service distribué. Nous allons revenir à des fondamentaux réseaux assez bas niveau, juste après le monde physique, pour se rendre compte qu’il existe bel et bien des vulnérabilités facilement exploitables par un attaquant. Nous verrons également qu’il existe des solutions pour s’en protéger.

Implémentation d’une architecture processeur non supportée sur IDA PRO et Ghidra

Magazine
Marque
MISC
HS n°
Numéro
21
|
Mois de parution
juillet 2020
|
Domaines
Résumé

Malgré le nombre d’outils aidant au désassemblage voire à la décompilation de programmes, il arrive parfois que les mécanismes ou les processeurs étudiés ne soient pas nativement supportés. Pour cette raison, certains outils proposent des API permettant d’implémenter une nouvelle architecture. Cet article détaillera les grandes étapes de ce travail pour deux outils majoritairement utilisés, à savoir IDA PRO et Ghidra.

Contrôles de suivi de la conformité RGPD et d’atteinte d’objectifs définis dans la politique de protection de la vie privée

Magazine
Marque
MISC
Numéro
110
|
Mois de parution
juillet 2020
|
Domaines
Résumé

Afin de mettre en application les exigences de contrôle de conformité (article 24 du RGPD), les directions générales, qu’elles aient désigné ou non un Délégué à Protection des Données (DPD), doivent mettre en œuvre des contrôles concernant les répartitions de responsabilités entre les acteurs impliqués par le traitement et l’application de règles opposables, l’effectivité des droits des personnes concernées, la sécurité des traitements et la mise à disposition des éléments de preuve pour démontrer la conformité des traitements de données à caractère personnel.

Par le même auteur