Tema 5
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.
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.
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 |
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 |
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.
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:
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.
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.
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.
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 |
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.
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 |
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.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.
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.
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.
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.
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.
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.
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.
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.
Algunos patrones de ensamblador aparecen con frecuencia durante el análisis de malware, aunque el detalle cambia entre compiladores, lenguajes y ofuscadores.
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.
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.
La lectura cuidadosa del estado del proceso ayuda a diferenciar una falla reproducible de una condición realmente explotable.
Al enfrentar un bloque desconocido, conviene aplicar una lectura ordenada.
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.
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.
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.