- 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
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.loglogo 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 hahaPensando 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.
> 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ísticaNossa, mas foi uma troca bem divertida, hahaha. Dá para ver que pensaram em umas gambiarras que a IA jamais teria imaginado.