- ブラウザで動作するJavaScriptベースのDRMは、復号された音声データが最終的にJavaScriptからアクセス可能な領域を通過するため、根本的に回避可能
- HotAudioはNSFW ASMR音声ホスティングプラットフォームで、MediaSource Extensions APIを活用した独自の暗号化・チャンク配信方式のコピー保護を実装
- 開発者による繰り返しのパッチ適用(グローバル変数の削除、ハッシュ検証、
.toString()整合性チェック、iframe/Shadow DOM隔離)に対し、攻撃者が毎回プロトタイプフックと偽装手法で対抗した3段階の攻防記録
- 実効的なDRMには**Trusted Execution Environment(TEE)**ベースのハードウェア保護(Widevine、FairPlayなど)が必要だが、小規模プラットフォームではライセンス費用やインフラの問題で導入困難
- JavaScript DRMは一般ユーザーに対しては有効な**摩擦(friction)**として機能するが、熟練した攻撃者は防げないため、「DRM」と呼ぶには期待値と現実の間に大きな乖離がある
背景: HotAudioとJavaScript DRMの本質的な限界
- HotAudioはNSFW ASMR音声ホスティングサイトで、クリエイター向けにDRM保護機能を提供すると主張するプラットフォーム
- 既存のSoundgasmやMegaなどのホスティングサービスがToS強化で制限される中、代替プラットフォームとして登場
- 開発者fermawがRedditでDRM実装について「面白かった」と言及したことが分析の出発点
- JavaScriptコードは本質的に**「userland」**に存在し、ユーザーがアクセス・改変可能なコードを配布する構造
- どれだけ精巧な鍵、nonce、暗号化ファイル形式を使っても、最終的にJavaScriptの復号ロジックを通過したデータは平文の状態でブラウザの音声エンジンに渡される必要がある
Trusted Execution Environment(TEE)の役割
- Microsoftの定義によれば、TEEは「暗号化で保護されたCPUとメモリの隔離領域」であり、外部コードが内部データを読み取ったり改ざんしたりできない構造
- TEEはハードウェアベースのセキュア領域(ARM TrustZone、Intel SGXなど)であり、その上で**Content Decryption Module(CDM)**であるWidevine、FairPlay、PlayReadyが動作
- これらのCDMは暗号鍵と復号済みメディアバッファがホストOSに露出しないことを保証
- Widevineのライセンス取得にはGoogleとのライセンス契約、ネイティブバイナリ統合、インフラ、法的手続き、多額の費用が必要
- 小規模なNSFW音声プラットフォームがWidevineライセンスを確保するのは現実的ではない
HotAudioの実装方式と「PCM境界」
- HotAudioは音声を暗号化された形で配信し、MediaSource Extensions(MSE) APIを通じてチャンク単位で復号・再生するJavaScriptベースのカスタム復号方式を採用
- この方式は一般ユーザーによる右クリック保存やネットワークタブからの直接ダウンロードを防ぐ点では有効
- **PCM(Pulse-Code Modulation)**はスピーカーに送られる最終的な非圧縮デジタル音声フォーマットであり、あらゆる音声パイプラインの終着点
- 実際の攻撃ではPCMまで追跡する必要はなく、JavaScriptがアクセス可能な最後の地点である**
SourceBuffer.appendBuffer()**メソッドが主要な攻撃対象
appendBufferが呼ばれる時点でデータはすでにJavaScriptによって復号済みであり、ブラウザのAAC/OpusデコーダはHotAudio独自の暗号化を理解できないため、標準コーデック形式の復号済みデータしか受け付けない
- 復号完了とブラウザのメディアエンジンへの受け渡しの間の瞬間こそが、インターセプト可能な**「ゴールデンモーメント」**
Act 1: V1.0 — グローバル変数の露出とプロトタイプフック
- HotAudioプレイヤーは
window.asというグローバル変数で音声ソースオブジェクトを露出していた
- V1拡張機能は、HotAudioが常に配信する
nozzle.jsファイルをネットワークリクエスト段階で横取りし、改変コードを注入
SourceBuffer.prototype.appendBufferをモンキーパッチして、復号済みチャンクを配列に保存しつつ元の関数も正常に呼び出す
window.as.elをミュートし、再生速度を16倍(ブラウザ上限)に設定して音声全体を高速バッファリングし、endedイベント発生時にBlobとして結合して.m4aファイルでダウンロード
- ブラウザ拡張APIを使った**クライアントサイド中間者攻撃(MITM)**であり、HotAudioサーバー側は改ざんを検知できない
-
fermawの最初の対応
- 公開リリースから約2週間後にfermawがパッチを適用
window.asグローバル変数の露出を削除し、初期化コードをクロージャで包んで外部アクセスを遮断
nozzle.jsに対するハッシュ検証チェックを導入(SRI、カスタム自己ハッシュ、サーバーサイドnonceシステムのいずれかと推定)
- 改変されたファイルが正規ハッシュと一致しない場合、プレイヤーが初期化されない構造
Act 2: V2.0 — 偽装手法と汎用フック
-
fermawのインメモリ防御
- JavaScriptでネイティブ関数に
.toString()を呼ぶと"function appendBuffer() { [native code] }"が返る一方、モンキーパッチされた関数は実際のソースコードを返す性質を利用
- fermawは
SourceBuffer.prototype.appendBuffer.toString()に'[native code]'が含まれていなければ再生を拒否する整合性チェックを追加
- プレイヤー初期化処理も難読化し、
AudioSourceクラスをポーリングループで見つけにくく変更
-
mockToString — 整合性チェックを欺く偽装関数
- フックされた関数の
.toString()が"function 名前() { [native code] }"を返すようにオーバーライド
- fermawの整合性チェックがfalse negativeを返すようにし、フックの有無を検出不能にした
-
HTMLMediaElement.prototype.play フック
window.asや特定クラス名を探す代わりに、**HTMLMediaElement.prototype.play**をフックする汎用アプローチを採用
- プレイヤーオブジェクトの名前やクロージャの深さに関係なく、
.play()呼び出し時点で音声要素を自動捕捉
- モバイル端末では通常1つのプレイヤーしかアクティブにならないため、多数の
.play()で逆解析を妨害するのは難しい
-
Object.definePropertyによる恒久固定
window.Audioを乗っ取ったコンストラクタに置き換えたうえで、writable: false、configurable: falseに設定
- fermawのコードが元の
Audioコンストラクタを復元しようとしてもブラウザがTypeErrorを投げる構造
- フックがページのライフサイクル全体を通して恒久的に維持される
Act 3: V3.0 — プロパティディスクリプタレベルでの全面フック
-
fermawのiframeおよびShadow DOM隔離の試み
<iframe>は独自のwindow、document、独立したプロトタイプチェーンを持つため、親windowのフックはiframe内部には適用されない
- Shadow DOMはメイン文書の
querySelectorでは内部要素を探索できない隔離DOMサブツリー
srcObjectを使ってMediaStream/MediaSourceオブジェクトを直接割り当て、URLベースのインターセプトを回避する手法も試された
-
V3の対応: ブラウザのプロパティディスクリプタ水準でのフック
Object.getOwnPropertyDescriptorを使い、HTMLMediaElement.prototypeの**srcとsrcObject setter**を直接フック
- 音声要素がメイン文書、iframe、Web Componentsのどこに存在していても、ソース割り当て時にフックが発動
document_start注入により、iframe初期化前にフックを設置
-
addSourceBufferフック: レースコンディションの解消
- 以前のバージョンでは
SourceBuffer.prototype.appendBufferをプロトタイプレベルでフックした場合、fermawのコードがフック設置前にappendBuffer参照をキャッシュすれば回避可能だった
- V3では
MediaSource.prototype.addSourceBufferをフックし、SourceBufferインスタンス生成時点を横取り
- インスタンスが返された直後に、そのインスタンスへ直接
appendBufferフックをown propertyとして設置
- ページコードがインスタンスを見る前にフックが完了するため、キャッシュによる回避が根本的に不可能
-
キャプチャ段階イベントリスナー — 最後の安全網
document.addEventListenerでuseCapture: true(キャプチャ段階)を指定し、play、loadedmetadataイベントを監視
- ブラウザイベントはキャプチャ段階(ルート→ターゲット)で先に伝播するため、HotAudioコードのイベントリスナーより常に先に実行される
addSourceBufferプロトタイプフック + src/srcObjectプロパティディスクリプタフック + play()フック + キャプチャ段階イベントリスナーという4層構成で、ブラウザのあらゆるメディア再生経路をカバー
自動化: 高速ダウンロードプロセス
- 捕捉した音声要素をミュートし、
playbackRateを16倍に設定して先頭から再生
- ブラウザは再生位置より先のバッファを埋めるため、高速にfetch→復号→
SourceBufferへの受け渡しを繰り返し、すべてのチャンクがフックされたappendBufferを通じて収集される
- Chromeは再生速度を16倍に制限している(HTML仕様には明示的な上限はないが、Chromium実装上の制約)
- fermawはバーストトラフィックへのスロットリング(数百KB/s → 約50 KB/s)を適用したが、リアルタイム再生と比べれば依然として数倍速い
- これ以上厳しく制限すると、通常ユーザーのストリーミングでも途切れが発生するため現実的ではない
-
適応型速度制御
- V3で追加された機能で、
bufferedタイムレンジを監視し、バッファ状態に応じて再生速度を動的制御
- バッファ余裕が15秒以上なら加速、3秒未満なら減速
- 低速回線でのブラウザ停止(stall)や
endedイベント未発火の問題を防止
-
最終ファイル生成
- 再生完了(
endedイベント、またはcurrentTimeがdurationに近づいた時点)で収集したチャンクをBlobに結合し、.m4aとしてダウンロード
- バッファ境界の不完全なチャンクにより無音パディングアーティファクトが発生することがあり、
ffmpeg後処理で整理可能
V3のspoof()関数: さらに精巧な偽装
- V2の
mockToStringはネイティブコード文字列をハードコードして返していたが、ブラウザやプラットフォームごとに[native code]文字列の空白や書式が微妙に異なる可能性があるという弱点があった
- V3の
spoof()は、フック前の元関数から実際のネイティブコード文字列を取得し、それをそのまま返す方式で完全な偽装を実現
_call.call(_toString, original)という形で、スクリプト開始時にキャッシュしたFunction.prototype.callとFunction.prototype.toString参照を使用
- 以後ほかのコードによって
.toStringが改変されても影響を受けない構造
DRMの本質的限界と倫理的考察
- DRMの歴史全体は「鍵のかかった箱を渡しながら同時に鍵も渡す」問題の繰り返し
- 1999年の最初のCSS暗号化DVDクラック以降、映画・音楽業界はこの戦いで負け続けてきた
- 最も高度なゲームDRMであるDenuvoですら、多くの主要ゲームで発売から数週間以内にクラックされている
- かつて著名クラッカーEmpressの引退でクラック速度は鈍化したが、ハイパーバイザ風エクスプロイトの登場で再び活発化している状況
- コンテンツと復号鍵の両方がクライアントマシン上に存在する限り、十分な動機とツールを持つユーザーによるインターセプトは避けられない
結論: JavaScript DRMは「精巧な摩擦」であって真のDRMではない
- HotAudioのDRMはfermawの能力不足ではなく、JavaScriptベースDRMが到達できる最善の結果
- クライアントサイド復号、チャンク配信、能動的な改ざん防止チェックまで実装しており、ブラウザ拡張を知らない大多数のユーザーに対しては完全な遮断効果がある
- しかしこれを「DRM」と呼ぶと、ハードウェアTEEベースの真のDRMと同じ期待値を設定してしまう点が問題
- ASMRクリエイターの熱心なファンは、オフラインコピーを欲しがるほど熱量が高く、Patreonのような有料チャネルがあれば喜んで購入する可能性がある層でもある
- どのような形であれ保護を必要とするコンテンツ制作者の事情は理解できるが、JavaScriptで実装するのは根本的に不適切なアプローチ
4件のコメント
本当にかなり面白い攻防だったんだろうな。
私も以前、APIのレスポンスが突然暗号化された状態で来たことがあって、暗号化された値を受け取ったならクライアントのどこかで復号するはずだと思い、バンドルされたJavaScriptコードを丸ごとそのままコピーして、復号コードの手前に
console.logを1行追加し、そのまま開発者コンソールに貼り付けたんです。意外にもそのまま動いたんですよね? とにかく、そうやって暗号鍵が分かると次は簡単でした。APIの別のレスポンスの中からキーを受け取って使っていたんです(笑)NSFW(Not Safe For Work)なASMRなら……
成人向けサイトをハッキングした話を、かなり技術的にディープに書いたものですね -. -;
やはり技術の進歩はすべてアダルト方面で起こります…?
考えてみると、音声にDRMをかけるというのは……本当に難しいのではないでしょうか?
複雑なハッキングをしなくても、音声を仮想ケーブルに流すだけで何とかできてしまいそうな気がします
> JavaScriptでネイティブ関数に
.toString()を呼び出すと"function appendBuffer() { [native code] }"を返しますが、モンキーパッチされた関数は実際のソースコードを返すという特性を活用しかし、かなり面白いやり取りでしたね(笑)。AIが絶対に思いつかなかったような裏技をひねり出している様子が見て取れます。