• Aucun résultat trouvé

Trois moyens d’injecter du code (furtivement) dans une machine virtuelle

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

5.3 Vers un outil d’analyse fiable

5.3.1 Trois moyens d’injecter du code (furtivement) dans une machine virtuelle

Nous présentons dans cette section plusieurs méthodes qui peuvent être utilisées pour instrumenter l’espace d’exé- cution d’un processus cible. L’utilisation d’un émulateur fournit un intervalle de possibilités puissant pour l’obser- vation du flot d’exécution d’un processus. Des hooks peuvent être insérés furtivement et du code peut être injectés en utilisant au moins trois mécanismes :

– Injection de shellcode forensique

– Injection de code intermédiaire forgé manuellement

– L’ancienne méthode (réécriture de code en mode noyau à l’intérieur de l’émulateur).

shellcode forensique

Une première méthode pour injecter du code dans une machine virtuelle consiste à écrire le code directement en mémoire virtuelle avant de rediriger le pointeur d’instruction du CPU virtuel vers cette zone mémoire.

Argos [PSB06a] implémente un injecteur de shellcode forensique [Argos-0.1.4/target-i386/argos-csi.c]. Un extrait du code source de l’injecteur de shellcode d’Argos est donné ci-dessous :

#define WIN32SC_RID_OFF 257 #define WIN32SC_LENGTH 293

static char win32_shellcode[] = "...";

void argos_csi(CPUX86State *env, int type, argos_rtag_t *eiptag) { ...

int rid = rand(); ...

argos_inject_sc(env, type, rid); ...

}

static void argos_inject_sc(CPUX86State *env, int type, int rid){ ...

sclen = WIN32SC_LENGTH; scp = win32_shellcode; scrid = WIN32SC_RID_OFF; ...

vaddr = env->eip & TARGET_PAGE_MASK;

if ((paddr = cpu_get_phys_page_debug(env, vaddr)) == -1) goto address_lookup;

while (!ARGOS_MAP_ISTAINTED(argos_memmap, paddr) && (paddr & TARGET_PAGE_MASK) != -1)

paddr++;

for (tlen = 0; ARGOS_MAP_ISTAINTED(argos_memmap, paddr); paddr++, tlen++) ; if (tlen >= sclen) goto code_inject; address_lookup: vaddr = 0;

paddr = find_page(env, &vaddr, max_vaddr, PG_USER_MASK | PG_RW_MASK, PG_USER_MASK);

if (paddr == -1) {

printf("[ARGOS] Forensics shellcode will not be injected - " "No available page found\n");

return; }

code_inject:

cpu_physical_memory_rw(paddr + (TARGET_PAGE_SIZE - sclen), scp, sclen, 1);

cpu_physical_memory_rw(paddr + (TARGET_PAGE_SIZE - sclen) + scrid, (unsigned char *)&rid, 4, 1);

env->eip = vaddr + (TARGET_PAGE_SIZE - sclen); }

Le shellcode forensique est injecté, remplaçant le shellcode viral, de manière à récupérer de l’information relative au processus attaqué. Lorsqu’une attaque est détectée, un shellcode spécifique à l’OS est injecté. En d’autres termes, le code attaqué est exploité, alors que l’attaque est en cours, pour extraire une information supplémentaire relative à l’attaque qui est ensuite utilisée pour générer une signature virale.

Après avoir détecté une attaque et journalisé l’état de la machine virtuelle, le shellcode forensique est placé directe- ment dans l’espace d’adressage virtuel du processus. L’emplacement où est injecté le code est la dernière page du segment texte au début de l’espace d’adressage. Il est important de placer le code dans le segment texte de manière à garantir qu’il ne sera pas réécrit par le processus, puisqu’il est en lecture seule. Une fois le shellcode en place, il ne reste plus qu’à faire pointer le registre EIP vers sa première instruction pour démarrer son exécution.

À titre d’exemple, un shellcode qui extrait le PID du processus victime, et le transmet via une connexion TCP a été implémenté dans le projet Argos.

Code intermédiaire forgé manuellement

Une autre manière d’injecter du code dans une machine virtuelle consiste à l’injecter au niveau du CPU virtuel. Nous présentons d’abord dans cette section les principes de conceptions d’un CPU virtuel basé sur le translation binaire dynamique [Tro04], puis expliquons l’utiliser pour injecter un code VCPU forgé manuellement.

La translation binaire dynamique permet de transformer à la volée des séquences d’instructions en code natif pour le CPU. Lorsque le code natif est mis en cache ou optimisé, il peut s’exécuter de manière significativement plus rapide.

Le moteur de translation dynamique effectue au moment du démarrage une conversion des instructions du CPU cible en ensemble d’instructions pour le CPU hôte. Le code binaire résultant est stocké dans un cache, appelé cache de translation, de telle sorte qu’il peut être réutilisé. L’avantage de cette méthode, comparée à celle de l’utilisation d’un interpréteur est que les instructions sont recherchées et décodées qu’une seule fois.

Les moteurs de translation dynamique sont en général difficiles à porter d’un hôte à un autre car tout le générateur de code doit être réécrit. Cela représente environ la même quantité de travail que l’ajout d’une nouvelle cible à un compilateur C. QEMU [Bel05] est plus simple car il concatène des morceaux de code générés au préalable par le compilateur GNU C.

La première étape consiste à découper chaque instruction CPU cible en plusieurs instructions plus simples appelées micro-opérations. Chaque micro-opération est implémentée par un petit morceau de code C. Ce petit code source C est compilé par GCC en un fichier objet. Les micro-opérations sont choisies de telle sorte que leur nombre est beaucoup plus petit (typiquement, quelques centaines) que toutes les combinaisons d’opérations et d’opérandes du CPU cible. La translation des instructions CPU cible vers les micro-opérations est faite entièrement par un codage manuel.

Un outil de compilation appelé dyngen [Bal06] utilise les fichiers objets contenant les micro-opérations en entrée pour fabriquer un générateur de code dynamique. Ce générateur de code dynamique est invoqué au moment de l’exécution pour générer une fonction hôte complète qui concatène plusieurs micro-opérations.

De manière à injecter un code qui déclenche une faute de page dans le système d’exploitation invité, les auteurs de TTAnalyze [BKK06] décrivent une méthode qui peut être implémentée de la manière suivante dans QEMU :

uint16_t opc_buf[OPC_BUF_SIZE]; // intermediate code uint32_t opparam_buf[OPPARAM_BUF_SIZE]; //

uint8_t code_buf[4096]; // host code int code_size; // generated host code size

if (cpu_memory_rw_debug(env, addr, buf, len, is_write)!=0 && is_write == 0){ opc_buf[0]=INDEX_op_movl_A0_im; opparam_buf[0]=addr; opc_buf[1]=INDEX_op_ldsb_user_T0_A0; opparam_buf[1]=0; opc_buf[2]=INDEX_op_exit_tb; opparam_buf[2]=0; opc_buf[3]=INDEX_op_end; opparam_buf[3]=0;

code_size=dyngen_code(code_buf, NULL, NULL, opc_buf, opparam_buf, NULL);

// intermediate code to host code translation // TB Execution

gen_func=(void *)code_buf; gen_func();

L’injection de code intermédiaire forgé manuellement est un moyen très intéressant pour injecter du code furtive- ment dans une machine virtuelle. Mais à la différence de l’injection de shellcode forensique, il est difficile de forger des fonctions complexes ou de réutiliser des fonctions d’instrumentation existantes.

Enfin, nous présentons l’ancien moyen d’injecter du code de manière fiable dans un processus. Cette méthode s’applique également au contexte d’un outil d’analyse basé sur l’émulation. Même s’il est plus facile pour un pro- gramme de détecter la présence de hooks d’API en mode noyau au sein du fonctionnement interne de l’exécutif NT, les méthodes sous-jacentes sont plus documentées et aisées à implémenter.

Réécriture de code en mode noyau à l’intérieur de l’émulateur

Le hook en mode noyau est un troisième moyen intéressant pour injecter du code dans un processus cible. S’il est moins furtif que les méthodes décrites précédemment, les méthodes sont déjà bien documentées et ont prouvé leur efficacité vis-à-vis d’infections informatiques pas trop évoluées, donc en fait la plupart d’entre elles.

5.3.2

Vers un moteur de désassemblage fiable (un moyen d’améliorer le moteur de