AFFICHER CET ARTICLE EN MODE PAGE ENTIERE
SOMMAIRE
2) Pourquoi le polymorphisme ?
3) Voyage au centre du Polymorphisme
4) L'assembleur, de l'instruction à l'octet
5) L'assembleur, de l'octet au bit
7) Rafinement du polymorphisme d'instruction
Retour dans les années 95. Je lisais beaucoup de documents sur l'assembleur, m'intéressant plus particulièrement aux techniques de furtivité utilisées par les virus ms-dos. Entre autres choses, j'avais trouvé un article qui traitait de ce dont je vais parler. Depuis, je n'ai pas particulièrement entendu reparler de cette technique. Pourtant, je l'a trouve intéressante. Primo car elle peut s'avérer efficace, secundo parce qu'elle entraine encore plus loin dans la compréhension de la machine.
Ah si j'étais invisible... Serais-je un gentil ou un méchant ? Avec les pleins-pouvoirs de l'invisibilité, je pense que je verserais plutôt du côté obscur. Ben quoi ? Et j'aurais beau croiser des policiers ou des douaniers, je n'aurais rien à craindre. Impossible de me reconnaitre. Quelle belle vie pour un méchant !
Dans l'ordinateur, c'est aussi la guerre. Antivirus et moniteurs de comportement y tiennent lieu de policiers et douaniers. Et si M. Phelps se met un bout de caoutchouc sur la figure pour leurer l'adversaire, le programme malicieux peut également recourir au postiche et se grimer. Là est l'art du polymorphisme.
Les antivirus se basent principalement sur des signatures. Une signature, ça peut être des séquences d'octets connues dans le code d'un programme dangeureux. Ca peut également être un CRC opéré sur tel bout du code. Le principe est donc de rendre le code du programme le plus insaisissable possible afin d'empêcher au maximum qu'une chaîne de signature ou un CRC puisse être constitué.
Le polymorphisme est un moyen utilisé pour déguiser le contenu d'un programme. Polymorphe -> plusieurs formes.
L'autre méthode antivirale est l'analyse heuristique. Elle est moins utilisée car plus gourmande en ressources. Raoul veut bien installer Norton sur son PC, mais il ne faut pas que Norton fasse ralentir Need For Speed. Un pc c'est fait pour jouer, bordel ! Dommage car elle est assez efficace. Axée "micro reverse engineering", elle analyse le code d'un programme et peut y déceler des comportements suspects, comme tenter d'écrire sur le secteur de boot du disque dur, ou même détecter une boucle de cryptage implémentée sans discrétion. Du temps de Windows 3.11, je ne sais plus lequel de F-Prot ou TBAV intégrait une analyse heuristique assez efficace. A l'époque, je praticais beaucoup l'assembleur 16 bits et cet anti-virus était pour moi un bon test de discrétion.
Cette méthode génère aussi son lot de faux positifs, et Raoul risquerait de ne plus acheter ses mises à jour d'antivirus si son Norton lui répète tous les jours que Format et Fdisk sont des programmes dangeureux. Raoul, je dois les effacer ou les ignorer ? Heuuuuu... N'oublions pas : fdisk, Raoul il connait pas.
C'est l'universelle loi du marché : incompétence et paresse des milliards de clients sont les ennemis de la performance d'un pauvre petit produit. Au passage, je fais remarquer que ce principe s'applique en politique et en culture également. D'ailleurs qui soutiendrait que culture et politique ne sont pas des marchandises ?
3) Voyage au centre du Polymorphisme
Je vous invite à un peu d'histoire. Rapidement, on va passer en revue les grandes familles de techniques de brouillage de code, depuis le cryptage jusqu'au polymorphisme.
Un programme avec une partie de son code brouillé par une fonction de cryptage n'est pas à proprement parler polymorphique : si c'est toujours ce même programme avec cette même boucle de cryptage qui est utilisée, alors cryptage ou pas il n'existe bel et bien qu'une seule forme. Et "un" c'est pas "plusieurs". Sisi j'insiste ;) . C'est un axiome fondamental pour le polymorphisme.
Le cryptage sert pour brouiller des informations, pour les rendre illisibles. Par exemple, crypter les chaines de caractères d'un programme pour éviter que Raoul, armé de Hex Edit, ne s'amuse à mettre son nom partout à la place du votre, le programmeur, avec le même Q.I. qu'un chien qui pisse sur les lampadaires. Pour éviter aussi que Norton n'utilise comme signature pour votre programme malicieux la subtile pensée dont vous êtes si fier et que vous imposez à la vue de vos victimes juste avant leur mort : "je té niké béto chuis mailleur ke toua". Une bien humiliante postérité peut s'éviter avec un simple XOR, sinon le monde entier apprendra dans les pages du Virus Informatique que DeathBringerKingOfRulez écrit aussi mal le français que le C++. Mais passons...
Routine
de décryptage |
Partie
cryptée |
Une des premières améliorations a été de transmettre, par exemple à chaque nouvelle infection d'un virus, une version du programme crypté avec une nouvelle valeur. Les variantes sont multiples comme le compteur de millisecondes tiré de l'horloge de l'ordinateur. Le code crypté change bien d'apparence à chaque fois. On a obtenu un polymorphisme de clé de cryptage.
Routine
de décryptage |
Clé
variable |
Partie
cryptée |
Puis on s'est rendu compte que la routine de décryptage suffisait à établir une signature. Se faire trahir par ses propres armes, quel coup du sort ! On a donc fourni au programme un stock de 4 ou 5 routines de décryptage parfaitement compatibles entre elles. Lors d'une nouvelle infection, on change la clé de cryptage mais on extrait du programme une nouvelle fonction de décryptage. Ainsi, cette fonction ne peut plus être utilisée pour établir une signature. On a du polymorphisme de fonction.
+-------------------------------------+
| Une des routines de décryptage _____| +-------------------------------------+ | Clé variable _______________________| +-------------------------------------+ +-------------------------------------+ | Partie cryptée _____________________| | +----------------------------------+ | | Stock crypté des autres routines | | | de décryptage ___________________| +--+---------------------------------+ |
Certains antivirus ont alors décidés de mettre 4 ou 5 signatures pour un même programme dangeureux, une par routine de décryptage. La stratégie a donc été de brouiller les routines. Toutes les deux ou trois instructions assembleur, le programme comporte quelques octets en plus. A chaque réplication, le programme y écrit les valeurs d'instructions qui ne perturbent pas le cours de a routine, comme un NOP, différentes à chaque fois. Le polymorphisme descend au niveau de l'instruction. La fonction elle-même change d'apparence tout en restant opérationnelle.
Un exemple de routine de décryptage :
+---------------+......
| Zoom _________|...... | +-------------+...... | | valide _____|...... | | remplissage |......+-------------------------------------+ | | valide _____|......| Une des routines de décryptage _____| | | valide _____|......+-------------------------------------+ | | remplissage |......| Clé variable _______________________| | | valide _____|......+-------------------------------------+ | | remplissage |......+-------------------------------------+ | | remplissage |......| Partie cryptée ____________________| | | ________... | .... | +----------------------------------+ | | ________... | .... | | Stock crypté des autres routines | +--+------------+..... | | de décryptage ___________________| _______________________+-+----------------------------------+ |
Pour finir cette fuite vers la finesse des altérations polymorphiques, reste le sujet de ce papier : le polymorphisme au niveau sub-instruction, par motif de bits.
4) L'assembleur,
de l'instruction à l'octet
Voici un exemple de code assembleur. Court et simple, il se contente de pousser la chaîne MessageBoxA dans un stack frame. L'exemple en soi est stupide, mais c'est sur celui là que j'ai commencé à travailler donc je continue avec. A côté des instructions assembleur, j'ai inscris les valeurs des octets correspondants.
mov
eax,'sseM' B8 4D 65 73 73 mov [ebp-0x1c], eax 89 45 E4 mov eax,'Bega' B8 61 67 65 42 mov [ebp-0x18], eax 89 45 E8 mov ax,'xo' 66 B8 6F 78 mov [ebp-0x14], ax 66 89 45 EC xor ax,ax 66 33 C0 mov al,'A' B0 41 mov [ebp-0x12], ax 66 89 45 EE |
Les octets des mnémoniques assembleur sont lues et décodées par le processeur. Basiquement, une instruction assembleur se décompose de la manière suivante :
OPERATION |
mode
d'adressage |
parametre
1 |
parametre
2 |
On peut décomposer la première ligne d'instruction de l'exemple. B8 contient, aux yeux du processeur, les informations disant qu'ici commence une instruction assembleur qui charge un registre 32 bits avec une valeur immédiate, et que ce registre est EAX.
|
||||
|
||||
|
Maintenant, voici la même routine
mais quatre fois de suite. A chaque coup, je change le registre de travail utilisé
par la routine. Dans sa grande majorité, les octets codant les opérations
sont similaires. On décèle juste quelques petites variations,
dont on vient de voir l'explication.
Entre un mov eax
et un mov ebx,
seul change de quoi dire au processeur quel est le registre ciblé. J'ai
mis entre parenthèses les différences à observer :
//Travail avec EAX //Travail avec EBX //Travail avec ECX //Travail avec EDX |
Seul un octet de chaque instruction est modifié. L'idée au coeur du polymorphisme sub-instruction est de modifier chaque octet sensible afin que toute une routine se mette à travailler avec EAX au lieu de EBX, puis EDX lors de la prochaine infection. La routine reste 100% fonctionnelle, sauf que son registre de travail principal change, et donc sa représentation en octet devient variable.
Le souci est que pour chaque instruction il faut stocker les 4 octets à utiliser : dans le cas de EAX, EBX, ECX et EDX. Rien que dans le court exemple, 4 octets sont modifiés lorsqu'on change de registre, multipliés par 4 registres, c'est un jeu de 16 octets qu'il faut avoir sous le coude en guise de maquillage. Plus un bout de code pour veiller à la mutation de la routine.
On arrive à cette modélisation du corps du programme :
+-------------------------------------+
|
5) L'assembleur, de l'octet au bit
Pour éviter de se trimbaler une énorme base de données des octets par fonction et par registre, un coup de microscope va s'avérer payant. Voici un détail des 4 octets correspondant aux 4 registres utilisés dans la fonction d'exemple, ainsi que leurs équivalents en binaire.
EAX |
ECX
|
_EDX
|
EBX |
VARIATIONS |
B8
10111000 45 01000101 C0 11000000 B0 10110000 |
B9
10111001 4D 01001101 C9 11001001 B1 10110001 |
BA
10111010 55 01010101 D2 11010010 B2 10110010 |
BB
10111011 5D 01011101 DB 11011011 B3 10110011 |
2
bits : 101110xx 2 bits : 010xx101 2x2 Bits : 110xx0xx 2 Bits : 101100xx |
bits
à 00 |
bits
à 01 |
bits
à 10 |
bits
à 11 |
Là, révélation. Les motifs binaires montrent bien que seuls deux bits sont utilisés pour coder l'un des quatre registres qui est utilisé.
On a vu comme une instruction assembleur était structurée en éléments que le processeur reconnait et décode. On a vu comme ces parties étaient parfois représentées par le même octet. Maintenant, en entrant au coeur du motif binaire de l'instruction, on retombe sur nos pieds et on arrive à identifier un élément en particulier dans un octet. Au coeur de l'octet représentant l'instruction, on se retrouve tout simplement en face d'un bête compteur sur deux bits, un véritable index du registre à utiliser.
A ce stade, il est facile de repérer par quelles manipulations logiques on peut transformer une opération sur un registre en la même opération sur un registre différent. Dans le cas qui nous concerne pour chaque registre il y a deux formules distinctes et un cas où les deux sont à utiliser à la suite.
Quelques rappels de logique binaire, avant tout :
Mise à zéro d'un bit : ?
and 0 -> 0
Mise à un d'un bit : ? or 1 ->
1
Armé de ces rappels, effectuer la translation de
registre d'une instruction, et par extension d'une routine, ne pose plus réellement
de problème. A partir de n'importe quelle valeur initiale parmi les quatre,
il est possible d'obtenir n'importe quelle autre de ces valeurs au moyen de
masquages de bits. Le tableau suivant récapitule les masquages à
faire, et souligne les deux formules qu'on peut alors utiliser :
EAX |
ECX |
EDX |
EBX |
Fonct. |
Formule |
and
FC |
and
FD or 1 |
and
FE or 2 |
or
3 |
mov
reg64, "xxxx" |
1 |
and
(FC<<3) or (1<<3) |
and
(FD<<3) or (2<<3) |
and
(FE<<3) |
or
(3<<3) |
mov
[ebx+x], reg |
2 |
and
FC and (FC<<3) |
and
FD or 1 and (FD<<3) or (1<<3) |
and
FE or 2 and (FE<<3) or (2<<3) |
or
3 or (3<<3) |
xor
reg16, reg16 Formule 1 + 2 |
1
+ 2 |
and
FC |
and
FD or 1 |
and
FE or 2 |
or
3 |
Mov
reg8, "x" |
1 |
En appliquant le bon lot de formules selon la translation de registre souhaitée, on modifie le registre utilisé par le bout de code. Il faut juste se souvenir qu'à tel offset du code de la routine il faut utiliser la formule 1, 2, ou bien les deux simultanément. Pour cela, une petite table permet de dire qu'à tel offset de la routine on doit appliquer telles formules.
Offset |
Formule(s) |
0d 6d 8d 14d 17d 22d 26d 27d 31d |
1 2 1 2 1 2 1+2 1 2 |
7) Rafinement du polymorphisme d'instruction
On a vu que des fonctions peuvent incorporer
des instructions ne servant à rien, seulement à brouiller d'avantage
les signatures. Avec la méthode du motif de bit, on gagne encore
plus de flexibilité.
Le moteur de mutation doit alors s'assurer de deux paramètres : _-.Les instructions "réelles" de la fonction doivent subir un masquage de bits afin de fonctionner avec un registre principal "actif" qui servira pour le bon déroulement.
_ç_________________________________________________________________________.-.Les instructions "pipeau" de la fonction doivent subir un masquage de bits afin de fonctionner avec un des trois autres registres principaux, pouvant ainsi faire n'importe quoi sans altérer le registre de travail "actif".
Entre DOS et Windows, que de changements. L'un d'entre eux concerne directement ce domaine. Sous DOS, rien n'empêche de manipuler le code comme s'il s'agissait de données, et donc de modifier directement les octets du segment de code. Cette possibilité permettait une grande souplesse dans la mutation de code ainsi que de mettre en place des stratégies anti-débuggage jouant sur le prefetch des processeurs, assez répandus sur les 486.
Sous Windows NT, le code est le code, et les données sont les données. On ne mélange pas les torchons et les serviettes, pas vrai ? Le gestionnaire de pages de mémoire virtuelle de Windows NT permet de bloquer l'accès en lecture ou écriture sur certains segments. Et il est de coutume que le segment de code soit protégé contre l'écriture.
Deux solutions : soit penser à déverrouiller la page de code au moyen des API adaptées, soit travailler dans des buffer contenus dans le segment de données, stack ou heap.
Le polymorphisme sub-instruction s'avère assez aisé à mettre en place. Combiné avec les autres niveaux où peut intervenir le polymorphisme, les résultats sont très satisfaisants. Mon but n'est pas de donner un outil clef en main, et dont la compréhention est en option. Donc pas plus de code que ça.
Intel a publié un pdf nommé "instruction set reference". Je m'attendais à y trouver facilement les motifs de bits des instructions en fonction des registres mis en jeu, mais son usage n'est franchement pas très aisé. La méthode empirique reste encore plus facile.
J'espère que cette plongée au coeur de l'instruction vous aura
rappellé l'Aventure Intérieure !
BY TOLWIN
Copyright © 2004 ARENHACK - DHS