Ofuscacion de Codigo: Tecnicas y Estrategias de Deobfuscacion
Tecnicas de ofuscacion de codigo en malware: cifrado de strings, control flow flattening, dead code insertion, opaque predicates, mixed boolean arithmetic. Estrategias y herramientas de deobfuscacion.
Ofuscacion: el arte de hacer ilegible lo funcional
Si el packing oculta el codigo completo, la ofuscacion lo transforma a nivel de instrucciones. Un binario desempaquetado pero ofuscado es funcional pero practicamente ilegible para un analista humano o un decompilador.
El malware moderno combina ambas tecnicas: un packer externo protege el binario, y dentro, la ofuscacion dificulta el analisis del codigo real. Los protectores comerciales como Themida integran ambas capas. El malware sofisticado de APTs implementa su propia ofuscacion custom.
Cifrado de strings
La tecnica de ofuscacion mas basica y mas comun. Los strings del malware (URLs de C2, nombres de procesos, claves de registro, mensajes) se almacenan cifrados y solo se descifran en el momento de usarlos.
Metodos comunes de cifrado de strings
| Metodo | Complejidad | Ejemplo |
|---|---|---|
| XOR con clave de un byte | Trivial | Cada byte XOR 0x37 |
| XOR con clave multibyte | Baja | XOR con "secretkey" ciclica |
| XOR + rotacion | Baja | XOR + ROL/ROR |
| ROT13 / Caesar cipher | Trivial | Desplazamiento fijo |
| Base64 + XOR | Baja | Doble capa simple |
| RC4 | Media | Stream cipher con clave variable |
| AES-CBC | Alta | Cifrado simetrico robusto |
| Custom | Variable | Algoritmo propio del autor |
Patron tipico en ensamblador
; String cifrada en .data o .rdata
encrypted_url: db 0x3A, 0x2F, 0x1C, 0x0B, ...
; Funcion de descifrado (XOR simple)
decrypt_string:
mov esi, [esp+4] ; puntero a string cifrada
mov ecx, [esp+8] ; longitud
xor edx, edx ; indice = 0
loop_start:
mov al, [esi+edx]
xor al, 0x37 ; clave XOR
mov [esi+edx], al
inc edx
cmp edx, ecx
jl loop_start
ret
Deobfuscacion de strings
FLOSS (FLARE Obfuscated String Solver):
FLOSS de Mandiant es la herramienta de referencia para strings ofuscadas. Usa emulacion para ejecutar las funciones de descifrado y extraer los strings resultantes:
floss sample.exe
# Output incluye:
# FLOSS decoded strings:
# http://c2server.evil/gate.php
# Software\Microsoft\Windows\CurrentVersion\Run
# cmd.exe /c whoami
Breakpoint en funcion de descifrado:
En x64dbg, identificar la funcion de descifrado, poner breakpoint al final (despues del loop) y loguear el contenido del buffer descifrado:
# x64dbg conditional log
bp decrypt_string_end
log "Decrypted: {s:eax}" ; o el registro que contiene el resultado
Scripting en Python:
Si el algoritmo es simple, reimplementarlo en Python:
def xor_decrypt(data, key):
result = bytearray()
for i, byte in enumerate(data):
result.append(byte ^ key[i % len(key)])
return result
encrypted = bytes([0x3A, 0x2F, 0x1C, 0x0B])
key = bytes([0x37])
print(xor_decrypt(encrypted, key))
Control Flow Flattening (CFF)
Control Flow Flattening transforma el grafo de flujo de control normal en una estructura plana con un dispatcher central.
Antes de CFF (flujo normal)
bloque_A:
cmp eax, 0
je bloque_C
jmp bloque_B
bloque_B:
... codigo ...
jmp bloque_D
bloque_C:
... codigo ...
jmp bloque_D
bloque_D:
... codigo final ...
El grafo tiene forma de arbol/red con conexiones directas entre bloques.
Despues de CFF (flujo aplanado)
dispatcher:
cmp state_var, 1
je bloque_A
cmp state_var, 2
je bloque_B
cmp state_var, 3
je bloque_C
cmp state_var, 4
je bloque_D
jmp exit
bloque_A:
... codigo ...
mov state_var, 3 ; siguiente = bloque_C si condicion
mov state_var, 2 ; siguiente = bloque_B si no
jmp dispatcher
bloque_B:
... codigo ...
mov state_var, 4
jmp dispatcher
bloque_C:
... codigo ...
mov state_var, 4
jmp dispatcher
bloque_D:
... codigo final ...
jmp exit
Ahora todos los bloques pasan por el dispatcher. El grafo de control en IDA o Ghidra se ve como una estrella con el dispatcher en el centro, destruyendo la estructura logica.
Deobfuscacion de CFF
D810 (plugin IDA): Automatiza la reconstruccion del flujo de control original.
Ejecucion simbolica: Herramientas como Miasm o angr pueden seguir el flujo a traves del dispatcher y reconstruir las transiciones reales entre bloques.
Manual: Identificar la variable de estado, trazar sus asignaciones y reconstruir el grafo original bloque por bloque.
Dead Code Insertion
Insertar instrucciones que no afectan el resultado del programa pero dificultan la lectura:
; Codigo real
mov eax, [ebp+8]
add eax, 10
; Con dead code insertado
mov eax, [ebp+8]
push ecx ; dead: se restaura antes de usarse
mov ecx, 0x12345 ; dead: valor nunca usado
xor ecx, ecx ; dead: ecx se borra
pop ecx ; dead: restaura valor original
add eax, 10
nop ; dead: no operation
lea ebx, [ebx+0] ; dead: no cambia nada
Las instrucciones insertadas son semanticamente neutras: no cambian registros que el codigo real use, no afectan flags relevantes, y no modifican memoria.
Deobfuscacion
Los decompiladores modernos (IDA Hex-Rays, Ghidra) eliminan automaticamente la mayoria del dead code durante la optimizacion de la decompilacion. El dead code que sobrevive suele ser instrucciones con side effects aparentes (como accesos a memoria que en realidad no se leen despues).
Opaque Predicates
Un opaque predicate es una condicion que siempre es verdadera (o siempre falsa) pero cuyo resultado no es obvio estaticamente:
; Siempre verdadero: x*x >= 0 para todo x entero
mov eax, [ebp-4] ; x
imul eax, eax ; x * x
cmp eax, 0
jge codigo_real ; siempre salta aqui
jmp codigo_falso ; nunca se ejecuta
; Siempre verdadero: (x * (x+1)) mod 2 == 0
mov eax, [ebp-4]
mov ecx, eax
inc ecx
imul eax, ecx
and eax, 1
jz codigo_real ; siempre salta aqui
El codigo_falso nunca se ejecuta pero confunde al decompilador, que no puede determinar estaticamente que la condicion es constante.
Variantes avanzadas
Opaque predicates basados en hash: El malware calcula un hash de una condicion del entorno (nombre del sistema, hora) que siempre produce el mismo resultado en el contexto real pero varia en sandboxes.
Opaque predicates con aliasing: Usan punteros que apuntan al mismo dato por diferentes vias, haciendo dificil para el analisis estatico determinar que la condicion es constante.
Mixed Boolean-Arithmetic (MBA)
MBA transforma expresiones aritmeticas simples en combinaciones de operaciones booleanas y aritmeticas equivalentes pero mucho mas complejas:
; Original: x + y
; MBA equivalente:
(x ^ y) + 2 * (x & y)
; Original: x - y
; MBA equivalente:
(x ^ y) - 2 * (~x & y)
; Original: x * 2
; MBA equivalente:
(x | x) + (x & x)
Las expresiones MBA son matematicamente correctas pero extremadamente dificiles de simplificar para un decompilador.
Deobfuscacion de MBA
SSPAM (Syntia): Herramienta de simplificacion de expresiones MBA.
Ejecucion simbolica + simplificacion: z3 (SMT solver) puede simplificar expresiones MBA si se le da el suficiente contexto.
Pattern matching: Scripts que reconocen patrones MBA comunes y los sustituyen por la forma simplificada.
Instruction Substitution
Reemplazar instrucciones simples por secuencias equivalentes mas largas:
| Original | Sustitucion |
|---|---|
mov eax, 0 | xor eax, eax |
add eax, 1 | sub eax, -1 o lea eax, [eax+1] o inc eax |
xor eax, key | not eax / and eax, key / or eax, key / combinaciones |
push eax | sub esp, 4 / mov [esp], eax |
call func | push return_addr / jmp func |
Metamorfismo
El malware metamorfico reescribe su propio codigo cada vez que se propaga, generando versiones funcionalmente equivalentes pero con instrucciones diferentes. A diferencia del polimorfismo (que cambia el stub de descifrado), el metamorfismo transforma el codigo real.
Tecnicas metamorficas:
- Reordenar bloques independientes
- Sustituir instrucciones por equivalentes
- Cambiar registros usados
- Insertar/eliminar dead code diferente en cada generacion
Ejemplos historicos: Win32.Simile, Win32.MetaPHOR.
Herramientas de deobfuscacion
FLOSS (Mandiant)
# Extraer strings ofuscadas
floss sample.exe --no-static-strings
# Solo strings decodificadas
floss sample.exe -n 6
IDA Pro con scripts
# Script IDA para descifrar strings XOR en un rango
import idautils
import idc
def xor_decrypt_range(start, end, key):
for addr in range(start, end):
byte = idc.get_wide_byte(addr)
idc.patch_byte(addr, byte ^ key)
# Descifrar seccion .data con clave 0x42
xor_decrypt_range(0x00404000, 0x00404100, 0x42)
Ghidra scripting
Ghidra permite escribir scripts en Java o Python para automatizar la deobfuscacion. El decompilador de Ghidra maneja bien dead code y algunas sustituciones de instrucciones.
D810 (plugin IDA)
Plugin especializado en deobfuscacion de control flow flattening. Identifica el dispatcher, traza las transiciones de estado y reconstruye el grafo original.
Miasm
Framework de analisis binario que incluye ejecucion simbolica, util para deobfuscacion de CFF y expresiones MBA.
Unicorn Engine
Motor de emulacion de CPU que permite ejecutar fragmentos de codigo en un entorno controlado. Util para emular funciones de descifrado de strings sin ejecutar el malware completo.
from unicorn import Uc, UC_ARCH_X86, UC_MODE_32
from unicorn.x86_const import UC_X86_REG_EAX, UC_X86_REG_ESP
# Emular funcion de descifrado
mu = Uc(UC_ARCH_X86, UC_MODE_32)
# Mapear memoria
ADDRESS = 0x1000000
mu.mem_map(ADDRESS, 2 * 1024 * 1024)
# Escribir codigo del descifrador + datos cifrados
mu.mem_write(ADDRESS, decrypt_function_bytes)
mu.mem_write(ADDRESS + 0x1000, encrypted_string_bytes)
# Configurar registros
mu.reg_write(UC_X86_REG_ESP, ADDRESS + 0x100000)
# Emular
mu.emu_start(ADDRESS, ADDRESS + len(decrypt_function_bytes))
# Leer resultado
result = mu.mem_read(ADDRESS + 0x1000, 100)
print(bytes(result).rstrip(b"\x00"))
Estrategia de deobfuscacion en la practica
-
Identificar el tipo de ofuscacion: Examinar el codigo en IDA/Ghidra. CFF se reconoce por el patron de dispatcher. MBA por expresiones aritmeticas complejas. Dead code por instrucciones sin efecto.
-
Empezar por los strings: Las strings descifradas son la recompensa mas inmediata. FLOSS primero, breakpoints si falla.
-
Deobfuscar selectivamente: No es necesario deobfuscar todo el binario. Concentrarse en las funciones relevantes (main, comunicacion de red, cifrado, persistencia).
-
Combinar estatico y dinamico: El analisis estatico identifica patrones, el dinamico los confirma y captura resultados.
-
Documentar el algoritmo: Una vez identificado el metodo de ofuscacion, documentarlo para aplicarlo a variantes futuras de la misma familia.
Conclusion
La ofuscacion es la segunda linea de defensa del malware despues del packing. Mientras que los packers ocultan el binario completo, la ofuscacion transforma el codigo a nivel de instrucciones. Los analistas necesitan dominar tanto las tecnicas de ofuscacion como sus contramedidas: FLOSS para strings, D810 para CFF, ejecucion simbolica para MBA, y el juicio para saber cuando el analisis dinamico es mas eficiente que intentar deobfuscar todo el codigo.
Preguntas frecuentes
Libros recomendados
Artículos relacionados
Packing y Unpacking: UPX, Themida, VMProtect y Tecnicas
Import Address Table: APIs Sospechosas y Resolucion Dinamica
Tecnicas Anti-Analisis: Anti-Debug, Anti-VM y Anti-Sandbox
Analisis de Binarios .NET: dnSpy, ILSpy y Decompilacion
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.