29/01/2011

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 ?

vtable_schema

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

vtable_schema

Après cette petite analyse sous gdb, on peut schématiser tout ça :

vtable_schema

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 :


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