DEV Community

Victor Gonzalez
Victor Gonzalez

Posted on

Guía de Reversing para vftables/vptr y RTTI en x64

Sup reversers

En este post desglosaremos qué son las funciones virtuales, cómo se implementan usando vpointers (punteros virtuales) y vftables (tablas de funciones virtuales), y cómo el compilador nos permite identificar tipos de objetos en tiempo de ejecución a través de RTTI (RunTime Type Information). Lo más importante es que veremos cómo identificar y analizar todo esto en nuestra herramienta favorita: IDA Pro.

El Layout de un Objeto en Memoria: La Base de Todo

Antes de hablar de punteros y tablas virtuales, entendamos cómo se ve un objeto simple en memoria.

Imagina esta clase:

// compilado con cl.exe /d1reportSingleClassLayoutDog x64
class Dog {
private:
    int age;
    char breed[16];

public:
    void bark() {
        // ...
    }
};
Enter fullscreen mode Exit fullscreen mode

En memoria, una instancia de Dog es súper simple. Es solo un bloque contiguo que contiene sus variables miembro, en el orden en que fueron declaradas.

++
| age (4 bytes)   |
++
| breed (16 bytes)|
++
Enter fullscreen mode Exit fullscreen mode

No hay nada más. Las funciones como bark() no se almacenan en cada objeto; existen en una única ubicación en la sección .text del binario. Cuando llamas a dog_instance.bark(), el compilador ya sabe en tiempo de compilación la dirección exacta de la función Dog::bark(), por lo que la llamada es directa.

La Magia del Polimorfismo: Funciones Virtuales, VPTR y VTABLE

Aquí es donde la cosa se pone interesante. El polimorfismo permite que una clase derivada reimplemente un método de su clase base.

Considera este ejemplo:

class Animal {
public:
    virtual void makeSound() { /* Sonido genérico de animal */ }
};

class Cat : public Animal {
public:
    void makeSound() override { /* "Meow!" */ }
};

class Dog : public Animal {
public:
    void makeSound() override { /* "Woof!" */ }
};
Enter fullscreen mode Exit fullscreen mode

Si tenemos un puntero de tipo Animal*, puede apuntar a un objeto Animal, Cat o Dog.
Animal* pAnimal = new Cat();

Cuando llamamos a pAnimal>makeSound(), ¿cómo sabe el programa que debe ejecutar la versión de Cat y no la de Animal? La respuesta está en las funciones virtuales y su implementación a través del vptr y la vftable.

¿Qué son el VPTR y la VTABLE?

  • VTABLE (Virtual Function Table): Por cada clase que tiene al menos una función virtual (o hereda de una que la tiene), el compilador crea una tabla estática de punteros a funciones. Esta es la "tabla de funciones virtuales" o vftable. Cada entrada en esta tabla es la dirección de una de las funciones virtuales de esa clase.

  • VPTR (Virtual Pointer): Cuando se crea una instancia de un objeto de una clase con funciones virtuales, el compilador añade un miembro "oculto" al objeto. Este miembro es un puntero, llamado vptr, que apunta a la vftable correspondiente a la clase del objeto. En x64, este puntero tiene 8 bytes y siempre se encuentra al principio del layout del objeto.

El layout de nuestro objeto Cat ahora se ve así:

++  < Puntero al objeto (this)
|   vptr (8 bytes)           | > apunta a la vftable de Cat
++
|   ... otros miembros ...   |
++
Enter fullscreen mode Exit fullscreen mode

La vftable de Cat se vería así en memoria:

Dirección de la VTABLE de Cat:
++
| &Cat::makeSound                 | > Puntero a la implementación de Cat
++
| ... otros punteros virtuales ...|
++
Enter fullscreen mode Exit fullscreen mode

¿Cómo funciona una llamada virtual en ensamblador (x64)?

Cuando el código hace pAnimal>makeSound(), el ensamblador generado hace lo siguiente:

Obtiene la dirección del objeto (pAnimal). En x64, el puntero this se pasa por el registro RCX.

Lee el vptr que está al inicio del objeto: MOV RAX, [RCX]. Ahora RAX contiene la dirección de la vftable.

Llama a la función correcta usando un offset en la vftable. Si makeSound es la primera función virtual, el offset es 0: CALL [RAX + 0]. Si es la segunda, sería CALL [RAX + 8], y así sucesivamente.

Esta indirección a través de la tabla es lo que permite que el mismo código de llamada (pAnimal>makeSound()) ejecute diferentes funciones dependiendo del tipo real del objeto.

Analizando VTables en IDA Pro

Carga tu binario en IDA.

Encuentra el constructor de la clase. A menudo, los constructores son fáciles de identificar porque es donde se inicializa el vptr. Busca una instrucción como esta (asumiendo que RCX es el puntero this al nuevo objeto):

```
LEA RAX, const Cat::`vftable' ; Carga la dirección de la vftable en RAX
MOV [RCX], RAX              ; Escribe la dirección en el primer campo del objeto (el vptr)
```
Enter fullscreen mode Exit fullscreen mode

Navega a la VTable. Haz doble clic en el nombre de la vftable (ej. Cat::vftable'). IDA te llevará a la sección de datos (.rdatao.data`) donde está definida.

Inspecciona la VTable. Verás una lista de punteros (DQ Define Quadword, para 8 bytes). Estos son los punteros a las funciones virtuales. IDA es lo suficientemente inteligente como para resolverlos y mostrarte los nombres de las funciones.

![IDA VTABLE](https://i.imgur.com/O8ZzYhL.png)
*(Ejemplo visual de cómo se ve una vtable en IDA)*
Enter fullscreen mode Exit fullscreen mode

Analiza una llamada virtual. Busca en el código lugares donde se use el vptr. Verás el patrón [REG_THIS], [REG_VTABLE], CALL.

```
MOV RAX, [RCX]   ; RCX tiene el puntero al objeto. RAX ahora tiene el vptr.
CALL [RAX+8]     ; Llama a la segunda función virtual de la tabla.
```
Si ves esto, ¡felicidades! Has encontrado una llamada a una función virtual.
Enter fullscreen mode Exit fullscreen mode

Identificación de Tipos: RTTI y el "Puntero Layout ID" en MSVC++

He mencionado PUNTERO LAYOUT ID y latitud T de MSVC++. Estos no son términos estándar, pero apuntan directamente a una característica crucial: RunTime Type Information (RTTI). RTTI es el mecanismo que C++ utiliza para permitir la identificación de tipos de objetos durante la ejecución (por ejemplo, para dynamic_cast y typeid).

En MSVC++, la información de RTTI está ingeniosamente vinculada a la vftable.

El CompleteObjectLocator

Justo antes de la vftable en la memoria, MSVC++ coloca un puntero a una estructura llamada _RTTICompleteObjectLocator. Podemos pensar en esta estructura como la "cédula de identidad" del objeto.

La vftable que vimos antes en realidad se ve así en memoria:


++
| &RTTICompleteObjectLocator | < En vftable[1]
++
| &Class::virtual_func_1 | < Comienzo real de la vftable (vftable[0])
++
| &Class::virtual_func_2 |
++

Este CompleteObjectLocator es la clave para todo. Su estructura es algo así:

cpp
struct _RTTICompleteObjectLocator {
DWORD signature; // Siempre '1' en x64, '0' en x86.
DWORD offset; // Offset del vptr dentro del objeto.
DWORD cdOffset; // Offset del constructor.
_RTTITypeDescriptor* pTypeDescriptor; // ¡La joya! Puntero a la descripción del tipo.
_RTTIClassHierarchyDescriptor* pClassHierarchy; // Describe la herencia.
};

El campo más importante para nosotros es pTypeDescriptor. Este puntero nos lleva a otra estructura que contiene el nombre de la clase.

Analizando RTTI en IDA Pro

Este es el proceso para identificar manualmente el tipo de un objeto del que solo tienes un puntero:

Encuentra el vptr del objeto. Ya sea en un registro (RCX, RDX...) o en la pila.

Ve a la vftable. Sigue el puntero. MOV RAX, [RCX].

Encuentra el CompleteObjectLocator. El puntero a esta estructura está en [RAX 8] (un QWORD antes del inicio de la vftable).

MOV RAX, [RCX] ; RAX = &vftable
MOV RDX, [RAX8] ; RDX = &CompleteObjectLocator

Decodifica el nombre. IDA hace esto automáticamente la mayor parte del tiempo. Si sigues el puntero en pTypeDescriptor (el cuarto campo del CompleteObjectLocator), llegarás a una estructura TypeDescriptor. A partir de un cierto offset dentro de ella, encontrarás el nombre "mangled" (decorado) de la clase, como .?AVCat@@. IDA a menudo lo decodifica y lo muestra como un comentario: 'class Cat'.

![IDA RTTI](https://i.imgur.com/eJd4fJp.png)
*(Ejemplo visual de cómo IDA muestra la información de RTTI)*
Enter fullscreen mode Exit fullscreen mode

Beneficio práctico en Reversing:
Imagina que estás analizando un malware orientado a objetos. Ves una llamada virtual: CALL [RAX+10h]. No sabes qué función se está ejecutando. Pero si puedes identificar el objeto, puedes usar RTTI para saber que es, por ejemplo, de la clase HttpConnection. Ahora sabes que esa llamada virtual probablemente es algo como send_data o receive_data, lo que te da un contexto invaluable.

Entender cómo el compilador de C++ implementa estas características es una skill muy importante para el analista de software.

  • VPTR y VTABLE son el corazón del polimorfismo. Recuerda: el vptr está al inicio del objeto y apunta a la vftable. Las llamadas virtuales usan este puntero para encontrar la función correcta.

  • RTTI es la libreta de identidad de los objetos. En MSVC++, está vinculada a la vftable a través del CompleteObjectLocator, que se encuentra justo antes de la tabla.

La próxima vez que veas un CALL [RAX+offset] en IDA, no te asustes. Sigue los punteros, busca la vftable, inspecciona el RTTI.

Happy reversing.

Top comments (0)