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.
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:
- Diversidad de modelos. Ningún modelo solo. Ensemble con features diferentes.
- Adversarial training. Entrenar con ataques conocidos.
- Feature hardening. Features difíciles de manipular.
- Monitorización. Detectar cuando el modelo está siendo atacado (concept drift abrupto, distribución anómala de scores).
- 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.