Tema 5

5. Arquitectura x86/x64, ensamblador básico y modelo de ejecución

El ensamblador es el puente entre el programa y el procesador. Para analizar malware, depurar una muestra o entender una falla de memoria no hace falta memorizar todas las instrucciones, pero sí comprender registros, stack, llamadas, saltos y flujo de ejecución.

Objetivo Leer instrucciones básicas y entender el flujo de ejecución
Enfoque Registros, stack, flags, llamadas y saltos
Resultado Preparar la base para debugging, reversing y explotación

5.1 Introducción

Cuando un programa se ejecuta, el procesador no ve clases, funciones de alto nivel ni nombres descriptivos. Ve instrucciones, registros, direcciones, saltos y accesos a memoria. El ensamblador representa esa capa cercana a la máquina.

En análisis de malware, leer ensamblador permite entender qué hace una muestra aunque no tengamos su código fuente. En explotación, ayuda a interpretar crashes, direcciones de retorno, uso del stack, llamadas a funciones y condiciones que cambian el flujo del programa.

Este tema no busca convertirte en programador avanzado de ensamblador. El objetivo es reconocer patrones básicos y ganar el vocabulario técnico necesario para usar debuggers y desensambladores con criterio.

5.2 Qué son x86 y x64

x86 y x64 son familias de arquitectura de procesador ampliamente usadas en computadoras personales y servidores. x86 suele referirse a ejecución de 32 bits, mientras que x64 o x86-64 se refiere a ejecución de 64 bits.

Las diferencias importan porque cambian el tamaño de direcciones, registros disponibles, convenciones de llamada y estructura de muchas operaciones observadas en debugging.

Característica x86 x64
Tamaño típico de dirección 32 bits 64 bits
Registros generales EAX, EBX, ECX, EDX, ESI, EDI, ESP, EBP RAX, RBX, RCX, RDX, RSI, RDI, RSP, RBP, R8-R15
Paso de argumentos Frecuentemente por stack Frecuentemente por registros y stack
Espacio de direcciones Más limitado Mucho más amplio
Relevancia actual Aún aparece en malware y software antiguo Predomina en sistemas modernos

5.3 Registros del procesador

Los registros son espacios pequeños y rápidos dentro del procesador. Guardan datos temporales, direcciones, resultados de operaciones y punteros importantes. En debugging, los registros muestran el estado inmediato de ejecución.

Registro x64 Equivalente x86 Uso frecuente
RAX EAX Resultados de funciones y operaciones aritméticas
RBX EBX Registro general, a menudo preservado por llamadas
RCX ECX Contadores, argumentos y operaciones repetitivas
RDX EDX Datos auxiliares, argumentos y operaciones extendidas
RSP ESP Puntero al tope del stack
RBP EBP Base del frame de stack, según compilador y optimización
RIP EIP Dirección de la próxima instrucción a ejecutar
En un crash, observar RIP/EIP, RSP/ESP y los registros de argumentos suele dar una primera pista sobre qué se estaba ejecutando y con qué datos.

5.4 RIP, EIP y flujo de ejecución

RIP en x64 y EIP en x86 señalan la dirección de la próxima instrucción. Cuando el programa avanza normalmente, ese puntero se mueve a la instrucción siguiente. Cuando hay un salto, una llamada o un retorno, cambia a otra dirección.

Para reversing, seguir RIP permite entender qué camino toma un programa. Para explotación, controlar indebidamente RIP/EIP suele indicar que una corrupción de memoria pudo alterar el flujo de ejecución.

Los debuggers permiten ejecutar instrucción por instrucción, entrar en llamadas, salir de funciones y detenerse en direcciones específicas. Estas operaciones tienen sentido cuando entendemos que todo gira alrededor del puntero de instrucción.

5.5 Memoria, direcciones y punteros

Una dirección identifica una ubicación de memoria. Un puntero es un valor que contiene una dirección. En ensamblador, muchas instrucciones trabajan directamente con direcciones o con registros que apuntan a memoria.

Es importante distinguir:

  • El valor inmediato escrito en una instrucción.
  • El contenido de un registro.
  • La memoria apuntada por un registro.
  • La dirección de una función, cadena, estructura o buffer.

Por ejemplo, en notación Intel, mov rax, rbx copia el valor de RBX en RAX, mientras que mov rax, [rbx] lee memoria en la dirección guardada por RBX y la copia en RAX.

5.6 Sintaxis Intel y AT&T

El ensamblador puede mostrarse con distintas sintaxis. Las más comunes son Intel y AT&T. Muchos desensambladores en Windows usan sintaxis Intel, mientras que herramientas en entornos Unix pueden mostrar AT&T.

Aspecto Intel AT&T
Orden de operandos destino, origen origen, destino
Registros rax, rcx %rax, %rcx
Inmediatos 10 $10
Acceso a memoria [rax] (%rax)

En este curso usaremos principalmente sintaxis Intel porque suele aparecer en debuggers y herramientas de reversing orientadas a Windows.

5.7 Instrucciones de movimiento de datos

Muchas instrucciones no hacen cálculos complejos: mueven datos entre registros, memoria y valores inmediatos. Reconocerlas permite seguir cómo un programa prepara argumentos, copia estructuras o guarda resultados.

  • mov: copia un valor desde origen hacia destino.
  • lea: calcula una dirección efectiva y la guarda en un registro.
  • push: coloca un valor en el stack.
  • pop: extrae un valor del stack.
  • xchg: intercambia valores.

lea suele confundir al principio. Aunque significa "load effective address", también puede aparecer como forma eficiente de hacer ciertos cálculos aritméticos simples.

5.8 Instrucciones aritméticas y lógicas

Las instrucciones aritméticas y lógicas modifican valores y actualizan flags. Aparecen en contadores, validaciones, decodificación, cifrado simple, cálculo de offsets y manipulación de bits.

Instrucción Uso general Ejemplo de lectura
add / sub Sumar o restar Actualizar contador, tamaño u offset
inc / dec Incrementar o decrementar Avanzar en un bucle
xor Operación lógica OR exclusiva Limpiar registro o codificar datos
and / or Operaciones bit a bit Máscaras, flags y configuración
shl / shr Desplazamientos de bits Cálculos rápidos, empaquetado o desempaquetado

5.9 Flags y comparación

El registro de flags guarda resultados secundarios de operaciones: si un resultado fue cero, si hubo acarreo, si fue negativo, si ocurrió overflow, entre otros. Las instrucciones de salto condicional dependen de esos flags.

Dos instrucciones aparecen constantemente:

  • cmp: compara dos valores restándolos internamente sin guardar el resultado.
  • test: aplica una operación lógica AND sin guardar el resultado, útil para evaluar cero o bits.

Después de una comparación, un salto condicional decide si cambia el flujo. Por eso, muchas validaciones de malware se ven como una secuencia de comparación seguida de salto.

5.10 Saltos y control de flujo

Los saltos cambian la dirección de ejecución. Pueden ser incondicionales o depender de flags. Entenderlos permite reconstruir decisiones, bucles y caminos alternativos.

Instrucción Significado general Uso común
jmp Salto incondicional Cambiar de bloque de código
je / jz Salta si es igual o cero Validación exitosa o valor nulo
jne / jnz Salta si no es igual o no es cero Validación fallida o bucle activo
jg / jl Mayor que / menor que con signo Comparaciones numéricas
ja / jb Mayor que / menor que sin signo Tamaños, offsets y valores binarios

5.11 Funciones, call y ret

Una llamada a función transfiere ejecución a otra rutina y conserva una dirección de retorno para volver al punto siguiente. En ensamblador, esto se observa principalmente con call y ret.

  • call: guarda la dirección de retorno y salta a una función.
  • ret: toma la dirección de retorno y vuelve al llamador.
  • Los argumentos pueden pasarse por stack o registros, según arquitectura y convención.
  • El resultado de una función suele volver en EAX/RAX u otro registro definido por la convención.

En reversing, identificar funciones ayuda a nombrar bloques de comportamiento. En explotación, una dirección de retorno corrompida puede explicar un crash o desvío de flujo.

5.12 Stack y frames de función

El stack es una región de memoria usada para llamadas, retornos, variables locales y datos temporales. Crece y decrece siguiendo operaciones de push, pop, call, ret y ajustes del puntero de stack.

Un frame de función es el espacio que una función usa dentro del stack. Puede contener dirección de retorno, valores guardados, variables locales y datos temporales.

Muchos errores clásicos de explotación se entienden mejor al observar cómo una escritura fuera de límites afecta el stack o datos cercanos.

Los compiladores modernos optimizan el uso del stack, por lo que no siempre veremos frames tradicionales con RBP/EBP. Aun así, el concepto sigue siendo fundamental.

5.13 Heap y memoria dinámica

El heap es memoria dinámica solicitada durante la ejecución. Programas que manejan entradas variables, objetos, buffers o estructuras complejas suelen usar heap intensivamente.

Para análisis de malware, el heap puede contener datos desempaquetados, configuración, URLs, claves o payloads temporales. Para explotación, errores como use-after-free, double free o heap overflow dependen de cómo se administra esa memoria.

En un debugger, revisar heap puede revelar información que no aparece en el archivo original porque se genera, descifra o descarga durante la ejecución.

5.14 Convenciones de llamada

Una convención de llamada define cómo se pasan argumentos, quién limpia el stack, qué registros deben preservarse y dónde se devuelve el resultado. Esto permite que funciones compiladas separadamente cooperen.

Convención Contexto habitual Detalle relevante
cdecl x86, C clásico Argumentos por stack; el llamador limpia
stdcall x86, APIs de Windows Argumentos por stack; la función llamada limpia
fastcall x86, optimizaciones Algunos argumentos pasan por registros
Microsoft x64 Windows de 64 bits Primeros argumentos en RCX, RDX, R8 y R9
System V AMD64 Linux y Unix de 64 bits Primeros argumentos en RDI, RSI, RDX, RCX, R8 y R9

Conocer la convención permite interpretar qué valores está recibiendo una función justo antes de una llamada.

5.15 Endianness y representación de datos

En x86/x64 se usa little endian: los bytes menos significativos se almacenan primero en memoria. Esto puede parecer extraño cuando se observan direcciones, enteros o valores hexadecimales en un volcado.

Por ejemplo, el valor hexadecimal 0x12345678 puede verse en memoria como 78 56 34 12. Entender esto evita interpretaciones incorrectas al leer buffers, firmas, direcciones o datos serializados.

En análisis de malware, la representación de datos importa al buscar claves, constantes, cabeceras, direcciones IP codificadas o estructuras internas.

5.16 Syscalls y APIs

Las aplicaciones suelen llamar APIs de alto nivel, y esas APIs eventualmente solicitan servicios al sistema operativo mediante llamadas al sistema. En análisis, podemos observar ambos niveles según la herramienta.

  • Una llamada API puede mostrar intención de alto nivel, como crear un archivo o abrir una conexión.
  • Una syscall muestra interacción más cercana con el kernel.
  • Algunas muestras resuelven APIs dinámicamente para dificultar el análisis estático.
  • Otras intentan llamar funciones de bajo nivel para evadir hooks o monitoreo.

El analista debe relacionar instrucciones, llamadas y comportamiento observable. Ver una API importada no prueba que haya sido ejecutada; verla en una traza dinámica sí aporta evidencia más directa.

5.17 Patrones comunes en malware

Algunos patrones de ensamblador aparecen con frecuencia durante el análisis de malware, aunque el detalle cambia entre compiladores, lenguajes y ofuscadores.

  • Bucles que recorren buffers para decodificar cadenas.
  • Llamadas dinámicas para resolver funciones por nombre o hash.
  • Comparaciones contra nombres de procesos, dominios o artefactos de VM.
  • Asignación de memoria con permisos de ejecución.
  • Copias de datos hacia otro proceso o región de memoria.
  • Saltos condicionales que activan o evitan comportamiento según el entorno.

Reconocer patrones no reemplaza el análisis. Sirve para orientar preguntas: qué datos procesa, qué condición evalúa, qué función llama y qué efecto produce.

5.18 Patrones relevantes para explotación

En explotación controlada, el ensamblador ayuda a entender qué ocurrió en un fallo. El objetivo inicial suele ser interpretar el estado, no construir inmediatamente un exploit funcional.

  • Crash por acceso a dirección inválida.
  • Desbordamiento que altera datos cercanos.
  • Excepción al ejecutar una región sin permiso de ejecución.
  • Corrupción de punteros, objetos o direcciones de retorno.
  • Control parcial o total de valores que llegan a registros críticos.
  • Comportamiento distinto por ASLR, DEP u otras mitigaciones.

La lectura cuidadosa del estado del proceso ayuda a diferenciar una falla reproducible de una condición realmente explotable.

5.19 Cómo leer un bloque de ensamblador

Al enfrentar un bloque desconocido, conviene aplicar una lectura ordenada.

  1. Identificar dónde empieza y termina el bloque lógico.
  2. Observar qué registros o memoria recibe como entrada.
  3. Buscar llamadas a funciones conocidas o APIs.
  4. Reconocer comparaciones y saltos condicionales.
  5. Seguir bucles y detectar qué dato procesan.
  6. Identificar qué valores salen como resultado.
  7. Renombrar mentalmente funciones o variables según su comportamiento.

Una buena práctica es no intentar entender todo a la vez. Primero se ubica la intención general; luego se profundiza en las partes que explican el comportamiento importante.

5.20 Ejemplo conceptual de validación

Una validación simple puede verse como una comparación seguida de un salto. Conceptualmente:

cmp rax, 0
je  etiqueta_error
call funcion_principal

La lectura sería: se compara RAX contra cero; si el resultado es igual, se salta a una rutina de error; si no, se continúa llamando a la función principal.

Este patrón aparece en comprobaciones de punteros, resultados de API, validaciones de entorno y decisiones internas. El valor está en relacionar la comparación con el dato que llega a RAX y con el comportamiento posterior.

5.21 Checklist para debugging inicial

  1. Identificar arquitectura: x86 o x64.
  2. Observar RIP/EIP y módulo donde ocurre la ejecución.
  3. Revisar registros generales y valores de argumentos.
  4. Inspeccionar stack cercano al puntero actual.
  5. Buscar llamadas recientes y dirección de retorno.
  6. Revisar permisos de memoria en regiones relevantes.
  7. Relacionar instrucciones con eventos del sistema observados.
  8. Registrar hipótesis antes de modificar la ejecución.

5.22 Errores frecuentes al comenzar

  • Intentar memorizar instrucciones en vez de entender patrones.
  • Confundir el valor de un registro con la memoria apuntada por ese registro.
  • Ignorar la convención de llamada y leer argumentos incorrectos.
  • Interpretar una API importada como si necesariamente se hubiera ejecutado.
  • No diferenciar x86 de x64 durante análisis de stack y registros.
  • Pasar por alto flags y saltos condicionales.
  • Modificar el flujo en el debugger sin registrar el estado original.

5.23 Qué debes recordar de este tema

  • El ensamblador permite observar el programa en la forma más cercana a la ejecución real.
  • Registros, memoria, stack, flags, llamadas y saltos son la base de la lectura.
  • x86 y x64 cambian registros, direcciones y convenciones de llamada.
  • RIP/EIP indica dónde está el flujo de ejecución en cada momento.
  • Para malware y explotación, importa más reconocer patrones que memorizar instrucciones aisladas.

5.24 Conclusión

La arquitectura x86/x64 y el ensamblador básico son herramientas de lectura. Nos permiten mirar debajo del código fuente, interpretar decisiones internas, entender fallos y conectar comportamiento observado con instrucciones concretas.

En el próximo tema estudiaremos formatos ejecutables como PE y ELF, sus secciones, imports, exports y metadatos, porque el análisis de un binario comienza antes de ejecutarlo.