Intermedioensambladorx86control-flowreverse-engineeringpatrones-compilador

Flujo de Control: Loops, Condicionales y Switch en Ensamblador

Como los compiladores traducen if/else, for, while, do-while y switch/case a ensamblador x86/x64. Patrones de reconocimiento, jump tables, y como el malware ofusca el flujo de control para dificultar el analisis.

MalwareIntel Research··10 min lectura
Serie: Lenguaje Ensamblador — Parte 5

Reconocer estructuras de alto nivel en ensamblador

Cuando un compilador traduce codigo C o C++ a ensamblador, las estructuras de control (if, for, while, switch) se convierten en secuencias de comparaciones y saltos. Reconocer estos patrones te permite reconstruir mentalmente la logica del programa original mientras lees ensamblador en Ghidra o IDA.

Los decompiladores automatizan este proceso, pero no siempre aciertan. El malware ofuscado o el codigo optimizado agresivamente puede confundir al decompilador. Conocer los patrones a nivel de ensamblador te da la capacidad de verificar y corregir la salida del decompilador.

If / else

Patron basico

Un if simple se traduce a una comparacion seguida de un salto condicional. El compilador invierte la condicion: si el codigo fuente dice "if (x == 5)", el ensamblador salta cuando x NO es 5 (para saltar por encima del bloque if).

Codigo fuente equivalente:

if (valor == 5)
    accion_a();

Ensamblador generado:

cmp dword [ebp-0x04], 5    ; compara variable local con 5
jne despues_if              ; si NO es igual, salta el bloque if
call accion_a               ; solo se ejecuta si valor == 5
despues_if:

If / else completo

if (resultado != 0)
    exito();
else
    error();
test eax, eax              ; resultado de la funcion anterior
jz  bloque_else            ; si es cero, salta al else
call exito                 ; bloque if: resultado no era cero
jmp despues_ifelse         ; salta el else
bloque_else:
call error                 ; bloque else: resultado era cero
despues_ifelse:

El patron clave es: salto condicional al else, codigo del if, JMP incondicional despues del else, codigo del else. Este patron de dos saltos (condicional + incondicional) es la marca de un if/else.

If con condiciones compuestas

if (a > 10 && b < 20)
    accion();

El AND logico se traduce a dos comparaciones con cortocircuito:

cmp dword [ebp-0x04], 10   ; a > 10?
jle despues_if              ; si a <= 10, no evaluar b (cortocircuito)
cmp dword [ebp-0x08], 20   ; b < 20?
jge despues_if              ; si b >= 20, no ejecutar accion
call accion
despues_if:

Con OR logico, la estructura se invierte:

if (a == 1 || a == 2)
    accion();
cmp dword [ebp-0x04], 1
je  ejecutar_accion         ; si a == 1, ejecutar directamente
cmp dword [ebp-0x04], 2
jne despues_if              ; si tampoco es 2, saltar
ejecutar_accion:
call accion
despues_if:

If anidados

Los if anidados producen cascadas de saltos condicionales. En Ghidra, el grafo de control flow muestra esto como una cadena de diamantes (cada condicion bifurca el flujo). Si ves muchos saltos condicionales consecutivos sin JMP incondicionales entre ellos, probablemente son condiciones anidadas o encadenadas.

Loops: while, do-while, for

while loop

while (contador > 0) {
    procesar(buffer[i]);
    contador--;
}

El compilador puede generar dos variantes:

Variante 1: comparacion al inicio (pre-test loop)

jmp test_condicion         ; salta a la condicion primero
cuerpo_loop:
  mov eax, [esi]           ; procesar(buffer[i])
  push eax
  call procesar
  add esp, 4
  dec dword [ebp-0x04]     ; contador--
test_condicion:
  cmp dword [ebp-0x04], 0  ; contador > 0?
  jg  cuerpo_loop          ; si es mayor, volver al cuerpo

Variante 2: duplicacion de la condicion

  cmp dword [ebp-0x04], 0  ; verificar antes de entrar
  jle fin_loop              ; si ya es 0 o negativo, no entrar
cuerpo_loop:
  ; ... cuerpo del loop ...
  dec dword [ebp-0x04]
  cmp dword [ebp-0x04], 0
  jg  cuerpo_loop           ; verificar al final
fin_loop:

do-while loop

do {
    byte = leer_byte(stream);
} while (byte != 0);
cuerpo_loop:
  push dword [ebp-0x08]    ; stream
  call leer_byte
  add esp, 4
  mov [ebp-0x04], al       ; byte = resultado
  test al, al              ; byte != 0?
  jnz cuerpo_loop          ; si no es cero, repetir

El do-while es el loop mas simple en ensamblador: el cuerpo, una comparacion y un salto hacia atras. No hay salto de entrada ni verificacion previa. En malware, los loops de descifrado suelen ser do-while porque el programador sabe que el buffer tiene al menos un byte.

for loop

for (int i = 0; i < longitud; i++) {
    buffer[i] ^= clave;
}
  xor ecx, ecx              ; i = 0
  jmp test_for               ; salta a la condicion
cuerpo_for:
  mov al, [esi+ecx]          ; buffer[i]
  xor al, [ebp-0x04]         ; ^= clave
  mov [esi+ecx], al          ; guarda resultado
  inc ecx                    ; i++
test_for:
  cmp ecx, [ebp-0x08]        ; i < longitud?
  jl  cuerpo_for             ; si es menor, continuar

El for y el while generan ensamblador practicamente identico. La unica pista para distinguirlos es que en el for, la inicializacion (XOR ECX, ECX) y el incremento (INC ECX) estan mas agrupados con la comparacion. Pero esto no es fiable: el decompilador elige for o while segun heuristicas internas.

Loops con instruccion LOOP

La instruccion LOOP es una instruccion legada que decrementa ECX y salta si ECX no es cero:

mov ecx, 256              ; 256 iteraciones
bucle:
  xor byte [esi], 0x5A    ; operacion del loop
  inc esi
  loop bucle              ; ECX--; si ECX != 0, saltar a bucle

LOOP es mas lenta que DEC ECX + JNZ en procesadores modernos, por lo que los compiladores actuales no la generan. Pero aparece en shellcode escrito a mano y en malware antiguo. Si ves LOOP en un binario moderno, es probable que esa seccion fue escrita manualmente en ensamblador.

Switch / case

Los compiladores traducen switch/case de tres formas segun el numero de cases y la densidad de los valores.

Pocos cases: cadena if-else

Con 3-4 cases, el compilador genera comparaciones secuenciales:

switch (tipo) {
    case 1: handler_tcp(); break;
    case 2: handler_udp(); break;
    case 3: handler_icmp(); break;
    default: handler_unknown();
}
cmp eax, 1
je  case_tcp
cmp eax, 2
je  case_udp
cmp eax, 3
je  case_icmp
jmp case_default

case_tcp:
  call handler_tcp
  jmp fin_switch
case_udp:
  call handler_udp
  jmp fin_switch
case_icmp:
  call handler_icmp
  jmp fin_switch
case_default:
  call handler_unknown
fin_switch:

Muchos cases densos: jump table

Cuando hay muchos cases con valores consecutivos o casi consecutivos, el compilador genera una jump table (tabla de saltos):

switch (comando) {
    case 0: cmd_ping(); break;
    case 1: cmd_shell(); break;
    case 2: cmd_download(); break;
    case 3: cmd_upload(); break;
    case 4: cmd_screenshot(); break;
    case 5: cmd_keylog(); break;
}
cmp eax, 5                     ; verificar rango valido
ja  case_default               ; si es mayor que 5, default
jmp dword [tabla_saltos + eax*4]  ; salta via la tabla

; Tabla de saltos (en .rdata o .text)
tabla_saltos:
  dd offset case_0_ping        ; comando 0
  dd offset case_1_shell       ; comando 1
  dd offset case_2_download    ; comando 2
  dd offset case_3_upload      ; comando 3
  dd offset case_4_screenshot  ; comando 4
  dd offset case_5_keylog      ; comando 5

Las jump tables son particularmente interesantes en malware porque revelan la tabla de comandos del C2. Si encuentras una jump table con 10-20 entradas, cada entrada es un handler de un comando diferente. Los RATs tipicamente usan jump tables para despachar los comandos recibidos del servidor C2.

Para reconocer una jump table en IDA o Ghidra:

  • Busca un CMP seguido de JA (verificacion de rango)
  • Busca un JMP con direccionamiento [base + reg*4]
  • En la seccion .rdata, busca un array de dwords que apuntan a la seccion .text

Cases dispersos: arbol binario

Si los valores de los cases son muy dispersos (por ejemplo 1, 100, 500, 9999), el compilador puede generar un arbol de busqueda binaria con comparaciones anidadas, o una tabla de lookup con indices.

Patrones del compilador que debes reconocer

Operador ternario

resultado = (condicion) ? valor_a : valor_b;
test eax, eax
cmovnz ecx, edx    ; si EAX != 0, ECX = EDX (sin salto)

La instruccion CMOVcc (Conditional Move) evita un salto condicional. Es mas eficiente porque no rompe la prediccion de ramas del procesador. Si ves CMOVcc, generalmente es un operador ternario o un min/max.

Min y max

menor = (a < b) ? a : b;
cmp eax, ecx        ; compara a y b
cmovg eax, ecx      ; si a > b, eax = b (se queda con el menor)

Comparacion con cero optimizada

Los compiladores nunca generan cmp eax, 0. Siempre usan test eax, eax que tiene el mismo efecto pero es 1 byte mas corto.

Multiplicacion por constantes

En lugar de IMUL, los compiladores generan combinaciones de LEA, SHL y ADD:

; Multiplicar por 3
lea eax, [ecx+ecx*2]       ; eax = ecx * 3

; Multiplicar por 5
lea eax, [ecx+ecx*4]       ; eax = ecx * 5

; Multiplicar por 10
lea eax, [ecx+ecx*4]       ; eax = ecx * 5
add eax, eax               ; eax = eax * 2 = ecx * 10

; Multiplicar por 7
lea eax, [ecx*8]            ; eax = ecx * 8
sub eax, ecx               ; eax = ecx * 8 - ecx = ecx * 7

Division por constantes

En lugar de DIV (que es lenta), los compiladores usan multiplicacion por el recirproco seguida de un desplazamiento:

; Division de EAX entre 10
mov ecx, 0xCCCCCCCD         ; "magic number" reciproco de 10
mul ecx                      ; EDX:EAX = EAX * 0xCCCCCCCD
shr edx, 3                   ; EDX = cociente (EAX / 10)

Si ves un MUL con una constante grande seguido de un SHR de EDX, es una division optimizada. Los decompiladores suelen reconocer este patron y mostrarlo como division.

Ofuscacion de flujo de control en malware

El malware avanzado usa tecnicas para dificultar el analisis del flujo de control:

Opaque predicates

Una condicion que siempre es true (o siempre false) pero parece una condicion real:

mov eax, [ebp-0x04]
imul eax, eax          ; eax = x * x
and eax, 1             ; x^2 es par si x es par, impar si x es impar
; Pero x*(x-1) siempre es par:
mov ecx, [ebp-0x04]
dec ecx
imul eax, ecx          ; eax = x * (x-1), siempre par
test eax, 1
jnz rama_falsa         ; NUNCA se toma (x*(x-1) siempre es par)
; codigo real aqui

Control flow flattening

Todos los bloques basicos se ponen al mismo nivel dentro de un gran switch/dispatch loop:

; Dispatcher
dispatch:
  mov eax, [estado]
  cmp eax, 1
  je bloque_1
  cmp eax, 2
  je bloque_2
  cmp eax, 3
  je bloque_3
  jmp fin

bloque_1:
  ; ... codigo ...
  mov dword [estado], 3   ; siguiente bloque = 3
  jmp dispatch

bloque_2:
  ; ... codigo ...
  mov dword [estado], 1
  jmp dispatch

bloque_3:
  ; ... codigo ...
  mov dword [estado], 2
  jmp dispatch

El flujo real es 1, 3, 2, pero mirando el ensamblador linealmente es imposible determinarlo sin trazar la variable de estado. El decompilador muestra un gran switch incomprensible. Herramientas como D810 (plugin IDA) y el script de deflattening de Ghidra pueden revertir parcialmente esta ofuscacion.

Saltos indirectos calculados

En lugar de JMP directo a una direccion, el malware calcula la direccion en runtime:

mov eax, 0x401000       ; direccion base
add eax, [ebp-0x04]     ; offset variable
xor eax, 0x12345678     ; descifra la direccion
jmp eax                 ; salta a direccion calculada

Este patron rompe el grafo de control flow porque el desensamblador no puede determinar estaticamente a donde salta el JMP.

Ejercicio de reconocimiento

Al analizar malware, practica identificar estas estructuras:

  1. Busca pares CMP/TEST + Jcc: cada par es una decision (if, loop, switch)
  2. Busca saltos hacia atras: cada salto hacia atras es un loop
  3. Busca JMP incondicional despues de un bloque de codigo: marca el fin de un bloque if o un case del switch
  4. Busca JMP [reg + reg4] o JMP [tabla + reg4]: jump tables de switch
  5. Busca secuencias de CMP + JE con el mismo registro: cadena if-else o switch con pocos cases

Con practica, estos patrones se vuelven automaticos y puedes reconstruir la logica de alto nivel mentalmente mientras recorres el desensamblado.

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.