Buffer-Overflow
1. Notions essentielles
Quand un programme est exécuté, différents éléments (par exemple des variables) sont stockés en mémoire.
Premièrement, l’OS crée des emplacements mémoire ou le programme pourra "tourner". Cet emplacement mémoire inclut les instructions du programme actuel.
Deuxièmement, les informations du programme sont chargées dans l’espace mémoire créé.Il existe trois types de segments dans le programme : .text, .bss et .data.
Le .text est en lecture seule tandis que le .bss et le .data sont en lecture/écriture.
Le .data et le .bss sont réservés pour les variables globales.
Le .data contient les données initialisées.
Le .bss contient les données non initialisées.lLe .text contient les instructions du programme.
Finalement, la pile (stack) et le heap (partie de la mémoire interne utilisée pour construire ou rejeter dynamiquement des objets de données) sont initialisés.
Stack (LIFO) : la donnée la plus récente placée (push) dans la pile sera la première sortie (pop).
Une LIFO est idéale pour mémoriser des données transitoires ou des informations qui n’ont pas besoin d’être stockées longtemps.
La pile stocke les variables locales, les appels à fonction et d’autres informations utilisées pour nettoyer la pile après qu’une fonction ou procédure ait été appelée.À chaque donnée stockée dans la pile, l’adresse contenue dans le pointeur de pile (ESP) décroît.
La première chose à trouver sur une machine UNIX lors d’une attaque locale est un programme vulnérable, mais cela ne suffit pas. Il faut injecter notre code dans un programme qui est exécuté avec les droits root même s’il est appelé par un utilisateur.
On se retrouvera avec les droits root et on sera en possession d’un shell qui permettra d’exécuter n’importe quelle commande en tant que root.
Comment connaître les programmes avec le SUID root activé ?
1.2 Un exemple simple pour comprendre
Pour pouvoir dérouler notre exemple, nous allons devoir désactiver le patch Linux sinon cela ne marchera pas du à la randomization des adresses :
Voici un programme très simple qui alloue de la mémoire.
Dans le programme ci-dessus, le buffer a été déclaré avec une taille de 512 octets.
Les sauvegardes des registres sur la pile sont codées sur 4 octets (registres 32 bits).
Les deux arguments de la fonction vuln sont des adresses de buffer, ils sont codés sur 4 octets.
Si nous arrivons à écrire 4 octets de plus que la taille du buffer, alors les 4 octets de EBP seront écrasés.
Si nous arrivons à écrire 4 octets de plus, alors c’est EIP qui sera écrasé.
Si EIP est écrasé par une valeur que nous aurons définie, lors de l’appel à ret, c’est cette valeur qui sera extraite de la pile et à laquelle le programme sautera.
Nous risquons d’avoir un écart de 4 octets suivant la version du compilateur.
Compilons d’abord le programme, donnons-lui le droit d’exécution et activons le bit SUID :
Nous allons pouvoir nous attaquer au buffer overflow.
La première chose à tester est la faillibilité de notre programme. Nous allons donc essayer d’injecter un grand nombre d’arguments en lançant notre programme. Si celui-ci est vulénrable, On aura un segment fault en retour.
ASTUCE : Voici ma commande préféré pour rapidement génerer des caractères. Pour écrire, par exemple, un grand nombre de "A" grâce à Python on écrit :
On lance notre programme avec en paramètre nos 1000 caractère "A" :
Notre programme nous renvoie une erreur de segmentation. Il est donc susceptible d'être vulnérable à un, buffer overflow.
Maintenant il faut déterminer exactement pour quel nombre de caractères le programme plante pour pouvoir écraser l’adresse de retour.
Nous allons pour l’instant tester manuellement jusqu’à trouver la taille du buffer. Nous verrons ultérieurement comment automatiser tout ça.
Pour visualiser le contenu de l’adresse de retour, on lance gdb sous Linux.
r signifie run, non lance le programme avec les 1000 "A" comme arguments.
On peut voir "Segmentation fault" avec une adresse inconnue qui est 0x41414141. Que représente cette adresse ? 41 en hexadécimal représente le A. Donc l’adresse de retour a été remplacée par quatre A. L’adresse de retour a donc bien été écrasée.
On va essayer, en tâtonnement, de trouver exactement le moment où nous écrasons l’adresse de retour.
Nous voyons que pour 520 A exactement, nous écrasons l’adresse de retour (EIP).
2. Exploitation d'un Buffer Overflow
Prérequis (Avant chaque buffer-overflow que vous ferez) :
1) Préparer votre environnement :
VM Attaque ( Kali Linux/ Parrot, ...)
Mona.py à mettre dans C:\Programmes_files\Immunity Inc\Pycommands
2) Configurer Mona :
Avant tout on crée un répertoire dédié à Mona :
3) Télécharger sur votre VM Windows l'exécutable sujet au Buffer-overflow
4) Lancez l'exécutable et Immunity Debugger en mode Administarteur
5) Appuyer sur le bouton run (à refaire à chaque fois que l'application crash), pour revenir au stade précèdent vous pouvez cliquer sur l'icone << et ensuite cliquer sur run.
1) Fuzzing
2) Crash EIP
Nous savons maintenant combien de bytes il nous faut envoyer pour faire crasher l'application. On va donc utiliser metasploit qui va créer un pattern spécial que nous allons utiliser ensuite pour trouver l'offset.
Copier le résultat et remplacer le dans la variable overflow="" dans le script exploit.py :
3) Trouver l'offset
On refait crasher l'application, on va donc maintenant regarder l'adresse de l'EIP et demander à metasploit de nous trouver l'offset grâce au pattern créer précédemment et cette adresse.
On remplace l'offset trouvé dans notre script exploit.py (exemple : offset=146)
4) Vérification de l'overflow de l'EIP
On ajoute 4 "B" après notre overflow de l'EIP et des "C" (BYTES SEND - OFFSET -4 fois la lettre B). Nous faisons cela pour vérifier que nous allons bien overflow l'EIP et que nous allons pouvoir déposer notre shell juste après.
On lance notre programme, on fait crash l'application. La valeur de L'EIP devrait être 42424242 = "BBBB". On a donc bien réussi à overflow l'EIP qui est maintenant rempli de "B".
5) Trouver les Badchars
Certains caractères peuvent faire "changer" le fonctionnement de certaines fonctions, nous allons donc les trouver et les exclure.
Dans Immunity Debugger, on exclu avec Mona, le bit \x00 :
On retourne dans Immunty Debugger :
On note les Badchars trouvés par Mona sous la forme "\x00\x01\x02"...
6) Trouver la fonction JMP
Pour être sur que notre shellcode va s'exécuter après le débordement de notre stack nous allons retourner une adresse qui contient une instruction JUMP (JMP) qui va nous permettre de "sauter" directement à la prochaine instruction.
Dans Immunity Debugger on tape :
Voici un exemple :
Mona vas nous retourner une ou plusieur adresse lié à des fonctions jump. On prend une addresse de function JMP et on la note à l'envers (little endian).
Exemple :
deviendras
7) On génère notre reverse_shell
On n'a plus qu'à générer notre reverse_shell en excluant bien les badchars avec l'option -b (\x00\x01\x02)
8) Padding
Pour être sur encore que rien n'empêche notre shellcode de s'exécuter on rajouter ce qu'on appelle un padding. C'est un petit espace juste après notre instruction JUMP (j'aime bien en mettre 32 mais vous pouvez en mettre moins ou un peu plus).
Pour résumer, on se retrouve avec Exploit.py :
On lance un listener :
Vous devriez récupérer un reverse shell.
Last updated