17 puntos por GN⁺ 2026-03-12 | 4 comentarios | Compartir por WhatsApp
  • El DRM basado en JavaScript que se ejecuta en el navegador es eludible por definición, porque los datos de audio ya descifrados inevitablemente deben pasar por un área accesible para JavaScript
  • HotAudio es una plataforma de hosting de audio ASMR NSFW que implementó una protección anticopia propia mediante cifrado y envío por chunks usando la API de MediaSource Extensions
  • Se documenta una confrontación de 3 etapas en la que, frente a los parches repetidos del desarrollador (eliminación de variables globales, verificación de hashes, comprobaciones de integridad con .toString(), aislamiento con iframe/Shadow DOM), el atacante respondió cada vez con hooking de prototipos y técnicas de spoofing
  • Un DRM real requiere protección por hardware basada en Trusted Execution Environment (TEE), como Widevine o FairPlay, pero las plataformas pequeñas no pueden acceder a ello por costos de licencia e infraestructura
  • El DRM en JavaScript puede funcionar como una fricción efectiva para usuarios comunes, pero no puede detener a atacantes con experiencia, por lo que llamarlo "DRM" crea una gran brecha entre expectativa y realidad

Contexto: HotAudio y el límite inherente del DRM en JavaScript

  • HotAudio es un sitio de hosting de audio ASMR NSFW que afirma ofrecer protección DRM para creadores
  • Surgió como plataforma alternativa cuando servicios de hosting como Soundgasm y Mega se volvieron más restrictivos tras endurecer sus ToS
  • El punto de partida del análisis fue un comentario del desarrollador fermaw en Reddit diciendo que implementar el DRM había sido "divertido"
  • El código JavaScript existe por naturaleza en el espacio de "userland", es decir, se distribuye como código al que el usuario puede acceder y modificar
  • Por más sofisticadas que sean las claves, los nonce y el formato de archivo cifrado, los datos que pasan por la lógica de descifrado en JavaScript finalmente deben entregarse en texto plano al motor de audio del navegador

El rol de Trusted Execution Environment (TEE)

  • Según la definición de Microsoft, un TEE es una "zona aislada de CPU y memoria protegida por cifrado", diseñada para que el código externo no pueda leer ni alterar los datos internos
  • El TEE es una zona de seguridad basada en hardware (ARM TrustZone, Intel SGX, etc.), y sobre ella corren Content Decryption Module (CDM) como Widevine, FairPlay y PlayReady
  • Estos CDM garantizan que ni las claves de cifrado ni los buffers de medios ya descifrados queden expuestos al sistema operativo anfitrión
  • Obtener una licencia de Widevine exige un acuerdo con Google, integración de binarios nativos, infraestructura, procesos legales y un costo considerable
  • Para una pequeña plataforma de audio NSFW, conseguir una licencia de Widevine es, en la práctica, imposible

Cómo funciona la implementación de HotAudio y la "frontera PCM"

  • HotAudio transmite el audio cifrado y adopta un método de descifrado personalizado en JavaScript que descifra y reproduce por chunks mediante la API de MediaSource Extensions (MSE)
  • Este enfoque sí sirve para impedir que un usuario común guarde el archivo con clic derecho o lo descargue directamente desde la pestaña de red
  • PCM (Pulse-Code Modulation) es el formato final de audio digital sin compresión que llega a las bocinas, y el destino final de cualquier pipeline de audio
  • En el ataque real ni siquiera hace falta rastrear hasta PCM: el punto clave es el último punto accesible desde JavaScript, el método SourceBuffer.appendBuffer()
  • Cuando se llama a appendBuffer, los datos ya fueron descifrados por JavaScript, y como los decodificadores AAC/Opus del navegador no entienden el cifrado propietario de HotAudio, solo aceptan datos descifrados en un formato de códec estándar
  • Ese instante entre el fin del descifrado y la entrega al motor multimedia del navegador es justo el "momento dorado" que puede interceptarse

Acto 1: V1.0 — exposición de variables globales y hooking de prototipos

  • El reproductor de HotAudio exponía el objeto de la fuente de audio como una variable global llamada window.as
  • La extensión V1 interceptaba en la etapa de solicitud de red el archivo nozzle.js, que HotAudio siempre enviaba, para inyectar código modificado
  • Hacía monkey patch de SourceBuffer.prototype.appendBuffer para guardar los chunks descifrados en un arreglo mientras seguía llamando normalmente a la función original
  • Luego silenciaba window.as.el, ponía la velocidad de reproducción en 16x (el máximo del navegador), almacenaba rápidamente el audio completo en buffer y, al dispararse el evento ended, unía todo en un Blob para descargarlo como archivo .m4a
  • Era un ataque MITM del lado del cliente usando APIs de extensiones del navegador, por lo que el servidor de HotAudio no podía detectar la manipulación
  • Primera respuesta de fermaw

    • Unas dos semanas después del lanzamiento público, fermaw aplicó un parche
    • Eliminó la exposición global de window.as y envolvió el código de inicialización en un closure para bloquear el acceso externo
    • También introdujo una verificación de hash para nozzle.js (probablemente SRI, hashing propio o un sistema de nonce del lado del servidor)
      • Si el archivo modificado no coincidía con el hash esperado, el reproductor no se inicializaba

Acto 2: V2.0 — técnicas de spoofing y hooking genérico

  • Defensas en memoria de fermaw

    • En JavaScript, si se llama .toString() sobre una función nativa, devuelve "function appendBuffer() { [native code] }", mientras que una función parcheada con monkey patch devuelve su código fuente real, y fermaw aprovechó esta diferencia
    • Agregó una verificación de integridad que rechazaba la reproducción si SourceBuffer.prototype.appendBuffer.toString() no contenía '[native code]'
    • También ofuscó la inicialización del reproductor para que ya no fuera fácil encontrar la clase AudioSource mediante un loop de polling
  • mockToString — una función de spoofing para engañar la verificación de integridad

    • Se sobrescribía el .toString() de la función hookeada para que devolviera "function nombre() { [native code] }"
    • Así, la verificación de integridad de fermaw daba un falso negativo y no podía detectar el hooking
  • Hooking de HTMLMediaElement.prototype.play

    • En vez de buscar window.as o nombres de clases concretos, se adoptó un enfoque genérico hookeando HTMLMediaElement.prototype.play
    • Sin importar el nombre del objeto del reproductor ni la profundidad del closure, el elemento de audio se capturaba automáticamente cuando se llamaba .play()
    • Como en dispositivos móviles normalmente solo hay un reproductor activo, usar múltiples llamadas a .play() para entorpecer la ingeniería inversa no resulta muy efectivo
  • Fijación permanente con Object.defineProperty

    • Se reemplazó window.Audio por un constructor secuestrado y luego se definió con writable: false y configurable: false
    • Así, aunque el código de fermaw intentara restaurar el constructor original de Audio, el navegador lanzaría un TypeError
    • El hooking quedaba permanentemente activo durante toda la vida de la página

Acto 3: V3.0 — hooking total al nivel de descriptores de propiedades

  • El intento de aislamiento con iframe y Shadow DOM de fermaw

    • Un <iframe> tiene su propio window, document y una cadena de prototipos independiente, así que los hooks del window padre no se aplican dentro del iframe
    • El Shadow DOM es un subárbol DOM aislado cuyos elementos internos no pueden recorrerse con el querySelector del documento principal
    • También se intentó evitar la intercepción basada en URL asignando directamente objetos MediaStream/MediaSource mediante srcObject
  • La respuesta de V3: hooking al nivel de descriptores de propiedades del navegador

    • Usando Object.getOwnPropertyDescriptor, se hookearon directamente los setter de src y srcObject en HTMLMediaElement.prototype
      • No importa si el elemento de audio existe en el documento principal, en un iframe o en un web component: el hook se activa cuando se asigna la fuente
      • Con inyección en document_start, el hook se instala antes de que se inicialice el iframe
  • Hooking de addSourceBuffer: resolver la condición de carrera

    • En versiones previas, si SourceBuffer.prototype.appendBuffer se hookeaba al nivel del prototipo, el código de fermaw podía cachear una referencia a appendBuffer antes de instalarse el hook y así esquivarlo
    • En V3, se hookea MediaSource.prototype.addSourceBuffer para interceptar el momento en que se crea una instancia de SourceBuffer
      • En cuanto se devuelve la instancia, se instala sobre ella misma un hook de appendBuffer como own property
      • Como el hook termina antes de que el código de la página vea la instancia, ya no hay forma de esquivar el cacheo
  • Listeners en fase de captura — la última red de seguridad

    • Se monitorean los eventos play y loadedmetadata con document.addEventListener usando useCapture: true
    • Como los eventos del navegador primero se propagan en la fase de captura (raíz → objetivo), estos listeners se ejecutan siempre antes que los listeners del código de HotAudio
    • La combinación de hook de prototipo en addSourceBuffer, hook de descriptores de propiedades para src/srcObject, hook de play() y listeners en fase de captura crea una capa cuádruple que cubre todas las rutas de reproducción multimedia del navegador

Automatización: proceso de descarga acelerada

  • El elemento de audio capturado se silencia, se ajusta playbackRate a 16x y se reproduce desde el inicio
  • Para llenar rápido el buffer por delante de la posición de reproducción, el navegador repite el ciclo fetch → descifrado → entrega a SourceBuffer, y todos los chunks quedan recogidos por el appendBuffer hookeado
  • Chrome limita la velocidad de reproducción a 16x (aunque el estándar HTML no fija un tope explícito, sí existe esta limitación en Chromium)
  • fermaw aplica throttling al tráfico en ráfagas (de cientos de KB/s a unos 50 KB/s), pero aun así sigue siendo varias veces más rápido que escuchar en tiempo real
    • Un límite más agresivo causaría cortes incluso en el streaming de usuarios legítimos, así que en la práctica no es viable
  • Control adaptativo de velocidad

    • Función añadida en V3: monitorea los rangos de tiempo en buffered y ajusta dinámicamente la velocidad según el estado del buffer
      • Si hay más de 15 segundos de margen en buffer, acelera; si hay menos de 3 segundos, reduce la velocidad
      • Esto evita bloqueos del navegador en conexiones lentas y que no se dispare el evento ended
  • Generación del archivo final

    • Cuando termina la reproducción (ended o cuando currentTime se acerca a duration), los chunks recolectados se combinan en un Blob para descargar un .m4a
    • Pueden aparecer artefactos de relleno de silencio causados por chunks incompletos en los límites del buffer, que luego pueden limpiarse con posprocesamiento en ffmpeg

La función spoof() de V3: un disfraz más preciso

  • En V2, mockToString devolvía una cadena de código nativo hardcodeada, pero eso era frágil porque el espaciado y formato de [native code] pueden variar ligeramente entre navegadores y plataformas
  • En V3, spoof() logra una falsificación perfecta capturando primero la cadena real de código nativo desde la función original antes del hooking, y devolviéndola tal cual
  • Usa referencias cacheadas al inicio del script para Function.prototype.call y Function.prototype.toString, con la forma _call.call(_toString, original)
    • Así, aunque otro código altere .toString() después, ya no afecta al mecanismo

Límites esenciales del DRM y reflexión ética

  • Toda la historia del DRM es una repetición del problema de "entregar una caja cerrada junto con la llave"
  • Desde el crackeo del DVD con cifrado CSS en 1999, la industria del cine y la música ha seguido perdiendo esta batalla
  • Incluso Denuvo, el DRM para videojuegos más sofisticado, suele ser vulnerado en la mayoría de lanzamientos importantes a las pocas semanas
    • Tras el retiro de Empress, una cracker muy conocida, el ritmo de crackeo bajó por un tiempo, pero volvió a acelerarse con la aparición de exploits estilo hipervisor
  • Mientras tanto el contenido como la clave de descifrado existan en la máquina cliente, la intercepción por parte de usuarios con suficiente motivación y herramientas será inevitable

Conclusión: el DRM en JavaScript es "fricción sofisticada", no DRM real

  • El DRM de HotAudio no representa una falla de capacidad de fermaw, sino el mejor resultado posible al que puede aspirar el DRM basado en JavaScript
  • Implementó descifrado del lado del cliente, envío por chunks y comprobaciones activas antimanipulación, y para la mayoría de usuarios que no conocen las extensiones del navegador, el efecto de bloqueo es prácticamente total
  • Pero llamarlo "DRM" es problemático porque fija la misma expectativa que un DRM real basado en hardware TEE
  • Los fans más dedicados de creadores ASMR pueden desear copias offline al punto de estar dispuestos a pagarlas si se ofrecen por un canal de pago como Patreon
  • Se puede entender que los creadores quieran algún tipo de protección para su contenido, pero implementarla en JavaScript es, en lo esencial, un enfoque inadecuado

4 comentarios

 
joyfui 2026-03-13

Debe haber sido un intercambio realmente divertido entre ambas partes.
A mí también me pasó antes que, de repente, las respuestas de una API empezaron a llegar cifradas, así que pensé: si recibo el valor cifrado, en algún lado del cliente lo deben estar descifrando. Entonces copié tal cual todo el JavaScript empaquetado, le agregué una sola línea de console.log antes del código de descifrado y lo pegué directamente en la consola del desarrollador. Sorprendentemente, simplemente funcionó. En fin, una vez que descubrí la clave de cifrado, lo demás fue fácil. Estaban recibiendo y usando la clave dentro de otra respuesta de la API jaja

 
xguru 2026-03-12

Si es ASMR NSFW (Not Safe For Work)...
Entonces es una historia sobre hackear un sitio para adultos, contada de forma muy técnica y a fondo -.-;
Como siempre, ¿todo el avance tecnológico ocurre primero en la industria para adultos...?

 
crawler 2026-03-12

Ahora que lo pienso, ponerle DRM al audio... ¿no es realmente muy difícil?
No hace falta hacer un hackeo complejo; da la impresión de que con solo pasar el audio por un cable virtual ya se podría lograr algo.

 
crawler 2026-03-12

> En JavaScript, si llamas .toString() sobre una función nativa, devuelve "function appendBuffer() { [native code] }", pero una función modificada con monkey patching devuelve el código fuente real; se aprovechó esa característica.

Y la verdad, estuvo muy divertido ese ida y vuelta jajaja. Se nota que idearon trucos ingeniosos que la IA jamás habría considerado.