Post

Partie 11 - Structures de contrôle - les sauts et conditions (2/3)

Structures de contrôle : les sauts et conditions

Nous nous étions arrêtés à ces deux instructions :

1
2
cmp dword ptr [eax], 2
jz short loc_12B2

Pour ce qui est de la comparaison, vous avez compris à quoi elle sert. Intéressons nous maintenant au saut jz short loc_12B2. Décortiquons tout d’abord cette instruction :

  • jz : type de saut (ici : jump if zero)
  • short : distance du saut
  • loc_12B2 : adresse de destination. IDA aime bien préfixer de manière générale les adresses censées contenir du code, ne soyez pas embrouillés par cela. On va faire comme s’il n’y avait marqué que 0x12B2.

Les sauts en assembleur peuvent également être appelés branchements.

Astuce IDA : Les adresses préfixées (labels), noms de variables et noms de fonctions peuvent être renommées avec le raccourcis N.

Les distances de sauts

Les distances de sauts ne nous intéressent pas tant que ça en reverse mais profitons en pour en toucher quelques mots.

Les distances de sauts étaient importantes lorsque les processeurs étaient en 16 bits car il n’était pas possible d’accéder à n’importe quelle zone mémoire avec seulement 16 bits. Ainsi, plusieurs registres étaient utilisés pour pouvoir accéder au segment de code (registre CS), à la zone mémoire de la stack (registre SS) …

Ainsi une adresse était désigné par un préfixe (segment) et un offset, par exemple : CS:0x1234CS a une valeur de 16 bits.

Ainsi les sauts courts (short ou near) permettaient de sauter vers une adresse située dans le même segment.

Tandis que les sauts longs (far) permettaient de sauter plus loin vers une adresse qui pouvait être dans un autre segment.

En 32 bits, et à plus forte raison en 64 bits, il est possible de sauter directement à n’importer quelle adresse sans avoir à se préoccuper du segment de destination.

Les types de sauts

Il existe principalement deux types de sauts en assembleur x86 :

  • Les sauts inconditionnels : Il s’agit d’un saut vers une adresse qui sera toujours réalisé, sans aucune condition particulière.
    • Exemple : Lorsque l’instruction jmp 0x401020 sera exécutée, eip aura pour valeur 0x401020 dans tous les cas.
  • Les sauts conditionnels : Il s’agit d’instructions dont le saut n’est réalisé que sous certaines conditions.
    • Exemple : jz n’est réalisé que lorsque ZF == 1tandis que jge n’est réalisé que lorsque SF == OF

Les sauts inconditionnels

Les sauts inconditionnels sont les sauts qui sont exécutés dans tous les cas. Nous en avons au moins un dans notre programme, plus précisément dans main :

Cela permet d’atteindre des zones de code qui ne sont pas contiguës en mémoire. En effet, si vous vous rendez dans le main à l’adresse 0x12B0 puis que vous appuyez sur espace (pour quitter le mode “graphe”), vous verrez que l’instruction à 0x12b0 est éloignée de l’instruction à 0x12dc.

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.

Le fonctionnement de ce type de saut est très simple : une fois que l’instruction est exécutée, eip (ou rip en 64 bits) est égal à l’adresse de destination, là où l’exécution du programme se poursuit.

Les sauts inconditionnels se font via l’instruction jmp1.

Les sauts conditionnels

Là, faut rester concentré 🤓 ! Parce que des sauts de ce type, en veux-tu en voilà !

Nous avons pris le temps de bien comprendre le fonctionnement de cmp et test ainsi que les EFLAGS qui pouvaient être modifiés : ce n’est pas pour rien !

En fait les sauts conditionnels dépendent de ces deux instructions car un saut sera pris, ou non, selon les EFLAGS modifiés par cmp ou test (même si techniquement n’importe quelle instruction qui modifie les flags en question peut être utilisée).

Je vous propose de comprendre le saut où nous nous étions arrêtés (à 0x1293) avant de voir les autres types de sauts :

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.

Comme vous l’avez vu, jz signifie jump if zero, c’est-à-dire que le processeur va sauter à l’adresse de destination seulement si ZF = 1 (ce qui signifie que les opérandes sont égales lors de la précédente instruction cmp).

Pour rappel la précédente instruction cmp dword ptr [eax], 2 comparait argc et 2. Or le saut ici jz ne s’intéresse qu’à ZF. Ainsi, on souhaite simplement savoir si argc == 2 auquel cas on saute vers le bloc vert à l’adresse 0x12b2. Le cas échéant, on entre dans le bloc rouge à l’adresse 0x1295.

On utilise le terme “entrer” dans le bloc rouge plutôt que “sauter” car vu que le saut jz 0x12b2 n’est pas exécuté, le prochaine instruction est celle qui est immédiatement après jz 0x12b2. En passant en mode “texte” dans IDA vous pourrez bien constater que le premier bloc de la fonction main et le bloc en rouge sont contiguës.

De toute façon, c’est bien ce que l’on a fait dans notre code source : vérifier qu’il y a exactement deux arguments :

Eh bien voilà ! Vous savez désormais comment sont gérés les if en assembleur ! En fait, nous avons même ici dans le code assembleur la structure d’un if / else bien que l’on ait pas écrit de bloc else dans le code : Lorsque la condition du if est vérifiée, on saute dans le bloc 🟢, sinon dans le bloc 🔴.

Normalement, si vous avez bien compris cet exemple vous ne devriez pas avoir trop de mal à comprendre comment fonctionnent les autres sauts conditionnels2.

Par ailleurs, vous avez maintenant tous les éléments pour comprendre le code assembleur jusqu’à l’instruction à 0x12CF call printBin 😎.

📝 Exercice : votre premier crackme

Avant d’aller plus loin, je vous propose un petit challenge de type “crackme”. Le but est de trouver une entrée valide permettant d’afficher le message de réussite. Le programme contient pas mal de sauts, cela vous permettra de mettre en pratique vos connaissances en assembleur.

Le but est de trouver le nombre permettant d’afficher la string de réussite.

L’analyse statique à elle seule permet de trouver l’entrée valide. Vous pourrez confirmer votre résultat en exécutant le programme avec votre entrée. Il faut y aller petit à petit et bien comprendre ce qui est fait à chaque fois.

⤵️ Vous pouvez le télécharger ici : mini_crackme.

Les indices sont donnés en base64 afin qu’ils ne soient pas directement visible.

💡 Indice 1 : Tidow6lzaXRleiBwYXMgw6AgdXRpbGlzZXIgdW5lIGZldWlsbGUvY2FoaWVyIGRlIGJyb3VpbGxvbiBlbiBhdmFuw6dhbnQgcGV0aXQgw6AgcGV0aXQu

💡 Indice 2 : VXRpbGlzZXogbGUgdGFibGVhdSByw6ljYXBpdHVsYXRpZiBkZXMgc2F1dHMgcG91ciBjb25uYcOudHJlIGxhIGNvbmRpdGlvbiBkZSBzYXV0Lg==

💡 Indice 3 : TCdlbnRyw6llIGRvaXQgw6p0cmUgc2Fpc2llIGVuIHRhbnQgcXUnYXJndW1lbnQgZW4gZMOpY2ltYWwu

Solution : TGEgc29sdXRpb24gZXN0IDk1Lg==

Une fois l’exercice terminé, on se donne rendez-vous dans la fonction printBin pour comprendre comment les conditions et sauts peuvent être utilisés pour réaliser des boucles 🔄.

ℹ️ Instructions mentionnées

1️⃣ L’instruction 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;
}

2️⃣ L’instruction 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.

⤴️ Notes

  1. Voir ci-dessus : 1️⃣ L’instruction jmp dest 

  2. Voir ci-dessus : 2️⃣ L’instruction jcc dest 

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