Exploitation – stack buffer overflow: Return Oriented Programming – 1/2: La théorie


Une petite introduction

Prérequis:

  • Connaître la définition d’un stack based buffer overflow (Voir par exemple cet article de blog)
  • Connaître globalement l’architecture d’un programme ELF (Lire ELF.h, présent sur tout système Linux)
  • Avoir les bases de l’assembleur x86_64
  • Avoir les bases du shellcoding et de l’exploitation de stack buffer overflow sans protection

Ici nous parlerons du Return-Oriented Programming ou ROP, et nous montrerons comment l’utiliser pour attaquer un programme linux, 64 bits, compilé statiquement, avec uniquement la protection NX. (Ne vous faites pas de souci on va clarifier tout ça).

Quelques rappels:

Le résultat de la compilation d’une fonction C commence par un prologue et est terminé par un épilogue. 

Lorsque l’instruction call vers une fonction est exécutée, l’adresse de la fonction appelante est mise dans la stack et l’épilogue de la fonction appelée se chargera de prendre cette adresse et de la remettre dans le pointeur d’instruction (ici, le registre rip) pour revenir dans la première fonction. Partant de là, si nous sommes capable de réécrire l’adresse de retour de cette fonction, nous contrôlons le flux d’exécution du programme.

Point de détail important, tout épilogue se termine par l’instruction ret, et c’est cette dernière qui se charge de paramétrer le pointeur d’exécution à la bonne adresse.

Dans un premier temps la stack était un segment permettant l’exécution et les adresses étaient statiques. Il suffisait donc de mettre un shellcode (une suite d’instructions) dans la stack et de faire pointer l’adresse de retour vers notre shellcode pour que lorsque la fonction se termine le programme exécute notre shellcode. (Pour plus de détails, nous recommandons cet excellent article sur les techniques de base de l’exploitation de stack overflow (EN))

Pour essayer de prévenir ce type d’exploitation, les fabricants ont  introduit la protection NX (stack Non eXécutable). Cette protection nous empêche d’exécuter un shellcode depuis la stack et c’est pour contourner cette contrainte que le ROP est apparu.

En quoi consiste le ROP?

Le ROP consiste à utiliser des gadgets en série (appelée ropchain) afin de prendre le contrôle du programme. Un gadget est une adresse mémoire à laquelle se trouve une suite d’instructions se terminant par ret (d’ou le nom de cette technique). Par exemple xor eax, eax; ret

Pour créer une ropchain il faut donc chercher dans l’exécutable ce type d’instructions et connaître leurs adresses.

Note: Ce dernier point peut poser problème lorsque le PIE et l’ASLR sont activés puisque l’exécutable est chargé de manière aléatoire dans la mémoire. Il est donc impossible de prime abord de connaître les adresses de nos gadgets. Cela reste tout de même exploitable, mais il y aura un prérequis supplémentaire: avoir une fuite d’une adresse d’un segment qui ne soit ni la stack, ni la heap. Une fois ce leak obtenu, vous devrez retrouver à quel offset elle correspond. De cette façon vous pourrez toujours recalculer l’adresse de base du programme grâce à l’égalité base = leak – offset et ensuite poursuivre le ROP de manière conventionnelle.


Exemple d’une partie d’une ropchain écrivant /bin/dash en mémoire

Voilà, pas à pas, ce que va exécuter cette ropchain:

pop rsi ; // stocke le prochain élément de la stack dans rsi (ici l'adresse du segment .data)
pop rax ; // stocke le prochain élément de la stack dans rax (ici la chaîne '/bin/das')
mov qword ptr [rsi], rax ; // écrit le contenu de rax à l'adresse contenue dans rsi.
pop rsi ; // stock le prochain élément de la stack dans rsi (ici l'adresse du segment .data + 8)
pop rax ; // stock le prochain élément de la stack dans rax (ici la chaine 'h\x00')
mov qword ptr [rsi], rax ; // écrit le contenu de rax à l'adresse contenue dans rsi.

Cette ropchain écrit donc /bin/dash\x00 au début du segment .data.

Note: Les gadgets de types mov qword ptr [rsi], rax; sont appelés des write-what-where puisqu’ils permettent d’écrire une valeur en mémoire.


Mais comment cela s’enchaîne-t-il dans la stack ?

Note: Ici, on utilisera une partie d’une autre ropchain exécutant /bin/sh
Voilà une représentation de la stack avant réécriture de l’adresse de retour (ici, 0x4025b0):

Il y a beaucoup d’informations dans ce screenshot, mais ici tout n’est pas important. Ce qu’il faut noter c’est que l’adresse de retour de la fonction main est juste en dessous du rbp, à savoir 0x4025b0, et que notre buffer commence là où nous voyons les “AAA…”. Les autres informations ne sont pas dans notre cas importantes 🙂

Voilà la même représentation, mais avec une réécriture de l’adresse de retour par des “Z” (donc toujours sans ropchain) :

Voilà une représentation avec la réécriture et un début de ropchain :

On peut voir dans ce dernier screenshot que l’ancienne adresse de retour a été réécrite par 0x40324d, c’est-à-dire l’adresse de l’instruction pop r13; ret. La deuxième chose mise en avant est 0x6873f6e69622f2f. Cette valeur est “hs/nib//”, c’est à dire “//bin/sh” à l’envers (et oui ! Nous sommes dans la stack sur un linux 64 bits en little-endian donc les valeur en stack sont à l’envers !). Enfin, le troisième point est l’adresse 0x401f6b, c’est à dire l’adresse de l’instruction pop rbx; ret.

Juste avant l’exécution de la ropchain, la stack ressemble à ça :

Voyons le déroulement pas à pas:

  • la fonction exécute l’instruction ret, et le haut de la stack contient l’adresse de l’instruction pop r13. Le haut de la stack, une fois le ret exécuté, contient 0x68732f6e69522f2f.

  • l’instruction pop r13 s’exécute alors, et écrit donc cette valeur dans le registre r13 :



    et le haut de la stack est désormais l’adresse de l’instruction pop rbx :

  • Le programme exécute l’instruction ret à nouveau, et l’adresse de l’instruction pop rbx est alors écrite dans le pointeur d’instruction et le haut de la stack contient désormais 0x4ab0e0.



    Cette dernière instruction (pop rbx) s’exécute et le haut de la stack est désormais 0x45c7a5, l’adresse de l’instruction suivante de la ropchain

Le déroulement de la ropchain va ainsi continuer jusqu’à la fin de celle ci.


Vous l’aurez compris, à chaque fois que le pointeur d’instruction arrive sur un ret, le programme prend la valeur à l’adresse du stack pointer (rsp) et remplace la valeur du pointeur d’instruction (rip) par celle-ci. En d’autres termes (et en conclusion :-P), chaque gadget va ret sur le gadget suivant. (Vous verrez avec le prochaine article qui traitera de la partie pratique, ces concepts apparaîtront plus clair)


Nos formations Sécurité

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

%d blogueurs aiment cette page :