Punteros en C++: Guía Completa de Reversing¶
Índice¶
- Introducción
- Parte 1: Concepto Fundamental de Punteros
- Parte 2: Declaración y Tipado de Punteros
- Parte 3: Punteros en Ensamblador (x64 Windows)
- Parte 4: Ejemplo del Curso - Análisis Detallado
- Parte 5: Punteros a Estructuras
- Parte 6: Punteros y Funciones
- Parte 7: Aritmética de Punteros
- Parte 7.5: Punteros Genéricos (void*)
- Parte 8: Puntero a Puntero (double pointer)
- Parte 9: Punteros y Arrays
- Parte 9.5: Array Bidimensional vs Array de Punteros
- Parte 10: Reconocimiento de Punteros en IDA Pro (Sin Símbolos)
- Parte 11: Importancia de Punteros en Reversing
- Parte 12: Stack Frame con Punteros
- Parte 13: Errores Comunes con Punteros
- Parte 14: Comparación Directa con Ensamblador del Video
- Resumen: Lo Más Importante
- Parte 15: Punteros en el Heap
- Apéndice A: Smart Pointers (Punteros Inteligentes)
Introducción¶
Los punteros son uno de los conceptos más importantes (y temidos) en C++. Sin embargo, son fundamentales para entender:
- Cómo se pasan estructuras grandes a funciones sin copiarlas completamente
- Cómo funcionan los arrays dinámicos
- Cómo se manipulan objetos complejos en ensamblador
- Cómo se realizan operaciones en memoria
Objetivo: Desmitificar los punteros y aprender a reconocerlos en ensamblador.
Parte 1: Concepto Fundamental de Punteros¶
¿Qué es un Puntero?¶
Un puntero es una variable que almacena la dirección de memoria de otra variable.
Conceptualmente:
Variable normal:
Nombre: a
Valor: 5
Dirección: 0x1000
Puntero a a:
Nombre: pa
Valor: 0x1000 (dirección de a)
Dirección: 0x2000
En el código:
Diferencia: Variable vs Puntero¶
| Característica | Variable Normal | Puntero |
|---|---|---|
| Almacena | Valor | Dirección de memoria |
| Tamaño | Depende del tipo (4, 8 bytes, etc.) | Siempre 8 bytes en x64 |
| Declaración | int a; |
int* pa; |
| Inicialización | a = 5; |
pa = &a; (operador &) |
| Acceso al valor | a |
*pa (operador *) |
Operadores Clave¶
Operador & (Dirección de - Address-of)¶
En ensamblador:
Nota: lea (Load Effective Address) obtiene la dirección sin desreferenciar.
Operador * (Desreferenciación - Dereference)¶
En ensamblador:
mov rax, [rsp+28] ; RAX = dirección de a (valor de pa)
mov ebx, [rax] ; EBX = valor en esa dirección (5)
Nota: mov ebx, [rax] desreferencia el puntero.
Operador -> (Flecha - Arrow, para punteros a estructuras)¶
struct Persona {
int edad;
};
Persona p;
Persona* pp = &p;
p.edad = 30; // Acceso directo
pp->edad = 30; // Acceso a través de puntero (equivalente)
Parte 2: Declaración y Tipado de Punteros¶
Sintaxis de Declaración¶
int* p_int; // Puntero a int
char* p_char; // Puntero a char
float* p_float; // Puntero a float
Persona* p_persona; // Puntero a estructura Persona
Nota: El * está asociado al tipo, no a la variable:
// Estos son equivalentes:
int* p1;
int *p1;
// Pero estos NO son equivalentes:
int* p1, p2; // p1 es puntero, p2 es int (confuso)
int *p1, *p2; // Ambos son punteros (más claro)
Tipado de Punteros¶
Un puntero solo puede apuntar a su tipo específico:
int a = 5;
char c = 'A';
int* p_int = &a; // Correcto
int* p_int = &c; // Error de compilación
int* p_int = (int*)&c; // Correcto (con cast)
En reversing: Si ves un puntero que apunta a un tipo incorrecto, probablemente hay:
- Un cast
- Un error del compilador
- Código intencional para eludir protecciones
Puntero Nulo¶
int* p = nullptr; // Puntero sin dirección asignada
int* p = NULL; // Equivalente (C++03)
int* p = 0; // También equivalente
En ensamblador:
Acceder a un puntero nulo causa crash:
Parte 3: Punteros en Ensamblador (x64 Windows)¶
Asignación: Obtener Dirección¶
En ensamblador:
lea rax, [rsp+20] ; LEA = Load Effective Address
; Obtiene la dirección de a sin desreferenciar
mov [rsp+28], rax ; pa = dirección de a (8 bytes en x64)
Notas clave:
leaobtiene la dirección (sin acceder a memoria)mov [dirección], valoralmacena el valor en esa dirección
Desreferenciación: Acceder al Valor¶
int a = 5;
int* pa = &a;
int b = *pa; // Leer valor de a a través de pa
*pa = 10; // Escribir 10 en a a través de pa
ACLARACIÓN CRÍTICA: *pa vs pa¶
Esta es la confusión más común. Veamos qué contiene cada cosa:
Memoria (ejemplo):
Dirección Contenido Variable
0x1000 5 ← a (el valor)
0x2000 0x1000 ← pa (la DIRECCIÓN de a)
0x3000 5 ← b (el valor que asignamos)
En el código:
pacontiene0x1000(la dirección de a)*paaccede a lo que hay EN esa dirección, que es5(el valor de a)b = *paasigna el valor5a b, NO la dirección
Comparación:
int b = pa; // INCORRECTO: b sería 0x1000 (tipo incorrecto)
int b = *pa; // CORRECTO: b es 5 (el valor)
En ensamblador, verás la diferencia:
; Si fuera int b = pa (INCORRECTO):
mov rax, [rsp+28] ; RAX = 0x1000 (la dirección, valor de pa)
mov [rsp+24], rax ; b = 0x1000 (la dirección)
; Problema: estamos asignando dirección a un int
; Correcto: int b = *pa
mov rax, [rsp+28] ; RAX = 0x1000 (la dirección)
mov edx, [rax] ; EDX = [0x1000] = 5 (DESREFERENCIA)
mov [rsp+24], edx ; b = 5 (el valor)
La clave del segundo mov:
En mov edx, [rax], los corchetes [] significan "accede a la dirección en RAX". Esto desreferencia el puntero.
Sin los corchetes sería diferente:
Resumen con símbolos:
| Operación | Significado | Valor |
|---|---|---|
pa |
El puntero mismo (contiene dirección) | 0x1000 |
*pa |
Lo que apunta el puntero | 5 |
&a |
La dirección de a | 0x1000 |
Por eso *pa es equivalente a &a → ambos acceden al mismo lugar
Lectura:
Escritura:
Operaciones Comunes¶
Lectura Simple¶
mov rax, [rsp+28] ; RAX = pa (dirección de a)
mov ebx, dword ptr [rax]; EBX = *pa (valor 5)
mov [rsp+32], ebx ; b = 5
Asignación a través de Puntero¶
Cadena de Lectura¶
mov rax, [rsp+28] ; RAX = pa
mov ebx, dword ptr [rax] ; EBX = *pa = 5
mov ecx, ebx ; ECX = 5
add ecx, 1 ; ECX = 6
mov [rsp+36], ecx ; c = 6
Parte 4: Ejemplo del Curso - Análisis Detallado¶
Código Fuente¶
#include <iostream>
using namespace std;
int main() {
int a; // Variable a (tipo int)
int* pa = &a; // Puntero pa que apunta a a
*pa = 5; // Asignar 5 a a a través del puntero
int b = *pa; // Leer valor de a a través del puntero
*pa = 10; // Asignar 10 a a a través del puntero
cout << "a = " << a << ", b = " << b << endl;
return 0;
}
Valores finales:
a = 10(fue modificado a través de pa)b = 5(se asignó antes de que a cambiara a 10)
Ensamblador Anotado (x64 Windows, con símbolos)¶
Prólogo¶
00401000: push rbp
00401001: mov rbp, rsp
00401004: sub rsp, 40h ; Reservar 64 bytes en el stack
00401008: mov rax, fs:[28h] ; Security Cookie
0040100F: mov [rsp+38h], rax ; Guardar cookie
Stack layout:
RSP+0x00: Shadow Space (32 bytes)
RSP+0x20: a (int, 4 bytes)
RSP+0x24: [padding, 4 bytes]
RSP+0x28: pa (puntero, 8 bytes)
RSP+0x30: [padding, 8 bytes]
RSP+0x38: Security Cookie (8 bytes)
RSP+0x40: RBP y return address
Asignación: pa = &a¶
00401018: lea rax, [rsp+20h] ; RAX = dirección de a
; [rsp+20h] es donde está a
; lea obtiene esta dirección
00401020: mov [rsp+28h], rax ; [rsp+28h] = pa
; pa ahora contiene dirección de a
Resultado:
Lectura Simple de pa¶
Registro RAX después:
Asignación a través del Puntero: *pa = 5¶
Resultado en memoria:
Lectura a través del Puntero: *pa¶
00401030: mov rax, [rsp+28h] ; RAX = pa (dirección de a)
00401034: mov edx, [rax] ; EDX = *pa = 5 (valor de a)
Asignación a b = *pa¶
Stack ahora:
Segunda Asignación: *pa = 10¶
0040103A: mov rax, [rsp+28h] ; RAX = pa (dirección de a)
00401040: mov dword ptr [rax], 10 ; Escribir 10 en [RAX]
; a ahora vale 10
Resultado:
Impresión y Epilogo¶
00401044: lea rcx, std::cout ; RCX = &cout
00401050: lea rdx, formato ; RDX = "a = %d, b = %d"
00401057: call std::operator<< ; Imprimir
00401060: mov ecx, 0 ; return 0
00401065: mov rax, [rsp+38h] ; Leer security cookie
0040106C: xor rax, fs:[28h] ; Verificar
00401074: call __security_check_cookie
00401080: add rsp, 40h ; Liberar stack
00401087: pop rbp
00401088: ret
Visualización en Depuración¶
Paso 1: Después de lea rax, [rsp+20h]
Paso 2: Después de mov [rsp+28h], rax
Paso 3: Después de mov dword ptr [rax], 5
Paso 4: Después de mov edx, [rax]
Paso 5: Después de mov dword ptr [rax], 10
Parte 5: Punteros a Estructuras¶
Declaración¶
struct Persona {
char nombre[65];
char direccion[65];
int anio_nacimiento;
};
Persona p;
Persona* pp = &p; // Puntero a estructura
Acceso a Campos¶
Método 1: Acceso Directo¶
Método 2: Acceso a través de Puntero¶
Persona p;
Persona* pp = &p;
pp->nombre[0] = 'J'; // Equivalente a p.nombre[0]
pp->anio_nacimiento = 1990; // Equivalente a p.anio_nacimiento
En Ensamblador¶
; pp = &p
lea rax, [rsp+28] ; RAX = dirección de p
mov [rsp+120], rax ; pp = RAX
; pp->anio_nacimiento = 1990
mov rax, [rsp+120] ; RAX = pp (dirección de p)
add rax, 130 ; RAX += offset de anio_nacimiento
mov dword ptr [rax], 1990 ; Escribir 1990
; int edad = pp->anio_nacimiento
mov rax, [rsp+120] ; RAX = pp
add rax, 130 ; RAX += offset
mov edx, [rax] ; EDX = *[RAX]
mov [rsp+64], edx ; edad = EDX
Parte 6: Punteros y Funciones¶
Pasar Puntero a Función¶
void modificar(int* ptr) {
*ptr = 20; // Modifica el valor al que apunta
}
int main() {
int a = 10;
modificar(&a); // Pasar puntero a a
cout << a << endl; // Imprime 20
}
Ventaja: No se copia el valor, solo se pasa la dirección (8 bytes).
En Ensamblador¶
Llamada a la función¶
lea rax, [rsp+20] ; RAX = &a
mov rcx, rax ; RCX = puntero (primer parámetro)
call modificar ; Llamar
x64 FastCall: El primer parámetro va en RCX.
Dentro de modificar()¶
modificar proc near:
mov rcx, [rbp+16] ; RCX = puntero pasado
mov dword ptr [rcx], 20 ; *ptr = 20
ret
Caso de Uso: Estructuras Grandes¶
struct DatosGrandes {
char datos[8000]; // 8 KB
};
void procesar(DatosGrandes* pdatos) { // Pasar puntero (8 bytes)
pdatos->datos[0] = 'A';
}
int main() {
DatosGrandes d;
procesar(&d); // Pasar puntero, no copiar 8 KB
}
Sin punteros: Sería necesario copiar 8000 bytes en el stack.
Con punteros: Solo se copia una dirección (8 bytes).
Parte 7: Aritmética de Punteros¶
El Concepto Fundamental¶
Cuando incrementamos un puntero, no sumamos 1 byte, sino que sumamos el tamaño del tipo de dato al que apunta:
int vector[10];
int* puntero = vector; // Apunta al primer elemento
puntero++; // A bajo nivel: suma 4 (sizeof(int))
puntero = puntero + 7; // A bajo nivel: suma 28 (7 * sizeof(int))
Regla de oro: puntero + N equivale a puntero + (N * sizeof(tipo))
Suma y Resta de Punteros¶
int arr[10];
int* p = arr; // p apunta al primer elemento
p++; // p ahora apunta al segundo elemento
p += 5; // p ahora apunta al sexto elemento
p--; // p ahora apunta al quinto elemento
En memoria:
arr[0] en 0x1000
arr[1] en 0x1004 (0x1000 + 4)
arr[2] en 0x1008 (0x1000 + 8)
arr[5] en 0x1014 (0x1000 + 20)
p = 0x1000
p++ => p = 0x1004 (suma el tamaño del tipo: 4 bytes para int)
p += 5 => p = 0x1014 (suma 5 * 4 = 20 bytes)
En ensamblador:
lea rax, arr ; RAX = dirección de arr[0]
mov rcx, 1
add rax, rcx ; Esto suma 1 byte, INCORRECTO
; Correcto:
lea rax, arr
mov rcx, 1
imul rcx, 4 ; Multiplicar offset por tamaño de int
add rax, rcx ; Ahora apunta a arr[1]
; O más directo:
lea rax, arr
add rax, 4 ; Sumar directamente 4 bytes = arr[1]
Diferencia de Punteros¶
En ensamblador:
lea rax, [arr+8] ; p1 = &arr[2]
lea rbx, [arr+20] ; p2 = &arr[5]
mov rcx, rbx
sub rcx, rax ; RCX = p2 - p1 = 12 bytes
sar rcx, 2 ; Dividir por 4 (tamaño de int)
; RCX = 3 (elementos)
Aritmética de Punteros con Estructuras¶
Cuando el puntero apunta a una estructura, el incremento suma el tamaño de toda la estructura:
// Estructura de números complejos
struct st_complejo {
float real; // 4 bytes
float imaginario; // 4 bytes
}; // Total: 8 bytes
st_complejo complejo[10]; // Array de 10 estructuras (global)
st_complejo* p_complejo = complejo; // Puntero al inicio
p_complejo++; // A bajo nivel: suma 8 bytes (sizeof(st_complejo))
Layout en memoria:
Dirección Contenido
0x7FF304 complejo[0].real
0x7FF308 complejo[0].imaginario
0x7FF30C complejo[1].real ← p_complejo después de ++
0x7FF310 complejo[1].imaginario
0x7FF314 complejo[2].real
...
En ensamblador:
; p_complejo = complejo
lea rax, complejo ; RAX = dirección de la estructura
mov [rsp+28], rax ; Guardar en p_complejo
; p_complejo++ (incrementar por sizeof(st_complejo) = 8)
mov rax, [rsp+28] ; RAX = p_complejo
add rax, 8 ; Sumar 8 (tamaño de la estructura)
mov [rsp+28], rax ; Guardar de vuelta
En IDA Pro:
Cuando tenemos símbolos, IDA nos muestra en Local Types:
Y en el stack frame vemos:
Notaciones Equivalentes: Punteros y Arrays¶
En C/C++, estas dos notaciones son equivalentes:
int arr[10];
int* p = arr;
// Estas dos líneas hacen lo mismo:
*(p + 7) = 5; // Forma con aritmética de punteros
p[7] = 5; // Forma con notación de array
También funciona al revés:
arr[7] = 5; // Notación de array
*(arr + 7) = 5; // Notación de puntero (arr se convierte a puntero)
Importante: La diferencia es que el nombre del array (arr) no puede reasignarse, mientras que un puntero sí:
int* p = arr;
p++; // ✅ Válido: p ahora apunta a arr[1]
arr++; // ❌ ERROR: "lvalue required" - arr es constante
Inicialización de Punteros en Posición Arbitraria¶
Un puntero puede inicializarse en cualquier posición del array:
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int* p = &arr[5]; // Iniciar en el campo 5
// Ahora los índices son relativos a esa posición:
p[0] = 100; // Modifica arr[5]
p[2] = 200; // Modifica arr[7]
p[-2] = 300; // Modifica arr[3] (índices negativos!)
En ensamblador:
lea rax, [arr+20] ; RAX = &arr[5] (5 * 4 = 20)
mov [rsp+28], rax ; p = RAX
; p[-2] = 300 (equivale a arr[3])
mov rax, [rsp+28] ; RAX = p
sub rax, 8 ; RAX = p - 2 (2 * 4 = 8)
mov dword ptr [rax], 300
Parte 7.5: Punteros Genéricos (void*)¶
Concepto¶
Un puntero genérico no tiene un tipo asociado. Puede apuntar a cualquier cosa:
void* b; // Puntero genérico, sin tipo
char* cadena = "Hola";
b = (void*)cadena; // Castear para asignar
Limitaciones¶
No se puede hacer aritmética sin castear:
void* p;
p++; // ❌ ERROR: ¿Cuánto suma? No hay tipo.
p += 5; // ❌ ERROR: No se puede calcular el offset.
Razón: El compilador necesita saber el tamaño del tipo para calcular el offset.
Uso Correcto¶
void* b;
int* enteros = (int*)malloc(10 * sizeof(int));
b = (void*)enteros; // Guardar como genérico
// Para usar, siempre castear:
int x = *((int*)b); // Castear a int* y desreferenciar
((int*)b)[5] = 100; // Castear y usar como array
En ensamblador:
; int x = *((int*)b)
mov rax, [rsp+28] ; RAX = b (dirección sin tipo)
mov edx, [rax] ; EDX = contenido como dword (4 bytes)
mov [rsp+30], edx ; x = EDX
Para reversing: El assembly es igual, pero en el código decompilado verás muchos casts.
Parte 8: Puntero a Puntero (double pointer)¶
Concepto¶
Estructura de memoria:
a en 0x1000, valor = 5
pa en 0x2000, valor = 0x1000 (dirección de a)
ppa en 0x3000, valor = 0x2000 (dirección de pa)
Desreferenciación¶
En ensamblador:
mov rax, [rsp+40] ; RAX = ppa (dirección de pa)
mov rax, [rax] ; RAX = pa (dirección de a)
mov eax, [rax] ; EAX = a (valor 5)
Asignación¶
En ensamblador:
Parte 9: Punteros y Arrays¶
Array como Puntero¶
int arr[10] = {1, 2, 3, 4, 5};
int* p = arr; // p apunta al primer elemento
cout << p[0] << endl; // 1
cout << p[1] << endl; // 2
cout << *(p+2) << endl; // 3
En ensamblador:
lea rax, arr ; RAX = dirección de arr[0]
mov edx, [rax] ; p[0] = EDX = 1
mov edx, [rax+4] ; p[1] = EDX = 2
mov edx, [rax+8] ; p[2] = EDX = 3
Iteración con Punteros¶
En ensamblador:
lea rax, arr ; p = arr
lea rbx, [arr+20] ; rbx = arr + 5 (5 elementos * 4 bytes)
loop_start:
cmp rax, rbx ; if (p < arr+5)
jge loop_end
mov edx, [rax] ; EDX = *p
; ... usar EDX ...
add rax, 4 ; p++ (siguiente elemento)
jmp loop_start
loop_end:
Parte 9.5: Array Bidimensional vs Array de Punteros¶
Esta es una diferencia crítica que confunde a muchos:
El Código de Ejemplo¶
#include <iostream>
using namespace std;
int main() {
// Array BIDIMENSIONAL (todo el contenido en el stack)
char Mes[][11] = {"Enero", "Febrero", "Marzo", "Abril",
"Mayo", "Junio", "Julio", "Agosto",
"Septiembre", "Octubre", "Noviembre", "Diciembre" };
// Array de PUNTEROS (solo las direcciones en el stack)
const char* Mes2[] = { "Enero", "Febrero", "Marzo", "Abril",
"Mayo", "Junio", "Julio", "Agosto",
"Septiembre", "Octubre", "Noviembre", "Diciembre" };
// Acceso a caracteres
cout << Mes[0][0] << endl; // 'E' (Enero)
cout << Mes[0][1] << endl; // 'n' (Enero)
cout << Mes[1][0] << endl; // 'F' (Febrero)
cout << Mes[1][1] << endl; // 'e' (Febrero)
// Array de punteros
cout << Mes2 << endl; // Dirección del array
cout << Mes2[0] << endl; // "Enero" (imprime el string)
cout << Mes2[1] << endl; // "Febrero"
// Incrementar puntero dentro del array
Mes2[0]++; // Ahora apunta a "nero"
cout << Mes2[0] << endl; // "nero"
// Tamaños
cout << sizeof(Mes) << endl; // 12 * 11 = 132
cout << sizeof(Mes2) << endl; // 12 * 8 = 96 (punteros de 64 bits)
return 0;
}
Diferencia Fundamental en Memoria¶
Array Bidimensional (Mes[][11])¶
Layout en el STACK:
┌────────────────────────────────────────┐
│ "Enero\0\0\0\0\0\0" (11 bytes) │ ← Mes[0]
├────────────────────────────────────────┤
│ "Febrero\0\0\0\0" (11 bytes) │ ← Mes[1]
├────────────────────────────────────────┤
│ "Marzo\0\0\0\0\0\0" (11 bytes) │ ← Mes[2]
├────────────────────────────────────────┤
│ ... │
├────────────────────────────────────────┤
│ "Diciembre\0\0" (11 bytes) │ ← Mes[11]
└────────────────────────────────────────┘
Total: 12 strings × 11 bytes = 132 bytes en stack
Características:
- Todo el contenido está contiguo en el stack
- Cada string ocupa exactamente 11 bytes (se completa con \0)
- El 11 es por el string más largo: "Septiembre" (10 chars + \0)
- No se puede incrementar Mes (es una dirección fija)
Array de Punteros (const char* Mes2[])¶
Layout en el STACK (solo punteros):
┌─────────────────────────────────────────────────┐
│ 0x7FF...54 (puntero a "Enero" en .data) │ ← Mes2[0]
├─────────────────────────────────────────────────┤
│ 0x7FF...5A (puntero a "Febrero" en .data) │ ← Mes2[1]
├─────────────────────────────────────────────────┤
│ 0x7FF...62 (puntero a "Marzo" en .data) │ ← Mes2[2]
├─────────────────────────────────────────────────┤
│ ... │
└─────────────────────────────────────────────────┘
Total: 12 punteros × 8 bytes = 96 bytes en stack
Los STRINGS están en la sección .data:
.data:
0x7FF...54: "Enero\0"
0x7FF...5A: "Febrero\0"
0x7FF...62: "Marzo\0"
...
Características: - Solo direcciones en el stack (8 bytes cada una en x64) - Los strings reales están en la sección .data - Sí se puede incrementar cada puntero individual - Ocupa menos espacio en stack (96 vs 132 bytes)
Comparación de Tamaños¶
| Tipo | Cálculo | Tamaño Total |
|---|---|---|
char Mes[][11] |
12 strings × 11 chars | 132 bytes |
const char* Mes2[] |
12 punteros × 8 bytes | 96 bytes |
Acceso a Caracteres Individuales¶
Array bidimensional:
Mes[0][0] // 'E' - primer carácter de "Enero"
Mes[0][1] // 'n' - segundo carácter de "Enero"
Mes[1][0] // 'F' - primer carácter de "Febrero"
En ensamblador:
; Mes[0][0] - calcular: (0 * 11) + 0 = 0
imul eax, 0Bh ; EAX = primer_indice × 11
add eax, 0 ; Sumar segundo índice
lea rcx, [rsp+Mes] ; RCX = dirección base
movzx edx, byte ptr [rcx+rax] ; EDX = caracter
Array de punteros:
Mes2[0][0] // 'E' - primer carácter del string apuntado por Mes2[0]
Mes2[0][1] // 'n'
Mes2[1][0] // 'F'
En ensamblador:
; Mes2[0][1] - dos desreferencias
imul eax, 8 ; EAX = primer_indice × 8 (tamaño de puntero)
lea rcx, [rsp+Mes2] ; RCX = dirección base del array de punteros
mov rax, [rcx+rax] ; RAX = Mes2[0] (el puntero al string)
movzx edx, byte ptr [rax+1] ; EDX = segundo carácter del string
Incrementar Punteros en el Array¶
Array bidimensional (ILEGAL):
Mes++; // ❌ ERROR: Mes es una dirección constante
Mes[0]++; // ❌ ERROR: Mes[0] tampoco se puede incrementar
Array de punteros (VÁLIDO):
Mes2[0]++; // ✅ VÁLIDO: Mes2[0] es un puntero modificable
// Antes: Mes2[0] → "Enero"
// Después: Mes2[0] → "nero"
En ensamblador (incremento de Mes2[0]):
; Mes2[0]++
lea rax, [rsp+Mes2] ; RAX = dirección del array
mov rcx, [rax] ; RCX = Mes2[0] (puntero actual a "Enero")
inc rcx ; RCX++ (ahora apunta a "nero")
mov [rax], rcx ; Guardar de vuelta en Mes2[0]
Impresión con cout¶
Imprimir un puntero vs un string:
cout << Mes2; // Imprime la DIRECCIÓN del array (0x7FF...)
cout << Mes2[0]; // Imprime "Enero" (cout detecta char* y lo imprime como string)
En ensamblador:
; cout << Mes2 (imprimir dirección)
lea rdx, [rsp+Mes2] ; RDX = dirección del array
call ostream::operator<< ; Sobrecarga para void*
; cout << Mes2[0] (imprimir string)
mov rdx, [rsp+Mes2] ; RDX = Mes2[0] (puntero al string)
call ostream::operator<< ; Sobrecarga para char* (imprime string)
En IDA Pro: Inicialización¶
Array bidimensional:
IDA muestra la copia de strings al stack con rep movsb o movimientos directos:
; Copiar "Enero" al stack
lea rdi, [rsp+Mes] ; Destino
lea rsi, aEnero ; Source: "Enero" en .data
mov ecx, 6 ; 6 bytes
rep movsb ; Copiar
; Rellenar con ceros (padding hasta 11 bytes)
lea rdi, [rsp+Mes+6] ; Posición después de "Enero"
xor eax, eax
mov ecx, 5
rep stosb ; Poner 5 ceros
Array de punteros:
IDA muestra asignación de direcciones:
; Mes2[0] = &"Enero"
lea rax, aEnero ; RAX = dirección de "Enero" en .data
mov [rsp+Mes2], rax ; Mes2[0] = RAX
; Mes2[1] = &"Febrero"
lea rax, aFebrero ; RAX = dirección de "Febrero"
mov [rsp+Mes2+8], rax ; Mes2[1] = RAX (8 bytes después)
Tip para reversing: Si ves muchos lea + mov asignando direcciones consecutivas (de 8 en 8), es un array de punteros. Si ves rep movsb copiando datos y rep stosb rellenando, es un array bidimensional.
Caso Especial: char[] vs char*¶
char cadena1[] = "Cadena 1"; // Array: contenido en el stack
const char* cadena2 = "Cadena 2"; // Puntero: string en .data
cadena1++; // ❌ ERROR: cadena1 es dirección fija
cadena2++; // ✅ VÁLIDO: cadena2 es un puntero
Después de cadena2++:
Subíndices Relativos a la Posición Actual¶
Cuando usamos un puntero con notación de array, los índices son relativos a la posición actual:
const char* p = "Enero"; // p[0] = 'E'
p++; // Ahora p apunta a "nero"
// p[0] = 'n'
// p[1] = 'e'
// p[2] = 'r'
// p[3] = 'o'
Contraste con arrays fijos:
Parte 10: Reconocimiento de Punteros en IDA Pro (Sin Símbolos)¶
Patrón 1: LEA + MOV (Asignación de Puntero)¶
Indicador: lea seguido de mov → Probablemente asignación de puntero.
Patrón 2: MOV Doble (Desreferenciación)¶
Indicador: Dos mov sucesivos → Probablemente lectura a través de puntero.
Patrón 3: Escritura a través de Puntero¶
Indicador: mov dword ptr [registro], valor → Escritura desreferenciada.
Patrón 4: Puntero a Estructura¶
lea rax, [rsp+28] ; RAX = dirección de estructura
mov [rsp+80], rax ; Guardar puntero
mov rax, [rsp+80] ; Cargar puntero
mov edx, [rax+82] ; Leer campo en offset 82
Indicador: Offsets dentro de desreferenciación → Puntero a estructura.
Patrón 5: Llamada a Función con Puntero¶
lea rax, [rsp+28] ; RAX = &variable
mov rcx, rax ; RCX = puntero (primer parámetro)
call sub_401234 ; Pasar puntero a función
Indicador: lea, luego mov rcx, luego call → Pasar puntero como parámetro.
Parte 11: Importancia de Punteros en Reversing¶
¿Por Qué Son Críticos?¶
1. Estructuras Grandes
Sin punteros, sería imposible pasar 8000 bytes eficientemente.
2. Modificación In-Place
El puntero permite modificar la variable original.
3. Arrays Dinámicos
Los arrays dinámicos se manejan completamente con punteros.
4. Listas Enlazadas, Árboles, etc.
Las estructuras recursivas requieren punteros.
En Reversing¶
Cuando ves punteros en ensamblador:
- Hay transferencia de estructuras entre funciones
- Hay modificación de datos desde otra función
- Hay operaciones complejas (búsquedas, iteraciones)
- El tamaño de datos es significativo
Parte 12: Stack Frame con Punteros¶
Ejemplo Completo¶
Stack Layout¶
RSP+0x00: Shadow Space (32 bytes)
RSP+0x20: a (int, 4 bytes) = 100 → 500
RSP+0x24: b (int, 4 bytes) = 200 → 600
RSP+0x28: pa (puntero, 8 bytes) = &a
RSP+0x30: pb (puntero, 8 bytes) = &b
RSP+0x38: Security Cookie (8 bytes)
Ensamblador Completo¶
; a = 100
mov dword ptr [rsp+20], 100
; b = 200
mov dword ptr [rsp+24], 200
; pa = &a
lea rax, [rsp+20]
mov [rsp+28], rax
; pb = &b
lea rax, [rsp+24]
mov [rsp+30], rax
; *pa = 500
mov rax, [rsp+28] ; RAX = pa
mov dword ptr [rax], 500 ; a ahora vale 500
; *pb = 600
mov rax, [rsp+30] ; RAX = pb
mov dword ptr [rax], 600 ; b ahora vale 600
Estado Final¶
Parte 13: Errores Comunes con Punteros¶
Error 1: Usar Puntero Nulo¶
En reversing: Si ves mov eax, [0] o similar → Probablemente crash intencional o bug.
Error 2: Puntero a Variable Local¶
int* puntero_global;
void funcion() {
int a = 5;
puntero_global = &a; // ¡Peligro! a se destruye al salir
}
int main() {
funcion();
int x = *puntero_global; // Crash o valor basura
}
En reversing: Punteros que apuntan a stack de funciones ya terminadas → Bug.
Error 3: Confundir Dirección con Valor¶
int a = 100;
int* p = &a;
cout << p << endl; // Imprime dirección (0x1000)
cout << *p << endl; // Imprime valor (100)
Error 4: Olvidar Desreferenciar¶
int a = 100;
int* p = &a;
int b = p; // Asigna dirección a int (error de tipos)
int c = *p; // Correcto: asigna valor
Checklist: Reconocer Punteros en Ensamblador¶
Cuando revises una función desconocida:
- ¿Hay
leaseguido demoven el stack? → Puntero asignado - ¿Hay
movdoble sobre el mismo registro? → Posible desreferenciación - ¿Hay
mov dword ptr [registro], valor? → Escritura desreferenciada - ¿Hay offsets dentro de desreferenciación? → Puntero a estructura
- ¿Se pasan registros a funciones con
leaantes? → Puntero como parámetro - ¿Hay operaciones de suma en punteros? → Aritmética de punteros (arrays)
Parte 14: Comparación Directa con Ensamblador del Video¶
Código Original (del video)¶
Ensamblador Esperado (sin optimizaciones)¶
00401000: push rbp
00401001: mov rbp, rsp
00401004: sub rsp, 40h
; pa = &a
00401008: lea rax, [rsp+20h] ; Obtener dirección de a
00401010: mov [rsp+28h], rax ; pa = dirección
; *pa = 5
00401018: mov rax, [rsp+28h] ; Cargar pa
0040101C: mov dword ptr [rax], 5 ; Escribir 5
; b = *pa
00401024: mov rax, [rsp+28h] ; Cargar pa
00401028: mov edx, [rax] ; Leer valor (5)
0040102C: mov [rsp+24h], edx ; b = 5
; *pa = 10
00401030: mov rax, [rsp+28h] ; Cargar pa
00401034: mov dword ptr [rax], 10 ; Escribir 10
; Epilogo
00401038: xor eax, eax ; return 0
0040103A: add rsp, 40h
00401041: pop rbp
00401042: ret
Análisis Paso a Paso en Depuración¶
| Instrucción | RAX | [RSP+20] (a) | [RSP+28] (pa) | [RSP+24] (b) |
|---|---|---|---|---|
| Inicial | - | ? | - | - |
lea rax, [rsp+20] |
0x...0x20 | ? | - | - |
mov [rsp+28], rax |
0x...0x20 | ? | 0x...0x20 | - |
mov rax, [rsp+28] |
0x...0x20 | ? | 0x...0x20 | - |
mov [rax], 5 |
0x...0x20 | 5 | 0x...0x20 | - |
mov rax, [rsp+28] |
0x...0x20 | 5 | 0x...0x20 | - |
mov edx, [rax] |
0x...0x20 | 5 | 0x...0x20 | - |
mov [rsp+24], edx |
0x...0x20 | 5 | 0x...0x20 | 5 |
mov rax, [rsp+28] |
0x...0x20 | 5 | 0x...0x20 | 5 |
mov [rax], 10 |
0x...0x20 | 10 | 0x...0x20 | 5 |
Resumen: Lo Más Importante¶
Conceptos Clave¶
- Puntero = Dirección de memoria
- Se obtiene con
&variable -
Se desreferencia con
*puntero -
En ensamblador:
leaobtiene dirección (sin desreferenciar)movcarga/almacena direcciones o valores-
[registro]desreferencia el puntero -
Aritmética de punteros:
puntero++suma el tamaño del tipo (no 1 byte)puntero + NsumaN * sizeof(tipo)- Puntero a
int: incrementa de 4 en 4 -
Puntero a
structde 8 bytes: incrementa de 8 en 8 -
Notaciones equivalentes:
*(p + N)es igual ap[N]-
Pero
arr++es ilegal (array fijo), mientras quep++es válido -
Array bidimensional vs Array de punteros:
| Característica | char arr[][11] |
char* arr[] |
|---|---|---|
| Contenido en stack | Todos los caracteres | Solo punteros (8 bytes c/u) |
| Strings en | Stack (contiguos) | Sección .data |
| Tamaño | filas × columnas | filas × 8 bytes |
| Incrementar elemento | ❌ Ilegal | ✅ Válido |
| Ejemplo | 12 × 11 = 132 bytes | 12 × 8 = 96 bytes |
- Patrones a reconocer:
lea + mov= asignación de punteromov + mov= desreferenciaciónmov dword ptr [reg], val= escritura desreferenciadaimul eax, 0Bh+add= acceso a array bidimensional (11 = 0Bh)imul eax, 8+ doble desreferencia = array de punteros
Para Reversing¶
- Los punteros son fundamentales para pasar datos entre funciones
- Sin punteros, es imposible trabajar con estructuras grandes
- La aritmética de punteros implica multiplicación por el tamaño del tipo
- Los punteros dobles (
**p) son raros pero aparecen en callbacks y manipulación dinámica - Array bidimensional: Ves
rep movsbyrep stosbpara inicializar en stack - Array de punteros: Ves múltiples
lea+movasignando direcciones
Consejo Final de Narvaja¶
El reversing es un proceso iterativo. Muchas veces uno se confunde y después lo arregla. No se asusten si algo no sale de movida. El trabajo real es equivocarse, corregir, y poco a poco llegar al resultado correcto. Nadie lo hace perfecto la primera vez.
Parte 15: Punteros en el Heap¶
Qué es el Heap (objetos dinámicos)¶
- Región de memoria dinámica del proceso (ni stack ni .data/.bss).
- Las direcciones cambian en cada ejecución y el tamaño se decide en runtime.
- Se usa para objetos que no conocemos de antemano o que pueden ser grandes.
New vs malloc (diferencias clave)¶
| Aspecto | new |
malloc |
|---|---|---|
| Tipo devuelto | Devuelve puntero tipado (no requiere cast) | void* (requiere cast) |
| Tamaño | Lo calcula el compilador (new T) |
Debes pasar sizeof(T) |
| En caso de error | Lanza excepción std::bad_alloc (por defecto) |
Devuelve NULL |
| Inicialización | Llama constructor (si lo hay) | No inicializa, solo reserva bytes |
| Liberación | delete / delete[] |
free |
Código del ejercicio (heap alocado dinámicamente)¶
#include <iostream>
using namespace std;
struct stComplejo {
int real, imaginario;
};
void imprimir(stComplejo*, stComplejo*);
int main() {
stComplejo* Complejo = new stComplejo; // Reserva en heap (tipado)
stComplejo* Complejo2 = (stComplejo*) malloc(sizeof(stComplejo)); // Reserva en heap (void*, requiere cast)
if (Complejo2 == NULL) { exit(1); } // malloc puede devolver NULL
Complejo->real = 1; Complejo->imaginario = 2;
Complejo2->real = 3; Complejo2->imaginario = 4;
imprimir(Complejo, Complejo2); // Pasamos direcciones (punteros)
free(Complejo2); // Liberar malloc
delete Complejo; // Liberar new
}
void imprimir(stComplejo* _Complejo, stComplejo* _Complejo2) {
_Complejo->real = 1;
_Complejo->imaginario = 2;
cout << "Complejo: " << _Complejo->real << " + " << _Complejo->imaginario << "i" << endl;
cout << "Complejo2: " << _Complejo2->real << " + " << _Complejo2->imaginario << "i" << endl;
}
Qué mirar en ensamblador¶
- Llamadas a heap:
call operator newocall malloc→ puntero devuelto enRAX. - Chequear NULL (solo para
malloc): comparación del puntero con cero y salto aexit. - Asignaciones a través de punteros:
mov dword ptr [rax], <valor>dondeRAXes el puntero devuelto. - Paso por puntero a función: argumentos en
RCX/RDX(Win64); solo viajan 8 bytes por puntero, no toda la estructura. - Liberación:
call freeycall operator deletecerca del final.
Ventajas de usar punteros en el heap¶
- Tamaño decidido en runtime (puede depender de entrada del usuario).
- Evitas desbordar el stack con estructuras grandes.
- Puedes compartir el mismo objeto entre funciones pasando solo la dirección.
Checklist rápida¶
- ¿Ves
operator newomalloc? → Objeto en heap. - ¿Ves
free/deleteemparejados? → Buen manejo de vida útil. - ¿Hay cast
(tipo*) malloc(...)? → C indicamallocdevolvióvoid*. - ¿Sin cast con
new? → C++ lo tipa solo.
Análisis del ensamblador de main() (heap dinámico)¶
; Prototipo: int __fastcall main(int argc, const char **argv, const char **envp)
sub rsp, 58h ; Reserva stack (locals: punteros/estado)
mov ecx, 8 ; size = sizeof(stComplejo)
call operator new(unsigned __int64)
mov [rsp+Complejo], rax ; Complejo = puntero devuelto por new
mov ecx, 8 ; size = sizeof(stComplejo)
call __imp_malloc ; malloc(8)
mov [rsp+Complejo2], rax ; Complejo2 = puntero devuelto por malloc
cmp [rsp+Complejo2], 0 ; ¿malloc devolvió NULL?
jnz short ok_malloc
mov ecx, 1
call __imp_exit ; exit(1) si falla malloc
ok_malloc:
mov rax, [rsp+Complejo]
mov dword ptr [rax], 1 ; Complejo->real = 1
mov rax, [rsp+Complejo]
mov dword ptr [rax+4], 2 ; Complejo->imaginario = 2
mov rax, [rsp+Complejo2]
mov dword ptr [rax], 3 ; Complejo2->real = 3
mov rax, [rsp+Complejo2]
mov dword ptr [rax+4], 4 ; Complejo2->imaginario = 4
mov rdx, [rsp+Complejo2] ; 2º parámetro (Win64): _Complejo2
mov rcx, [rsp+Complejo] ; 1º parámetro: _Complejo
call imprimir(stComplejo *, stComplejo *)
mov rcx, [rsp+Complejo2]
call __imp_free ; free(Complejo2)
mov rax, [rsp+Complejo]
mov [rsp+block], rax ; block = Complejo
mov edx, 8 ; tamaño para delete
mov rcx, [rsp+block]
call operator delete(void *, unsigned __int64) ; delete Complejo, size-aware
cmp [rsp+block], 0
jnz short set_status ; si había puntero, marca estado
mov [rsp+var_18], 0
jmp short end_status
set_status:
mov [rsp+Complejo], 8123h ; patrón/estado (diagnóstico)
mov rax, [rsp+Complejo]
mov [rsp+var_18], rax ; guarda 0x8123 como estado
end_status:
xor eax, eax ; return 0
add rsp, 58h
retn
Puntos clave de reconocimiento:
- operator new y malloc para reservar en heap; puntero en RAX.
- Chequeo de NULL y exit(1) solo para malloc.
- Escrituras a través de puntero ([rax], [rax+4]) para campos real e imaginario.
- Paso de parámetros en Win64: RCX primer puntero, RDX segundo puntero.
- Liberación emparejada: free para malloc, operator delete para new.
- Bloque de “estado” (8123h) tras delete: relleno/diagnóstico del compilador; no afecta la lógica.
Función imprimir() (parámetros, escrituras y cout)¶
; Firma: void __fastcall imprimir(stComplejo* _Complejo, stComplejo* _Complejo2)
; Prologue: guarda parámetros en stack y reserva espacio
mov [rsp+_Complejo2], rdx
mov [rsp+_Complejo], rcx
sub rsp, 68h
; Escrituras: _Complejo->real = 1; _Complejo->imaginario = 2
mov rax, [rsp+68h+_Complejo]
mov dword ptr [rax], 1
mov rax, [rsp+68h+_Complejo]
mov dword ptr [rax+4], 2
; cout << "Complejo: "
lea rdx, _Val ; puntero a literal
mov rcx, cs:std::cout ; ostream
call std::operator<<<...>(std::ostream&, char const*)
mov [rsp+68h+var_38], rax ; guardar ostream
; cout << _Complejo->real
mov rax, [rsp+68h+_Complejo]
mov eax, [rax] ; cargar real
mov [rsp+68h+var_48], eax
mov edx, [rsp+68h+var_48]
mov rcx, [rsp+68h+var_38]
call std::ostream::operator<<(int)
; cout << " + "
lea rdx, asc_1400032C4
mov rcx, rax
call std::operator<<<...>(std::ostream&, char const*)
mov [rsp+68h+var_30], rax
; cout << _Complejo->imaginario
mov rax, [rsp+68h+_Complejo]
mov eax, [rax+4]
mov [rsp+68h+var_44], eax
mov edx, [rsp+68h+var_44]
mov rcx, [rsp+68h+var_30]
call std::ostream::operator<<(int)
; cout << "i" << std::endl
lea rdx, aI_0
mov rcx, rax
call std::operator<<<...>(std::ostream&, char const*)
mov [rsp+68h+var_28], rax
lea rdx, std::endl<char,std::char_traits<char>>(std::ostream &)
mov rcx, [rsp+68h+var_28]
call std::ostream::operator<<(std::ostream& (*)(std::ostream&))
; Repite patrón para _Complejo2
lea rdx, aComplejo2
mov rcx, cs:std::cout
call std::operator<<<...>(std::ostream&, char const*)
mov [rsp+68h+var_20], rax
mov rax, [rsp+68h+_Complejo2]
mov eax, [rax]
mov [rsp+68h+var_40], eax
mov edx, [rsp+68h+var_40]
mov rcx, [rsp+68h+var_20]
call std::ostream::operator<<(int)
lea rdx, asc_1400032D8
mov rcx, rax
call std::operator<<<...>(std::ostream&, char const*)
mov [rsp+68h+var_18], rax
mov rax, [rsp+68h+_Complejo2]
mov eax, [rax+4]
mov [rsp+68h+var_3C], eax
mov edx, [rsp+68h+var_3C]
mov rcx, [rsp+68h+var_18]
call std::ostream::operator<<(int)
lea rdx, aI
mov rcx, rax
call std::operator<<<...>(std::ostream&, char const*)
mov [rsp+68h+var_10], rax
lea rdx, std::endl<char,std::char_traits<char>>(std::ostream &)
mov rcx, [rsp+68h+var_10]
call std::ostream::operator<<(std::ostream& (*)(std::ostream&))
add rsp, 68h
retn
Claves:
- Prologue guarda
RCX/RDXen el stack y reserva 0x68 bytes (temporales para ostream y valores). - Escrituras a
_Complejo:[rax]y[rax+4]sonrealeimaginario(structint,int). - Secuencia típica de
cout: literal → entero → literal → entero → literal →endl. - Para
_Complejo2, se repite el mismo patrón conRDXcomo base.
Notas prácticas en IDA (símbolos, optimizaciones y tecla T)¶
- Sin optimizaciones: mantenemos
/Ody símbolos para que el código se parezca al C++. Con/O2, el compilador puede inlinearimprimir()dentro demain()y mover variables a registros (ni siquiera verás el stack slot temporal). Para enseñar es mejor empezar sin optimizaciones; luego podrás comparar y ver cómo desaparecen funciones o se convierten en registros. newvsmallocen binario:newya incluye chequeo/reintento (llama alnew_handlero lanzastd::bad_alloc), por eso no vesif (ptr==0)trasnew. Conmallocel chequeo es manual (aquí elif (Complejo2 == NULL) exit(1);).- Temporales no optimizados: notarás variables intermedias inútiles (guardar en
[rsp+var]y reescribir). Son artefactos de código no optimizado; en build optimizado desaparecerán. - Tecla
Ten IDA: selecciona un puntero y pulsaTpara aplicar el tipo de estructura; así los accesos[rax]/[rax+4]muestranreal/imaginarioen vez de offsets crudos. - Distinguir instancias: cuando dos punteros apuntan al mismo tipo (
ComplejoyComplejo2), duplica el tipo (Copy struct type) y renombra las instancias. Luego, conT, eligesComplejooComplejo2y ves los campos correctos en cada uso, evitando confundir qué objeto está tocando cadamov. - Detectar estructuras sin símbolos: si ves un puntero y accesos con desplazamientos constantes (
[reg],[reg+4], etc.), asume estructura/objeto. Aunque no veas elmalloc/newcercano, el patrón “puntero + offset = campo” es la pista clave.
Apéndice A: Smart Pointers (Punteros Inteligentes)¶
Introducción a Smart Pointers¶
Los smart pointers son objetos que actúan como punteros pero manejan automáticamente la memoria. En C++11 y posteriores, encontrarás tres tipos principales en <memory>:
unique_ptr- Propiedad exclusivashared_ptr- Propiedad compartidaweak_ptr- Referencia débil ashared_ptr
¿Por Qué Son Importantes en Reversing?¶
En binarios compilados con C++11+, verás:
- Más llamadas a funciones de destrucción automática
- Contadores de referencias (
refcount) - Deletores personalizados
- Patrones de inicialización más complejos
unique_ptr: Propiedad Única¶
#include <memory>
using namespace std;
int main() {
unique_ptr<int> ptr(new int(42));
cout << *ptr << endl; // 42
// ptr se destruye automáticamente al salir del scope
return 0;
}
Características:
- Solo un propietario
- No se puede copiar (se mueve)
- Automáticamente destruido al salir del scope
- Sin overhead de conteo de referencias
En ensamblador:
; Creación: unique_ptr<int> ptr(new int(42));
mov ecx, 4 ; size = 4
call operator new ; RAX = puntero a memoria
mov edx, 42
mov [rax], edx ; Escribir valor
mov [rsp+28], rax ; ptr = RAX (almacenar puntero)
; Destrucción automática (cuando sale del scope):
mov rcx, [rsp+28] ; RCX = ptr
cmp rcx, 0
je skip_delete
call operator delete ; Liberar memoria
skip_delete:
Indicadores en IDA:
- Llamadas a
operator newyoperator delete - Destructor registrado (probablemente en tabla de funciones)
- Un único puntero de propiedad
shared_ptr: Propiedad Compartida¶
int main() {
shared_ptr<int> ptr1(new int(100));
shared_ptr<int> ptr2 = ptr1; // Compartir propiedad
cout << ptr1.use_count() << endl; // 2
// Ambos se destruyen automáticamente
// Memoria liberada cuando último shared_ptr se destruye
return 0;
}
Características:
- Múltiples propietarios
- Conteo de referencias interno
- Memoria liberada cuando refcount llega a cero
- Overhead de sincronización en ambientes multi-thread
Estructura interna:
template<typename T>
class shared_ptr {
T* ptr; // Puntero a datos
control_block* control; // Bloque de control (refcount, deletor)
};
struct control_block {
long refcount; // Contadores de referencias
T* ptr; // Puntero a objeto
deletor del; // Función de destrucción personalizada
};
En ensamblador:
; Creación: shared_ptr<int> ptr1(new int(100));
mov ecx, 4
call operator new ; RAX = puntero a datos (0x1000)
; Crear control_block
mov ecx, size_control_block
call operator new ; RBX = puntero a control_block (0x2000)
mov [rbx], 1 ; control_block.refcount = 1
mov [rbx+8], rax ; control_block.ptr = datos
mov [rbx+16], del_func ; control_block.deletor
; Almacenar en shared_ptr
mov [rsp+28], rax ; shared_ptr.ptr = 0x1000
mov [rsp+36], rbx ; shared_ptr.control = 0x2000
; Asignación: shared_ptr<int> ptr2 = ptr1;
mov rax, [rsp+28] ; ptr2.ptr = ptr1.ptr
mov rbx, [rsp+36] ; ptr2.control = ptr1.control
mov [rsp+44], rax ; Guardar ptr2.ptr
mov [rsp+52], rbx ; Guardar ptr2.control
; Incrementar refcount
mov rax, [rbx] ; RAX = refcount
inc rax
mov [rbx], rax ; refcount = 2
; Destrucción: cuando ptr1 sale del scope
mov rbx, [rsp+36] ; RBX = control_block
mov rax, [rbx] ; RAX = refcount
dec rax
mov [rbx], rax ; refcount--
cmp rax, 0
jne skip_destroy ; Si refcount > 0, no destruir
mov rcx, [rbx+8] ; RCX = ptr a datos
mov rax, [rbx+16] ; RAX = deletor
call rax ; Llamar deletor
mov rcx, rbx ; RCX = control_block
call operator delete ; Liberar control_block
skip_destroy:
Indicadores en IDA:
- Dos asignaciones de puntero (ptr + control_block)
- Incremento de contador (
inc [memoria]) - Decremento de contador (
dec [memoria]) - Saltos condicionales basados en refcount
- Múltiples llamadas a
operator delete
weak_ptr: Referencia Débil¶
int main() {
shared_ptr<int> sptr(new int(50));
weak_ptr<int> wptr = sptr; // Referencia débil
// wptr NO incrementa el refcount
cout << sptr.use_count() << endl; // 1 (no contado)
// Para usar weak_ptr, convertir a shared_ptr:
if (auto locked = wptr.lock()) {
cout << *locked << endl; // 50
}
return 0;
}
Uso común: Evitar ciclos de referencias que prevengan destrucción automática.
En ensamblador:
; weak_ptr = shared_ptr (NO incrementa refcount)
mov rax, [rsp+28] ; RAX = ptr1.ptr
mov rbx, [rsp+36] ; RBX = ptr1.control
mov [rsp+44], rax ; wptr.ptr = ptr1.ptr
mov [rsp+52], rbx ; wptr.control = ptr1.control
; Nota: NO hay incremento de refcount
; weak_ptr::lock() - convertir a shared_ptr
mov rbx, [rsp+52] ; RBX = control_block
mov rax, [rbx] ; RAX = refcount
test rax, rax
jz lock_failed ; Si refcount == 0, objeto ya destruido
; Incrementar refcount (ahora es un shared_ptr)
inc [rbx]
mov rax, [rsp+44] ; RAX = ptr a datos
; ... resultado: shared_ptr válido
jmp lock_success
lock_failed:
; ... resultado: nullptr
lock_success:
Comparación: Raw Pointer vs Smart Pointers¶
| Operación | Raw int* |
unique_ptr<int> |
shared_ptr<int> |
|---|---|---|---|
| Creación | int* p = new int(5); |
unique_ptr<int> p(new int(5)); |
shared_ptr<int> p(new int(5)); |
| Copia | int* q = p; (comparte propiedad) |
❌ Compilación falla | shared_ptr<int> q = p; (refcount++) |
| Movimiento | N/A | unique_ptr<int> q = move(p); |
N/A |
| Destrucción | Manual delete p; |
Automática (scope) | Automática (refcount == 0) |
| Bytes en stack | 8 bytes (x64) | 8 bytes (x64) | 16 bytes (ptr + control) |
| Overhead | Ninguno | Ninguno | 1 long (refcount) |
Reconocimiento en IDA Pro¶
Patrones de unique_ptr:
; Creación
call operator new
mov [rsp+28], rax ; Guardar único puntero
; Destrucción (al salir)
mov rcx, [rsp+28]
call operator delete
Indicador: Un solo puntero, sin operaciones de refcount.
Patrones de shared_ptr:
; Creación (dos asignaciones)
call operator new ; RAX = datos
call operator new ; RBX = control_block
mov [rsp+28], rax ; ptr a datos
mov [rsp+36], rbx ; ptr a control
; Incremento de refcount (para copias)
mov rax, [rsp+36] ; RAX = control_block
inc [rax] ; Incrementar refcount
; Decremento y verificación (destrucción)
dec [rax]
cmp [rax], 0
jne skip_delete
Indicador: Dos punteros, operaciones inc/dec en memoria, saltos condicionales.
Stack Frame: Ejemplo Completo con shared_ptr¶
void procesar_datos(shared_ptr<vector<int>> datos) {
datos->push_back(100);
}
int main() {
shared_ptr<vector<int>> vec(new vector<int>());
procesar_datos(vec);
return 0;
}
Stack layout en main():
RSP+0x00: Shadow Space (32 bytes)
RSP+0x20: vec.ptr (8 bytes)
RSP+0x28: vec.control (8 bytes)
RSP+0x30: Padding
RSP+0x38: Security Cookie
En procesar_datos():
En ensamblador:
; Paso de shared_ptr a función
lea rax, [rsp+20] ; RAX = &vec.ptr
mov rcx, [rax] ; RCX = vec.ptr (parámetro 1)
lea rbx, [rsp+28] ; RBX = &vec.control
mov rdx, [rbx] ; RDX = vec.control (parámetro 2)
call procesar_datos ; Llamar función
; Dentro de procesar_datos:
procesar_datos proc near:
; RCX = datos.ptr
; RDX = datos.control
; Incrementar refcount (copia para parámetro)
inc [rdx]
; ... usar datos ...
; Decrementar refcount (al salir)
dec [rdx]
cmp [rdx], 0
jne ret_func
; Liberar memoria
call delete_vector
call operator delete ; Liberar control_block
ret_func:
ret
Impacto en Reversing¶
Cuando veas smart pointers:
- El código es probablemente C++11 o posterior
- Hay automatización de memoria (más seguro)
- Las operaciones de refcount aparecen como
inc [memoria]/dec [memoria] - Destructores pueden ser complejos (deletores personalizados)
- Es más difícil de debuggear (abstracciones adicionales)
Ventaja de reversing: Si ves refcount == 0 → memoria se libera automáticamente → potencial fuga de información u use-after-free.
Vulnerabilidades Comunes¶
1. Use-After-Free:
shared_ptr<int> ptr1(new int(100));
weak_ptr<int> wptr = ptr1;
ptr1.reset(); // Libera la memoria
auto locked = wptr.lock();
if (locked) {
*locked = 200; // ❌ Use-after-free!
}
2. Ciclo de Referencias:
struct Nodo {
shared_ptr<Nodo> siguiente;
shared_ptr<Nodo> anterior;
};
Nodo n1, n2;
n1.siguiente = &n2;
n2.anterior = &n1;
// ❌ Ambos nunca se destruyen (refcount nunca llega a cero)
Solución: Usar weak_ptr para referencias hacia atrás.