Post

Partie 25 - Annexes

Annexes

Dans cette page, vous trouverez plusieurs informations regroupées ensemble dont on a pu parler lors de ce cours :

  • des astuces Ida
  • des astuces gdb
  • les principales instructions x86

Si vous cherchez une info ou commande bien prĂ©cise, n’hĂ©sitez pas Ă  utiliser Ctrl+F 😉.

Astuces IDA

Astuce IDA : Vous pouvez utiliser le raccourcis N pour renommer une fonction, un label ou une variable en ayant préalablement cliqué dessus avant de la renommer.

Astuce IDA : Pour modifier le type d’une fonction ou d’une variable, il suffit de cliquer dessus et d’appuyer sur Y.

Astuce IDA : Le raccourcis permettant d’assigner Ă  des constantes des Ă©numĂ©rations est M.

Astuce IDA : Il est possible de mettre un commentaire sur la mĂȘme ligne que l’instruction sĂ©lectionnĂ©e dans la fenĂȘtre de dĂ©compilation avec le raccourcis /.

Dans la fenĂȘtre du code dĂ©sassemblĂ©, cela est possible avec : ou ;.

Astuce IDA : Vous pouvez utiliser le raccourcis Inser pour saisir un commentaire avant l’instruction sĂ©lectionnĂ©e.

Astuce IDA : En utilisant la touche EntrĂ©e, vous pouvez ajouter des sauts de lignes, pratique lorsque l’on souhaite espacer le code.

Astuce IDA : Les variables nommĂ©es v1, v2 etc. correspondent Ă  des variables locales d’une fonction tandis que les variables a1, a2 etc. correspondent aux arguments de la fonction.

Astuce IDA : Vous pouvez utiliser le raccourcis G pour aller Ă  une adresse en particulier.

Astuce IDA : Vous pouvez utiliser le raccourcis espace pour basculer du mode “graphe” vers le mode “texte” et inversement.

Astuce IDA : En mode “graphe”, vous pouvez modifier la couleur des blocs de base en cliquant sur l’icîne la plus à gauche en haut du bloc.

Astuce IDA : Parfois, au lieu d’afficher une chaĂźne de caractĂšres, IDA affiche un offset en mĂ©moire plutĂŽt que la string directement. Pour y remĂ©dier, aller dans EditâžĄïž Plugins âžĄïž Hex-Rays Decompiler âžĄïž Options âžĄïž Analysis options 1 et dĂ©cocher Print only constant string literals.

Astuce IDA : Il est souvent intĂ©ressant d’avoir les deux onglets dĂ©sassembleur / dĂ©compilateur sur la mĂȘme vue. Vous pouvez faire cela en dĂ©plaçant l’un des deux onglets. Vous pouvez ensuite synchroniser les deux vues en faisant un clic droit dans la fenĂȘtre de dĂ©compilation et en cliquant sur Synchronize with > IDA View.

De cette maniĂšre, lorsque vous cliquerez sur un ligne ou que vous changerez de fonction, IDA affichera la ligne adĂ©quate dans la fenĂȘtre de dĂ©sassemblage.

Astuce IDA : Pour dĂ©sactiver (ou rĂ©activer) le cast des variables, c’est le raccourcis Alt Gr + \. Cela permet d’avoir du code plus lisible.

Mais attention, parfois les casts donnent des informations importantes, notamment lorsque l’on souhaite reprogrammer un algorithme en C, Python ou autre, il est nĂ©cessaire de faire attention Ă  la taille des variables.

Astuce IDA : Une fois que vous avez trouvĂ© l’adresse de base de votre programme, il suffit, dans IDA, d’aller dans Edit âžĄïž Segments âžĄïž Rebase program puis saisir l’adresse de base trouvĂ©e dans gdb avec libs et cliquer sur Ok.

Astuces gdb

Certaines de ces commandes sont propres Ă  pwndbg.

Liste des formats :

  • o : octal
  • x : hexadĂ©cimal
  • u : dĂ©cimal non signĂ©
  • t : binaire
  • f : nombre Ă  virgule (ou flottant)
  • a : adresse
  • c : char
  • s : chaĂźne de caractĂšres

Tailles définies dans gdb :

AbréviationSignificationTaille (en octets)
bbyte1
hhalf word2
wword4
ggiant word8

Astuce gdb : Si un programme accepte des arguments via argv, il est possible de les spécifier lors de la commande run.

Exemple : run arg1 arg2

Astuce gdb : La commande hb *0xaddr (hardware breakpoint) permet d’insĂ©rer un point d’arrĂȘt matĂ©riel Ă  l’adresse 0xaddr .

Astuce gdb : Vous pouvez utiliser i b (pour info breakpoints) afin de lister les points d’arrĂȘts du programme.

Cela est trĂšs utile pour s’y retrouver. Chaque point d’arrĂȘt ayant un numĂ©ro unique, il sera affichĂ© dans cette commande.

Astuce gdb : Pour supprimer un point d’arrĂȘt vous pouvez utiliser d N (pour delete N) afin de supprimer le breakpoint numĂ©ro N.

Astuce gdb : Vous pouvez lister les zones mémoire mappées avec la commande libs.

Astuce gdb : L’instruction starti permet de charger le programme en mĂ©moire et de s’arrĂȘter Ă  la premiĂšre instruction de ce dernier, sans l’exĂ©cuter.

Astuce gdb : Vous pouvez quitter gdb avec les commandes quit ou exit. De maniĂšre plus rapide, vous pouvez utiliser Ctrl+D.

Astuce gdb : Pour exĂ©cuter l’instruction courante et s’arrĂȘter Ă  la prochaine, il est possible d’utiliser si ou ni (pour step instruction et next isntruction).

La diffĂ©rence entre les deux est que lors de l’appel d’une fonction, ni exĂ©cute la fonction jusqu’au retour alors que si entre dans la fonction et s’arrĂȘte Ă  la premiĂšre instruction.

Astuce gdb : Le fait de saisir Ă  chaque fois si pour avancer d’une instruction peut ĂȘtre fastidieux đŸ˜€. Vous pouvez spammer utiliser la touche EntrĂ©e dans le terminal gdb afin de rĂ©-exĂ©cuter la derniĂšre commande que vous avez lancĂ©e prĂ©cĂ©demment.

Astuce gdb : Vous pouvez utiliser la commande c (ou continue) pour poursuivre l’exĂ©cution du processus jusqu’à arriver Ă  un point d’arrĂȘt.

Astuce gdb : Vous pouvez utiliser le raccourcis fin (ou finish) pour finir l’exĂ©cution d’une fonction jusqu’à atteindre l’adresse de retour et s’y arrĂȘter.

Astuce gdb : La commande p (ou print) permet d’afficher une valeur quelconque ou la valeur d’une registre.

Si la valeur à afficher est une adresse (ou pointeur), elle ne sera pas déréférencée.

Astuce gdb : Pour afficher un registre, il suffit de le préfixer avec le signe $. Exemple : print $reg.

Astuce gdb : Vous pouvez utiliser le raccourcis x ( pour explore) afin d’examiner le contenu d’une zone mĂ©moire.

Astuce gdb : Vous pouvez spĂ©cifier un nombre d’élĂ©ments Ă  afficher avant les formats afin d’afficher plus ou moins de donnĂ©es en mĂ©moire.

Le nombre d’élĂ©ments Ă  afficher ainsi que la taille ne sont utilisables qu’avec x. Cela ne fonctionnera pas avec print oĂč seuls les formats (dĂ©cimal, binaire, hexadĂ©cimal 
) sont utilisables.

Astuce gdb : Avec x, vous pouvez également donner en argument une expression avec des opérations (addition, soustraction, multiplication 
).

Cela peut ĂȘtre pratique pour afficher une donnĂ©e dans un tableau dont on connait l’index et l’adresse de base. Par exemple, pour afficher la 5Ăšme case d’un tableau d’élĂ©ments de 64 bits : x 0x401000+8*5 (en supposant que le tableau soit stockĂ© Ă  partir de l’adresse 0x401000).

Astuce gdb : La commande search de pwndbg permet de rechercher des motifs en mémoire.

Astuce gdb : La commande set permet d’écrire dans des registres, variables et la mĂ©moire.

Astuce gdb : Vous pouvez utiliser rel (pour reload) afin de rafraĂźchir la GUI de pwndbg et voir les changements effectifs.

Astuce gdb : Pour modifier une zone mĂ©moire pointĂ©e par un registre, il est possible d’utiliser set *$reg = value.

Pour modifier directement les données pointées par une adresse : set *0xaddr = value.

Astuce gdb : Si vous ne souhaitez modifier qu’un seul octet (au lieu de 4 par dĂ©faut) vous devez le spĂ©cifier. Exemple : set {byte}0x401020 = 0xf5.

Instructions x86

mov reg_d, value

Opérandes

  • reg_d : registre de destination
  • value : valeur immĂ©diate (ou concrĂšte, constante).

Détails

Cette forme est la plus simple : elle affecte la valeur value au registre de destination reg_d.

C’est une maniĂšre de rĂ©aliser des affectations de valeurs concrĂštes (immĂ©diates).

Exemple

Imaginons que eax vaille 0xaabbccdd puis que l’on exĂ©cute l’instruction mov eax, 0xdeadbeef. Alors la valeur de eax deviendra 0xdeadbeef.

Équivalent en C

1
2
3
4
5
// Initilisation du registre
int x = 0xaabbccdd; // eax

// Equivalent de : mov eax, 0xdeadbeef
x = 0xdeadbeef;

mov reg_d, reg_s

Opérandes

  • reg_d : registre de destination
  • reg_s : registre source

Détails

Le contenu du registre source reg_s est copié dans le registre de destination reg_d.

C’est une maniùre d’affecter le contenu d’une variable à une autre.

Exemple

1
2
3
4
mov eax, 0xaabbccdd
mov ebx, 0x11223344 

mov ebx, eax ; ebx == 0xaabbccdd

Équivalent en C

1
2
3
4
5
6
// Initilisation des registres
int a = 0xaabbccdd; // eax
int b = 0x11223344; // ebx

// Equivalent de : mov ebx, eax
b = a; // b = 0xaabbccdd

mov reg_d, [reg_p]

Opérandes

  • reg_d : registre de destination
  • reg_p : registre pointant vers une zone mĂ©moire

Détails

Cette forme est un peu plus complexe que les précédentes car elle fait appel à la notion de pointeur.

Ici reg_d est le registre de destination qui recevra une valeur, jusque-là rien de bien nouveau. Par contre, reg_p ne contient pas la valeur qui sera copiée mais un pointeur vers la valeur en question.

Ainsi, c’est la valeur pointĂ©e par reg_p qui est copiĂ©e dans reg_d.

C’est une maniĂšre de lire des donnĂ©es depuis la mĂ©moire.

Exemple

Imaginons que je veuille exécuter ces instructions :

1
2
3
4
mov eax, 0x700000F0 ; 0x700000F0 -> 0x1a2b3c4d
mov ebx, 0xcafebabe

mov ebx, [eax]

On suppose Ă©galement que l’adresse 0x700000F0 pointe vers l’entier de 4 octets 0x1a2b3c4d. Lorsque la derniĂšre instruction mov ebx, [eax] sera exĂ©cutĂ©e, alors ebx vaudra 0x1a2b3c4d. Vous voyez la logique ?

LégÚres variantes

Il existe quelques variantes oĂč un offset (positif ou nĂ©gatif) est ajoutĂ© au registre reg_p, par exemple :

1
2
mov edx, [eax + 8]
mov ecx, [esi - 0x2000]

Équivalent en C

Cette forme est trùs similaire à l’utilisation de pointeurs en C :

1
2
3
4
5
6
7
8
9
// Initilisation des registres
int *a = 0x700000f0; // eax
int b = 0xcafebabe; // ebx

// Initilisation de la mémoire 
*a = 0x1a2b3c4d;

// Equivalent de : mov ebx, [eax]
b = *a; // b = 0x1a2b3c4d

mov [reg_p], reg_s

Opérandes

  • reg_p : registre pointant vers une zone mĂ©moire
  • reg_s : registre source

Détails

Normalement, si vous avez bien saisi le principe de l’instruction mov reg_d, [reg_p] vous devriez deviner le fonctionnement de celle-ci.

En fait il s’agit de l’inverse de la prĂ©cĂ©dente instruction. En effet, ici on copie la valeur du registre reg_s vers la zone mĂ©moire pointĂ©e par reg_p.

C’est une maniĂšre d’écrire des donnĂ©es en mĂ©moire.

Exemple

Reprenons le précédent exemple, nous avons cette fois-ci :

1
2
3
4
mov eax, 0x700000F0 ; 0x700000F0 -> 0x1a2b3c4d
mov ebx, 0xcafebabe

mov [eax], ebx ; 0x700000F0 -> 0xcafebabe

LégÚres variantes

Il existe quelques variantes oĂč un offset (positif ou nĂ©gatif) est ajoutĂ© au registre reg_p. Il est Ă©galement possible de remplacer reg_s par une valeur immĂ©diate. Par exemple :

1
2
mov [ebp + 8], edi
mov [esi - 0x200], 0xdeadbeef

Équivalent en C

1
2
3
4
5
6
7
8
9
// Initilisation des registres
int *a = 0x700000f0; // eax
int b = 0xcafebabe; // ebx

// Initilisation de la mémoire 
*a = 0x1a2b3c4d; // 0x700000f0 -> 0x1a2b3c4d

// Equivalent de : mov [ebx], eax
*a = b; // 0x700000f0 -> 0xcafebabe

Résumé des différentes formes de mov

Je sais, ça fait beaucoup d’informations d’un coup, voici ainsi un rĂ©sumĂ© avec un exemple pour chacun des 4 formes possibles. Supposons que dans les 4 cas l’état initial est le suivant :

Alors le résultat est :

Les valeurs en 🔮 sont celles qui ont changĂ© lors de l’exĂ©cutions de l’instruction tandis que celles en ⚫ sont les valeurs Ă  l’origine du changement.

lea reg, [...]

Opérandes

  • reg : registre de destination
  • [...] : valeur qui est souvent une adresse mĂ©moire

Détails

Cette instruction a ainsi une seule forme oĂč la premiĂšre opĂ©rande est toujours un registre, la seconde opĂ©rande est une valeur qui est souvent une adresse vers une zone mĂ©moire.

Ce que fait lea est tout simplement la copie de l’opĂ©rande de droite, sans la dĂ©rĂ©fĂ©rencer, vers le registre de destination.

Voici quelques exemples :

1
2
3
lea eax, [0x400000] ; ici eax = 0x400000 
lea edx, [ebp+8]    ; ici edx = ebp +8
lea ecx, [ebx+eax]  ; ici ecx = ebx+eax

Exemple

Comme lea ne dĂ©rĂ©fĂ©rence pas la seconde opĂ©rande, l’instruction lea eax, [0x400000] copie bien 0x400000 dans eax et non pas la valeur pointĂ©e par 0x400000.

En fait, plus simplement, lea copie la valeur entre les crochets vers le registre de destination. En d’autres termes, lea reg, [...] est Ă©quivalente Ă  mov reg, ....

J’en vois dĂ©jĂ  certains froncer les sourcils đŸ€š.

Mais si cela est Ă©quivalent Ă  faire un mov, pourquoi se casser la tĂȘte avec une instruction en plus ?

En fait, contrairement Ă  mov, l’instruction lea permet de faire de petites opĂ©rations au niveau de l’opĂ©rande de droite. Par exemple, si je souhaite affecter Ă  ecx la somme de ebx et eax en utilisant mov, je suis obligĂ© d’utiliser une instruction supplĂ©mentaire telle que add pour faire l’addition et ensuite stocker le rĂ©sultat dans ecx avec mov.

Tandis qu’avec lea, je peux simplement faire : lea ecx, [ebx + eax]. Vous savez quoi ? On peut mĂȘme faire lea ecx, [ebx + eax*2]😎.

Ainsi, lea permet de :

  • Stocker le rĂ©sultat de simples opĂ©rations en Ă©crivant une seule instruction
  • De manipuler des adresses en y ajoutant, ou non, un offset

S’il n’y avait qu’une seule chose à retenir de lea : il s’agit d’un mov qui copie la “valeur entre crochets” vers la destination.

add reg_d, reg_s

Opérandes

  • reg_d : registre de destination
  • reg_s : registre source

Détails

“Add” en anglais signifie “ajouter”.

Cette instruction réalise ainsi deux actions :

  • addition de la valeur du registre source avec celui de destination
  • stockage du rĂ©sultat (la somme) dans le registre de destination

C’est de cette maniĂšre que sont rĂ©alisĂ©es les additions.

Lorsque la somme des deux termes dĂ©passe le plus grand entier que peut stocker le registre de destination, le rĂ©sultat est tronquĂ© pour qu’il puisse y ĂȘtre stockĂ©

Exemple

Faisons la somme de 0xf0000034 et 0x20001200 :

1
2
3
4
mov eax, 0xf0000034
mov ebx, 0x20001200

add eax, ebx ; eax = 0x10001234 et non pas 0x110001234 car le résultat est tronqué aux 32 bits de poids faible

Équivalent en C

1
2
3
4
5
// Initilisation des registres
int a = 0xf0000034; 
int b = 0x20001200; 

a = a + b;

Autres formes

Il existe plusieurs autres formes :

  • add reg, value
  • add [ptr], value
  • add reg, [ptr]

Leur fonctionnement est toujours le mĂȘme : somme des deux termes et stockage dans l’opĂ©rande de destination.

Toutes les instructions, sauf mention contraire (comme lea), déréférencent les pointeurs vers des zones mémoire.

Dans les prĂ©cĂ©dentes formes, ce n’est donc pas le pointeur ptr qui est utilisĂ© dans la somme mais la valeur pointĂ©e par ptr qui est [ptr] (qui serait *ptr en C).

and ope_d, ope_s

Opérandes

  • ope_d : opĂ©rande de destination. Peut ĂȘtre :
    • un registre
    • un pointeur
  • ope_s : opĂ©rande source. Peut ĂȘtre
    • une valeur immĂ©diate
    • un registre
    • un pointeur (vers une zone mĂ©moire)

Détails

L’instruction and rĂ©alise un “et logique” entre les bits des deux opĂ©randes. Le rĂ©sultat est ensuite sauvegardĂ© dans la premiĂšre opĂ©rande (qui ne peut donc pas ĂȘtre une valeur immĂ©diate).

Exemple

1
2
3
4
mov eax, 0xff00ff00
mov ebx, 0xabcdef12

and eax, ebx ; eax = 0xab00ef00

Équivalent en C

1
2
3
4
int a = 0xff00ff00; 
int b = 0xabcdef12; 

a = a & b;

Autres formes

Il existe d’autres formes en fonction du type d’opĂ©randes mais le principe est toujours le mĂȘme.

sub ope_d, ope_s

Opérandes

  • ope_d : opĂ©rande de destination. Peut ĂȘtre :
    • un registre
    • un pointeur
  • ope_s : opĂ©rande source. Valeur soustraite. Peut ĂȘtre
    • une valeur immĂ©diate
    • un registre
    • un pointeur

Détails

“Sub” provient de “substract” qui signifie soustraire.

Cette instruction réalise ainsi deux actions :

  • soustraction de l’opĂ©rande source avec l’opĂ©rande de destination ope_d - ope_s.
  • stockage du rĂ©sultat (la diffĂ©rence) dans l’opĂ©rande de destination

C’est de cette maniĂšre que sont rĂ©alisĂ©es les soustractions.

Contrairement Ă  add, l’ordre des opĂ©randes est important dans sub. En effet, en inversant les opĂ©randes, on inverse le signe du rĂ©sultat.

Exemple

Faisons la différence de 0xf0000034 avec 0x10000034 :

1
2
3
4
mov eax, 0xf0000034
mov ebx, 0x10000034

sub eax, ebx ; eax = 0xe0000000

Équivalent en C

1
2
3
4
int a = 0xf0000034; 
int b = 0x10000034; 

a = a - b;

Autres formes

Il existe d’autres formes mais le principe est toujours le mĂȘme.

cmp ope_d, ope_s

Opérandes

  • ope_d : opĂ©rande de destination. Peut ĂȘtre :
    • un registre
    • un pointeur
  • ope_s : opĂ©rande source. Peut ĂȘtre :
    • une valeur immĂ©diate
    • un registre
    • un pointeur

Détails

La comparaison avec cmp est effectuĂ©e d’une maniĂšre qui peut nous paraĂźtre bizarre. En effet, cmp effectue la soustraction suivante sub ope_d, ope_s mais sans stocker le rĂ©sultat. Ainsi le contenu des opĂ©randes restent inchangĂ©es.

Par contre, quelques flags parmi les EFLAGS vont ĂȘtre changĂ©s en fonction des valeurs des opĂ©randes et du rĂ©sultat. C’est Ă  partir de ces EFLAGS que l’on saura si les opĂ©randes sont Ă©gales ou s’il y en a une plus grande/petite que l’autre etc.

Il est important que vous ayez en tĂȘte la maniĂšre dont les entiers sont reprĂ©sentĂ©s en informatique, notamment les entiers signĂ©s avec le complĂ©ment Ă  deux.

Plus précisément, ce sont les flags ZF, SF, CF et OF qui nous intéressent principalement (et dans une moindre mesure PF). Nous les avions déjà vus briÚvement précédemment, profitons-en pour nous rafraßchir la mémoire et rentrer plus dans les détails.

  • ZF (Zero Flag) :
    • 1 si les deux opĂ©randes sont Ă©gales. La diffĂ©rence des deux termes vaut donc 0.
    • 0 si les deux opĂ©randes sont diffĂ©rentes.
  • SF (Sign Flag) :
    • 1 si le bit de poids fort du rĂ©sultat est non nul. Dans le cas d’une opĂ©ration signĂ©e cela implique qu’il est nĂ©gatif. Dans le cas oĂč elle est non signĂ©, ce flag n’a pas d’importance.
    • 0 si le bit de poids fort du rĂ©sultat est nul
    • Exemple : Prenons la soustraction signĂ©e suivante :0x5 - 0x20 = -0x1b. Le rĂ©sultat Ă©tant nĂ©gatif, le complĂ©ment Ă  deux de 0x1b est 0xe5 qui s’écrit sur 8 bits en binaire 0b11100101. Le bit de poids fort Ă©tant Ă  1, SF l’est Ă©galement. Etant donnĂ© qu’il s’agit d’une opĂ©ration signĂ©e SF nous permet de savoir que le rĂ©sultat est nĂ©gatif.
  • CF (Carry Flag) :
    • 1 si le rĂ©sultat possĂšde une retenue.
    • 0 si le rĂ©sultat ne possĂšde pas de retenue
    • Exemple : Par exemple, pour l’instruction add al, bl sur 8 bits oĂč al vaut 0xFF et bl vaut 0x01, le rĂ©sultat est 0xFF + 0x01 = 0x100 qui ne tient pas sur les 8 bit de al. Cela gĂ©nĂšre donc une retenue. Lors d’une soustraction a - b, une retenue est gĂ©nĂ©rĂ©e lorsque b est plus grand que a.
  • OF (Overflow Flag) :
    • 1 si un dĂ©bordement a lieu avec des valeurs signĂ©es. Par exemple, cela peut avoir lieu lorsqu’il y a un rĂ©sultat nĂ©gatif d’opĂ©randes positifs et inversement. Ce bit n’a pas d’importance lorsque l’on manipule des valeurs non signĂ©es.
    • 0 s’il n’y a pas eu de dĂ©bordement
    • Exemple : Prenons l’addition signĂ©e suivante :0x7F + 0x8 = 0x87. Ici, le bit de poids fort de 0x87 est Ă  1 : il s’agit donc d’un rĂ©sultat nĂ©gatif (-121). Pourtant, les deux termes sont strictement positifs. Il y a donc eu un dĂ©bordement (overflow).
  • PF (Parity Flag) :
    • 1 si le nombre de bits su rĂ©sultat est pair
    • 0 sinon

N’hĂ©sitez pas Ă  utiliser asmdebugger pour faire quelques tests. Les 4 flags Ă©tudiĂ©s sont affichĂ©s sur le site lors de l’exĂ©cution des instructions.

En effet, si l’utilisation de ces flags vous paraĂźt difficile, sachez que c’est normal car cela fait intervenir des notions que l’on utilise pas, en tant qu’humain, tous les jours comme le complĂ©ment Ă  deux pour reprĂ©senter des nombres nĂ©gatifs.

Lors d’une comparaison avec cmp, le processeur ne sait pas si les opĂ©randes sont signĂ©es ou non. En fait, il s’en moque Ă  ce stade. C’est pourquoi il va modifier, si besoin est, ces 4 flags bien que certains soient plutĂŽt utilisĂ©s lors d’opĂ©rations signĂ©es (SF et OF) ou non signĂ©es (CF).

Exemples

Voici quelques exemples :

InstructionZFSFCFOF
cmp 1, 5 ✅✅ 
cmp 5, 1    
cmp 5, 5✅   
cmp 4, 255  ✅ 
cmp 127, 129 ✅✅✅

Je vous conseille de reprĂ©senter les entiers sous forme binaire et de faire attention Ă  la reprĂ©sentation du complĂ©ment Ă  deux. En effet, 129 s’il n’est pas signĂ© vaut 129 mais s’il est signĂ©, il vaut -127.

Équivalent en C

Pour l’instruction cmp, il n’y a pas rĂ©ellement d’équivalent en C. En fait, cmp n’est jamais (sauf exceptions) utilisĂ©es autrement qu’avec des sauts. Ainsi, reprĂ©senter cmp tout seul dans du code C n’a pas de sens. Par contre, dans toutes les conditions du type if, else vous y trouverez un cmp (ou test) dans le code assembleur associĂ©.

test ope_d, ope_s

Opérandes

  • ope_d : opĂ©rande de destination. Peut ĂȘtre :
    • un registre
    • un pointeur
  • ope_s : opĂ©rande source. Peut ĂȘtre :
    • une valeur immĂ©diate
    • un registre

Détails

Cette instruction est également utilisée pour réaliser des comparaisons mais son fonctionnement sous-jacent est différent de cmp.

test va exĂ©cuter l’instruction and ope_d, ope_s sans stocker le rĂ©sultat mais en mettant Ă  jour des flags suivants : SF, ZF et PF. test est souvent utilisĂ© pour savoir si un registre est nul ou non.

Exemple

L’instruction test eax, eax permet de voir si eax est nul ou non. En effet, lors de l’exĂ©cution de cette instruction, si ZF == 1, c’est que eax est nul. Sinon, cela signifie qu’il est non nul.

Équivalent en C

MĂȘme remarque que pour cmp : il n’y a pas rĂ©ellement d’équivalent direct en C.

jmp dest

Opérandes

  • dest : destination du saut. Peut ĂȘtre :
    • une valeur immĂ©diate (exemple : adresse relative ou absolue)
    • un registre
    • un pointeur

Détails

Unique instruction permettant de rĂ©aliser des sauts inconditionnels afin de “sauter” vers l’adresse de destination. Cela permet de pouvoir exĂ©cuter des instructions qui ne sont pas toujours situĂ©es linĂ©airement dans le code.

La diffĂ©rence entre un saut et un appel de fonction call est que l’on ne se prĂ©occupe pas de sauvegarder l’adresse de retour afin de pouvoir y retourner plus tard.

Lorsque l’opĂ©rande dest est une valeur immĂ©diate, il peut s’agir d’une adresse absolue ou relative :

  • adresse absolue : l’adresse est “codĂ©e en dur” dans l’opcode de l’instruction. Cela permet de sauter plus loin dans le code mais l’instruction prend plus de place.
    • Exemple : e9 d8 12 00 00 jmp 0x12dd
  • adresse relative : seule la diffĂ©rence entre l’adresse courante de eip et l’adresse de destination est insĂ©rĂ©e dans l’opcode. Cela permet d’avoir des opcodes plus courts mais de sauter moins loin.
    • Exemple : eb 2a jmp short 0x12DC

Concernant les adresses absolues, elles ne sont pas insĂ©rĂ©es tel quel dans l’opcode. En effet, il est nĂ©cessaire de prendre en compte la taille de l’instruction de saut (par exemple 5 octets) avant d’insĂ©rer l’adresse de destination. C’est pourquoi l’opcode de l’exemple contient e9 d8 12 et non pas e9 dd 12.

Bien que le mnĂ©monique jmp utilisĂ© soit le mĂȘme, il existe diffĂ©rentes forme oĂč dest n’est pas toujours une adresse. Cela peut, en effet, ĂȘtre un pointeur ou registre.

Le souci, en tant que reverser, est qu’il ne sera pas toujours possible de savoir directement vers quelle adresse le processeur va sauter lorsqu’un registre (ou pointeur) va ĂȘtre utilisĂ©. En analyse statique, il sera nĂ©cessaire de dĂ©terminer les diffĂ©rentes valeurs que peut prendre le registre afin de trouver les potentielles destinations.

Le fait d’utiliser un registre comme opĂ©rande est trĂšs commun dans la modĂ©lisation des switch en assembleur aprĂšs compilation.

Exemple

1
2
3
4
jmp 0x401020
jmp rax
jmp [ebx]

Équivalent en C

Les sauts inconditionnels jmp sont l’équivalent de goto en C :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main() {
    int i = 0;

    start_loop:

    if (i < 5) {
        printf("i = %d\n", i);
        i++;
        goto start_loop;  // Sauter à l'étiquette start_loop
    }

    return 0;
}

jcc dest

Opérandes

  • dest : destination du saut. Peut ĂȘtre :
    • une valeur immĂ©diate (exemple : adresse relative ou absolue)

Détails

jcc n’est pas un mnĂ©monique en soi. Il s’agit d’un terme gĂ©nĂ©rique pour dĂ©signer le mnĂ©monique de tous les sauts conditionnels. Les points communs de tous ces sauts sont les suivants :

  • Ils utilisent certains flags parmi les EFLAGS afin de savoir s’il faut sauter
  • Lorsque que le saut n’est pas exĂ©cutĂ©e, c’est l’instruction situĂ©e immĂ©diatement aprĂšs le saut qui est rĂ©alisĂ©e
  • Ils sont prĂ©cĂ©dĂ©s d’une instruction cmp ou test

Si vous retenez ça, vous avez retenu 60% du fonctionnement des sauts conditionnels. Le reste consiste seulement à se rappeler de ce que signifie chaque mnémonique et quels flags sont utilisés.

Voici les principaux sauts que vous pourrez rencontrer :

Selon le dĂ©sassembleur utilisĂ©, il peut y avoir quelques diffĂ©rences dans le mnĂ©monique comme jz (jump if zero) qui peut ĂȘtre dĂ©signĂ© je (jump if equal) mais qui reprĂ©sentent exactement la mĂȘme instruction.

MnĂ©monique(s)DescriptionSigne des opĂ©rationsCas d’utilisationCondition de saut
joJump if overflow Détection de débordementOF == 1
jnoJump if not overflow Détection de débordementOF == 0
jsJump if sign Tester le signeSF == 1
jnsJump if not sign Tester le signeSF == 0
jz / jeJump if zero / equal Tester l’(in)Ă©galitĂ©ZF == 1
jnz / jneJump if not zero / not equal Tester l’(in)Ă©galitĂ©ZF == 0
jb / jnae / jcJump if below / not above or equal / carryNon signéTester la supériorité / inférioritéCF == 1
jnb / jae / jncJump if not below / above or equal / not carryNon signéTester la supériorité / inférioritéCF == 0
jbe / jnaJump if below or equal / not aboveNon signéTester la supériorité / inférioritéCF == 1 \|\| ZF == 1
jnbe / jaJump if not below or equal / aboveNon signéTester la supériorité / inférioritéCF == 0 && ZF == 0
jl / jngeJump if less / not greater or equalSignéTester la supériorité / inférioritéSF != OF
jnl / jgeJump if not less / greater or equalSignéTester la supériorité / inférioritéSF == OF
jng / jleJump if not greater / less or equalSignéTester la supériorité / inférioritéZF == 1 \|\| SF != OF
jg / jnleJump if greater / not less or equalSignéTester la supériorité / inférioritéZF == 0 && SF == OF

Il est Ă  noter qu’il n’existe pas une seule maniĂšre de reprĂ©senter une condition du C vers l’assembleur. Prenons par exemple le code suivant :

1
2
3
4
5
6
7
8
9
10
unsigned int x = ...;
unsigned int y = ...;
if (x > y )
{
	// Code A
}
else
{
	// Code B
}

On peut trĂšs bien faire :

1
2
3
cmp x, y
ja addr_code_A
code_B

ou :

1
2
3
cmp x, y
jbe addr_code_B
code_A

Il faut donc ĂȘtre attentif lorsque l’on analyse du code assembleur pour savoir ce qui va ĂȘtre exĂ©cutĂ© et sous quelles conditions.

Exemples

1
2
jz 0x555555550102
jns 0x405987

Équivalent en C

Selon le signe des variables comparĂ©es et le type de comparaison utilisĂ©, certains sauts vont ĂȘtre utilisĂ©s plutĂŽt que d’autres (les diffĂ©rents mnĂ©moniques d’une mĂȘme instruction ont Ă©tĂ© omis par souci de concision) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int x = ...;
int y = ...;

if (x < 0) // js ou jns
{
	//...
}

if (x == y) //jz ou jnz
{
	//...
}

if(x < y) // jl ou jnl 
{
	//...
}

if(x >= y) // jnl ou jl
{
	//...
}

if(x <= y) // jle ou jnle
{
	//...
}

Autres formes

Il existe d’autres sauts mais que l’on rencontre moins souvent.

cdq

Opérandes

  • Cette instruction n’a pas d’opĂ©randes

Détails

cdq est l’abrĂ©viation de convert dword to qword. Vous l’avez compris, cela devrait donc permettre de convertir un dword (4 octets) en un qword (8 octets), mais comment ?

Tout d’abord, cette instruction ne s’applique que sur le registre eax (ou ses dĂ©rivĂ©es). C’est pourquoi elle ne dispose pas d’opĂ©randes. De plus, cette instruction garde le signe de l’ancienne valeur lors de la conversion vers la nouvelle valeur.

En x86_64 on a des registres de 64 octets, ce qui n’est pas le cas en x86. Ainsi, pour doubler la taille des donnĂ©es contenues dans eax, c’est le registre edx (ou ses dĂ©rivĂ©es) qui va ĂȘtre utilisĂ© de cette maniĂšre :

  • si le nombre dans eax est nĂ©gatif (bit de poids fort Ă©gal Ă  1), alors edx est rempli de 1
  • si le nombre dans eax est positif (bit de poids fort Ă©gal Ă  0), alors edx est rempli de 0

Cette maniĂšre de gĂ©nĂ©rer une nouvelle valeur Ă  partir d’une valeur signĂ©e est ce que l’on appelle l’extension de signe.

Ainsi on obtient une valeur de taille double en concaténant les deux registres sous la forme : edx:eax.

Cette instruction est trĂšs utilisĂ©e lors des divisions signĂ©es afin d’avoir un rĂ©sultat cohĂ©rent et correct.

Exemples

1
2
3
4
5
mov eax, 0x70001234
cdq ; edx:eax = 0x00000000:0x70001234

mov eax, 0x80001234
cdq ; edx:eax = 0xffffffff:0x80001234

Équivalent en C

Il n’y pas a pas d’équivalent directe en C.

Autres formes

Il existe plusieurs dĂ©rivĂ©es mais dont le principe d’extension de signe est le mĂȘme :

  • cwd (convert word to dword): la valeur convertie est contenue dans dx:ax
  • cqo (convert qword to double qword): la valeur convertie est contenue dans rdx:rax (disponible seulement en x86_64)

shr ope_d, n et sar ope_d, n

Opérandes

  • ope_d : opĂ©rande de destination. Peut ĂȘtre :
    • un registre
    • un pointeur
  • n : opĂ©rande source. Peut ĂȘtre :
    • une valeur immĂ©diate
    • un registre (seulement le registre cl)

Détails

L’instruction shr (ou shift right) permet de rĂ©aliser un dĂ©calage des bits de ope_d de n bits vers la droite.

Avec l’instruction shr et toutes les autres instruction de shift (dĂ©calage), il n’y a pas de rotation des bits sortants.

Il existe d’autres instructions comme ror/rol qui rĂ©alise un dĂ©calage rotatif des bits. C’est-Ă -dire que des bits qui sortent, par exemple, par la gauche, “rerentrent” par la droite.

Ainsi, le dĂ©calage de 0b01110011 d’un bit vers la droite est 0b00111001.

En fait, lorsqu’il y a un bit sortant, il n’est pas rĂ©ellement perdu dans la nature : il est sauvegardĂ© dans le flag CF des EFLAGS.

Il existe l’instruction sar (ou shift aritmetic right) est basĂ©e sur le mĂȘme principe de dĂ©calage que shr. La seule diffĂ©rence est que sar prend en compte le signe du nombre qui sera dĂ©calĂ©.

Ainsi, si le bit de poids fort de ope_d est 1, il sera réinitialisé à 1 aprÚs décalage. En fait sar agit en deux temps :

  1. exécuter shr
  2. si le précédent nombre était signé, mettre le bit de poids fort du résultat à 1

Voir les exemples ci-dessous pour comprendre de quoi il s’agit.

Ces instructions sont trĂšs utilisĂ©es pour rĂ©aliser des divisions par 2 d’un nombre (et dont le reste est dans le flag CF). En effet, le dĂ©calage d’un bit vers la droite revient Ă  diviser par 2. Le dĂ©calage de n bits vers la droite revient Ă  diviser par 2 puissance n.

Je ne vois pas en quoi dĂ©caler d’un bit vers la droite revient Ă  diviser par deux ?

Pourtant c’est bien ce qui se passe lorsque l’on note un nombre en dĂ©cimal et que l’on le dĂ©cale d’une unitĂ© vers la droite, cela revient Ă  diviser par 10.

Prenons par exemple 213950, en le dĂ©calant d’une unitĂ© vers la droite on obtient 21395, ce qui revient bien Ă  diviser par 10.

Avec la notation en binaire, c’est la mĂȘme chose : dĂ©caler d’un bit revient Ă  diviser par deux.

Ainsi, sar et shr sont trÚs utilisés pour réaliser des divisions de puissances de 2.

Exemple

1
2
3
4
5
6
7
    mov eax, 0x80000001   (0b10.....001)
    shr eax, 1 ; eax = 0x40000000 (0b01.....000)
               ; CF == 1
               
    mov eax, 0x80000001   
    sar eax, 1 ; eax = 0xc0000000 (0b11.....000)
               ; CF == 1

Équivalent en C

1
2
3
4
5
int x = 0x80000001;
x = x >> 1; // x = 0xc0000000

int y = 0xdeadbeef;
y = y >> 13; // y = 0xfffef56d

Autres formes

De la mĂȘme maniĂšre que shr/sar permettent de rĂ©aliser des dĂ©calages vers la droite, shl/sal permettent de rĂ©aliser des dĂ©calages vers la gauche avec le mĂȘme principe.

A l’instar de la division par puissances de 2 de shr/sar, shl/sal permettent de rĂ©aliser des multiplications par puissances de 2 :

  • âžĄïž shr/sar : division par puissances de 2
  • âŹ…ïžshl/sal : multiplication par puissances de 2

Vous pouvez Ă©galement jeter un Ɠil aux instructions rcl/rcr/rol/ror. Leur fonctionnement de dĂ©calage est le mĂȘme. La principale diffĂ©rence est qu’il y a une rotation des bits sortants.

This post is licensed under CC BY-NC 4.0 by the author.