# Cómo funcionan los streams piratas del Mundial


Me quería sacar la duda de cómo funcionaba ese truco a la vista de todos. Los sitios piratas de streaming existen desde siempre, pero durante el Mundial no podías abrir un foro deportivo sin que alguien tirara un link a un partido en vivo — full HD, sin buffering, sin login. Yo siempre supuse que había algo exótico funcionando por detrás. Un amigo me pasó un link antes del Inglaterra-Croacia. "Mira esto", me dijo. Así que abrí DevTools.

Esperaba una playlist HLS scrapeada o un feed re-streameado. En cambio encontré una etiqueta `<video>`, una instancia de Shaka Player apuntando a un CDN legítimo de Akamai, y dos strings hexadecimales en un bloque `<script>` que hacían todo el trabajo. Esos strings hexadecimales eran las claves de descifrado DRM. En el código fuente de la página. No detrás de una API. No encriptados. Solo envueltos en JavaScript que parecía amenazante y no hacía nada.

Después alguien me mostró una app de Android haciendo lo mismo para cada partido del Mundial — pero "como Dios manda", con configs encriptados, Firebase Remote Config, un servidor de licencias, todo el paquete. Una sección dedicada "MUNDIAL FIFA 2026" con 28 canales que incluían multicam, player cam, y manager cam para cada partido. Me tomó cerca de una hora darme cuenta de que era la misma vulnerabilidad con un mejor traje.

Lo que empezó como treinta minutos de curiosidad se convirtió en un fin de semana. Al final había mapeado **670 transmisiones en vivo en 169 CDNs en 33 países** del lado web, y **525 canales más de una sola app Android** — todas protegidas por un esquema DRM que el propio W3C llama una herramienta "de pruebas". Durante un Mundial donde Inglaterra le mete cuatro a Croacia y cada gol está simultáneamente disponible en cientos de transmisiones no autorizadas.

{{< admonition type="warning" title="Disclosure" open=true >}}
**Esto es un análisis de vulnerabilidad.** No publico claves reales, URLs de streams, ni herramientas funcionales. Nombres de apps y dominios están redactados. La meta es documentar una debilidad sistémica en cómo las plataformas despliegan DRM — no habilitar la piratería.
{{< /admonition >}}

## El candado con la combinación atrás

Cada navegador que reproduce video encriptado usa Encrypted Media Extensions (EME) del W3C. EME soporta cuatro key systems. Tres de ellos — Widevine, FairPlay, PlayReady — usan servidores de licencia, intercambios de clave encriptados, y descifrado respaldado por hardware. El cuarto es ClearKey.

ClearKey le manda la clave de descifrado al navegador en texto plano.

Eso es todo. La misma encriptación AES-128 que Widevine. La misma entrega MPEG-DASH. Pero donde Widevine negocia claves a través de un canal seguro y descifra video dentro de una sandbox de hardware en la que el navegador no puede meter la nariz, ClearKey le entrega la clave cruda a JavaScript y dice "ahí la tienes".

```
    WIDEVINE L1                          CLEARKEY
    ───────────────────────              ───────────────────────
    Servidor de licencia (HTTPS)         Sin servidor de licencia
    Intercambio de clave encriptado      Clave en JS plano
    Descifrado en TEE de hardware        Descifrado por software
    La clave nunca está en memoria JS    La clave ES el JS
    Política por dispositivo, por sesión Sin política alguna
```

El DASH Industry Forum dice que ClearKey está "recomendado solo para propósitos de prueba". Algunas plataformas decidieron usarlo en producción de todas formas. Tanto los sitios web como la app que analicé dependen de esa decisión.

## Parte 1: Los sitios web

Examiné docenas de sitios de streaming pirata. Todos siguen el mismo patrón.

### La arquitectura

```
    ┌──────────────────────────────────────────────────────────────┐
    │                  CÓMO FUNCIONA EN REALIDAD                   │
    └──────────────────────────────────────────────────────────────┘

    PASO 1                    PASO 2                    PASO 3
    Usuario visita            Hace click en un canal    La página carga un <iframe>
    pirate-site.co            (ej. "Sky Sports")        de un dominio distinto

    ┌──────────────┐          ┌──────────────┐          ┌──────────────────┐
    │              │          │  ┌────┐┌────┐│          │  player-host.co  │
    │  Página de   │  click   │  │ESPN││DAZN││  iframe  │                  │
    │  Canales     │ ───────> │  ├────┤├────┤│ ───────> │  Shaka Player    │
    │              │          │  │Sky ││beIN││          │  + claves DRM    │
    │  (HTML       │          │  └────┘└────┘│          │  + URL manifest  │
    │   estático)  │          └──────────────┘          └────────┬─────────┘
    └──────────────┘                                             │
                                                                 │ pide
                                                                 ▼
                                                    ┌──────────────────────┐
                                                    │  CDN LEGÍTIMO        │
                                                    │  (akamaized.net,     │
                                                    │   skycdp.com,        │
                                                    │   indazn.com)        │
                                                    │                      │
                                                    │  Segmentos DASH      │
                                                    │  encriptados         │
                                                    └──────────────────────┘

    El sitio pirata sirve CERO video.
    La cuenta del CDN le pertenece al proveedor legítimo.
    El sitio pirata aloja ~50KB de HTML.
```

El sitio pirata es solo un intermediario que conoce dos strings hexadecimales. Apunta el navegador del espectador a un CDN real, le entrega la clave de descifrado, y el navegador hace el resto. El CDN nunca sabe que el espectador no es un suscriptor de pago.

### La clave está en el código fuente

La página del player host contiene un bloque `<script>` con las claves, envuelto en obfuscación que un script de Node de 30 líneas deshace en menos de un segundo:

```javascript
// Antes: 40 líneas de _0x4a2f, IIFEs, lookups de string-arrays
// Después: esto es lo que en realidad hace

var drmKeyId = 'a1b2c3d4e5f6a7b80000000000000000';
var drmKey   = '0123456789abcdef0123456789abcdef';

player.configure({
    drm: { clearKeys: { [drmKeyId]: drmKey } }
});
player.load('https://cdn.legitimate-provider.com/manifest.mpd');
```

Dos variables. Dos strings hexadecimales. Esa es toda la "protección DRM". La obfuscación — renombrado de variables, arrays de strings, aplanado de control-flow — la deshace `eval()`, porque el navegador también tiene que correrla. Si el navegador la puede ejecutar, un script la puede ejecutar.

Una vez extraídas, las claves se distribuyen vía playlists M3U en GitHub y Telegram. Encontré un solo archivo M3U con **545 entradas de ClearKey en 92 CDNs**. Público, buscable, indexado por Google. Tiempo de extracción a distribución global: minutos. Tiempo para que la clave sea revocada: usualmente nunca.

## Parte 2: La app de Android

No toda operación pirata es una página estática. Algunas envían una app Android completa — con pantallas de login, analytics de Firebase, configs encriptados, y una UI profesional. Una app que analicé durante el Mundial toma este enfoque. 525 canales. 36 categorías. Una sección dedicada "MUNDIAL FIFA 2026" con ángulos multicámara para cada partido.

{{< image src="images/app-config.png" caption="El config encriptado de canales de la app — 525 canales en 36 categorías, con URLs encriptadas con AES y credenciales DRM. [URLs redactadas]" >}}

### La encriptación (AES-128-ECB)

La app pide su lista de canales como un JSON encriptado con AES desde un servidor remoto. Cada campo — URLs de stream, URIs de licencia DRM, headers HTTP — es ciphertext AES en base64. La clave de descifrado es un string de 16 caracteres guardado en Firebase Remote Config, descargado al arrancar.

Suena serio. Después miras el ciphertext:

```
    236 URLs comparten los mismos primeros 35 bloques encriptados.

    Bloque 0: f91b7f273abccc6e...  ← idéntico en los 236
    Bloque 1: 932b23560f1d43ba...  ← idéntico
    Bloque 2: a7d788a399cea962...  ← idéntico
    ...

    Modo ECB. Mismo bloque plano = mismo bloque cifrado.
    Todas las URLs empiezan con el mismo prefijo de dominio del CDN.
```

AES-ECB es el ejemplo de manual de cómo *no* usar AES. Cualquier curso de criptografía enseña esto con el [pingüino ECB](https://blog.filippo.io/the-ecb-penguin/) — encripta un bitmap con ECB y la imagen sigue siendo reconocible porque bloques idénticos en plano producen bloques idénticos en cifrado. Esta app encripta 236 URLs que arrancan con el mismo dominio de CDN, produciendo 236 ciphertexts con un **prefijo idéntico de 560 bytes**. El análisis de patrones por sí solo revela la estructura antes de que siquiera encuentres la clave.

### El "servidor de licencia" que no lo es

Una vez descifradas, las credenciales de ClearKey no están en un intercambio de claves separado. Están en el **license URI mismo**, como parámetros plano en el query string:

```
    drm_license_uri (después de descifrar):

    https://[redactado]/?keyid=49eb924b...&key=6e131b04...
                              ^^^^^^^^        ^^^^^^^^
                              ClearKey ID     ClearKey Key
                              (en la URL)     (en la URL)
```

El "servidor de licencia" de la app es una URL que *contiene la clave*. No hay reto-respuesta. No hay vinculación de sesión. La app pide la URL, la URL *es* la clave, y la clave descifra el stream.

Tres capas de indirección — Firebase Remote Config, encriptación AES, un endpoint de "servidor de licencia" — todas colapsando en el mismo fallo: la clave de descifrado termina en el cliente, en plano, porque ClearKey lo exige.

### El stack completo de la app

```
    ┌──────────────────────────────────────────────────────────────┐
    │              ARQUITECTURA DE LA APP ANDROID                  │
    └──────────────────────────────────────────────────────────────┘

    1. La app arranca
       └──> Firebase Remote Config
            └──> Descarga clave AES ("claveapp") + URLs de config

    2. La app descarga JSON encriptado (525 canales)
       └──> Decodifica base64 ──> Descifra AES-128-ECB
            └──> URLs de stream, URIs de licencia DRM, headers (plano)

    3. Para canales CLEARKEY (348 de 525):
       └──> "License URI" = https://[redactado]/?keyid=XXX&key=YYY
            └──> Key ID y Key están EN LA URL

    4. La app configura ExoPlayer con ClearKey
       └──> Descarga el manifest DASH del CDN legítimo
            └──> Descifra el video con la clave del paso 3

    Package name: com.example.myapplication
    Encriptación: AES/ECB/PKCS5Padding (de libro, inseguro)
    Almacén clave: Firebase Remote Config (una llamada API para extraerla)
    Largo clave:  16 caracteres, estática, nunca rotada
```

El package name `com.example.myapplication` lo dice todo sobre el rigor de desarrollo detrás de esta operación. El template por defecto de Android Studio. Ni siquiera renombrado.

### La lista de canales en vivo vive en un repo público

La parte más calladamente lapidaria — y la que muestra dónde *realmente* hay sofisticación en esta operación, que es sobre todo logística — es dónde está alojado el config encriptado. La app descarga su JSON de canales desde un repositorio público de GitHub. Cualquiera puede navegarlo. Cualquiera puede `git clone`. Cualquiera puede ver el historial de commits.

Y el historial de commits es lo interesante:

{{< image src="images/commits.png" caption="El repo público de GitHub que aloja la lista encriptada de canales de la app. Un commit cae cada 4–5 minutos — ediciones automáticas al JSON de canales mientras los streams upstream se rotan, se mueren o se reemplazan. Usuario y dueño del repo redactados." >}}

Un commit cae cada 4–5 minutos. El diff siempre es contra `tv (10).json` o `bearer.json` — la lista encriptada de canales, y las credenciales de auth usadas para pedir streams nuevos al upstream. Los mensajes de commit son todos `"Actualizar tv (10).json desde tv.json"` o `"Actualizar bearer.json desde panel - <timestamp>"`. Traducción: un job automatizado, en algún lado, está reescribiendo el config de canales cada pocos minutos y empujando el update para que los celulares corriendo la app levanten las nuevas URLs de stream y las credenciales DRM rotadas en el siguiente intervalo de polling.

Este es el verdadero foso de la operación. No el AES-ECB (roto). No el Firebase Remote Config (una sola llamada). No el "license server" (una URL con la clave dentro). El foso es **operacional**: un pedazo de automatización que mantiene el JSON de canales alineado con cualquier feed upstream que esté vivo en cualquier minuto, alojado a la vista en un CDN gratis que ellos no tuvieron que construir. Cuando un proveedor legítimo rota una clave, el bot lo nota, descarga la nueva, re-encripta el config y commitea. Cuando un CDN bloquea la IP de origen, el bot encuentra otra. Los celulares de los clientes hacen polling, ven el commit nuevo, descargan el config nuevo, y siguen mirando el partido. El usuario ni siquiera ve que pasó la rotación.

La defensa a la que todo el mundo recurre es "revocar la clave". Pero no puedes revocar una clave más rápido de lo que un script puede empujar una nueva a un repo de GitHub. Ese desfase de cadencia es lo que decide la partida.

## Los números

| | Auditoría web | App Android |
|---|---|---|
| **Streams** | 670 | 525 (348 ClearKey) |
| **CDNs** | 169 | 34 |
| **Países** | 33 | ~8 (foco LATAM) |
| **Almacén de claves** | Bloque `<script>` en JS | Firebase Remote Config |
| **Obfuscación** | Renombrado JS + IIFEs | AES-128-ECB (roto) |
| **Clave en plano en el cliente** | Sí | Sí |
| **Cobertura del Mundial** | Decenas de canales deportivos | 28 canales dedicados + multicam |

Los dos enfoques — el sitio web estático y la app Android completa — terminan en el mismo lugar: un par de claves ClearKey en la memoria del cliente, sin mecanismo para prevenir la extracción, sin vinculación de sesión, sin revocación, y sin rotación de claves.

## El dinero, y por qué no va a parar

La economía explica todo. Los sitios piratas alojan ~50KB de HTML estático y apuntan tu navegador al CDN de otra persona. La app es un wrapper delgado de ExoPlayer alrededor de la infraestructura de otra persona. Ninguno sirve video. Los dos monetizan a través de redes de publicidad.

StreamEast sirvió 1.6 mil millones de visitas antes de ser allanado en septiembre de 2025. La red de IPTV de "Operation Takendown" tenía 22 millones de usuarios y 250 millones de euros al mes. Un sitio de tier medio durante el Mundial — un millón de visitantes el día de partido, seis impresiones de anuncio cada uno, $1.50 CPM — saca **$9.000 al día** contra costos de hosting esencialmente cero.

La aplicación de la ley sigue escalando — sentencias de prisión en España, fallos de $18.75 millones en Texas, 27.000 feeds dados de baja la semana antes del Mundial — y siguen apareciendo nuevos sitios. Porque la vulnerabilidad es arquitectónica. Ninguna cantidad de arrestos arregla una especificación que pone la clave de descifrado en el navegador. Y tampoco hay encriptación que valga, como lo demuestra la app de Android: tres capas de encriptación, y la clave igual termina en texto plano en el cliente, porque ClearKey lo exige.

El arreglo es migrar de ClearKey por completo — a Widevine, FairPlay, o PlayReady. Hasta entonces, la combinación está escrita en la parte de atrás del candado, y el Mundial se está asegurando de que todos sepan dónde mirar.

