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 4.0 by the author.