17 pontos por GN⁺ 2026-03-12 | 3 comentários | Compartilhar no WhatsApp
  • O DRM baseado em JavaScript executado no navegador é inerentemente contornável, porque os dados de áudio já descriptografados inevitavelmente precisam passar por uma área acessível ao JavaScript
  • O HotAudio é uma plataforma de hospedagem de áudio ASMR NSFW que implementou sua própria proteção contra cópia com criptografia e envio em chunks usando a API MediaSource Extensions
  • O texto registra um embate em 3 etapas, no qual o desenvolvedor aplicou patches repetidamente — remoção de variáveis globais, verificação de hash, checagem de integridade com .toString(), isolamento com iframe/Shadow DOM — e o atacante respondeu toda vez com hooking de protótipos e técnicas de spoofing
  • Um DRM de fato exige proteção de hardware baseada em Trusted Execution Environment (TEE), como Widevine e FairPlay, mas plataformas pequenas não conseguem adotá-la por causa de custos de licença e infraestrutura
  • O DRM em JavaScript funciona como uma fricção eficaz para usuários comuns, mas não consegue deter atacantes experientes; por isso, há um grande abismo entre expectativa e realidade ao chamá-lo de "DRM"

Contexto: HotAudio e os limites inerentes do DRM em JavaScript

  • O HotAudio é um site de hospedagem de áudio ASMR NSFW que se apresenta como uma plataforma que oferece proteção DRM para criadores
  • Surgiu como alternativa depois que serviços de hospedagem como Soundgasm e Mega ficaram mais restritivos com o endurecimento dos ToS
  • A análise começou quando o desenvolvedor fermaw comentou no Reddit que implementar o DRM tinha sido “divertido”
  • O código JavaScript existe, por natureza, no espaço de "userland", ou seja, é distribuído em uma forma que o usuário pode acessar e modificar
  • Não importa quão sofisticadas sejam a chave, o nonce ou o formato de arquivo criptografado: no fim, os dados que passam pela lógica de descriptografia em JavaScript precisam ser entregues em texto claro ao engine de áudio do navegador

O papel do Trusted Execution Environment (TEE)

  • Segundo a definição da Microsoft, um TEE é uma "área isolada de CPU e memória protegida por criptografia", estruturada para que código externo não consiga ler ou adulterar os dados internos
  • O TEE é uma área de segurança baseada em hardware, como ARM TrustZone e Intel SGX, e é sobre ele que operam os Content Decryption Modules (CDM) como Widevine, FairPlay e PlayReady
  • Esses CDMs garantem que as chaves de criptografia e os buffers de mídia já descriptografados não sejam expostos ao sistema operacional hospedeiro
  • Obter uma licença do Widevine exige contrato com o Google, integração de binários nativos, infraestrutura, processos legais e um custo significativo
  • Para uma pequena plataforma de áudio NSFW, conseguir uma licença do Widevine é, na prática, inviável

Como o HotAudio implementa isso e o "limite do PCM"

  • O HotAudio envia o áudio em forma criptografada e adota um método customizado de descriptografia em JavaScript com reprodução em chunks por meio da API MediaSource Extensions (MSE)
  • Essa abordagem é eficaz para bloquear ações como salvar com clique direito ou baixar diretamente pela aba de rede para usuários comuns
  • PCM (Pulse-Code Modulation) é o formato final de áudio digital sem compressão enviado aos alto-falantes, o ponto de chegada de qualquer pipeline de áudio
  • No ataque real, não foi necessário rastrear até o PCM: o alvo central foi o método SourceBuffer.appendBuffer(), o último ponto acessível ao JavaScript
  • No momento em que appendBuffer é chamado, os dados já foram descriptografados pelo JavaScript; como o decoder AAC/Opus do navegador não entende a criptografia proprietária do HotAudio, ele só aceita dados descriptografados em formato de codec padrão
  • O instante entre o fim da descriptografia e a entrega ao engine de mídia do navegador é exatamente o "momento de ouro" que pode ser interceptado

Ato 1: V1.0 — exposição de variável global e hooking de protótipo

  • O player do HotAudio expunha o objeto da fonte de áudio como uma variável global em window.as
  • A extensão V1 interceptava o arquivo nozzle.js, que o HotAudio sempre enviava, na etapa de requisição de rede, injetando código modificado
  • Ela fazia monkey patch em SourceBuffer.prototype.appendBuffer para armazenar os chunks descriptografados em um array enquanto também chamava a função original normalmente
  • Depois, silenciava window.as.el, definia a velocidade de reprodução em 16x (o máximo do navegador), fazia o buffer de todo o áudio rapidamente e, quando o evento ended ocorria, juntava tudo em um Blob para baixar como arquivo .m4a
  • Isso configurava um ataque man-in-the-middle (MITM) no lado do cliente usando a API de extensões do navegador; o servidor do HotAudio não tinha como perceber a adulteração
  • Primeira resposta de fermaw

    • Cerca de duas semanas após o lançamento público, fermaw aplicou um patch
    • Removeu a exposição global de window.as e envolveu a lógica de inicialização em um closure para bloquear acesso externo
    • Introduziu uma verificação de hash para nozzle.js (provavelmente SRI, hashing próprio ou um sistema de nonce no servidor)
      • Se o arquivo modificado não batesse com o hash esperado, o player simplesmente não inicializava

Ato 2: V2.0 — técnicas de disfarce e hooking genérico

  • Defesa em memória de fermaw

    • Em JavaScript, chamar .toString() em uma função nativa retorna algo como "function appendBuffer() { [native code] }", enquanto uma função alterada por monkey patch retorna o código-fonte real, e fermaw explorou essa característica
    • Ele adicionou uma checagem de integridade que recusava a reprodução se SourceBuffer.prototype.appendBuffer.toString() não contivesse '[native code]'
    • O processo de inicialização do player também foi ofuscado, dificultando localizar a classe AudioSource com um loop de polling
  • mockToString — uma função de disfarce para enganar a checagem de integridade

    • O .toString() da função hookada foi sobrescrito para retornar "function nome() { [native code] }"
    • Isso fazia a checagem de integridade de fermaw produzir um falso negativo, tornando a presença do hook indetectável
  • Hooking de HTMLMediaElement.prototype.play

    • Em vez de procurar window.as ou nomes específicos de classe, foi adotada uma abordagem genérica com hook em HTMLMediaElement.prototype.play
    • Assim, o elemento de áudio era capturado automaticamente no momento da chamada a .play(), independentemente do nome do objeto do player ou da profundidade do closure
    • Como dispositivos móveis normalmente mantêm apenas um player ativo por vez, era difícil atrapalhar a engenharia reversa disparando muitos .play()
  • Fixação permanente com Object.defineProperty

    • window.Audio foi substituído por um construtor sequestrado e definido com writable: false e configurable: false
    • Isso fazia o navegador lançar um TypeError mesmo se o código de fermaw tentasse restaurar o construtor Audio original
    • O hook passava a persistir durante toda a vida da página

Ato 3: V3.0 — hooking total no nível de descritor de propriedade

  • A tentativa de isolamento com iframe e Shadow DOM por fermaw

    • Um <iframe> possui seu próprio window, document e uma cadeia de protótipos independente, então hooks aplicados no window pai não alcançam o interior do iframe
    • O Shadow DOM é uma subárvore de DOM isolada, cujos elementos internos não podem ser explorados a partir do documento principal com querySelector
    • Também foi tentada uma abordagem de atribuir diretamente objetos MediaStream/MediaSource via srcObject, contornando interceptações baseadas em URL
  • A resposta da V3: hooking no nível do descritor de propriedade do navegador

    • Usando Object.getOwnPropertyDescriptor, os setters de src e srcObject em HTMLMediaElement.prototype foram hookados diretamente
      • Assim, o hook era disparado sempre que uma fonte fosse atribuída ao elemento de áudio, estivesse ele no documento principal, em um iframe ou em um web component
      • Com injeção em document_start, o hook era instalado antes mesmo da inicialização do iframe
  • Hooking de addSourceBuffer: resolvendo a race condition

    • Em versões anteriores, ao hookar SourceBuffer.prototype.appendBuffer no nível do protótipo, o código de fermaw ainda podia escapar se armazenasse em cache a referência de appendBuffer antes da instalação do hook
    • Na V3, MediaSource.prototype.addSourceBuffer foi hookado para interceptar o momento em que a instância de SourceBuffer é criada
      • Assim que a instância era retornada, um hook em appendBuffer era instalado diretamente nela como own property
      • Como o hook era concluído antes que o código da página visse a instância, escapar via cache tornava-se impossível desde a origem
  • Event listeners em fase de captura — a última linha de defesa

    • Em document.addEventListener, os eventos play e loadedmetadata eram monitorados com useCapture: true
    • Como os eventos do navegador primeiro se propagam pela fase de captura (raiz → alvo), esse listener sempre roda antes dos listeners do código do HotAudio
    • Hook de protótipo em addSourceBuffer + hook de descritor de propriedade em src/srcObject + hook em play() + listener em fase de captura formavam uma camada quádrupla cobrindo todos os caminhos de reprodução de mídia do navegador

Automação: processo de download em alta velocidade

  • O elemento de áudio capturado era silenciado e playbackRate era ajustado para 16x, então a reprodução começava do início
  • Para preencher rapidamente o buffer à frente da posição atual, o navegador repetia fetch → descriptografia → envio ao SourceBuffer, e todos os chunks eram coletados pelo appendBuffer hookado
  • O Chrome limita a velocidade de reprodução a 16x (não há teto explicitado na especificação HTML, mas existe essa limitação na implementação do Chromium)
  • fermaw aplicou throttling ao tráfego em rajadas (de algumas centenas de KB/s para cerca de 50 KB/s), mas ainda assim a velocidade seguia várias vezes acima da escuta em tempo real
    • Limitar ainda mais seria pouco realista, porque causaria interrupções até mesmo no streaming de usuários legítimos
  • Controle adaptativo de velocidade

    • Recurso acrescentado na V3: monitorar os intervalos de tempo em buffered para ajustar dinamicamente a velocidade de reprodução conforme o estado do buffer
      • Se houvesse mais de 15 segundos de folga no buffer, a velocidade aumentava; se houvesse menos de 3 segundos, ela diminuía
      • Isso evitava travamentos em conexões lentas e o problema de o evento ended não disparar
  • Geração do arquivo final

    • Ao fim da reprodução (ended ou quando currentTime se aproximava de duration), os chunks coletados eram unidos em um Blob e baixados como .m4a
    • Podem surgir artefatos de padding silencioso por causa de chunks incompletos nas bordas do buffer, mas isso pode ser corrigido com pós-processamento no ffmpeg

A função spoof() da V3: disfarce mais sofisticado

  • O mockToString da V2 retornava uma string de código nativo hardcoded, mas isso tinha a fragilidade de que espaços e formatação de [native code] podem variar sutilmente entre navegadores e plataformas
  • O spoof() da V3 alcança uma falsificação perfeita capturando primeiro a string real de código nativo da função original antes do hook, e depois devolvendo exatamente esse valor
  • Para isso, usa referências armazenadas no início do script a Function.prototype.call e Function.prototype.toString, na forma _call.call(_toString, original)
    • Assim, mesmo que outro código altere .toString() depois, o mecanismo não é afetado

Os limites fundamentais do DRM e uma reflexão ética

  • Toda a história do DRM é uma repetição do problema de "entregar uma caixa trancada e ao mesmo tempo a chave para abri-la"
  • Desde a quebra do CSS dos DVDs em 1999, a indústria de cinema e música tem perdido repetidamente essa batalha
  • Até mesmo o DRM de jogos mais sofisticado, o Denuvo, costuma ser quebrado em poucas semanas após o lançamento da maioria dos grandes títulos
    • Em certo momento, a velocidade desacelerou após a aposentadoria da cracker Empress, mas exploits em estilo hipervisor voltaram a aquecer esse cenário
  • Enquanto o conteúdo e a chave de descriptografia coexistirem na máquina do cliente, a interceptação por usuários com motivação e ferramentas suficientes será inevitável

Conclusão: DRM em JavaScript é "fricção sofisticada", não DRM de verdade

  • O DRM do HotAudio não representa uma limitação de habilidade de fermaw, e sim o melhor resultado que um DRM baseado em JavaScript pode alcançar
  • Ele implementou descriptografia no lado do cliente, transmissão em chunks e checagens ativas contra adulteração; para a maioria dos usuários que não conhece extensões de navegador, isso equivale a um bloqueio efetivo
  • O problema é que chamá-lo de "DRM" cria a mesma expectativa associada ao DRM real, baseado em TEE de hardware
  • Fãs mais dedicados de criadores de ASMR são justamente o tipo de público que pode querer uma cópia offline e, se houver um canal pago como Patreon, provavelmente estará disposto a comprar
  • Dá para entender por que criadores querem algum tipo de proteção para seu conteúdo, mas implementar isso em JavaScript é, na raiz, uma abordagem inadequada

3 comentários

 
joyfui 2026-03-13

Deve ter sido uma disputa bem divertida entre eles.
Uma vez, no passado, as respostas de uma API começaram a vir criptografadas de repente; pensei: se estou recebendo valores criptografados, o cliente deve descriptografá-los em algum lugar. Então copiei o código JavaScript empacotado inteiro, adicionei uma linha de console.log logo antes do código de descriptografia e colei tudo no console do desenvolvedor. Surpreendentemente, simplesmente funcionou. Enfim, depois que descobri a chave de criptografia, o resto ficou fácil. Estavam recebendo e usando a chave em outra resposta da API haha

 
crawler 2026-03-12

Pensando bem, colocar DRM em áudio... não é realmente muito difícil?
Parece que nem seria preciso fazer um hack complexo; só de passar o áudio por um cabo virtual já dá a sensação de que alguma coisa funcionaria.

 
crawler 2026-03-12

> No JavaScript, chamar .toString() em uma função nativa retorna "function appendBuffer() { [native code] }", mas funções alteradas com monkey patch retornam o código-fonte real — explorando essa característica

Nossa, mas foi uma troca bem divertida, hahaha. Dá para ver que pensaram em umas gambiarras que a IA jamais teria imaginado.