Die Illusion von JavaScript-DRM: Wie sich der Kopierschutz von HotAudio in nur 3 Runden aushebeln ließ
(therantydev.com)- JavaScript-basiertes DRM, das im Browser läuft, lässt sich grundsätzlich umgehen, weil entschlüsselte Audiodaten am Ende zwangsläufig einen Bereich passieren müssen, auf den JavaScript zugreifen kann
- HotAudio ist eine NSFW-ASMR-Audio-Hosting-Plattform und implementierte einen eigenen Kopierschutz mit Verschlüsselung und Chunk-Übertragung auf Basis der MediaSource Extensions API
- Dokumentiert wird ein 3-stufiger Schlagabtausch, bei dem der Angreifer auf wiederholte Patches des Entwicklers (Entfernung globaler Variablen, Hash-Prüfung,
.toString()-Integritätschecks, Isolation per iframe/Shadow DOM) jedes Mal mit Prototype Hooking und Tarntechniken reagierte - Echtes DRM erfordert hardwaregestützten Schutz auf Basis einer Trusted Execution Environment (TEE) wie bei Widevine oder FairPlay, ist für kleine Plattformen aber wegen Lizenzkosten und Infrastruktur kaum erreichbar
- JavaScript-DRM erzeugt für normale Nutzer zwar wirksame Reibung (friction), kann geübte Angreifer aber nicht aufhalten; zwischen der Bezeichnung „DRM“ und der Realität besteht daher eine große Erwartungslücke
Hintergrund: HotAudio und die angeborenen Grenzen von JavaScript-DRM
- HotAudio ist eine NSFW-ASMR-Audio-Hosting-Seite, die nach eigener Aussage DRM-Schutzfunktionen für Creator bietet
- Die Plattform entstand als Alternative, nachdem bestehende Hosting-Dienste wie Soundgasm und Mega durch verschärfte ToS eingeschränkt wurden
- Ausgangspunkt der Analyse war, dass Entwickler fermaw auf Reddit erwähnte, die DRM-Implementierung habe „Spaß gemacht“
- JavaScript-Code existiert grundsätzlich im „userland“-Bereich, also in einer Struktur, in der an Nutzer ausgelieferter Code von ihnen eingesehen und verändert werden kann
- Unabhängig davon, wie ausgefeilt Schlüssel, Nonce oder verschlüsselte Dateiformate sind, müssen Daten nach der JavaScript-Entschlüsselung letztlich im Klartext an die Audio-Engine des Browsers übergeben werden
Die Rolle der Trusted Execution Environment (TEE)
- Laut Microsoft ist eine TEE ein „isolierter Bereich von CPU und Speicher, der kryptografisch geschützt ist“, sodass externer Code interne Daten weder lesen noch manipulieren kann
- Eine TEE ist ein hardwarebasierter Sicherheitsbereich (etwa ARM TrustZone oder Intel SGX), auf dem Content Decryption Modules (CDMs) wie Widevine, FairPlay und PlayReady laufen
- Diese CDMs stellen sicher, dass Verschlüsselungsschlüssel und entschlüsselte Medienpuffer nicht dem Host-OS offengelegt werden
- Für eine Widevine-Lizenz braucht es einen Lizenzvertrag mit Google, die Integration nativer Binärdateien, Infrastruktur, rechtliche Prozesse und erhebliche Kosten
- Für eine kleine NSFW-Audio-Plattform ist es praktisch unrealistisch, eine Widevine-Lizenz zu erhalten
HotAudios Implementierung und die „PCM-Grenze“
- HotAudio überträgt Audio in verschlüsselter Form und setzt auf eine JavaScript-basierte benutzerdefinierte Entschlüsselung, die Chunks über die MediaSource Extensions (MSE) API entschlüsselt und abspielt
- Gegen das Speichern per Rechtsklick oder einen direkten Download über den Netzwerk-Tab ist dieser Ansatz für gewöhnliche Nutzer wirksam
- PCM (Pulse-Code Modulation) ist das finale unkomprimierte digitale Audioformat, das an die Lautsprecher weitergereicht wird, und damit das Endziel jeder Audiopipeline
- Für den praktischen Angriff war es jedoch nicht nötig, bis zu PCM zu verfolgen; das zentrale Ziel war stattdessen die letzte für JavaScript zugängliche Stelle: die Methode
SourceBuffer.appendBuffer() - Wenn
appendBufferaufgerufen wird, sind die Daten durch JavaScript bereits entschlüsselt; da die AAC-/Opus-Decoder des Browsers HotAudios proprietäre Verschlüsselung nicht verstehen, akzeptieren sie nur entschlüsselte Daten in Standard-Codec-Form - Der Moment zwischen abgeschlossener Entschlüsselung und Übergabe an die Medien-Engine des Browsers ist genau der abfangbare „goldene Moment“
Act 1: V1.0 — Offengelegte globale Variablen und Prototype Hooking
- Der HotAudio-Player legte das Audio-Source-Objekt über die globale Variable
window.asoffen - Die Erweiterung V1 fing die von HotAudio stets gelieferte Datei
nozzle.jsauf Ebene der Netzwerkanfrage ab und injizierte modifizierten Code SourceBuffer.prototype.appendBufferwurde per Monkeypatching verändert, sodass entschlüsselte Chunks in einem Array gespeichert wurden, während die Originalfunktion weiterhin normal aufgerufen wurdewindow.as.elwurde stummgeschaltet und die Wiedergabegeschwindigkeit auf 16x (Browser-Maximum) gesetzt, um das komplette Audio schnell zu puffern; beimended-Event wurden die Chunks zu einemBlobzusammengefügt und als.m4aheruntergeladen- Es handelte sich um einen clientseitigen Man-in-the-Middle-Angriff (MITM) per Browser-Erweiterungs-API; der HotAudio-Server konnte die Manipulation nicht erkennen
-
fermaws erste Reaktion
- Rund zwei Wochen nach dem öffentlichen Release spielte fermaw einen Patch ein
- Die Offenlegung der globalen Variable
window.aswurde entfernt, und der Initialisierungscode wurde in eine Closure verpackt, um externen Zugriff zu blockieren - Zudem wurde eine Hash-Prüfung für
nozzle.jseingeführt (vermutlich SRI, ein eigenes Hashing oder ein serverseitiges Nonce-System)- Wenn die modifizierte Datei nicht mit dem erwarteten Hash übereinstimmte, wurde der Player nicht initialisiert
Act 2: V2.0 — Tarntechniken und generisches Hooking
-
fermaws In-Memory-Abwehr
- In JavaScript liefert
.toString()auf nativen Funktionen"function appendBuffer() { [native code] }"zurück, während monkeygepatchte Funktionen ihren tatsächlichen Source-Code zeigen; genau diese Eigenschaft nutzte fermaw - Er ergänzte einen Integritätscheck, der die Wiedergabe verweigerte, wenn
SourceBuffer.prototype.appendBuffer.toString()nicht'[native code]'enthielt - Auch die Initialisierung des Players wurde obfuskiert, sodass sich die
AudioSource-Klasse nicht mehr einfach per Polling-Schleife finden ließ
- In JavaScript liefert
-
mockToString — Tarnfunktion zum Täuschen des Integritätschecks
- Die
.toString()-Ausgabe gehookter Funktionen wurde überschrieben, sodass sie"function Name() { [native code] }"zurückgab - Dadurch lieferte fermaws Integritätsprüfung ein False Negative, womit sich das Hooking nicht mehr erkennen ließ
- Die
-
Hooking von
HTMLMediaElement.prototype.play- Statt nach
window.asoder bestimmten Klassennamen zu suchen, wurde ein generischerer Ansatz gewählt: Hooking vonHTMLMediaElement.prototype.play - So ließ sich das Audio-Element automatisch in dem Moment erfassen, in dem
.play()aufgerufen wurde, unabhängig vom Namen des Player-Objekts oder der Tiefe der Closure - Auf Mobilgeräten ist normalerweise nur ein Player aktiv, weshalb es schwer ist, Reverse Engineering mit vielen
.play()-Aufrufen zu erschweren
- Statt nach
-
Dauerhafte Fixierung über
Object.definePropertywindow.Audiowurde durch einen gekaperten Konstruktor ersetzt und anschließend mitwritable: falseundconfigurable: falsefestgeschrieben- Selbst wenn fermaws Code versuchte, den ursprünglichen
Audio-Konstruktor wiederherzustellen, löste der Browser eineTypeErroraus - Das Hooking blieb damit für die gesamte Lebensdauer der Seite dauerhaft erhalten
Act 3: V3.0 — Vollständiges Hooking auf Ebene der Property Descriptors
-
fermaws Versuch der Isolation über iframe und Shadow DOM
- Ein
<iframe>besitzt ein eigeneswindow,documentund eine separate Prototyp-Kette, sodass Hooking im Parent-Window innerhalb des iframe nicht wirkt - Shadow DOM ist ein isolierter DOM-Subtree, dessen interne Elemente nicht mit
querySelectoraus dem Hauptdokument heraus durchsucht werden können - Außerdem wurde versucht, URL-basiertes Intercepting zu umgehen, indem
MediaStream-/MediaSource-Objekte übersrcObjectdirekt zugewiesen wurden
- Ein
-
Die Reaktion in V3: Hooking auf Ebene der Browser-Property-Descriptors
- Mit
Object.getOwnPropertyDescriptorwurden die Setter fürsrcundsrcObjectaufHTMLMediaElement.prototypedirekt gehookt- Unabhängig davon, ob das Audio-Element im Hauptdokument, in einem iframe oder in einer Web Component existierte, wurde das Hooking beim Zuweisen der Quelle ausgelöst
- Durch Injection bei
document_startwurde das Hooking vor der Initialisierung des iframe installiert
- Mit
-
Hooking von
addSourceBuffer: Lösung des Race Conditions- In früheren Versionen konnte Hooking von
SourceBuffer.prototype.appendBufferauf Prototyp-Ebene umgangen werden, wenn fermaws Code vor der Installation des Hooks eine Referenz aufappendBuffercachte - In V3 wurde stattdessen
MediaSource.prototype.addSourceBuffergehookt, um den Zeitpunkt der Erzeugung vonSourceBuffer-Instanzen abzufangen- Sobald die Instanz zurückgegeben wurde, wurde direkt auf dieser Instanz ein Hook für
appendBufferals own property installiert - Weil das Hooking abgeschlossen war, bevor der Seitencode die Instanz sehen konnte, war eine Umgehung per Caching grundsätzlich unmöglich
- Sobald die Instanz zurückgegeben wurde, wurde direkt auf dieser Instanz ein Hook für
- In früheren Versionen konnte Hooking von
-
Event-Listener in der Capture-Phase — das letzte Sicherheitsnetz
- Über
document.addEventListenerwurden die EventsplayundloadedmetadatamituseCapture: trueüberwacht - Browser-Events breiten sich zuerst in der Capture-Phase (Root → Target) aus, sodass diese Listener immer vor den Event-Listenern von HotAudio laufen
- Durch die vierfache Schicht aus Prototype-Hooking von
addSourceBuffer, Property-Descriptor-Hooking vonsrc/srcObject, Hooking vonplay()und Event-Listenern in der Capture-Phase wurden sämtliche Medienwiedergabepfade des Browsers abgedeckt
- Über
Automatisierung: der Download-Prozess mit hoher Geschwindigkeit
- Das erfasste Audio-Element wurde stummgeschaltet,
playbackRateauf 16x gesetzt und die Wiedergabe von Anfang an gestartet - Damit der Browser den Puffer vor der Wiedergabeposition füllt, wiederholte er schnell fetch → Entschlüsselung → Übergabe an
SourceBuffer, und alle Chunks wurden über das gehookteappendBuffergesammelt - Chrome begrenzt die Wiedergabegeschwindigkeit auf 16x (im HTML-Standard gibt es zwar keine explizite Obergrenze, wohl aber in der Chromium-Implementierung)
- fermaw führte Throttling für Burst-Traffic ein (mehrere hundert KB/s → etwa 50 KB/s), dennoch blieb der Download um ein Mehrfaches schneller als das Hören in Echtzeit
- Eine noch stärkere Begrenzung würde auch bei normalen Nutzern Streaming-Aussetzer verursachen und ist daher praktisch kaum umsetzbar
-
Adaptive Geschwindigkeitssteuerung
- Als zusätzliche Funktion in V3 wurde der
buffered-Zeitraum überwacht, um die Wiedergabegeschwindigkeit dynamisch an den Pufferzustand anzupassen- Bei mehr als 15 Sekunden Pufferreserve wurde beschleunigt, bei weniger als 3 Sekunden verlangsamt
- So wurden Browser-Stalls und das Ausbleiben des
ended-Events bei langsamen Verbindungen vermieden
- Als zusätzliche Funktion in V3 wurde der
-
Erzeugung der finalen Datei
- Wenn die Wiedergabe beendet war (
ended-Event odercurrentTimenaheduration), wurden die gesammelten Chunks zu einemBlobzusammengefügt und als.m4aheruntergeladen - Durch unvollständige Chunks an den Puffergrenzen können Artefakte mit stiller Auffüllung entstehen, die sich per
ffmpegnachbearbeiten lassen
- Wenn die Wiedergabe beendet war (
Die spoof()-Funktion in V3: noch ausgefeiltere Tarnung
mockToStringin V2 gab den String für nativen Code noch hartkodiert zurück, was anfällig dafür war, dass sich Leerzeichen und Formatierung von[native code]je nach Browser oder Plattform leicht unterscheiden könnenspoof()in V3 fing stattdessen den echten nativen Code-String der Originalfunktion vor dem Hooking ab und gab ihn anschließend unverändert zurück, wodurch eine perfekte Fälschung möglich wurde- Verwendet wurden dabei Referenzen auf
Function.prototype.callundFunction.prototype.toString, die zu Skriptbeginn in der Form_call.call(_toString, original)zwischengespeichert wurden- Selbst wenn
.toStringspäter von anderem Code manipuliert wurde, blieb diese Konstruktion davon unbeeinflusst
- Selbst wenn
Die grundlegenden Grenzen von DRM und ethische Überlegungen
- Die gesamte Geschichte von DRM ist eine Wiederholung des Problems, dass man „eine verschlossene Kiste übergibt und gleichzeitig den Schlüssel mitliefert“
- Seit dem ersten Knacken der CSS-verschlüsselten DVDs im Jahr 1999 hat die Film- und Musikindustrie diesen Kampf immer wieder verloren
- Selbst Denuvo, das ausgefeilteste DRM im Spielebereich, wird bei den meisten großen Titeln innerhalb weniger Wochen nach Release geknackt
- Nach dem Rückzug der bekannten Crackerin Empress verlangsamte sich das Tempo zeitweise, doch mit dem Auftauchen von Hypervisor-artigen Exploits gewann das Knacken wieder an Fahrt
- Solange sich sowohl der Inhalt als auch der Entschlüsselungsschlüssel auf der Client-Maschine befinden, ist ein Abfangen durch Nutzer mit genügend Motivation und den passenden Tools unvermeidlich
Fazit: JavaScript-DRM ist „ausgefeilte Reibung“, aber kein echtes DRM
- HotAudios DRM scheiterte nicht an mangelnden Fähigkeiten von fermaw, sondern daran, dass dies das Beste ist, was JavaScript-basiertes DRM erreichen kann
- Es implementierte clientseitige Entschlüsselung, Chunk-Übertragung und aktive Anti-Tamper-Prüfungen und bot für die große Mehrheit der Nutzer ohne Kenntnisse über Browser-Erweiterungen einen faktisch vollständigen Schutz
- Problematisch wird es, wenn man das als „DRM“ bezeichnet, weil damit dieselbe Erwartungshaltung wie bei echtem, hardwaregestütztem DRM auf Basis einer TEE erzeugt wird
- Besonders engagierte Fans von ASMR-Creatorn sind oft so motiviert, dass sie Offline-Kopien möchten, und würden bei kostenpflichtigen Kanälen wie Patreon wahrscheinlich auch bereitwillig bezahlen
- Es ist nachvollziehbar, dass Content-Creator irgendeine Form von Schutz wünschen, doch eine Umsetzung in JavaScript ist dafür grundsätzlich kein geeigneter Ansatz
4 Kommentare
Das dürfte wirklich ein ziemlich unterhaltsames Kräftemessen gewesen sein.
Ich hatte auch mal den Fall, dass API-Antworten plötzlich verschlüsselt ankamen. Da dachte ich mir: Wenn ich einen verschlüsselten Wert bekomme, wird der Client ihn irgendwo auch wieder entschlüsseln. Also habe ich einfach den gebündelten JavaScript-Code als Ganzes kopiert, direkt vor den Entschlüsselungscode eine Zeile mit
console.logeingefügt und das Ganze dann unverändert in die Entwicklerkonsole eingefügt. Überraschenderweise hat es einfach funktioniert. Jedenfalls war danach alles leicht, sobald ich den Verschlüsselungsschlüssel herausgefunden hatte. Der Schlüssel wurde nämlich aus einer anderen API-Antwort übernommen und verwendet, haha.Wenn es sich um NSFW- (Not Safe For Work) ASMR handelt ...
Dann ist das wohl eine sehr technische und tiefgehende Schilderung darüber, wie eine Erwachsenenseite gehackt wurde --.;
Wie immer findet technischer Fortschritt also zuerst im Erwachsenenbereich statt ...?
Wenn man darüber nachdenkt, ist es nicht wirklich sehr schwierig, Audio mit DRM zu versehen, oder?
Es wirkt so, als könnte man schon etwas erreichen, ohne komplexes Hacking zu betreiben, indem man den Ton einfach über ein virtuelles Kabel umleitet.
Aber es war trotzdem ein ziemlich unterhaltsamer Schlagabtausch, hahaha. Man sieht, dass sie sich Tricks ausgedacht haben, auf die die AI niemals gekommen wäre.