Introduction à l’assembleur

To the non-french speaker, note that you can translate the articles using the Google Trad widget situated at the bottom of all pages.


Cinq mois après avoir écrit les articles sur les microprocesseurs et les calculs arithmétiques et logiques je me suis dit qu’il était temps de continuer cette série  consacrée au cracking.

Donc au point où on en est, on sait comment fonctionne un microprocesseur et comment sont effectués les calculs arithmétiques et logiques.

Il nous reste donc deux choses à voir avant d’attaquer le cracking à proprement parler : les instructions et la pile.

Quand on programme en C, en Java ou encore en Python, on a l’habitude de pouvoir écrire des lignes de codes longues et complexes qui utilisent plusieurs fonctions à la fois. En assembleur c’est très différent. Chaque ligne correspond à une instruction qui fait une opération et une seul sur une ou des  opérandes.

Ci dessous je vous ai mis le code nécessaire pour afficher la phrase « Hello World » :

helloworld

Quinze lignes de code pour deux mots… Terrible nan ? Pour comprendre comment fonctionne l’assembleur il faut comprendre ce qu’est une instruction donc on va commencer par ça 😉 !

I/ Les instructions

Une instruction est définie par :
-Une opération à réaliser : une addition, une soustraction…
-Des opérandes : les « objets » qui sont manipulés.

Comme vous pouvez le voir dans le code ci-dessus, une opération peut avoir 0, 1 ou 2 opérandes et respecte toujours ce pattern :

,

Attention ! Il existe deux types de syntaxe lorsque l’on travaille sur de l’assembleur : la syntaxe Intel et la syntaxe AT&T. La syntaxe Intel est principalement retrouvée dans les environnements Windows tandis que la syntaxe AT&T est utilisée dans les environnements Unix. Pourquoi est ce que c’est important ? Eh bien tout simplement parce que suivant la syntaxe utilisée, l’ordre des opérandes ainsi que la manière dont elles sont déclarées sont différents !

Voyez :

syntaxeCrédits photo : imada.sdu.dk

Première chose à noter, en syntaxe Intel il n’y a pas de préfixe. En revanche en AT&T lorsque l’on parle d’un registre on doit utiliser le préfixe « % » et lorsque l’on utilise une constante on utilise un « $ ». Par contre en syntaxe Intel lorsque l’on déclare/utilise une constante on doit ajouter le suffixe « h » si la valeur est hexadécimale, « b » si c’est un binaire ou « o » si c’est une valeur octale.

Deuxièmement, l’ordre des opérandes est inversé. Si on prend la première ligne du screen vous voyez  que le « eax, 1 » de la syntaxe Intel devient « $1, %eax » en syntaxe AT&T.

sens

Crédits photo : imada.sdu.dk

Vous aurez peut être remarqué que dans la syntaxe AT&T, le mov est suivi d’un « l ». Cela indique en fait la taille des paramètres utilisés (q pour qword =64 bits, l pour long ou double word =32 bits, w pour word = 16 bits et b pour byte = 8 bits).

taille

Crédits photo : imada.sdu.dk

Mais qu’est ce que c’est que ce « dword ptr ». Pour répondre à cette question nous allons prendre un exemple :

mov eax, 2

L’instruction mov permet de déplacer le contenu de l’opérande 2 (2) dans l’opérande 1 (eax). A priori il n’y a pas de problèmes, on transfert la valeur 2 dans eax. Sauf que quand on regarde le format des deux opérandes on remarque rapidement qu’il y a un souci. Le registre eax est de 32 bits soit 4 octets tandis que la valeur 2 est codée sur 2 bits. Que fait on ? On déplace la valeur binaire de 2 (10)  dans le premier octet du registre eax ou on se sert de la représentation sur 32 bits de la valeur 2 pour la stocker sur les 4 octets du registre eax ?

Pour spécifier explicitement ce que l’on veut faire on va se servir du fameux « dword ptr« . Ainsi l’instruction suivante :

mov dword ptr eax, 2 

se lira : « déplace 2 dans les 32 bits du registre eax.

De même, les instructions suivantes :

mov byte ptr eax, 2

mov word ptr eax, 2

se liront respectivement : « déplace la valeur 2 dans le premier octet du registre eax » et  « déplace la valeur 2 dans les 2 premiers octets du registre eax ».

Ok, passons aux opérations ! Il existe plusieurs types d’opérations que nous allons regrouper dans 4 groupes :

-Mouvement de données
-Opération arithmétique
-Opération logique
-Transfert de contrôle

Dans cet article nous verrons seulement les opérations élémentaires. Pour ce qui est des instructions plus complexes, je les détaillerai lorsque l’on travaillera avec dans les prochains articles.

Mouvement de données :

-L’opération mov copie le contenu de la seconde opérande dans le première :

mov eax, [ecx]

Contrairement à ce que l’on pourrait croire, cette instruction ne va pas déplacer le contenu du registre ecx dans eax. En effet quand on utilise la notation « [] » c’est pour indiquer que l’on se réfère à ce qui est contenu à l’adresse mémoire contenue dans le registre ecx. On va donc déplacer ce qui est contenu à l’adresse mémoire contenue dans ecx dans eax. C’est pas clair hein… Ouais moi aussi j’ai beaucoup galéré avec ces notations puis j’ai trouvé ce document qui va vous faire tout comprendre 😉 !

-L’opération lea permet de charger l’adresse mémoire d’une variable dans un registre :

lea eax, [var]

Dans ce cas ci, l’instruction lea charge l’adresse mémoire de l’objet var dans le registre eax.

« Wait, trente secondes plus tôt tu as dit que les « [] » signifiaient qu’il fallait se référer à ce qui est situé à l’adresse mémoire contenue dans les crochets. Et là tu dis que lea charge l’adresse mémoire de var dans eax ? C’est contradictoire ? »

Eh oui, lea est une exception. On charge bien l’adresse mémoire de la variable var.

-L’opération push place le contenu de l’opérande sur le dessus de la pile :

push eax

Cette instruction place le contenu du registre eax sur le sommet de la pile.

-L’opération pop fait l’inverse, elle décharge les 4 premiers octets situés sur le dessus de  la pile et les stocke dans l’opérande :

pop esi

Ici on récupère les 4 premiers octets de la pile et on les stocke dans le registre esi.

 Opérations arithmétiques :

-L’opération add additionne le contenu de deux registres, constantes, valeurs contenues en mémoire et stocke le résultat dans la première opérande:

add eax, 10

Donc ici on ajoute 10 au registre eax.

-L’opération sub soustrait le contenu de deux registres, constantes, valeurs contenues en mémoire et stocke le résultat dans la première opérande:

sub eax, 10

A l’inverse ici on enlève 10 au registre eax.

-L’opération inc/dec incrémente ou décrémente le contenu de l’opérande de un :

inc eax

Les opérations logiques :

-Les opérations and, sub, or et xor permettent d’effectuer des calculs logiques entre deux registres, constantes ou valeurs contenues en mémoire :

and eax, ebx

-L’opération not inverse les bits d’un registre, ou d’une valeur contenue en mémoire. Par exemple si le registre eax contient ça : 10001 et qu’on le « not » :

not eax

Alors il contiendra la valeur suivante : 01110.

Les contrôles de flux :

-L’opération cmp compare deux valeurs contenues dans des registres, des constantes ou encore la mémoire :

cmp v, 5

Ici on compare la valeur de v avec 5.

-Les jmp permettent de sauter d’un bout de code à un autre soit directement soit en fonction d’une condition. Il existe différent jump conditionnel par exemple : je (jump if equal), jz (jump on zero), jle (jump if less or equal to) etc… Ce type de jump nécessite un test de la condition en amont ainsi que l’utilisateur des flags.

Voici un exemple :

cmp v, 5
je local

Quand on déclare un jump il faut spécifier où le programme doit se rendre si le jump est réalisé. Pour cela on lui donne un label (ici local) ou une adresse mémoire qui lui indique où il doit jumper. Du coup dans cet exemple, on compare la valeur de v à 5. Si le résultat de la comparaison vaut 0 (le bit du Zero Flag est à 1) alors on jump au label « local ».

Voilà pour les instructions, avec ça on sera capable de résoudre le premier crackme que vous trouverez dans l’article suivant. Passons au dernier pré requis : la pile !

II/ La pile

La pile, c’est une zone mémoire qui est utilisée pour la sauvegarde temporaire des registres ainsi que les variables utilisées lors d’un appel de fonction. C’est une structure de données de type LIFO (Last In First Out).

Pour que vous compreniez bien cette histoire de LIFO on va utiliser une analogie très utilisée, celle de la pile de livre. Vous avez cette pile de livre :

pile
Crédits images : dreamstime.com

Si vous voulez ajouter un livre sur le danois sur la pile vous allez l’empiler sur le dessus. Logique.

Maintenant si vous voulez lire le livre sur le portugais, vous allez devoir dépiler le livre sur le danois, puis celui sur l’anglais, sur l’allemand, sur le français et pour finir celui sur l’espagnol.

Conclusion, vous avez dépilé en premier le livre que vous avez empilé en dernier –> Last In First Out 🙂 !

Pourquoi est ce qu’on se sert de la pile ? Je me suis posé la même question quand j’ai commencé l’assembleur et j’ai seulement compris quand j’ai eu un exemple concret sous les yeux. Voyez le code suivant :

code

Dans le main on déclare un int et on lui assigne la valeur 98. Ensuite on appelle la fonction f qui affiche le nombre 98 puis cette fonction en appelle une autre qui affiche le caractère dont le code ASCII vaut 98.

Donc schématiquement on se retrouve avec cet enchaînement :

loop

Et c’est là que la pile prend tout son intérêt. En fait lorsque dans le main on va faire un appel à la fonction f, l’assembleur lui va créer ce qu’on appelle une stack frame qu’il va empiler sur la stack. Cette stack frame sera exclusivement dédiée à la fonction. Pour vous donner une idée de son utilité, c’est grâce à la stack frame du main que l’on peut envoyer des paramètres à des fonctions.

Du coup on se retrouve dans cette configuration :

stack

Attention : sur ce schéma la stack est représentée par la partie en noir. Le schéma en entier correspond à l’espace mémoire allouée au processus (i.e : au programme).

Il est intéressant de noter qu’on empile par le bas ! Eh oui, regardez : le main est en haut, puis on empile la fonction f et enfin la fonction g !

Donc plus on empile plus on se déplace vers les adresses basses.

Crédits image : hack-and-fun.blogspot.fr

Ok donc on sait ce qu’est la pile mais dans l’espace mémoire on retrouve d’autres emplacements : le tas (heap), le segment bss, le segment data, et le segment text. Commençons par le tas.

-Le tas est un espace mémoire utilisé par les programmes pour allouer dynamiquement de la mémoire. L’idée est la suivante, quand on crée un programme on utilise de nombreuses variables sauf que suivant les conditions de déroulement du programme on va les utiliser ou non. Or si une variable est déclarée mais qu’elle n’est pas utilisée alors il y aura une perte d’espace mémoire.

Pour pallier à ce problème, on a l’allocation dynamique (les fonctions calloc, malloc free, new et delete en c) qui permet d’allouer de la mémoire en fonction du besoin. Toutes ces variables allouées dynamiquement sont stockées sur le tas.

A l’inverse de la stack qui grandit en se déplaçant vers les adresses basses, le tas, lui, grandit en se déplaçant vers les adresses hautes.

piletas
Crédit image : http://slideplayer.fr

Et là on est en droit de se demander ce qu’il se passe lorsque la pile se superpose au tas et vice vers ça. Sur les processeurs 64 bits ça n’arrive presque jamais. Mais, quand ça arrive eh bien on arrive à faire des choses plutôt sympathiques mais que je ne saurais vous expliquer (du moins pour le moment 😉 ) !

Ensuite nous avons trois segments différentes : bss, data et text.

-Le segment texte (ou code) contient le code du programme donc les instructions en assembleur.

-Les segments data et bss contiennent l’ensemble des variables déclarées. Si la variable est déclarée et initialisée alors elle sera stockée dans la section data sinon elle sera stockée dans la section bss.

Pour s’assurer de la véracité de mes propos vous pouvez utiliser la commande « size » suivie du nom de l’exécutable qui vous affichera la taille de chacune de ces sections :

 size code

size

Maintenant si j’ajoute une variable initialisée :

test

Et que je relance la commande size :

res

La section data a été incrémenté de 4 octets pour pouvoir stocker la variable test 🙂 !
Dernière chose (et après on passera au cracking), si on reprend le code assembleur pour afficher le « Hello World » on retrouve ceci :
modelstack
le .stack 100h permet de stipuler que l’on veut réserver 100h d’espace mémoire pour la pile soit 256 bits.
Quant au .model ça désigne en fait la façon dont la mémoire est gérée :
-Le modèle small supporte un et un seul segment de données et de text.
-Le modèle large supporte plusieurs segments de données et de text.
-Le modèle medium est un compromis des deux (il supporte les deux).
-Le modèle compact supporte plusieurs segments de données mais un seul segment de text.
Il existe d’autres types de modèles mais bon ça ne nous sera pas utile pour le moment donc je ne les détaillerai pas plus !

Tout ce que je vous ai expliqué ici s’applique plus ou moins à toutes les architectures. Cependant chaque microprocesseur a son propre jeu d’instruction. Donc l’instruction mov ne sera pas toujours « mov ». De même les données ne seront peut être pas stockées de la même manière.

Maintenant qu’on a les bases on va pouvoir passer au premier crackme 😉 !

Un commentaire

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion /  Changer )

Photo Google

Vous commentez à l'aide de votre compte Google. Déconnexion /  Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion /  Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion /  Changer )

Connexion à %s