Principianteensambladorx86instruccionesreverse-engineeringfundamentos

Instrucciones Basicas: MOV, ADD, SUB, CMP, JMP y Variantes

Referencia practica de las instrucciones x86/x64 mas frecuentes en malware: movimiento de datos (MOV, LEA, XCHG), aritmetica (ADD, SUB, MUL, DIV), comparacion (CMP, TEST) y saltos (JMP, JE, JNE, JG, JL). Ejemplos reales de malware.

MalwareIntel Research··14 min lectura
Serie: Lenguaje Ensamblador — Parte 3

Las instrucciones que componen el 90% del malware

El set de instrucciones x86 contiene cientos de opcodes diferentes. Pero cuando analizas malware real, un subconjunto reducido aparece una y otra vez. Este articulo cubre las instrucciones fundamentales que necesitas dominar para leer ensamblador con fluidez: movimiento de datos, aritmetica, comparacion y saltos.

Cada instruccion se presenta con su comportamiento exacto, los flags que modifica y ejemplos de como aparece en malware real.

Instrucciones de movimiento de datos

MOV: la instruccion mas comun

MOV copia un valor del operando fuente al operando destino. El fuente no se modifica. MOV no afecta ningun flag.

mov eax, ebx          ; registro a registro: EAX = EBX
mov eax, 0x401000     ; inmediato a registro: EAX = 0x401000
mov eax, [ebp-0x4]    ; memoria a registro: EAX = valor en (EBP-4)
mov [ebp-0x8], ecx    ; registro a memoria: escribe ECX en (EBP-8)
mov dword [eax], 0    ; inmediato a memoria: escribe 0 en la direccion de EAX

Restriccion fundamental: MOV no permite memoria a memoria. No puedes escribir mov [eax], [ebx]. Si necesitas copiar de una direccion de memoria a otra, necesitas un registro intermedio o usar MOVSB/MOVSD.

En malware, MOV es omnipresente. Se usa para preparar parametros de funciones, guardar valores de retorno, acceder a campos de estructuras y manipular datos en general.

LEA: Load Effective Address

LEA calcula una direccion de memoria pero no accede a ella. El resultado es la direccion misma, no el contenido de esa direccion.

lea eax, [ebp-0x40]   ; EAX = direccion de la variable local en EBP-0x40
lea ecx, [eax+ebx*4]  ; ECX = EAX + EBX*4 (aritmetica rapida, no acceso a memoria)
lea edx, [eax+eax*2]  ; EDX = EAX * 3 (truco del compilador para multiplicar por 3)

LEA es una de las instrucciones mas versatiles y a veces confusas. Los compiladores la usan de dos formas:

Para obtener la direccion de una variable (puntero): lea eax, [ebp-0x40] carga la direccion de la variable local en EAX, equivalente a &variable en C.

Como aritmetica rapida: lea eax, [ecx+ecx*4] multiplica ECX por 5 en una sola instruccion. Es mas rapido que la secuencia MOV+IMUL equivalente. Los compiladores generan LEA frecuentemente para multiplicaciones por constantes pequenas (3, 5, 9).

LEA no modifica ningun flag, a diferencia de ADD o MUL.

MOVZX y MOVSX: extension de tamano

MOVZX (Move with Zero-Extend) copia un valor mas pequeno a un registro mas grande, rellenando con ceros:

movzx eax, byte [esi]   ; lee 1 byte, lo extiende a 32 bits con ceros
movzx ecx, word [edi]   ; lee 2 bytes, extiende a 32 bits con ceros

MOVSX (Move with Sign-Extend) hace lo mismo pero preservando el signo (rellena con el bit de signo):

movsx eax, byte [esi]   ; si el byte es 0xFF (-1), EAX = 0xFFFFFFFF (-1)

En malware, MOVZX aparece constantemente en loops que procesan strings byte a byte. Cuando ves movzx eax, byte [esi] seguido de operaciones sobre EAX, el codigo esta leyendo caracteres individuales.

XCHG: intercambio

XCHG intercambia los valores de dos operandos:

xchg eax, ebx    ; EAX recibe el valor de EBX, EBX recibe el de EAX

XCHG EAX, EAX es equivalente a NOP (opcode 0x90). XCHG con un operando de memoria es atomico (incluye un LOCK implicito), lo que lo hace util en codigo multihilo.

PUSH y POP: operaciones de pila

PUSH decrementa ESP en 4 (o 8 en x64) y escribe el valor en el tope de la pila. POP lee el valor del tope y incrementa ESP.

push eax          ; ESP -= 4; [ESP] = EAX
push 0x401000     ; ESP -= 4; [ESP] = 0x401000
pop ebx           ; EBX = [ESP]; ESP += 4

En malware de 32 bits, la secuencia de PUSHes antes de un CALL son los parametros de la funcion (ultimo parametro primero, segun la convencion cdecl/stdcall):

push 0                    ; dwFlagsAndAttributes = 0
push 0                    ; hTemplateFile = NULL
push 3                    ; dwCreationDisposition = OPEN_EXISTING
push 0                    ; lpSecurityAttributes = NULL
push 1                    ; dwShareMode = FILE_SHARE_READ
push 0x80000000           ; dwDesiredAccess = GENERIC_READ
push offset szFilename    ; lpFileName
call CreateFileA

Instrucciones aritmeticas

ADD y SUB

ADD suma fuente a destino y guarda el resultado en destino. SUB resta fuente de destino. Ambas actualizan flags (ZF, SF, CF, OF).

add eax, 1        ; EAX = EAX + 1
add eax, ebx      ; EAX = EAX + EBX
sub ecx, 0x10     ; ECX = ECX - 16

En malware, ADD y SUB se usan para manipular punteros (avanzar por un buffer), modificar contadores y realizar calculos de direcciones.

INC y DEC

INC incrementa en 1. DEC decrementa en 1. Actualizan ZF, SF y OF pero NO modifican CF (diferencia con ADD/SUB).

inc eax           ; EAX = EAX + 1 (no modifica CF)
dec ecx           ; ECX = ECX - 1 (no modifica CF)

Son comunes en loops de descifrado:

xor_loop:
  xor byte [esi], 0x5A
  inc esi           ; avanza al siguiente byte
  dec ecx           ; decrementa contador
  jnz xor_loop      ; continua si ECX != 0

MUL e IMUL: multiplicacion

MUL multiplica sin signo. El resultado se almacena en EDX:EAX (64 bits):

mov eax, 100
mov ecx, 200
mul ecx           ; EDX:EAX = EAX * ECX = 20000

IMUL tiene tres formas y es mas flexible:

imul eax, ebx        ; EAX = EAX * EBX (resultado truncado a 32 bits)
imul eax, ebx, 10    ; EAX = EBX * 10
imul ecx             ; EDX:EAX = EAX * ECX (con signo)

En malware, IMUL aparece en funciones de hashing. El hash de API (tecnica donde el malware calcula un hash del nombre de la funcion en lugar de importarla directamente) frecuentemente usa multiplicaciones como parte del algoritmo de hash.

DIV e IDIV: division

DIV divide EDX:EAX entre el operando. EAX recibe el cociente, EDX el resto:

xor edx, edx      ; limpia EDX (parte alta del dividendo)
mov eax, 100
mov ecx, 7
div ecx            ; EAX = 100/7 = 14, EDX = 100%7 = 2

La instruccion xor edx, edx antes de DIV es un patron constante. Sin ella, EDX contendria basura que alteraria el resultado de la division. Si ves DIV sin el XOR EDX previo, es un bug o codigo intencionalmente confuso.

NEG: negacion

NEG cambia el signo del operando (complemento a dos):

neg eax            ; EAX = -EAX (0 - EAX)

En malware, NEG seguido de SBB (Subtract with Borrow) es un patron de compilador para convertir un valor a 0 o -1 basado en si era cero o no.

Instrucciones logicas

AND, OR, XOR

Operaciones bit a bit. Actualizan flags y ponen CF y OF a cero.

and eax, 0xFF      ; mascara: conserva solo el byte bajo de EAX
or  eax, 0x80      ; activa el bit 7 de EAX
xor eax, eax       ; pone EAX a cero (mas rapido que MOV EAX, 0)
xor byte [esi], 0x5A  ; descifrado XOR de un byte

XOR de un registro consigo mismo (xor eax, eax) es la forma estandar de poner un registro a cero. Es mas corto (2 bytes vs 5 de MOV EAX, 0) y mas rapido en procesadores modernos.

XOR es la operacion mas importante en malware para cifrado y ofuscacion. Un bucle XOR con una clave fija o rotativa es el metodo de ofuscacion mas comun en samples reales.

NOT: complemento

NOT invierte todos los bits del operando:

not eax            ; EAX = ~EAX (complemento a uno)

SHL, SHR, SAR, ROL, ROR: desplazamientos y rotaciones

shl eax, 4         ; desplazamiento izquierda: EAX = EAX * 16
shr eax, 1         ; desplazamiento derecha (sin signo): EAX = EAX / 2
sar eax, 1         ; desplazamiento derecha (con signo): preserva bit de signo
rol eax, 8         ; rotacion izquierda: rota bits 8 posiciones
ror eax, 13        ; rotacion derecha: rota bits 13 posiciones

ROL y ROR aparecen frecuentemente en algoritmos de hashing de malware. El famoso hash de API ROR13 (usado por Metasploit y muchos malware) rota cada byte del nombre de la funcion 13 posiciones a la derecha.

Instrucciones de comparacion

CMP: comparacion por resta

CMP resta fuente de destino y actualiza flags, pero descarta el resultado. Los operandos no se modifican.

cmp eax, 10        ; calcula EAX - 10, actualiza flags
je  igual_a_diez   ; salta si ZF=1 (EAX era 10)

cmp ecx, edx       ; calcula ECX - EDX, actualiza flags
jg  ecx_mayor      ; salta si ECX era mayor que EDX (con signo)

CMP es la instruccion de comparacion principal. Siempre aparece inmediatamente antes de un salto condicional.

TEST: comparacion por AND

TEST realiza un AND logico y actualiza flags, descartando el resultado.

test eax, eax      ; AND de EAX consigo mismo
jz  es_cero        ; salta si EAX era 0

test eax, 0x80     ; verifica si el bit 7 esta activo
jnz bit_activo     ; salta si el bit 7 era 1

El patron test eax, eax seguido de jz o jnz es ubicuo en malware. Es la forma que usan los compiladores para verificar si el valor de retorno de una funcion es cero (error) o no cero (exito).

call CreateFileA
test eax, eax       ; Realmente: cmp eax, INVALID_HANDLE_VALUE (-1)
jz   error_handler  ; Si handle invalido, maneja el error

Un matiz: INVALID_HANDLE_VALUE es -1 (0xFFFFFFFF), que no es cero. Pero TEST EAX, EAX no seria cero para -1. El compilador a veces usa cmp eax, 0xFFFFFFFF o cmp eax, -1 para este caso especifico. Identifica que API se llamo para interpretar correctamente la verificacion del valor de retorno.

Instrucciones de salto

JMP: salto incondicional

JMP cambia EIP/RIP a la direccion especificada. Siempre salta, sin condicion.

jmp 0x401050       ; salta a la direccion absoluta 0x401050
jmp short etiqueta ; salto corto (offset de 1 byte, rango -128 a +127)
jmp eax            ; salto indirecto: salta a la direccion en EAX
jmp [eax+ecx*4]    ; salto via tabla: lee la direccion de memoria y salta

JMP indirecto (jmp eax, jmp [eax+ecx*4]) es comun en:

  • Tablas de salto generadas por switch/case (jump tables)
  • Shellcode que calcula la direccion de destino en runtime
  • Ofuscacion de flujo de control

Saltos condicionales

Los saltos condicionales leen los flags puestos por la instruccion anterior (CMP, TEST, ADD, SUB, etc.) y saltan solo si la condicion se cumple.

; Patron: comparar y saltar
cmp eax, 5
je  destino        ; salta si eax == 5 (ZF=1)
jne destino        ; salta si eax != 5 (ZF=0)
jg  destino        ; salta si eax >  5 (con signo, SF=OF y ZF=0)
jge destino        ; salta si eax >= 5 (con signo, SF=OF)
jl  destino        ; salta si eax <  5 (con signo, SF!=OF)
jle destino        ; salta si eax <= 5 (con signo, ZF=1 o SF!=OF)
ja  destino        ; salta si eax >  5 (sin signo, CF=0 y ZF=0)
jb  destino        ; salta si eax <  5 (sin signo, CF=1)

La distincion entre saltos con signo (JG, JL) y sin signo (JA, JB) es critica. Un valor como 0xFFFFFFFF es -1 con signo pero 4294967295 sin signo. JG lo trataria como menor que 5, JA como mayor.

CALL y RET: llamadas a funciones

CALL empuja la direccion de retorno al stack y salta a la funcion:

call 0x401000      ; push EIP+5 (tamaño de instruccion); jmp 0x401000
call eax           ; llamada indirecta: push EIP; jmp eax
call [eax+0x1C]    ; llamada via vtable: lee direccion de memoria y llama

RET saca la direccion de retorno del stack y salta a ella:

ret                ; pop EIP (vuelve al caller)
ret 0x10           ; pop EIP; ESP += 0x10 (limpia 16 bytes de parametros, stdcall)

CALL indirecto (call eax, call [eax+0x1C]) es muy comun en malware:

  • Llamadas a funciones resueltas dinamicamente (GetProcAddress retorna direccion en EAX, luego CALL EAX)
  • Llamadas a metodos virtuales en C++ (vtable)
  • Shellcode que calcula direcciones en runtime

NOP: no operation

NOP no hace nada. Su opcode es 0x90 (equivalente a XCHG EAX, EAX).

nop                ; no operation, 1 byte
nop dword [eax]    ; NOP multibyte (2-9 bytes de padding)

Los NOPs aparecen en malware por varias razones: alineacion de codigo, relleno de secciones parcheadas, NOP sleds en exploits (secuencias largas de 0x90 que garantizan que el shellcode se ejecute sin importar la direccion exacta de aterrizaje).

Instrucciones de cadena (string operations)

Las instrucciones de cadena operan sobre bloques de memoria usando ESI como fuente y EDI como destino:

; Copiar ECX bytes de [ESI] a [EDI]
mov esi, fuente
mov edi, destino
mov ecx, longitud
cld                ; direccion: incrementar (forward)
rep movsb          ; repeat move string byte

; Llenar ECX bytes en [EDI] con el valor de AL
mov edi, buffer
mov ecx, longitud
mov al, 0
cld
rep stosb          ; repeat store string byte (memset)

; Comparar ECX bytes entre [ESI] y [EDI]
mov esi, cadena1
mov edi, cadena2
mov ecx, longitud
cld
repe cmpsb         ; repeat while equal, compare string byte (memcmp)

En malware, REP MOVSB/MOVSD se usa para copiar shellcode desempaquetado a su ubicacion final. REP STOSB se usa para limpiar buffers (borrar evidencias en memoria). REPE CMPSB compara strings (verificacion de nombres de procesos, DLLs, etc.).

Instrucciones menos comunes pero relevantes

CDQ: Convert Double to Quad

CDQ extiende el signo de EAX a EDX:EAX. Si EAX es positivo, EDX = 0. Si EAX es negativo, EDX = 0xFFFFFFFF.

cdq                ; EDX = 0 si EAX >= 0, EDX = 0xFFFFFFFF si EAX es menor que 0
idiv ecx           ; division con signo de EDX:EAX entre ECX

CDQ antes de IDIV es tan comun como XOR EDX, EDX antes de DIV.

BSWAP: invertir bytes

BSWAP invierte el orden de los bytes de un registro de 32 bits (big-endian a little-endian o viceversa):

mov eax, 0x01020304
bswap eax           ; EAX = 0x04030201

En malware de red, BSWAP convierte entre el orden de bytes de red (big-endian) y el del procesador (little-endian). Si ves BSWAP cerca de operaciones con sockets, el malware esta convirtiendo direcciones IP o puertos.

CPUID: identificacion del procesador

CPUID devuelve informacion sobre el procesador. El malware la usa para:

  • Detectar maquinas virtuales (VMware, VirtualBox tienen strings identificables)
  • Verificar si el procesador soporta ciertas instrucciones (AES-NI, AVX)
  • Fingerprinting del hardware
mov eax, 1         ; funcion 1: feature flags
cpuid              ; resultados en EAX, EBX, ECX, EDX
test ecx, 0x80000000  ; verificar bit hypervisor (indica VM)
jnz es_vm

RDTSC: leer timestamp counter

RDTSC lee el contador de ciclos del procesador en EDX:EAX. El malware lo usa como medida de tiempo para detectar debuggers (si entre dos instrucciones pasan demasiados ciclos, alguien esta single-stepping).

rdtsc
mov esi, eax       ; guarda el timestamp
; ... codigo sospechoso ...
rdtsc
sub eax, esi       ; diferencia de ciclos
cmp eax, 0x100000  ; umbral: si paso demasiado tiempo
ja  anti_debug     ; probablemente hay un debugger

Reconocer patrones en la practica

Cuando abres un binario en Ghidra o IDA, no necesitas leer cada instruccion secuencialmente. Busca patrones de alto nivel:

Secuencias de PUSH seguidas de CALL: parametros de funcion en 32 bits. Cuenta los PUSHes para saber cuantos parametros recibe la funcion.

MOVs a RCX, RDX, R8, R9 seguidos de CALL: parametros de funcion en 64 bits.

XOR reg, reg: inicializacion a cero. Frecuente al inicio de funciones.

TEST EAX, EAX seguido de JZ/JNZ: verificacion de valor de retorno de API.

LEA con operandos complejos: aritmetica optimizada del compilador (multiplicaciones, calculos de offsets).

REP MOVSB/MOVSD: copia de bloques de memoria (shellcode, buffers).

Loop con XOR/ADD/ROL sobre bytes: descifrado o hashing.

El siguiente articulo profundiza en la pila, los stack frames, las convenciones de llamada y como los buffer overflows explotan la estructura del stack.

Preguntas frecuentes

Artículos relacionados

Este contenido tiene fines exclusivamente educativos y de investigación en ciberseguridad defensiva. No se proporcionan binarios maliciosos ni payloads ejecutables. El uso indebido de esta información es responsabilidad exclusiva del usuario. Leer disclaimer completo.