Saltar a contenido

Definición de Tipos con typedef en C++: Guía Completa de Reversing

Índice


Introducción

El typedef es una palabra clave en C++ que permite crear nombres alternativos (alias) para tipos de datos existentes. Esta funcionalidad:

  • Mejora la legibilidad del código
  • Facilita la portabilidad entre plataformas
  • Simplifica la declaración de tipos complejos (como punteros a funciones)
  • Reduce la duplicación de código

Objetivo: Entender cómo typedef funciona internamente, cómo se ve en ensamblador y cómo reconocerlo en reverse engineering.


Parte 1: ¿Qué es typedef?

Concepto Fundamental

typedef es un mecanismo de compilación que crea un alias para un tipo existente. No crea un nuevo tipo en tiempo de ejecución, sino que es una instrucción para el compilador que dice: "usa este nuevo nombre como sinónimo para este tipo".

Importante: A nivel de ensamblador, typedef desaparece completamente. El compilador lo resuelve y usa el tipo original.

Ejemplo Conceptual

// Declaración original
unsigned int contador = 0;

// Con typedef
typedef unsigned int UINT;
UINT contador = 0;  // Exactamente lo mismo a nivel de compilación

En ambos casos, contador es un unsigned int de 4 bytes. La única diferencia es la legibilidad.


Parte 2: Sintaxis Básica

Forma General

typedef <tipo_original> <nuevo_nombre>;

Componentes:

Componente Significado
typedef Palabra clave
<tipo_original> Cualquier tipo C++ (fundamental o derivado)
<nuevo_nombre> El alias que queremos crear

Cosas Que Puedes Hacer con typedef

  • Alias para tipos fundamentales
  • Alias para estructuras
  • Alias para uniones
  • Alias para punteros
  • Alias para punteros a funciones (lo más complejo)

Parte 3: typedef con Tipos Fundamentales

Ejemplos Comunes

// Tipos sin signo
typedef unsigned int UINT;
typedef unsigned char BYTE;
typedef unsigned short int WORD;
typedef unsigned long int DWORD;

// Tipos con signo
typedef signed int INT;
typedef signed char CHAR;

// Enteros de tamaño fijo (modernamente se usan <cstdint>)
typedef unsigned int uint32_t;
typedef unsigned char uint8_t;

Caso Práctico: WORD

typedef unsigned short int WORD;

// Ahora podemos usar WORD en lugar de unsigned short int
WORD resultado = 1024;

¿Por qué esto es útil?

En algunas plataformas, una palabra (WORD) es de 16 bits, pero en otras puede ser diferente. Si defines WORD en un archivo de cabecera central, puedes cambiar fácilmente la definición según la plataforma:

// En plataforma A (16 bits)
typedef unsigned short int WORD;

// En plataforma B (32 bits)
typedef unsigned int WORD;

// El resto del código no cambia

A Nivel de Compilación

typedef unsigned int UINT;
UINT a = 5;

El compilador internamente reemplaza UINT por unsigned int. A nivel de ensamblador:

mov dword ptr [rbp - 8], 5h    ; a es un DWORD (4 bytes)

Parte 4: typedef con Estructuras

Simplificación de Sintaxis

Sin typedef, para usar una estructura necesitas escribir la palabra struct:

struct stpunto {
    int x;
    int y;
    int z;
};

// Declarar variable
stpunto p;  // ERROR: falta 'struct'
struct stpunto p;  // CORRECTO: necesitas 'struct'

Con typedef, puedes simplificar:

typedef struct stpunto tipoPunto;

// Ahora puedes usar directamente
tipoPunto p;  // SIN necesidad de 'struct'

Forma Compacta

También puedes definir el nombre del tipo al mismo tiempo que la estructura:

typedef struct {
    int x;
    int y;
    int z;
} Punto3D;

// Declarar variable
Punto3D punto;  // Directo, sin 'struct'

En este caso, la estructura no tiene nombre (es anónima), pero el tipo Punto3D sí existe.

Comparación

// Forma clásica (sin typedef)
struct Punto3D {
    int x;
    int y;
    int z;
};
struct Punto3D p1;  // Necesitas 'struct'

// Forma con typedef
typedef struct {
    int x;
    int y;
    int z;
} Punto3D;
Punto3D p1;  // Directo

A Nivel de Compilación

A nivel de ensamblador, ambas formas son idénticas. El compilador genera el mismo código para acceder a los miembros:

typedef struct {
    int x;      // offset +0
    int y;      // offset +4
    int z;      // offset +8
} Punto3D;

Punto3D p;
p.x = 10;  // mov dword ptr [rbp - 12], 10 (p está 12 bytes abajo, x está en +0)

Parte 5: typedef con Punteros a Funciones

El Caso Más Complejo

Los punteros a funciones tienen una sintaxis complicada en C++. Aquí es donde typedef realmente brilla:

Sin typedef (Confuso)

// Puntero a función que retorna int y no toma argumentos
int (*pf)();

// Múltiples punteros
int (*pf1)();
int (*pf2)();
int (*pf3)();

Problema: La sintaxis int (*pf)() es confusa y repetitiva.

Con typedef (Claro)

// Define el tipo una vez
typedef int (*PFI)();

// Ahora crea variables fácilmente
PFI f1;
PFI f2;
PFI f3;

Anatomía de la Sintaxis

typedef int (*PFI)();
         |   |  |  |
         |   |  |  +-- Parámetros (vacío = sin parámetros)
         |   |  +------ Nombre del tipo (PFI)
         |   +---------- * indica que es un puntero
         +-------------- Tipo de retorno (int)

Con Parámetros

// Puntero a función que toma (int, float) y retorna double
typedef double (*CALC)(int, float);

CALC calculadora;

Parte 6: Análisis en Ensamblador

Ejemplo Completo

typedef unsigned int UINT;

int main() {
    UINT a = 1;
    return 0;
}

A nivel de ensamblador:

main:
    push rbp
    mov rbp, rsp
    mov dword ptr [rbp - 4], 1h   ; a = 1, DWORD (4 bytes)
    xor eax, eax                   ; return 0
    pop rbp
    ret

Observación: UINT desaparece completamente. El compilador ve unsigned int, que es un DWORD (4 bytes).

Ejemplo con Estructuras

typedef struct {
    int x;
    int y;
    int z;
} Punto3D;

int main() {
    Punto3D p;
    p.x = 10;
    p.y = 20;
    p.z = 30;
}

A nivel de ensamblador:

main:
    push rbp
    mov rbp, rsp
    sub rsp, 12h              ; Reservar 12 bytes para p (3 ints)

    mov dword ptr [rbp - 12], 10h  ; p.x = 10 (offset +0)
    mov dword ptr [rbp - 8], 14h   ; p.y = 20 (offset +4)
    mov dword ptr [rbp - 4], 1eh   ; p.z = 30 (offset +8)

    xor eax, eax
    mov rsp, rbp
    pop rbp
    ret

El nombre Punto3D desaparece. Solo quedan los accesos a memoria con offsets.


Parte 7: Debugging y Análisis en IDA Pro

Reconocimiento de typedef

Cuando abres un programa compilado en IDA Pro o x64dbg, es difícil saber si un tipo fue creado con typedef porque:

  1. typedef desaparece en el binario (es información del compilador)
  2. Solo ves los tipos subyacentes en el código ensamblador
  3. Sin símbolos de debug, es imposible conocer los nombres de typedef

Debugging de Punteros a Funciones

Cuando encuentras un call indirecto (call a través de un puntero), puedes usar un truco en IDA Pro para anotar adónde va:

Truco 1: Ver la Dirección de la Función

typedef int (*PFI)();
int miFunc() { return 2; }

int main() {
    PFI f = &miFunc;
    int result = f();
    return 0;
}

En ensamblador:

lea rax, miFunc      ; Cargar dirección de miFunc en rax
mov [rbp - 8], rax   ; Guardar en f
call qword ptr [rbp - 8]  ; Call indirecto a través de f

En IDA Pro:

  1. Ve a donde está la función (ej: miFunc en 0x140001000)
  2. Haz clic derecho en miFunc → "Copy address"
  3. Ve al call qword ptr [...] → Haz clic derecho
  4. EditChange call address
  5. Pega la dirección con 0x al inicio: 0x140001000

Esto añade una anotación y permite hacer doble clic para navegar a la función.

Truco 2: Usar el Menú "Assemble"

call qword ptr [rbp - 8]

Si haces clic derecho → Assemble, ves la sintaxis clásica:

call qword ptr [rbp - 8]  ; call  qword ptr [rbp-8]

Esto te ayuda a confirmar que es un puntero a función.

Debugging en Tiempo de Ejecución

typedef int (*PFI)();
int miFunc() { return 2; }

PFI q = &miFunc;  // Global

int main() {
    int result = q();
    printf("%d", result);  // Imprime: 2
    return 0;
}

En x64dbg o WinDbg:

  1. Establece un breakpoint en q()
  2. Inspecciona q → Verás la dirección (ej: 0x140001000)
  3. Navega a esa dirección para ver el código de miFunc
  4. Los call registros como call rax o call qword ptr [...] son indicios de punteros a funciones

Parte 8: Portabilidad y Dependencia de Plataforma

Problema: Tamaños Diferentes

En distintas plataformas, los tipos pueden tener tamaños diferentes:

// Plataforma de 32 bits
sizeof(int) = 4 bytes
sizeof(long) = 4 bytes
sizeof(long long) = 8 bytes

// Plataforma de 64 bits
sizeof(int) = 4 bytes
sizeof(long) = 8 bytes  // DIFERENTE
sizeof(long long) = 8 bytes

Solución: typedef Condicional

Si necesitas una palabra de exactamente 16 bits en cualquier plataforma, puedes usar typedef con condicionales:

// En windows_types.h para plataforma Windows
#ifdef _WIN32
    typedef unsigned short int WORD;
#endif

// En unix_types.h para plataforma Unix
#ifdef __unix__
    typedef unsigned short int WORD;
#endif

// El resto del código no cambia
WORD valor = 1000;

O mejor aún, usa los tipos estándar de C++11:

// En <cstdint> (moderno y recomendado)
typedef uint16_t WORD;
typedef uint32_t DWORD;
typedef uint8_t BYTE;

Resumen: TL;DR

¿Qué es typedef?

  • Crea un alias para un tipo existente
  • Desaparece en la compilación (el compilador lo resuelve)
  • Mejora legibilidad y facilita portabilidad

Casos de Uso

Caso Ejemplo Beneficio
Tipos fundamentales typedef unsigned int UINT; Portabilidad
Estructuras typedef struct { ... } Punto3D; No necesita struct
Punteros a funciones typedef int (*PFI)(); Sintaxis clara

En Ensamblador

  • typedef desaparece completamente
  • Solo ves los tipos subyacentes
  • Los offsets en memoria se calculan según los tipos originales

En Debugging/Reversing

  • Sin símbolos de debug, es difícil saber si un tipo tiene typedef
  • Los call indirectos son indicio de punteros a funciones
  • Usa IDA Pro para anotar direcciones y mejorar el análisis

Lo Más Importante

En reversing:

mov dword ptr [rbp - 4], 5h       ; ¿Es UINT? ¿unsigned int? ¿int? 
                                   ; A nivel de asm, da lo mismo.

call qword ptr [rbp - 8]          ; Aquí hay un puntero a función.
                                   ; ¿Con typedef? Sin símbolo, no sabes.

El tipo de dato (typedef o no) es irrelevante para la ejecución, pero es crucial para la comprensión humana del código.


Ejercicio BCD

Este ejercicio demuestra un caso práctico donde typedef simplifica significativamente la sintaxis de tipos complejos. Pero más importante, enseña:

  • Cómo trabajar con datos personalizados que no son tipos primitivos
  • Manipulación de bits a bajo nivel
  • Cómo se ve a nivel de ensamblador
  • La diferencia entre la interpretación lógica (lo que queremos que haga) y la física (lo que el procesador realmente hace)

¿Qué es BCD (Binary Coded Decimal)?

BCD es una forma de codificar números decimales donde:

  • Cada dígito decimal (0-9) se almacena en 4 bits
  • Un byte (8 bits) almacena 2 dígitos decimales
  • El compilador no sabe nada de esto - es nuestra interpretación

Ejemplo:

Número decimal: 98
En BCD: 0x98

Desglose:
  Byte: [1001 1000]
         ↑    ↑
         9    8 (dígito mayor y menor)

Cada nibble (4 bits) representa un dígito decimal

La Línea Clave: typedef

const unsigned int cadmax = 32;
typedef unsigned char numero[cadmax];

Esto define: - numero = array de 32 bytes = 64 dígitos decimales - Permite sumar números mucho más grandes que unsigned long long (máx 19 dígitos)

Sin typedef:

bool Sumar(unsigned char n1[32], unsigned char n2[32], unsigned char r[32]);
unsigned char num1[32];
unsigned char num2[32];
unsigned char resultado[32];

Con typedef:

bool Sumar(numero n1, numero n2, numero r);
numero num1, num2, resultado;

El Código C++

int main() {
    numero n1 = {
        0x00,0x00,0x00,0x00,    // ...ceros...
        0x00,0x00,0x10,0x00      // = 0010 = 1, 0 = 10
    };

    numero n2 = {
        0x00,0x00,0x00,0x00,    // ...ceros...
        0x00,0x09,0x98,0x12      // = 0x09 (09), 0x98 (98), 0x12 (12) = 9812
    };

    numero suma;
    Sumar(n1, n2, suma);        // 10 + 9812 = 9822
}

La Función Sumar() - Paso a Paso

bool Sumar(numero n1, numero n2, numero r) {
    int acarreo = 0;
    int c;

    for(int i = cadmax-1; i >= 0; i--) {  // Empieza desde el final (dígito menos significativo)
        // ========== SUMAR DÍGITO DE MENOR PESO (4 bits bajos) ==========
        c = acarreo + (n1[i] & 0x0f) + (n2[i] & 0x0f);
        //             ↑                 ↑
        //        Extrae 4 bits    Extrae 4 bits
        //        inferiores       inferiores

        if(c > 9) { 
            c -= 10;           // Si se pasó de 9, restar 10
            acarreo = 1;       // Marcar acarreo para el siguiente dígito
        } else 
            acarreo = 0;

        r[i] = c;             // Guardar en la parte baja del resultado

        // ========== SUMAR DÍGITO DE MAYOR PESO (4 bits altos) ==========
        c = acarreo + ((n1[i] >> 4) & 0x0f) + ((n2[i] >> 4) & 0x0f);
        //             ↑                        ↑
        //        Desplaza 4 bits   Desplaza 4 bits
        //        y extrae el       y extrae el
        //        dígito mayor      dígito mayor

        if(c > 9) { 
            c -= 10;
            acarreo = 1;
        } else 
            acarreo = 0;

        r[i] |= (c << 4);     // Guardar en la parte alta (desplaza 4 bits a la izquierda)
    }
    return !acarreo;          // Retorna true si NO hay acarreo final
}

Ejemplo Concreto: 10 + 9812 = 9822

Iteración i=31 (último byte):

n1[31] = 0x10  (dígitos: 1, 0)
n2[31] = 0x12  (dígitos: 1, 2)

Dígito menor:
  c = 0 + (0x10 & 0x0f) + (0x12 & 0x0f)
  c = 0 + 0 + 2 = 2
  r[31] = 0x02  ✓

Dígito mayor:
  c = 0 + ((0x10 >> 4) & 0x0f) + ((0x12 >> 4) & 0x0f)
  c = 0 + 1 + 1 = 2
  r[31] |= (2 << 4) = r[31] | 0x20 = 0x02 | 0x20 = 0x22 ✓

Iteración i=30:

n1[30] = 0x00  (dígitos: 0, 0)
n2[30] = 0x98  (dígitos: 9, 8)

Dígito menor:
  c = 0 + 0 + 8 = 8
  r[30] = 0x08  ✓

Dígito mayor:
  c = 0 + 0 + 9 = 9
  r[30] |= 0x90 = 0x08 | 0x90 = 0x98 ✓

Resultado: 0x00...9822 = 9822 ✓

La Función MostrarBCD() - Filtrado de Ceros

void MostrarBCD(numero n) {
    char c;
    bool cero = true;  // Flag: ¿estamos en los ceros iniciales?

    for(unsigned int i = 0; i < cadmax; i++) {
        // Procesar dígito mayor (4 bits altos)
        c = '0' + ((n[i] >> 4) & 0x0f);  // Convertir a carácter ASCII
        if(c != '0') cero = false;        // Ya pasamos los ceros iniciales
        if(!cero) cout << c;              // Solo imprimir si no estamos en ceros iniciales

        // Procesar dígito menor (4 bits bajos)
        c = '0' + (n[i] & 0x0f);
        if(c != '0') cero = false;
        if(!cero) cout << c;
    }
}

¿Por qué la conversión '0' +?

Para convertir un dígito (0-9) a su carácter ASCII: - Dígito 0 + '0' (ASCII 48) = '0' (ASCII 48) - Dígito 8 + '0' (ASCII 48) = '8' (ASCII 56)

El flag cero:

Evita imprimir los ceros a la izquierda. Ejemplo:

Array: 0x00 0x00 0x98 0x12
Sin filtro: 00000000999812
Con filtro: 9812  ✓

Análisis en Ensamblador (x64 MSVC)

La Inicialización

main:
    sub     rsp, 152              ; Reservar espacio en stack
    mov     BYTE PTR n1$[rsp+30], 16    ; 0x10 en posición 30
    mov     BYTE PTR n1$[rsp+31], 0    ; 0x00 en posición 31
    mov     BYTE PTR n2$[rsp+29], 9    ; 0x09
    mov     BYTE PTR n2$[rsp+30], 152  ; 0x98 (152 decimal)
    mov     BYTE PTR n2$[rsp+31], 18   ; 0x12 (18 decimal)

Observación: Los valores se guardan en decimal, no hexadecimal. 0x98 = 152 decimal.

El Loop de Sumar()

Sumar PROC:
    mov     DWORD PTR i$1[rsp], 31        ; i = 31
    jmp     SHORT $LN4@Sumar

$LN2@Sumar:
    dec     DWORD PTR i$1[rsp]            ; i--

$LN4@Sumar:
    cmp     DWORD PTR i$1[rsp], 0         ; Comparar i con 0
    jl      $LN3@Sumar                    ; Si i < 0, salir

    movsxd  rax, DWORD PTR i$1[rsp]       ; Cargar índice i en rax
    mov     rcx, QWORD PTR n1$[rsp]       ; rcx = dirección de n1
    movzx   eax, BYTE PTR [rcx+rax]       ; Cargar n1[i]
    and     eax, 15                       ; AND con 0x0f (máscara 4 bits)
    ; ... continuar sumando ...

Patrón clave:

and     eax, 15              ; Máscara para 4 bits bajos (0x0f)
sar     eax, 4               ; Desplaza 4 bits a la derecha (para altos)
shl     ecx, 4               ; Desplaza 4 bits a la izquierda (para guardar en altos)
or      eax, ecx             ; Combina ambas partes

Debugging en x64dbg

Paso 1: Ver las variables iniciales

n1[31] = 0x10  (10 en decimal = 0x0A, pero se almacena como 0x10)
n2[29] = 0x09
n2[30] = 0x98
n2[31] = 0x12

Observar: El array está "de atrás para adelante"
porque las sumas se hacen desde el final

Paso 2: Ejecutar hasta Sumar()

rcx = dirección de n1
rdx = dirección de n2
r8  = dirección de suma (basura inicialmente)

Paso 3: Seguir el loop

En cada iteración: 1. Cargar n1[i] y n2[i] 2. Extraer los 4 bits bajos y sumarlos 3. Extraer los 4 bits altos y sumarlos (con acarreo) 4. Guardar el resultado en r[i] 5. El acarreo se propaga a la siguiente iteración

Paso 4: Ver el resultado

Después de Sumar():
suma[30] = 0x98
suma[31] = 0x22

MostrarBCD() convierte esto a: "9822"

Lo Más Importante de Este Ejercicio

1. typedef Simplifica Sintaxis Compleja

// Sin typedef: verboso y fácil de equivocarse
unsigned char resultado[32];

// Con typedef: claro y mantenible
numero resultado;

2. Los Compiladores "Resuelven" typedef

  • En ensamblador, no existe numero
  • Solo ves operaciones con bytes: mov BYTE PTR ...
  • typedef es puramente para el programador, no para la máquina

3. Manipulación de Bits es Fundamental en Reversing

and  eax, 15             ; Aislar 4 bits
sar  eax, 4              ; Extraer bits altos
shl  ecx, 4              ; Desplazar para combinar
or   eax, ecx            ; Fusionar nibbles

Estas operaciones aparecen constantemente en código compilado.

4. Los Datos Tienen "Dos Vidas"

  • Vida física: bytes en memoria (0x98 es solo un byte)
  • Vida lógica: cómo los interpretamos (0x98 = dígitos 9 y 8 en BCD)

El procesador solo ve bytes. Nosotros (como reversers) debemos interpretar el significado.


Resumen: BCD vs Tipos Modernos

Aspecto BCD (Este Ejercicio) Tipos Modernos
Tamaño 2 dígitos por byte (compacto) Depende del tipo
Precisión Exacta (sin punto flotante) Exacta solo con BigInt
Velocidad Lenta (manipulación manual) Rápida (nativa del HW)
Uso Moderno Raro (legacy systems) Lo normal
Educativo EXCELENTE (bits, acarreo, lógica) Menos instructivo

Conclusión: Este ejercicio es "rebuscado" por una razón: enseña conceptos fundamentales que encontrarás en reversing de sistemas legacy, firmware, y código optimizado.

El typedef aquí es el mecanismo que hace todo esto manejable sin repetir unsigned char[32] constantemente.