Post

Partie 5 - Solutions des exercices

Solutions des exercices

Plusieurs exercices ont Ă©tĂ© proposĂ©s dans ce cours. Cette section comporte des solutions pour chacun d’eux. Il est Ă  noter que les maniĂšres de rĂ©soudre un mĂȘme exercice peuvent ĂȘtre multiples. Il ne s’agit donc pas de solutions “optimales” mais seulement de solutions qui permettent de rĂ©soudre un exercice donnĂ©.

Dans tous les scripts proposĂ©s, il est normal que vous n’ayez pas les mĂȘmes adresses car cela dĂ©pend en partie de votre compilateur.

Il suffit de les adapter en fonction de ce que vous avez comme adresses dans le code désassemblé.

Il n’est pas trĂšs utile de regarder les solutions aux exercices si vous n’avez pas cherchĂ© par vous mĂȘme Ă  une maniĂšre de rĂ©soudre l’exercice, vous n’apprendrez pas grand chose 😅 


Exercice 1ïžâƒŁ - Introduction

Objectif

Trouver le bon input de ce programme :

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include "stdio.h"  
#include "stdlib.h"  
#include "string.h"  
  
unsigned long long algo(unsigned long long arg)  
{  
 unsigned long long result = 0;  
 unsigned char x =0;  
 unsigned long long temp =0;  
    
 unsigned long long key =0xef9e8bd8f3afe9eb;  
 for (int i =0;i<8;i++)  
 {  
   x = (arg >> (i*8)) &0xff;  
   switch(x % 2)    
   {  
           case 0:  
               temp = 0xff;  
               break;  
           case 1:  
               temp = x ^ (unsigned char)((key >> (i*8)) &0xff);  
               break;  
  
     }  
  
     result = result | (temp << (i*8));  
 }  
   return result;  
}  
int main()    
{  
   char key_buffer[16] = {0};  
   puts("Give me the key in hexadecimal : ");  
   read(0,key_buffer,16);  
   unsigned long long arg = strtoull(key_buffer,NULL,16);  
   if (algo(arg) == 0xdeadbeefcafebabe)    
   {  
     puts("Win !");  
     return 1337;  
   }    
   else    
   {  
     puts("Loose !");  
     return -1;  
   }  
}

Solution

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
29
30
31
import angr  
import claripy  
  
p = angr.Project("./exe")  
  
arg_symb = claripy.BVS('input', 8*8)  
state_0 = p.factory.blank_state(addr= 0x401245) # Adresse de "push   rbp" dans le "main"  
  
sm = p.factory.simulation_manager(state_0)  
  
def hook_strtoull(state):  
   print("[i] La fonction strtoull a été hookée")  
   state.regs.rax = arg_symb  
  
p.hook(0x4012a7,hook_strtoull,5)  
print("[+] Exploration en Cours ....")  
sm.explore( find = 0x4012cb, avoid = 0x4012e1)  
  
print("[+] Arrivé à destination")  
  
if len(sm.found) == 0:  
       print("[-] Il n'a pas été possible d'atteindre la destination")  
       quit()  
else :  
       print("[+] Détermination de l'input valide")  
  
       # Récupération de l'état qui est arrivé dans le bon bloc  
       found = sm.found[0]  
       # Appel au solveur pour retourner au moins une solution  
       res = found.solver.eval(arg_symb)  
       print("[+] Le bon input est : ",hex(res))

Commentaire

Ce qui posait problĂšme dans ce script Ă©tait principalement la fonction strtoull. Il s’agit d’une fonction qui ressemble Ă  atoi mais qui retourne un entier de 64 bits. On l’a donc hookĂ©e afin qu’angr puisse poursuivre l’exĂ©cution sans problĂšme.

Pas besoin de hook puts et read qui sont hookées correctement de base par angr.

Résultat

1
2
3
4
5
6
7
8
9
10
11
12
13
[+] Exploration en Cours ....  

WARNING  | 2023-09-16 15:30:04,501 | angr.storage.memory_mixins.default_filler_mixin | The program is accessing register with an unspecified value. This could indicate unwanted behavior.                                
WARNING  | 2023-09-16 15:30:04,501 | angr.storage.memory_mixins.default_filler_mixin | angr will cope with this by generating an unconstrained symbolic variable and continuing. You can resolve this by:                
WARNING  | 2023-09-16 15:30:04,501 | angr.storage.memory_mixins.default_filler_mixin | 1) setting a value to the initial state  
WARNING  | 2023-09-16 15:30:04,501 | angr.storage.memory_mixins.default_filler_mixin | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null                         
WARNING  | 2023-09-16 15:30:04,501 | angr.storage.memory_mixins.default_filler_mixin | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to suppress these messages.                             
WARNING  | 2023-09-16 15:30:04,502 | angr.storage.memory_mixins.default_filler_mixin | Filling register rbp with 8 unconstrained bytes referenced from 0x401245 (main+0x4 in exe (0x401245))                                                                         

[i] La fonction strtoull a été hookée  
[+] Arrivé à destination  
[+] Détermination de l'input valide  
[+] Le bon input est :  0x3133353739515355

Exercice 2ïžâƒŁ - Lecture de la stack

Objectif

Ecrire une fonctionread_from_stack(state,n) qui affiche les n premiĂšre valeurs ( de 64 bits par ex) sur la stack de l’état state.

Solution

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
29
30
import archinfo  
import angr  
  
def read_from_stack(state, n):  
   stack_pointer = state.regs.rsp  
  
   values = []  
  
   for _ in range(n):  
  
       # Lecture de 8 octets (64 bits)  
       value = state.memory.load(stack_pointer, 8, endness=archinfo.Endness.LE)     
          
       values.append(value)  
  
       # Incrémentation de RSP pour lire la prochaine valeur  
       stack_pointer += 8     
  
   return values  
  
# Programme quelconque  
binary_path = "/bin/true"  
proj = angr.Project(binary_path)  
initial_state = proj.factory.entry_state()  
  
# Lecture des 10 premiĂšres valeurs de 64 bits de la pile  
values_on_stack = read_from_stack(initial_state, 5)  
  
for i, value in enumerate(values_on_stack):  
   print(f"Valeur {i + 1}: {value}")

Commentaire

Déjà, merci ChatPGT pour les travaux ;) !

Ensuite, on utilise la lecture en mémoire pour lire les différentes valeurs de 8 octets sur la stack.

Le programme /bin/true est utilisĂ© ici mais vous pouvez spĂ©cifier n’importe quel programme.

Résultat

1
2
3
4
5
Valeur 1: <BV64 0x1>  
Valeur 2: <BV64 0x7fffffffffeffc8>  
Valeur 3: <BV64 0x0>  
Valeur 4: <BV64 0x0>  
Valeur 5: <BV64 0x19>

Exercice 3ïžâƒŁ - Gestion de l’input et output

Objectif

Tester la gestion de stdin en compilant un programme basique en C qui lit, par exemple, 8 octets et vĂ©rifie qu’il s’agit du bon mot de passe.

Utiliser ensuite angr afin de trouver le mot de passe automatiquement sans avoir Ă  hook les fonctions qui lisent depuis stdin.

Programme utilisé

Voici le programme C utilisé pour cet exercice compilé avec gcc -no-pie main.c -o exe :

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include "stdio.h"  
#include "stdlib.h"  
#include "string.h"  
  
unsigned long long algo(unsigned char *arg)  
{  
 unsigned long long result = 0;  
 unsigned char x =0;  
 unsigned long long temp =0;  
    
 unsigned long long key =0xef9e8bd8f3afe9eb;  
 for (int i =0;i<8;i++)  
 {  
   x = arg[i] ;  
   switch(x % 2)    
   {  
           case 0:  
               temp = 0xff;  
               break;  
           case 1:  
               temp = x ^ (unsigned char)((key >> (i*8)) &0xff);  
               break;  
  
     }  
  
     result = result | (temp << (i*8));  
 }  
   return result;  
}  
int main()    
{  
   // solution : USQ97531  
   unsigned char key_buffer[8] = {0};  
   puts("Give me the key in hexadecimal : ");  
   read(0,key_buffer,8);  
   if (algo(key_buffer) == 0xdeadbeefcafebabe)    
   {  
     puts("Win !");  
     return 1337;  
   }    
   else    
   {  
     puts("Loose !");  
     return -1;  
   }  
}

Solution

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
29
30
31
32
33
34
import angr  
import sys  
import claripy  
  
p = angr.Project("./exe")  
flag = claripy.BVS('flag', 8*8)  

# Utilisation d'un buffer symbolique dans stdin
state_0 = p.factory.blank_state(addr= 0x4011ea,stdin=flag)  
  
sm = p.factory.simulation_manager(state_0)  
  
def is_output_good(state):  
       # Est-ce que "Win !" est présent dans l'output ?  
       output = state.posix.dumps(sys.stdout.fileno())  
       return b'Win !' in output  
  
def is_output_bad(state):  
       # Est-ce que "Loose !" est présent dans l'output ?  
       output = state.posix.dumps(sys.stdout.fileno())  
       return b'Loose !' in output  
  
print("[+] Exploration en Cours ....")  
sm.explore( find = is_output_good, avoid = is_output_bad)  
if len(sm.found) == 0:  
       print("[-] Il n'a pas été possible d'atteindre la destination")  
       quit()  
else :  
       print("[+] Détermination de l'input valide")  
       # Récupération de l'état qui est arrivé dans le bon bloc  
       found = sm.found[0]
       # Conversion en bytes du résultat  
       res = found.solver.eval(flag,cast_to=bytes)  
       print("[+] Le bon input est : ",res.decode())

Commentaire

Les points importants sont les suivants :

  • Utilisation d’un buffer symbolique dans l’input via state_0 = p.factory.blank_state(addr= 0x4011ea,stdin=flag)
  • Exploration en fonction de l’output et non pas en utilisant des adresses avec sm.explore( find = is_output_good, avoid = is_output_bad)

Et au final, aucun hook n’a Ă©tĂ© nĂ©cessaire !

Résultat

1
2
3
4
5
6
7
8
9
10
11
12
13
WARNING  | 2023-09-16 15:03:47,468 | angr.simos.simos | stdin is constrained to 8 bytes (has_end=True). If you are only providing the first 8 bytes instead of the entire stdin, please use stdin=SimFileStream(name='stdin', content=your_first_n_bytes, has_end=False).                                                                                             

[+] Exploration en Cours ....  

WARNING  | 2023-09-16 15:03:47,479 | angr.storage.memory_mixins.default_filler_mixin | The program is accessing register with an unspecified value. This could indicate unwanted behavior.                        
WARNING  | 2023-09-16 15:03:47,479 | angr.storage.memory_mixins.default_filler_mixin | angr will cope with this by generating an unconstrained symbolic variable and continuing. You can resolve this by:                
WARNING  | 2023-09-16 15:03:47,479 | angr.storage.memory_mixins.default_filler_mixin | 1) setting a value to the initial state  
WARNING  | 2023-09-16 15:03:47,479 | angr.storage.memory_mixins.default_filler_mixin | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null                         
WARNING  | 2023-09-16 15:03:47,479 | angr.storage.memory_mixins.default_filler_mixin | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to suppress these messages.                              
WARNING  | 2023-09-16 15:03:47,479 | angr.storage.memory_mixins.default_filler_mixin | Filling register rbp with 8 unconstrained bytes referenced from 0x4011ea (PLT.__cxa_finalize+0x19a in exe (0x11ea))

[+] Détermination de l'input valide  
[+] Le bon input est :  USQ97531

Exercice 4ïžâƒŁ - Gestion des fichiers

Objectif

Le programme suivant lit depuis un fichier des données afin de les valider ou non. A vous de trouver le contenu adéquat grùce à angr !

Cet exercice vous permettra de comprendre le fonctionnement global des SimFiles.

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
29
30
31
32
#include <stdio.h>  
#include <stdint.h>  
  
int main() {  
   FILE *file = fopen("mdp.bin", "rb");   
   if (file == NULL) {  
       perror("Erreur lors de l'ouverture du fichier");  
       return 1;  
   }  
  
   uint64_t win_value = 0xdeadbeefcafebabe;  
   uint64_t read_value;  
  
   // Lecture de 8 octets depuis le fichier  
   size_t bytes_read = fread(&read_value, 8, 1, file);  
   if (bytes_read != 1) {  
       perror("Erreur lors de la lecture du fichier");  
       fclose(file);  
       return 1;  
   }  
  
   // Fermeture du fichier  
   fclose(file);  
  
   if (read_value == win_value) {  
       printf("Win\n");  
   } else {  
       printf("Loose\n");  
   }  
  
   return 0;  
}

Pour le compiler : gcc -no-pie main.c -o exe. Indice : aucun hook n’est nĂ©cessaire pour la rĂ©ussite de cet exercice ;) !

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import angr  
import claripy    
  
p = angr.Project("./exe")  
  
data = claripy.BVS('mdp', 8 * 8)  
simfile = angr.storage.SimFile("mdp.bin", content=data)  
  
state = p.factory.entry_state(addr = 0x4011E9, fs={ "mdp.bin" : simfile})  
  
sm = p.factory.simulation_manager(state)  
sm.explore(find=0x4012b9, avoid=0x4012c7)  
  
found = sm.found[0]  
print("[+] Les données à utiliser sont : ",found.solver.eval(data, cast_to=bytes))

Commentaire

  1. On crée un SimFile contenant des données symboliques
  2. On insĂšre le SimFile dans l’état initial (son filesystem) avec fs={ "mdp.bin" : simfile}
  3. On explore jusqu’à arriver à destination

On aurait Ă©galement pu utiliser l’output pour explorer au lieu d’utiliser des adresses en dur.

Résultat

1
[+] Les données à utiliser sont :  b'\xbe\xba\xfe\xca\xef\xbe\xad\xde'
Cet article est sous licence CC BY-NC 4.0 par l'auteur.