- 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
appendBuffer aufgerufen 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.as offen
- Die Erweiterung V1 fing die von HotAudio stets gelieferte Datei
nozzle.js auf Ebene der Netzwerkanfrage ab und injizierte modifizierten Code
SourceBuffer.prototype.appendBuffer wurde per Monkeypatching verändert, sodass entschlüsselte Chunks in einem Array gespeichert wurden, während die Originalfunktion weiterhin normal aufgerufen wurde
window.as.el wurde stummgeschaltet und die Wiedergabegeschwindigkeit auf 16x (Browser-Maximum) gesetzt, um das komplette Audio schnell zu puffern; beim ended-Event wurden die Chunks zu einem Blob zusammengefügt und als .m4a heruntergeladen
- 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.as wurde entfernt, und der Initialisierungscode wurde in eine Closure verpackt, um externen Zugriff zu blockieren
- Zudem wurde eine Hash-Prüfung für
nozzle.js eingefü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ß
-
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ß
-
Hooking von HTMLMediaElement.prototype.play
- Statt nach
window.as oder bestimmten Klassennamen zu suchen, wurde ein generischerer Ansatz gewählt: Hooking von HTMLMediaElement.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
-
Dauerhafte Fixierung über Object.defineProperty
window.Audio wurde durch einen gekaperten Konstruktor ersetzt und anschließend mit writable: false und configurable: false festgeschrieben
- Selbst wenn fermaws Code versuchte, den ursprünglichen
Audio-Konstruktor wiederherzustellen, löste der Browser eine TypeError aus
- 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 eigenes window, document und 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
querySelector aus dem Hauptdokument heraus durchsucht werden können
- Außerdem wurde versucht, URL-basiertes Intercepting zu umgehen, indem
MediaStream-/MediaSource-Objekte über srcObject direkt zugewiesen wurden
-
Die Reaktion in V3: Hooking auf Ebene der Browser-Property-Descriptors
- Mit
Object.getOwnPropertyDescriptor wurden die Setter für src und srcObject auf HTMLMediaElement.prototype direkt 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_start wurde das Hooking vor der Initialisierung des iframe installiert
-
Hooking von addSourceBuffer: Lösung des Race Conditions
- In früheren Versionen konnte Hooking von
SourceBuffer.prototype.appendBuffer auf Prototyp-Ebene umgangen werden, wenn fermaws Code vor der Installation des Hooks eine Referenz auf appendBuffer cachte
- In V3 wurde stattdessen
MediaSource.prototype.addSourceBuffer gehookt, um den Zeitpunkt der Erzeugung von SourceBuffer-Instanzen abzufangen
- Sobald die Instanz zurückgegeben wurde, wurde direkt auf dieser Instanz ein Hook für
appendBuffer als own property installiert
- Weil das Hooking abgeschlossen war, bevor der Seitencode die Instanz sehen konnte, war eine Umgehung per Caching grundsätzlich unmöglich
-
Event-Listener in der Capture-Phase — das letzte Sicherheitsnetz
- Über
document.addEventListener wurden die Events play und loadedmetadata mit useCapture: 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 von src/srcObject, Hooking von play() und Event-Listenern in der Capture-Phase wurden sämtliche Medienwiedergabepfade des Browsers abgedeckt
Automatisierung: der Download-Prozess mit hoher Geschwindigkeit
- Das erfasste Audio-Element wurde stummgeschaltet,
playbackRate auf 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 gehookte appendBuffer gesammelt
- 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
-
Erzeugung der finalen Datei
- Wenn die Wiedergabe beendet war (
ended-Event oder currentTime nahe duration), wurden die gesammelten Chunks zu einem Blob zusammengefügt und als .m4a heruntergeladen
- Durch unvollständige Chunks an den Puffergrenzen können Artefakte mit stiller Auffüllung entstehen, die sich per
ffmpeg nachbearbeiten lassen
Die spoof()-Funktion in V3: noch ausgefeiltere Tarnung
mockToString in 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önnen
spoof() 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.call und Function.prototype.toString, die zu Skriptbeginn in der Form _call.call(_toString, original) zwischengespeichert wurden
- Selbst wenn
.toString später von anderem Code manipuliert wurde, blieb diese Konstruktion davon unbeeinflusst
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.
> Wenn man in JavaScript bei nativen Funktionen
.toString()aufruft, erhält man"function appendBuffer() { [native code] }", während bei per Monkey-Patch veränderten Funktionen der eigentliche Quellcode zurückgegeben wird – diese Eigenschaft wurde ausgenutzt.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.