Estructuras y Constructores en C++: Guía de Reversing¶
Introducción¶
Este documento cubre la teoría y práctica de reversing de estructuras (structs) y constructores en C++. A diferencia de los tipos primitivos (int, char), las estructuras agrupan múltiples campos de diferentes tipos, lo que añade complejidad al análisis en ensamblador.
Objetivo: Entender cómo aparecen las estructuras en IDA Pro, cómo leer sus campos en el stack, y cómo reconocer patrones de inicialización (constructores).
Parte 1: Teoría de Estructuras en C++¶
¿Qué es una Estructura?¶
Una estructura (struct) es un tipo de dato que agrupa múltiples campos (fields) de diferentes tipos:
struct Persona {
char nombre[65]; // Array de caracteres
char direccion[65]; // Array de caracteres
int anio_nacimiento; // Entero (4 bytes)
};
Características principales:
| Característica | Descripción |
|---|---|
| Campos heterogéneos | A diferencia de arrays, los campos pueden tener diferentes tipos |
| Tamaño en memoria | Suma de todos los campos + padding por alineamiento |
| Acceso a campos | Mediante el operador punto . (si es un objeto) o flecha -> (si es puntero) |
| Inicialización | Puede tener constructor para inicializar automáticamente |
Diferencia: Arrays vs Estructuras¶
Arrays:
int numeros[10]; // 10 enteros, todos del mismo tipo
char texto[100]; // 100 caracteres, todos del mismo tipo
numeros[0], numeros[1]
- Tamaño homogéneo
Estructuras:
struct Persona {
char nombre[65]; // Campo 1: array
char direccion[65]; // Campo 2: array
int anio_nacimiento; // Campo 3: entero
};
persona.nombre, persona.direccion
- Tamaño heterogéneo (suma de todos los campos)
Declaración de Estructuras¶
Forma 1: Declaración global con variables
struct Persona {
char nombre[65];
char direccion[65];
int anio_nacimiento;
} fulanito, menganito; // Variables declaradas directamente
Forma 2: Declaración de tipo, variables separadas
struct Persona {
char nombre[65];
char direccion[65];
int anio_nacimiento;
};
// Después, en otra parte del código:
Persona variable1;
Persona variable2;
Forma 3: Typedef para simplificar
typedef struct {
char nombre[65];
char direccion[65];
int anio_nacimiento;
} Persona;
Persona p1; // Acceso directo sin escribir "struct"
Acceso a Campos¶
Mediante objeto directo:
Persona p;
p.nombre = "Juan"; // Asignar a campo
p.anio_nacimiento = 1990;
int edad = p.anio_nacimiento;
Mediante puntero:
En ensamblador (sobre RSP en x64):
mov byte ptr [rsp+10], 'A' ; Primer campo (array de chars)
mov dword ptr [rsp+40], 1990 ; Tercer campo (int, offset 40)
Parte 2: Estructuras en Memoria¶
Cálculo del Tamaño (sizeof)¶
El tamaño de una estructura es la suma de todos sus campos más padding para alineamiento.
Ejemplo simple:
Cálculo manual:
int x: 4 bytes (offset 0-3)char c: 1 byte (offset 4)- Padding: 3 bytes (offset 5-7) para alinear
int ya múltiplo de 4 int y: 4 bytes (offset 8-11)
Total: 12 bytes
En IDA Pro:
Local Types > A > Properties
Size: 12 (0xC)
Fields:
x @ offset 0x0 (4 bytes)
c @ offset 0x4 (1 byte)
[padding] @ offset 0x5 (3 bytes)
y @ offset 0x8 (4 bytes)
Ejemplo del Curso: Estructura Persona¶
struct Persona {
char nombre[65]; // Offset 0x00, tamaño 65
char direccion[65]; // Offset 0x41 (65), tamaño 65
int anio_nacimiento; // Offset 0x82 (130), tamaño 4
};
// sizeof(Persona) = 139 bytes (0x8B)
// + 5 bytes de padding para alineamiento = 144 bytes (0x90)
// Pero en el ejemplo del curso: 88 bytes (variable optimizada)
Nota: En el ejemplo específico del video, la estructura ocupa 88 bytes (probablemente con diferentes tamaños de arrays o alineamiento específico del compilador).
Stack Layout en la Función¶
Pseudocódigo:
En el stack (windows x64):
RSP+0x00: [Shadow Space (32 bytes)] RSP+0x20
RSP+0x20: [Variable suma (4 bytes)]
RSP+0x24: [Padding (4 bytes)]
RSP+0x28: [menganito (88 bytes)]
├─ nombre[65] (offset 0x00-0x40)
├─ direccion[65](offset 0x41-0x81)
└─ anio_nacimiento (offset 0x82-0x85)
RSP+0x80: [Security Cookie (8 bytes)]
RSP+0x88: [Previous RBP (8 bytes)]
RSP+0x90: [Return Address (8 bytes)]
En dirección absoluta (ejemplo con RSP=0x1000):
Parte 3: Estructuras Anidadas¶
Definición¶
Una estructura puede contener otra estructura dentro:
struct Nombre {
char nombre[30];
char apellido[30];
};
struct Persona {
Nombre nombre_completo; // Estructura anidada
int anio_nacimiento;
char direccion[65];
};
Acceso a Campos Anidados¶
En C++:
Persona p;
p.nombre_completo.nombre = "Juan"; // Campo dentro de estructura
p.nombre_completo.apellido = "Pérez";
p.anio_nacimiento = 1990;
En ensamblador:
; Acceso a p.nombre_completo.nombre
lea rcx, [rsp+20] ; RSP+20 = dirección de nombre_completo
mov byte ptr [rcx], 'J' ; Primer byte del campo nombre
En Local Types de IDA¶
Después de importar estructuras:
Persona
├─ nombre_completo (Nombre)
│ ├─ nombre[30]
│ └─ apellido[30]
├─ anio_nacimiento (int)
└─ direccion[65]
Parte 4: Constructores¶
¿Qué es un Constructor?¶
Un constructor es una función especial que se ejecuta automáticamente cuando se crea un objeto. Su propósito principal es inicializar los campos.
Sintaxis:
struct Punto {
int x;
int y;
// Constructor (mismo nombre que la estructura)
Punto() {
x = 0;
y = 0;
}
};
// Uso:
Punto p; // Automáticamente llama al constructor, p.x=0, p.y=0
Constructor Inline vs Separado¶
Inline (dentro de la definición):
Separado (fuera de la definición):
struct Punto {
int x;
int y;
Punto(); // Solo declaración
};
// Definición fuera:
Punto::Punto() {
x = 0;
y = 0;
}
En ensamblador, ambos generan el mismo código ejecutable.
En Ensamblador (x64)¶
Constructor simple:
struct Persona {
char nombre[65];
char direccion[65];
int anio_nacimiento;
Persona() {
// Constructor vacío (no hace nada)
}
};
En ensamblador:
; Llamada al constructor en main()
lea rcx, [rsp+68] ; RCX = dirección de menganito
call Persona::Persona() ; Ejecutar constructor
; Dentro del constructor:
Persona::Persona proc near:
push rbp
mov rbp, rsp
mov qword ptr [rbp+8], rcx ; "this" pointer
pop rbp
ret
Nota: El constructor recibe el puntero this en RCX (primer parámetro implícito).
Constructor que Inicializa Campos¶
struct Persona {
char nombre[65];
char direccion[65];
int anio_nacimiento;
Persona() {
anio_nacimiento = 0; // Inicializar
}
};
En ensamblador:
lea rcx, [rsp+68] ; RCX = &menganito (this)
call Persona::Persona()
; Dentro del constructor:
Persona::Persona proc near:
push rbp
mov rbp, rsp
mov qword ptr [rbp+8], rcx
; Inicializar anio_nacimiento (offset 0x82 dentro de la estructura)
lea rax, [rcx+82h] ; RAX = dirección del campo
mov dword ptr [rax], 0 ; Asignar 0
pop rbp
ret
Constructor con Parámetros¶
struct Persona {
int anio_nacimiento;
Persona(int anio) {
anio_nacimiento = anio;
}
};
// Uso:
Persona p(1990); // Pasar parámetro
En ensamblador (x64 FastCall):
; Llamada:
mov edx, 1990 ; EDX = segundo parámetro (anio)
lea rcx, [rsp+68] ; RCX = this (primer parámetro implícito)
call Persona::Persona(int)
; Dentro del constructor:
Persona::Persona(int) proc near:
push rbp
mov rbp, rsp
mov qword ptr [rbp+8], rcx ; this
mov dword ptr [rbp+10], edx ; anio (parámetro)
; Asignar: this->anio_nacimiento = anio
lea rax, [rcx+82h]
mov eax, [rbp+10] ; Cargar anio
mov dword ptr [rax], eax ; Guardar en anio_nacimiento
pop rbp
ret
Parte 5: Ejemplo Práctico del Curso¶
Código Fuente¶
struct Persona {
char nombre[65];
char direccion[65];
int anio_nacimiento;
};
// Variables globales
Persona fulanito;
Persona menganito;
int main() {
int suma;
Persona menganito_local;
// Asignar valores
menganito.anio_nacimiento = 1991;
fulanito.anio_nacimiento = 1990;
// Ingresar datos
cout << "Ingresar dirección de fulanito: ";
cin.getline(fulanito.direccion, 65);
cout << "Ingresar dirección de menganito: ";
cin.getline(menganito_local.direccion, 65);
// Imprimir resultados
cout << "Dirección de fulanito: " << fulanito.direccion << endl;
cout << "Dirección de menganito: " << menganito_local.direccion << endl;
cout << "Año de nacimiento fulanito: " << fulanito.anio_nacimiento << endl;
// Sumar y imprimir
suma = menganito_local.anio_nacimiento + fulanito.anio_nacimiento;
cout << "Suma de años: " << suma << endl;
return 0;
}
Stack Frame en IDA Pro¶
Prólogo de main():
00401000: push rbp
00401001: mov rbp, rsp
00401004: sub rsp, 0xA0 ; Reservar 160 bytes
00401008: mov rax, fs:[28h] ; Security Cookie
0040100F: mov [rsp+0x98], rax ; Guardar cookie
Stack layout:
RSP+0x00: Shadow Space (32 bytes)
RSP+0x20: suma (DWORD, 4 bytes)
RSP+0x24: [Padding, 4 bytes]
RSP+0x28: menganito_local (Persona, 88 bytes)
├─ nombre[65]
├─ direccion[65]
└─ anio_nacimiento
RSP+0x80: [Padding]
RSP+0x98: Security Cookie (8 bytes)
RSP+0xA0: Prólogo (RBP guardado)
Inicialización de Campos¶
En ensamblador:
; Asignar fulanito.anio_nacimiento = 1990
mov dword ptr [rip+offset_fulanito], 1990
; (fulanito es global, en sección .data)
; Asignar menganito_local.anio_nacimiento = 1991
mov dword ptr [rsp+0xA8], 1991
; (offset 0x28 + 0x80 = 0xA8 para el campo anio_nacimiento)
Lectura de Estructuras (cin.getline)¶
Pseudocódigo:
cin.getline(fulanito.direccion, 65);
// Equivalente a leer 65 caracteres del stdin
// y guardarlos en fulanito.direccion
En ensamblador:
; Obtener dirección de fulanito.direccion
lea rax, [rip+offset_fulanito] ; RAX = &fulanito (global)
add rax, 0x41 ; RAX += 65 (offset del campo direccion)
; Parámetros para cin.getline(buffer, maxlen)
mov rdx, 65 ; RDX = maxlen
lea rcx, std::cin ; RCX = this (&cin)
; Llamada (signature: void getline(char*, size_t))
call std::istream::getline
Parte 6: Identificación de Estructuras en IDA Pro (Sin Símbolos)¶
Reconocimiento de Patrones¶
Patrón 1: Múltiples campos secuenciales en el stack
lea rcx, [rsp+20] ; Campo 1
call some_function
lea rcx, [rsp+30] ; Campo 2 (offset diferente)
call some_function
lea rcx, [rsp+88] ; Campo 3 (offset mayor)
Indicador: Diferentes offsets sobre RSP → Probablemente campos de una estructura.
Patrón 2: Tamaño de inicialización consistente
sub rsp, 0x60 ; Reserva 96 bytes
mov [rsp+0x20], rax ; Inicializa algo
mov [rsp+0x28], rbx
mov [rsp+0x80], rcx ; Offset grande
Indicador: Offsets grandes (>80) sugieren estructura con campos grandes.
Patrón 3: Constructor call
Verificar: Si sub_401234 solo inicializa algunos campos sin parámetros adicionales → Probablemente constructor.
Importación de Estructuras en IDA¶
Si tienes el archivo .pdb:
Si no tienes símbolos pero tienes el código fuente:
File > Produce file > Dump typeinfo to IDC file...
(En otro IDA con símbolos)
File > Script file... (en el IDA sin símbolos)
Cargar el .idc exportado
Manual (crear estructura desde cero):
View > Open subviews > Local Types (Shift+F1)
Botón derecho > New struct...
Agregar campos con offsets correctos
Aplicar Estructura al Stack¶
Una vez que tienes la estructura definida:
Resultado:
Pseudocódigo actualizado:
// Antes:
char var_28[96];
char var_28_41[65];
int var_28_82;
// Después:
Persona menganito;
menganito.nombre[65];
menganito.direccion[65];
menganito.anio_nacimiento;
Parte 7: Alineamiento (Alignment)¶
Concepto¶
El compilador alinea los campos de una estructura para acceso eficiente en memoria.
Regla típica (x64):
char(1 byte): alineado a 1 byte (sin restricción)short(2 bytes): alineado a 2 bytesint(4 bytes): alineado a 4 byteslong long(8 bytes): alineado a 8 bytespuntero(8 bytes): alineado a 8 bytes
Padding por Alineamiento¶
struct Ejemplo {
char a; // Offset 0, tamaño 1
// [3 bytes de padding]
int b; // Offset 4, tamaño 4
char c; // Offset 8, tamaño 1
// [7 bytes de padding]
};
// sizeof = 16 bytes (para alinear la estructura a 8 bytes)
En IDA Pro Local Types:
Ejemplo @ 0x0:
a @ 0x0 (char, 1 byte)
[padding] @ 0x1 (3 bytes)
b @ 0x4 (int, 4 bytes)
c @ 0x8 (char, 1 byte)
[padding] @ 0x9 (7 bytes)
Total size: 0x10 (16 bytes)
Impacto en Reversing¶
Cuando veas offsets raros en ensamblador:
mov [rcx+0], eax ; Offset 0
mov [rcx+4], ebx ; Offset 4 (después de padding)
mov [rcx+8], ecx ; Offset 8
mov [rcx+10], edx ; Offset 16 (después de más padding)
→ Probablemente hay padding por alineamiento de la estructura.
Solución: Verificar el tamaño de cada campo declarado y contar bytes reales vs offsets.
Parte 8: Campos de Bits (Bit Fields)¶
Concepto¶
Los bit fields permiten empaquetar múltiples booleanos o valores pequeños en un entero:
struct Flags {
unsigned int bit0 : 1; // 1 bit
unsigned int bit1 : 1; // 1 bit
unsigned int bit2 : 3; // 3 bits (valores 0-7)
unsigned int bit3 : 5; // 5 bits (valores 0-31)
};
// Total: 10 bits = 2 bytes
En Ensamblador¶
; Asignar bit0 = 1
mov al, [rcx] ; Cargar byte
or al, 1 ; Setear bit 0
mov [rcx], al
; Asignar bit2 = 5 (3 bits)
mov al, [rcx]
and al, 0xE3 ; Limpiar bits 2-4
or al, (5 << 2) ; Setear a 5
mov [rcx], al
Nota: Los bit fields son raros en código moderno, pero puedes encontrarlos en:
- Estructuras de flags de Windows
- Código de bajo nivel (drivers)
- Binarios muy optimizados
Parte 9: Patrones Comunes en Reversing¶
Patrón 1: Variable Global vs Local¶
Global:
Local (en stack):
Indicador: [rip+offset] → Global | [rsp+offset] → Local
Patrón 2: Constructor Llamado¶
Verificar: Dentro de sub_401234, busca:
- Solo inicialización de campos (sin lectura de parámetros)
- Parámetro implícito
RCX(this) - Sin parámetros explícitos (o con pocos)
Si todo esto se cumple → Probablemente es un constructor.
Patrón 3: Campo Accedido Múltiples Veces¶
mov rax, [rsp+28] ; Cargar dirección
mov [rax+10], 1990 ; Asignar anio_nacimiento
mov eax, [rax+10] ; Leer anio_nacimiento
add eax, 1 ; Incrementar
mov [rax+10], eax ; Guardar de nuevo
Indicador: Mismo offset accedido múltiples veces → Probablemente el mismo campo.
Patrón 4: Getline y Estructuras¶
lea rdx, [rsp+41] ; RDX = buffer (offset 41 = 65+1 bytes)
mov r8d, 65 ; R8 = maxlen
lea rcx, std::cin ; RCX = this (&cin)
call std::istream::getline
lea rdx, [rsp+82] ; RDX = otro buffer (offset 82)
mov r8d, 65
lea rcx, std::cin
call std::istream::getline
Indicador: Dos campos de tamaño similar (65 bytes) accedidos secuencialmente → Probablemente dos arrays en una estructura.
Parte 10: Security Cookie¶
¿Qué es?¶
Un valor aleatorio guardado en el stack al inicio de una función para detectar buffer overflows.
void funcion() {
int x;
Persona p;
// ... código ...
// Si algo sobrescribe el stack, lo detectará
}
En ensamblador:
mov rax, fs:[28h] ; Leer security cookie de TLS
mov [rsp+0x98], rax ; Guardar en el stack
; ... resto de la función ...
mov rcx, [rsp+0x98] ; Leer cookie
xor rcx, fs:[28h] ; Comparar con valor original
call __security_check_cookie ; Si no coincide, crash
En IDA Pro¶
Reconocerlo:
Si lo encuentras:
- Hay overflow protection activada
- El offset donde se guarda la cookie → Conoces el tamaño exacto del stack
No es parte de la lógica del programa, solo seguridad.
Resumen: Checklist para Reversing de Estructuras¶
Cuando Veas una Función Desconocida:¶
- ¿Hay reserva de stack? (
sub rsp, 0x60) - ¿Hay múltiples offsets sobre RSP? (
[rsp+20],[rsp+28],[rsp+88]) - ¿Hay un constructor llamado? (
call sub_xxxxxinmediatamente después delea rcx) - ¿Hay inicialización de campos? (movs en offsets específicos)
- ¿Hay funciones de I/O? (
cin.getline,cin >>,cout <<) - ¿Hay security cookie? (
mov rax, fs:[28h])
Si Respondiste SÍ a 3+ Preguntas:¶
→ Probablemente hay estructuras involucradas
Pasos para Análisis:¶
- Estimar el tamaño de la estructura (último offset accedido + tamaño del campo)
- Identificar tipos de campos (array, int, char, etc.)
- Importar o crear estructuras en IDA Pro Local Types
- Aplicar estructura al stack
- Re-analizar pseudocódigo con nombres claros
- Buscar constructores y funciones miembro
Ejemplo Completo: Reversing del Ejemplo del Curso¶
Binario Compilado con Símbolos¶
Ensamblador crudo:
00401000: push rbp
00401001: mov rbp, rsp
00401004: sub rsp, 0xA0
00401008: mov rax, fs:[28h]
0040100F: mov [rsp+0x98], rax
; Asignar menganito_local.anio_nacimiento = 1991
00401018: mov dword ptr [rsp+0xA8], 1991
; Asignar fulanito.anio_nacimiento = 1990
00401020: lea rax, [rip+0x2FD0] ; &fulanito (global)
00401027: mov dword ptr [rax], 1990
; Imprimir y leer datos
00401030: lea rcx, [rip+0x2FD5] ; &std::cout
00401037: lea rdx, aIngresar ; "Ingresar dirección..."
0040103E: call std::operator<<
; getline para fulanito.direccion
00401050: lea rdx, [rip+0x2FD0] ; &fulanito
00401057: add rdx, 0x41 ; Offset de direccion
0040105B: mov r8d, 65
00401062: lea rcx, std::cin
00401069: call std::istream::getline
; getline para menganito_local.direccion
0040107F: lea rdx, [rsp+0x28] ; &menganito_local
00401083: add rdx, 0x41 ; Offset de direccion
00401087: mov r8d, 65
0040108E: lea rcx, std::cin
00401095: call std::istream::getline
; Suma de años
000401AA: mov eax, [rsp+0xA8] ; menganito_local.anio_nacimiento
000401B1: mov ecx, [rip+0x2FB0] ; fulanito.anio_nacimiento
000401B8: add eax, ecx
000401BA: mov [rsp+0x20], eax ; suma
; Epilogo y verificación de cookie
000401BC: mov rcx, [rsp+0x98]
000401C3: xor rcx, fs:[28h]
000401CB: call __security_check_cookie
000401D2: add rsp, 0xA0
000401D9: pop rbp
000401DA: ret
Análisis Paso a Paso¶
1. Identificar el tamaño de la estructura:
- Último offset accedido:
[rsp+0xA8](168) - Campo accedido: 4 bytes (DWORD)
- Offset = 0xA8 - 0x28 (base de menganito) = 0x80 (128 bytes)
- Tamaño estimado: 128 + 4 = 132 bytes
2. Identificar campos:
[rsp+0x28]: Inicio de estructura[rsp+0x28] + 0x41 = [rsp+0x69]: Offset 0x41 (65) → Segundo array de 65 chars[rsp+0xA8]: Offset 0x80 (128) → Última parte del campo anio_nacimiento
Estructura deducida:
struct Persona {
char nombre[65]; // Offset 0x00
char direccion[65]; // Offset 0x41
int anio_nacimiento; // Offset 0x82
// 1 byte padding
// 6 bytes padding para alineamiento
};
// sizeof = 136 bytes, pero el compilador lo alineó a 0x90 (144 bytes)
3. Aplicar en IDA Pro:
View > Local Types > New Struct
Nombre: Persona
Campos:
nombre @ 0x00 (char[65])
direccion @ 0x41 (char[65])
anio_nacimiento @ 0x82 (int)
Aplicar al offset [rsp+0x28]:
Posicionarse > Presionar Y > Escribir "Persona"
4. Pseudocódigo resultante:
void __cdecl main() {
Persona menganito_local;
int suma;
menganito_local.anio_nacimiento = 1991;
fulanito.anio_nacimiento = 1990;
cout << "Ingresar dirección de fulanito: ";
cin.getline(fulanito.direccion, 65);
cout << "Ingresar dirección de menganito: ";
cin.getline(menganito_local.direccion, 65);
// ... resto del código ...
suma = menganito_local.anio_nacimiento + fulanito.anio_nacimiento;
// ... imprimir suma ...
}
Lecciones Finales¶
Sin Símbolos¶
Cuando no tienes símbolos (como en binarios ofuscados o compilados sin debug info):
- El análisis se complica significativamente
- Necesitas automatización (scripts IDAPython)
- Necesitas usar Bindiff para comparar con versión compilada con símbolos
- Importar estructuras manualmente es tedioso
Con Símbolos (Recomendado para Aprendizaje)¶
- Los nombres de funciones y campos son claros
- IDA Pro muestra la estructura automáticamente
- Puedes enfocarte en la lógica en vez de la mecánica
Estrategia de Reversing Progresivo¶
- Primero: Aprender con código compilado con símbolos (como el del curso)
- Luego: Practicar sin símbolos pero con código fuente disponible
- Finalmente: Binarios reales sin información adicional
Recursos y Referencia Rápida¶
Comando de IDA Rápido¶
| Comando | Efecto |
|---|---|
Shift+F1 |
Abrir Local Types |
Y |
Cambiar tipo de variable |
; |
Agregar comentario |
N |
Renombrar |
G |
Saltar a dirección |
X |
Ver referencias cruzadas |
Patrones a Buscar¶
lea rcx, [rsp+XX]
call sub_XXXXX // Posible constructor
[rsp+OFFSET1] // Campo 1
[rsp+OFFSET2] // Campo 2
[rsp+OFFSET3] // Campo 3
mov rax, fs:[28h] // Security cookie