L’illusion du DRM JavaScript : comment la protection contre la copie de HotAudio a été neutralisée en 3 manches
(therantydev.com)- Le DRM basé sur JavaScript exécuté dans le navigateur reste fondamentalement contournable, car les données audio déchiffrées doivent inévitablement transiter par une zone accessible à JavaScript
- HotAudio est une plateforme d’hébergement audio ASMR NSFW qui a mis en place une protection contre la copie maison via l’API MediaSource Extensions, avec chiffrement et transfert par chunks
- L’article retrace un affrontement en 3 étapes où, face aux correctifs successifs du développeur (suppression des variables globales, vérification de hash, contrôle d’intégrité via
.toString(), isolation par iframe/Shadow DOM), l’attaquant a répondu à chaque fois par du hooking de prototypes et des techniques d’usurpation - Un DRM réellement efficace exige une protection matérielle fondée sur un Trusted Execution Environment (TEE) comme Widevine ou FairPlay, mais ce type de solution reste hors de portée des petites plateformes à cause des coûts de licence et d’infrastructure
- Le DRM JavaScript peut servir de friction utile contre les utilisateurs ordinaires, mais il ne peut pas arrêter un attaquant expérimenté, d’où un fort décalage entre l’étiquette « DRM » et la réalité
Contexte : HotAudio et les limites intrinsèques du DRM JavaScript
- HotAudio est un site d’hébergement audio ASMR NSFW, présenté comme une plateforme offrant des fonctions de protection DRM aux créateurs
- La plateforme s’est imposée comme alternative après le durcissement des conditions d’utilisation de services d’hébergement comme Soundgasm ou Mega
- L’analyse est partie d’un commentaire du développeur fermaw sur Reddit, où il disait que l’implémentation du DRM avait été « amusante »
- Le code JavaScript existe par nature dans le « userland », c’est-à-dire dans un espace où l’utilisateur peut accéder au code et le modifier
- Même avec des clés, nonces et formats de fichiers chiffrés très sophistiqués, les données qui passent par la logique de déchiffrement JavaScript doivent finir par être transmises en clair au moteur audio du navigateur
Le rôle du Trusted Execution Environment (TEE)
- Selon la définition de Microsoft, un TEE est une « zone isolée du CPU et de la mémoire protégée par chiffrement », conçue pour empêcher un code externe de lire ou modifier les données qu’elle contient
- Le TEE est une zone de sécurité matérielle, comme ARM TrustZone ou Intel SGX, sur laquelle s’exécutent des Content Decryption Modules (CDM) tels que Widevine, FairPlay et PlayReady
- Ces CDM garantissent que les clés de chiffrement et les buffers média déchiffrés ne sont pas exposés au système d’exploitation hôte
- Obtenir une licence Widevine suppose un accord de licence avec Google, l’intégration de binaires natifs, une infrastructure adaptée, des procédures juridiques et des coûts importants
- Pour une petite plateforme audio NSFW, obtenir une licence Widevine est en pratique irréaliste
L’implémentation de HotAudio et la « frontière PCM »
- HotAudio transmet l’audio sous forme chiffrée et adopte une méthode de déchiffrement personnalisée en JavaScript avec lecture et déchiffrement par chunks via l’API MediaSource Extensions (MSE)
- Cette approche est efficace pour bloquer l’enregistrement par clic droit ou le téléchargement direct depuis l’onglet réseau pour les utilisateurs ordinaires
- Le PCM (Pulse-Code Modulation) est le format audio numérique final non compressé envoyé aux haut-parleurs, et constitue le point d’aboutissement de toute la chaîne audio
- Dans l’attaque réelle, il n’est pas nécessaire de remonter jusqu’au PCM : la cible clé est la méthode
SourceBuffer.appendBuffer(), dernier point accessible à JavaScript - Au moment où
appendBufferest appelée, les données ont déjà été déchiffrées par JavaScript, et comme les décodeurs AAC/Opus du navigateur ne comprennent pas le chiffrement propriétaire de HotAudio, ils ne peuvent accepter que des données déchiffrées dans un format de codec standard - L’instant entre la fin du déchiffrement et la remise au moteur média du navigateur est précisément ce « moment en or » où l’interception devient possible
Acte 1 : V1.0 — exposition de variables globales et hooking de prototypes
- Le lecteur HotAudio exposait l’objet source audio via une variable globale appelée
window.as - L’extension V1 interceptait au stade de la requête réseau le fichier
nozzle.js, toujours envoyé par HotAudio, afin d’y injecter un code modifié SourceBuffer.prototype.appendBufferétait patché à la volée pour stocker les chunks déchiffrés dans un tableau tout en continuant d’appeler normalement la fonction d’origine- En coupant le son de
window.as.elet en réglant la vitesse de lecture sur 16x — le maximum du navigateur —, l’extension mettait rapidement tout l’audio en buffer, puis assemblait les données dans unBlobtéléchargé au format.m4alors de l’événementended - Il s’agissait d’une attaque MITM côté client reposant sur les API d’extensions de navigateur, sans que les serveurs de HotAudio puissent détecter l’altération
-
Première réponse de fermaw
- Environ deux semaines après la sortie publique, fermaw a déployé un correctif
- L’exposition de la variable globale
window.asa été supprimée, et le code d’initialisation enfermé dans une closure pour empêcher l’accès externe - Une vérification de hash a été ajoutée sur
nozzle.js— probablement via SRI, un hachage personnalisé ou un système de nonce côté serveur- Si le fichier modifié ne correspondait pas au hash attendu, le lecteur ne s’initialisait pas
Acte 2 : V2.0 — usurpation et hooking générique
-
Défense en mémoire de fermaw
- En JavaScript, appeler
.toString()sur une fonction native renvoie"function appendBuffer() { [native code] }", alors qu’une fonction patchée renvoie son véritable code source - fermaw a ajouté un contrôle d’intégrité refusant la lecture si
SourceBuffer.prototype.appendBuffer.toString()ne contenait pas'[native code]' - L’initialisation du lecteur a aussi été davantage obfusquée pour rendre plus difficile la recherche de la classe
AudioSourcepar boucle de polling
- En JavaScript, appeler
-
mockToString— une fonction d’usurpation qui trompe le contrôle d’intégrité- La méthode
.toString()de la fonction hookée a été redéfinie pour renvoyer"function nom() { [native code] }" - Le contrôle d’intégrité de fermaw produisait alors un faux négatif, rendant la détection du hooking impossible
- La méthode
-
Hooking de
HTMLMediaElement.prototype.play- Au lieu de chercher
window.asou un nom de classe spécifique, l’attaquant a adopté une approche générique en hookantHTMLMediaElement.prototype.play - Quel que soit le nom de l’objet lecteur ou la profondeur de la closure, l’élément audio était capturé automatiquement au moment de l’appel à
.play() - Sur mobile, un seul lecteur est généralement actif, ce qui rend difficile l’obfuscation par multiplication des appels à
.play()
- Au lieu de chercher
-
Verrouillage permanent via
Object.definePropertywindow.Audioa été remplacé par un constructeur détourné, puis défini avecwritable: falseetconfigurable: false- Si le code de fermaw essayait de restaurer le constructeur
Audiod’origine, le navigateur levait alors uneTypeError - Le hooking restait ainsi permanent pendant toute la durée de vie de la page
Acte 3 : V3.0 — hooking généralisé au niveau des descripteurs de propriétés
-
Tentative d’isolation par iframe et Shadow DOM chez fermaw
- Un
<iframe>possède son proprewindow, son propredocumentet une chaîne de prototypes indépendante, de sorte que les hooks duwindowparent ne s’appliquent pas à son contenu - Le Shadow DOM forme un sous-arbre DOM isolé, dont les éléments internes ne sont pas accessibles via le
querySelectordu document principal - Une tentative a aussi consisté à assigner directement des objets
MediaStream/MediaSourceviasrcObject, afin de contourner l’interception fondée sur les URL
- Un
-
Réponse de V3 : hooking au niveau des descripteurs de propriétés du navigateur
- En utilisant
Object.getOwnPropertyDescriptor, V3 hooke directement les setterssrcetsrcObjectdeHTMLMediaElement.prototype- Dès qu’une source est affectée à un élément audio, le hook se déclenche, que l’élément se trouve dans le document principal, un iframe ou un composant web
- L’injection en
document_startpermet d’installer les hooks avant même l’initialisation des iframes
- En utilisant
-
Hooking de
addSourceBuffer: résolution de la race condition- Dans les versions précédentes, hooker
SourceBuffer.prototype.appendBufferau niveau du prototype pouvait être contourné si le code de fermaw mettait en cache une référence àappendBufferavant l’installation du hook - V3 hooke
MediaSource.prototype.addSourceBufferpour intercepter le moment exact de création d’une instance deSourceBuffer- Dès que l’instance est renvoyée, un hook
appendBufferest installé directement sur cette instance comme propriété propre - Le hook est donc en place avant même que le code de la page ne voie l’instance, rendant tout contournement par cache impossible à la source
- Dès que l’instance est renvoyée, un hook
- Dans les versions précédentes, hooker
-
Écouteurs d’événements en phase de capture — dernier filet de sécurité
document.addEventListenersurveille les événementsplayetloadedmetadataavecuseCapture: true- Les événements du navigateur se propagent d’abord en phase de capture (racine → cible), de sorte que ces hooks s’exécutent toujours avant les écouteurs du code de HotAudio
- La combinaison du hooking de prototype
addSourceBuffer, du hooking des descripteurs de propriétéssrc/srcObject, du hooking deplay()et des écouteurs en phase de capture crée une couverture en 4 couches de toutes les voies de lecture média du navigateur
Automatisation : le processus de téléchargement accéléré
- L’élément audio capturé est mis en sourdine, sa
playbackRateest réglée sur 16x, puis la lecture repart depuis le début - Le navigateur enchaîne alors rapidement fetch → déchiffrement → envoi au
SourceBufferpour remplir le buffer devant la position de lecture, et tous les chunks sont collectés via le hookappendBuffer - Chrome limite la vitesse de lecture à 16x — une contrainte de l’implémentation Chromium, sans plafond explicite dans la spécification HTML
- fermaw applique un throttling contre les pics de trafic — de plusieurs centaines de Ko/s à environ 50 Ko/s —, mais cela reste plusieurs fois plus rapide qu’une écoute en temps réel
- Des limites plus strictes provoqueraient des coupures même pour les utilisateurs légitimes, ce qui les rend difficiles à mettre en œuvre en pratique
-
Contrôle adaptatif de la vitesse
- Fonction ajoutée dans V3 : le suivi des plages temporelles
bufferedpermet d’ajuster dynamiquement la vitesse de lecture selon l’état du buffer- Si plus de 15 secondes sont d’avance, la vitesse augmente ; si l’avance tombe sous 3 secondes, elle ralentit
- Cela évite les blocages (
stall) du navigateur et les cas où l’événementendedne se déclenche pas sur connexion lente
- Fonction ajoutée dans V3 : le suivi des plages temporelles
-
Génération du fichier final
- Une fois la lecture terminée — via l’événement
endedou lorsquecurrentTimeapprocheduration— les chunks collectés sont assemblés enBlobpuis téléchargés en.m4a - Des artefacts de padding silencieux peuvent apparaître à cause de chunks incomplets en bordure de buffer, mais ils peuvent être nettoyés avec un post-traitement
ffmpeg
- Une fois la lecture terminée — via l’événement
La fonction spoof() de V3 : une usurpation plus sophistiquée
- Dans V2,
mockToStringrenvoyait une chaîne de code natif codée en dur, avec une fragilité potentielle car les espaces et le format de[native code]varient légèrement selon les navigateurs et plateformes - Dans V3,
spoof()capture la véritable chaîne de code natif de la fonction originale avant le hooking, puis la renvoie telle quelle, ce qui permet une imitation parfaite - La forme
_call.call(_toString, original)s’appuie sur des références àFunction.prototype.calletFunction.prototype.toStringmises en cache au démarrage du script- Ainsi, même si un autre code modifie ensuite
.toString, le mécanisme reste intact
- Ainsi, même si un autre code modifie ensuite
Les limites fondamentales du DRM et la réflexion éthique
- Toute l’histoire du DRM répète le même problème : « on donne une boîte verrouillée tout en fournissant aussi la clé »
- Depuis le premier crack des DVD chiffrés CSS en 1999, les industries du cinéma et de la musique ont continuellement perdu ce combat
- Même Denuvo, le DRM de jeu le plus sophistiqué, finit le plus souvent cracké quelques semaines après la sortie des grands titres
- Le rythme des cracks avait ralenti après la retraite d’Empress, célèbre crackeuse, mais l’apparition d’exploits de type hyperviseur a relancé le phénomène
- Tant que le contenu et la clé de déchiffrement résident tous deux sur la machine cliente, l’interception par un utilisateur suffisamment motivé et outillé reste inévitable
Conclusion : le DRM JavaScript est une « friction sophistiquée », pas un vrai DRM
- Le DRM de HotAudio ne reflète pas une insuffisance de fermaw, mais plutôt la meilleure limite atteignable par un DRM fondé sur JavaScript
- Il implémente bien un déchiffrement côté client, un transfert par chunks et des contrôles anti-tampering actifs, et il constitue une barrière réellement efficace pour la majorité des utilisateurs qui ne connaissent pas les extensions de navigateur
- Mais l’appeler « DRM » pose problème, car cela crée le même niveau d’attente qu’un véritable DRM fondé sur un TEE matériel
- Les fans les plus investis d’un créateur ASMR sont précisément ceux qui veulent souvent une copie hors ligne ; s’ils ont accès à un canal payant comme Patreon, ils sont susceptibles de payer volontairement
- On peut comprendre que les créateurs de contenu aient besoin d’une forme de protection, mais une implémentation en JavaScript reste fondamentalement une approche inadaptée
4 commentaires
Ça a dû être une passe d’armes vraiment amusante entre eux.
Il m’est arrivé quelque chose de similaire il y a quelque temps : comme des réponses d’API arrivaient soudainement chiffrées, je me suis dit que si le client recevait une valeur chiffrée, il devait bien la déchiffrer quelque part. J’ai donc copié tel quel tout le code JavaScript bundlé, ajouté une simple ligne
console.logjuste avant le code de déchiffrement, puis collé le tout dans la console de développement. Et contre toute attente, ça a simplement marché. Bref, une fois la clé de chiffrement trouvée, la suite a été facile. En fait, elle était récupérée et utilisée depuis une autre réponse de l’API hahaSi c’est de l’ASMR NSFW (Not Safe For Work)…
C’est en gros le récit, très technique et très fouillé, du piratage d’un site pour adultes -.-;
Comme toujours, les avancées technologiques viennent toutes du secteur pour adultes… ?
Quand on y réfléchit, mettre un DRM sur de l’audio, c’est… vraiment difficile, non ?
Sans même recourir à un piratage complexe, j’ai l’impression qu’il suffit déjà de faire passer l’audio par un câble virtuel pour obtenir quelque chose.
Et sinon, leurs échanges étaient vraiment très amusants à suivre hahaha. On voit qu’ils ont imaginé des petites astuces auxquelles une IA n’aurait jamais pensé.