39 KiB
Netflix Message Security Layer (MSL) — Android 動的解析レポート
Netflix Android アプリ (com.netflix.mediaclient v9.57.0 / build 63928) を Frida で動的フックし、MSL 通信データをキャプチャした結果をまとめる。ProGuard 難読化済み Java バイトコードをランタイムでインターセプトしている。
キャプチャ環境: Widevine セキュリティレベルを L3 (ソフトウェア) に強制変更した状態で取得。L1 (TEE) 環境でのキャプチャデータは未取得。L3 では暗号処理がソフトウェアで実行されるため、Frida による decrypt 平文の取得が可能になっている。
1. MSL プロトコル概要
MSL (Message Security Layer) は Netflix が独自開発した TLS 上のアプリケーション層セキュリティプロトコル。iOS 版と同一の仕様だが、Android 版では Widevine ベースの暗号コンテキストを使用する。
プロトコルの目的
- エンティティ認証: デバイス (ESN) をサーバーに証明する (Widevine デバイス証明書)
- ユーザー認証: Netflix アカウントの認証 (SSO Token, UserIdToken 等)
- 鍵交換: Widevine 鍵交換によるセッション鍵の安全な確立
- メッセージ保護: ペイロードの暗号化 (Widevine CryptoContext) と署名 (HMAC-SHA256)
- トークン管理: MasterToken / UserIdToken / ServiceToken によるセッション維持
- ALE (Application Level Encryption): ソケットレベル暗号化 (RSA-OAEP-256 + A128GCM)
2. アーキテクチャ
2.1 アプリ構成
%%{init:{'theme':'dark'}}%%
graph TB
subgraph "com.netflix.mediaclient (Android)"
A[Netflix App<br/>Samurai UI Framework] --> B[com.netflix.msl.*<br/>MSL プロトコルコア]
A --> C[com.netflix.ale.*<br/>Application Level Encryption]
A --> D[OkHttp3<br/>HTTP クライアント]
A --> E[android.media.MediaDrm<br/>Widevine DRM]
A --> F[Bugsnag<br/>クラッシュ分析]
B --> G[WidevineCryptoContext<br/>暗号化/復号/署名]
B --> H[MslControl<br/>プロトコル制御]
B --> I[BaseHandler<br/>リクエスト処理]
C --> J[AleCryptoBouncyCastle<br/>ALE 暗号実装]
end
2.2 ProGuard 難読化マッピング
Android アプリは ProGuard により難読化されており、暗号メソッド名が短縮されている:
| 元メソッド名 | 難読化名 | クラス |
|---|---|---|
encrypt |
(元のまま) | WidevineCryptoContext |
decrypt |
c (2引数: [B, MslEncoderFactory) |
WidevineCryptoContext |
sign |
b (3引数) |
WidevineCryptoContext |
verify |
c (3引数, 戻り値 boolean) |
WidevineCryptoContext |
wrap |
c (3引数, 戻り値 [B) |
JsonWebEncryptionCryptoContext |
unwrap |
d |
JsonWebEncryptionCryptoContext |
2.3 デバイス情報
| 項目 | 値 |
|---|---|
| ESN | NFANDROID1-PRV-P-GOOGLPIXEL=4A==5G=-{userId}-{fingerprint} |
| デバイス | Google Pixel 4a (5G), codename bramble |
| SoC | Qualcomm lito |
| Android | 14 (API 34), build UP1A.231005.007 |
| アプリバージョン | 9.57.0 (build 63928) |
| プラットフォーム | Android Tanto (Samurai UI) |
| Widevine CDM | v17.0.0 |
| セキュリティレベル | L3 (ソフトウェア) に強制変更済み — 本来は L1 (TEE), OEMCrypto v1.56 |
| Bugsnag | v6.25.0 |
| jailbroken | true (root 検出) |
3. MSL メッセージ構造
iOS 版と同一仕様。ただしエンコーディングは CBOR を使用。
3.1 全体構造
%%{init:{'theme':'dark'}}%%
graph LR
subgraph "MSL メッセージ (CBOR)"
H[Header] --> P1[PayloadChunk 1]
P1 --> P2[PayloadChunk 2]
P2 --> PN[PayloadChunk N<br/>endofmsg=true]
end
3.2 CBOR エンコーディング
キャプチャされた Widevine encrypt の平文データから、MSL メッセージが CBOR (Concise Binary Object Representation) でエンコードされていることを確認:
- プレフィックス
d9d9f7= CBOR Self-Described Tag (RFC 8949) - 埋め込みフィールド:
profileidx: ユーザープロファイル ID (例:ZEULH5S2GNGCRAABCSG6J2EGGA)- 圧縮アルゴリズム:
GZIP - エンコーダ:
CBOR - 発行者:
Netflix maxpayloadchunksize: 最大ペイロードチャンクサイズ
3.3 CBOR 整数キーマッピング
CBOR エンコード時、JSON の文字列キーはプロトコル仕様で定義された固定の整数キーに置換される。完全なマッピングは userauthdata_android.md §6 を参照。主要キー:
| CBOR キー | JSON フィールド名 | 使用箇所 |
|---|---|---|
15 |
tokendata |
MasterToken, UserIdToken, keyrequestdata |
16 |
signature |
同上 |
17 |
mastertoken |
MessageHeader |
18 |
useridtoken |
MessageHeader |
19 |
renewable |
MessageHeader |
20 |
sender |
MessageHeader |
21 |
handshake |
MessageHeader |
22 |
messageid |
MessageHeader, PayloadChunk |
24 |
timestamp |
MessageHeader |
30 |
scheme |
keyrequestdata, userauthdata |
36 |
capabilities |
MessageHeader |
42 |
keyrequestdata |
MessageHeader |
47 |
userauthdata |
MessageHeader |
14 |
sequencenumber |
PayloadChunk |
44 |
compressionalgo |
PayloadChunk |
62 |
data |
PayloadChunk |
63 |
endofmsg |
PayloadChunk |
3.4 MessageHeader / PayloadChunk / ErrorHeader
構造は iOS 版 (msl_ios.md §3) と同一。暗号化アルゴリズムが AES-CBC から Widevine CryptoContext に変更されている点のみ異なる。
4. 暗号スタック
4.1 鍵交換フロー
%%{init:{'theme':'dark'}}%%
sequenceDiagram
participant C as Client (Android)
participant S as Server (netflix.com)
C->>S: MessageHeader + keyrequestdata<br/>(scheme: WIDEVINE)
Note over C: Widevine デバイス証明書で<br/>エンティティ認証
S->>C: MessageHeader + keyresponsedata<br/>(mastertoken 付き)
Note over C: Widevine CryptoContext で<br/>セッション鍵を確立
C->>S: 以降のメッセージは<br/>WidevineCryptoContext.encrypt + sign で保護
4.2 暗号コンテキスト
iOS 版が C++ の aesCbcEncrypt / signHmacSha256 を使用するのに対し、Android 版は Java の WidevineCryptoContext を通じて Widevine CDM に暗号処理を委譲する。
L1 vs L3: L1 (TEE) では暗号処理がハードウェアセキュアエレメント内で行われ、Frida から平文を取得できない。本キャプチャでは L3 (ソフトウェア) に強制変更しており、
encrypt/decryptの平文を Frida で捕捉可能になっている。
| コンポーネント | クラス | 用途 | キャプチャ数 |
|---|---|---|---|
| Widevine 暗号化 | WidevineCryptoContext.encrypt |
MSL ペイロード暗号化 | 322 |
| Widevine 復号 | WidevineCryptoContext.c (2引数) |
MSL レスポンス復号 | 36 |
| Widevine 署名 | WidevineCryptoContext.b (3引数) |
MSL メッセージ HMAC 署名 | 322 |
| HMAC-SHA256 | HmacSha256Signer.a |
エンティティ認証データ署名 | 1 |
| AES-CBC | AesCbcEncryptor |
(Widevine 使用時は不使用) | 0 |
| JWE | JsonWebEncryptionCryptoContext |
(未観測) | 0 |
4.3 Widevine 署名の詳細
キャプチャされた msl.widevine.sign イベントから:
- データ: CBOR エンコードされた MSL メッセージヘッダー/ペイロード (典型的に ~1247 バイト)
- 署名サイズ: 32 バイト (HMAC-SHA256 相当)
- 全送信メッセージに対して
encryptとsignが 1:1 で呼ばれる (322:322)
4.4 初回エンティティ認証
msl.hmacSha256.sign が 1 回だけ呼ばれ、ESN (NFANDROID1-PRV-P-GOOGLPIXEL=4A==5G=) で始まるデータ (2722 バイト) を署名。これは Widevine デバイス証明書を含むエンティティ認証データ。
5. ALE (Application Level Encryption)
iOS 版には存在しない Android 固有の暗号化レイヤー。ソケットレベルの通信を追加で暗号化する。
5.1 アーキテクチャ
%%{init:{'theme':'dark'}}%%
sequenceDiagram
participant App as Netflix App
participant ALE as AleService
participant Crypto as AleCryptoBouncyCastle
participant Server as Netflix Server
App->>ALE: getProvisioningRequest()
Note over ALE: RSA-2048 エフェメラル鍵ペア生成
ALE->>Server: provisionRequest<br/>(RSA-OAEP-256 公開鍵, A128GCM, SOCKETROUTER)
Server->>ALE: provisionResponse<br/>(JWE トークン + ラップ済みセッション鍵)
ALE->>ALE: createSession(token)
App->>ALE: encrypt(plaintext)
ALE->>Crypto: aesGcmEncrypt(plaintext)
Crypto->>ALE: ciphertext
ALE->>ALE: JweBase.encrypt(ciphertext)
5.2 プロビジョニングリクエスト
キャプチャから確認された構造:
{
"provisionRequest": {
"keyx": {
"data": { "pubkey": "<RSA-2048 公開鍵 base64url>" },
"scheme": "RSA-OAEP-256"
},
"scheme": "A128GCM",
"type": "SOCKETROUTER",
"ver": 1
}
}
- 鍵交換: RSA-OAEP-256 でセッション鍵を安全に交換
- 対称暗号: A128GCM (AES-128-GCM)
- セッションタイプ:
SOCKETROUTER - 2 回のプロビジョニング呼び出しを観測 (それぞれ異なるエフェメラル RSA-2048 鍵)
5.3 セッション作成
プロビジョニング応答で受け取った JWE トークンでセッションを確立:
- JWE ヘッダー:
alg: A128GCMKW,enc: A128GCM,ver: 1 - セッション TTL: 1800 秒
- 更新ウィンドウ: 600 秒
- 鍵交換:
RSA-OAEP-256でラップされたセッション鍵
5.4 暗号パイプライン
同一の 24 バイト平文が 3 つのレイヤーを通過するのを確認:
ale.encrypt—AleSession.encrypt(String)で暗号化開始ale.aesGcmEncrypt—AleCryptoBouncyCastle.aesGcmEncryptで AES-GCM 暗号化ale.jwe.encrypt—JweBase.encrypt(byte[])で JWE 形式にラップ
6. Widevine DRM
6.1 DRM セッション管理
%%{init:{'theme':'dark'}}%%
sequenceDiagram
participant App as Netflix App
participant DRM as android.media.MediaDrm
participant CDM as Widevine CDM (TEE)
App->>DRM: openSession()
DRM->>CDM: セッション開始
CDM->>App: sessionId (sid74, sid75, ...)
App->>DRM: getKeyRequest(sessionId, initData, "application/xml", STREAMING)
DRM->>CDM: ライセンスチャレンジ生成
CDM->>App: keyRequest (Widevine protobuf ~2378B)
Note over App: keyRequest を MSL /license API で<br/>challengeBase64 として送信
App->>DRM: provideKeyResponse(sessionId, response)
DRM->>CDM: コンテンツ鍵をインストール
6.2 デバイス証明書情報
drm.keyRequest の Widevine protobuf から抽出 (L3 強制環境):
L1 との差異: L1 環境では OEMCrypto がハードウェア TEE 内で動作し、
oem_crypto_build_informationにOEMCrypto Level1と表示される。L3 ではOEMCrypto Level3 Codeとなり、暗号処理がユーザー空間で実行されるため Frida によるインターセプトが可能。ストリーミングプロファイルも L1 では HEVC HDR10 / 高解像度が利用可能だが、L3 では H.264 / VP9 の低解像度に制限される。
| 項目 | 値 |
|---|---|
| CDM バージョン | 17.0.0 |
| OEMCrypto API | v16.3 |
| LibOEMCrypto | v1.56 |
| TA バージョン | 1.1382 |
| セキュリティレベル | L3 (ソフトウェア) — 本来は L1 (TEE) |
| ベンダー | |
| デバイス名 | bramble |
| アーキテクチャ | arm64-v8a |
| ビルド | google/bramble/bramble:14/UP1A.231005.007/10754064:user/release-keys |
6.3 キャプチャされた DRM イベント
| イベント | 件数 | 内容 |
|---|---|---|
drm.openSession |
4 | セッション ID: sid74, sid75, sid77, sid78 |
drm.keyRequest |
4 | keyType=STREAMING, mimeType=application/xml |
drm.keyResponse |
2 | ライセンスメタデータ (ESN, movieId, issuetime) |
drm.propertyString |
6 | securityLevel=L1, vendor=Google, description=Widevine CDM |
drm.property |
1 | deviceUniqueId (32 バイト) |
7. 通信先ドメイン
7.1 Appboot — 初回起動
appboot.netflix.com
| 項目 | 内容 |
|---|---|
| プロトコル | HTTPS (非 MSL) |
| 通信内容 | アプリ起動時に MSL TrustStore、JS TrustStore、TLS 設定を取得 |
| 復号状況 | レスポンス: 取得済み — AppbootHandlerImpl.appbootExecute フックで完全捕捉 |
キャプチャされた Appboot レスポンス:
{
"jtruststore": {
"keys": { "CHB4+ATbieuZVWvMdlu3Ow==": "<RSA-2048 公開鍵 PEM>" },
"jsVerificationEnabled": true
},
"msltruststore": {
"keys": { "MSL_TRUSTED_NETWORK_SERVER_KEY": "<RSA-2048 公開鍵 PEM>" }
},
"tls_cipher_suites": "ECDHE-ECDSA-AES128-GCM-SHA256:...",
"tls_cipher_suites_by_version": {
"1.2": "ECDHE-ECDSA-AES128-GCM-SHA256:...",
"1.3": "TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256"
},
"servertime_seconds": 1773370561
}
msltruststore: MSL 鍵交換で使用するサーバー RSA 公開鍵jtruststore: JavaScript 検証用の公開鍵tls_cipher_suites: TLS 1.2/1.3 で使用する暗号スイート一覧
7.2 MSL API エンドポイント
android14.prod.ftl.netflix.com — FTL (低遅延) エンドポイント
| 項目 | 内容 |
|---|---|
| プロトコル | HTTPS + MSL |
| メソッド | POST |
| 通信内容 | MSL で暗号化されたペイロードを送受信。DRM・マニフェスト・再生イベント系 |
MSL API (ApiHandlerImpl.apiRequest 経由):
| MSL パス / URL | 用途 | 詳細 | 復号状況 |
|---|---|---|---|
/aleProvision |
ALE プロビジョニング | RSA-OAEP-256 公開鍵と A128GCM スキームを送信し、ALE セッション鍵を取得。netflixClientPlatform: androidNative |
リクエスト: 取得済み (2件) |
/licensedManifest |
マニフェスト + ライセンス一括取得 | viewableId を指定し、ストリームプロファイル一覧と Widevine ライセンスチャレンジを同時送信。drmType: "widevine", flavor: "PRE_FETCH", manifestVersion: "v2" |
リクエスト: 取得済み (1件、3 viewableIds 同時) |
/license |
DRM ライセンス取得 | Widevine challengeBase64 (CDM ライセンスチャレンジ ~4KB) を送信。licenseType=standard, playbackContextId, drmContextId, esn をクエリパラメータで指定 |
リクエスト: 取得済み (1件) |
bundle (/events) |
再生イベント送信 | keepAlive イベントを ~60秒間隔で送信。position (再生位置), trackId, mediaId, playTimes, CDN 情報を含む | リクエスト: 取得済み (5件) |
config (Falcor) |
設定取得 | /nq/androidui/samurai/v1/config — deviceConfig, hendrixConfig, networkScoreConfig, streamingConfig2 等 |
リクエスト: 取得済み (1件) |
android14.prod.cloud.netflix.com — Cloud (AWS) エンドポイント
| 項目 | 内容 |
|---|---|
| プロトコル | HTTPS + MSL |
| メソッド | POST |
| パス | /graphql |
| 通信内容 | GraphQL API による UI データ取得。FTL のフォールバック先 |
GraphQL オペレーション:
| オペレーション名 | Persisted Query ID | 用途 | 件数 |
|---|---|---|---|
RenewSSOToken |
a4d00303-b02d-47c9-a53f-776b6a63b001 |
SSO トークン更新 | 1 |
InterstitialForLolomo |
3658ef5b-0c6d-4c7d-a5c0-6ae11405ee1d |
ホーム画面インタースティシャル | 1 |
InterstitialForProfileGate |
18f3ae27-a0f1-45c9-88e6-c6bd39159ecb |
プロフィール選択画面 | 1 |
AccountQuery |
4043dd89-0ed5-4d7f-ac5c-40c7ffcec7ae |
アカウント情報取得 | 2 |
InterstitialForPlayback |
07d88886-1ec5-4115-98f2-7b6a20dbeae6 |
再生前インタースティシャル | 1 |
android14.logs.netflix.com — ログエンドポイント
| 項目 | 内容 |
|---|---|
| プロトコル | HTTPS + MSL |
| メソッド | POST |
| パス | /log/android/logblob/1 |
| 通信内容 | クライアントログ (telemetry, FTL ステータス, NRTS サブスクリプション等) の送信 |
| 件数 | 150 件 |
ログエントリの例:
{
"clver": "9.57.0-63928 R android-34-EXO",
"type": "nrts",
"ftlstatus": {
"target": "primary",
"hostname": "android14.prod.ftl.netflix.com",
"targets": ["primary", "fallback-aws", "fallback-ftl-anycast"]
},
"nrtsSubscriptionResponses": [
{"topic": {"name": "LIVE_EVENT_STATE_CHANGE", "parameters": {"country": "JP", "videoId": 82146341}}}
]
}
7.3 FTL ターゲット構成
| ターゲット名 | ホスト |
|---|---|
primary |
android14.prod.ftl.netflix.com |
fallback-aws |
android14.prod.cloud.netflix.com |
fallback-ftl-anycast |
(Anycast FTL エンドポイント) |
7.4 その他のドメイン
| ドメイン | 用途 | 復号状況 |
|---|---|---|
android14.push.prod.netflix.com |
プッシュ通知 (WebSocket?) | URL のみ。リクエスト/レスポンスは空 |
android14.ws.prod.cloud.netflix.com |
WebSocket 通信 | URL のみ。リクエスト/レスポンスは空 |
sessions.bugsnag.com |
クラッシュ分析セッション報告 | 取得済み (HTTP 202 accepted) |
7.5 Cronet HTTP トランスポート
Netflix Android は 全 HTTP 通信を Chromium Cronet ネットワークスタック (org.chromium.net.impl.CronetUrlRequest) 経由で行っている。OkHttp はヘッダーレベルの処理に使われるが、実際のソケット通信は Cronet が担当する。
hook_cronet.js で CronetUrlRequest.start() と VersionSafeCallbacks.i (コールバックラッパー) をフックし、全 Cronet HTTP リクエスト/レスポンスをキャプチャした結果:
キャプチャ結果
| # | メソッド | URL | ステータス | プロトコル | 用途 |
|---|---|---|---|---|---|
| 1 | POST | https://android14.appboot.netflix.com/appboot/NFANDROID1-PRV-P-?keyVersion=1&suspended=false |
200 | HTTP/1.1 | Appboot (初期設定取得) |
| 2 | POST | https://android14.prod.ftl.netflix.com/nq/androidui/samurai/~9.0.0/api |
200 | h2 | MSL API (Samurai UI) |
| 3 | POST | https://android14.prod.cloud.netflix.com/graphql ×3 |
200 | h2 | GraphQL (UI データ、Cloud エンドポイント) |
| 4 | POST | https://android14.prod.ftl.netflix.com/graphql ×2 |
200 | h2 | GraphQL (FTL プライマリ) |
| 5 | POST | https://android14.prod.ftl.netflix.com/nq/androidui/samurai/v1/config |
200 | h2 | Samurai Config (Falcor) |
| 6 | POST | https://android14.logs.netflix.com/log/android/cl/2?TAG=LOG_CLV2 ×3 |
FAIL | — | クライアントログ (CLv2) — DNS 解決失敗 |
ドメイン別集計
| ドメイン | リクエスト数 | プロトコル | 状態 |
|---|---|---|---|
android14.prod.ftl.netflix.com |
4 | HTTP/2 | 全成功 |
android14.prod.cloud.netflix.com |
3 | HTTP/2 | 全成功 |
android14.logs.netflix.com |
3 | — | 全失敗 (ERR_NAME_NOT_RESOLVED) |
android14.appboot.netflix.com |
1 | HTTP/1.1 | 成功 |
発見事項
- 全通信が Cronet 経由: Appboot、MSL API、GraphQL、Config、ログ送信すべてが
CronetUrlRequest.start()を通過 - HTTP/2 がデフォルト: Appboot (HTTP/1.1) を除き、全エンドポイントで HTTP/2 を使用
- MSL 暗号化は Cronet の上位: Cronet は純粋な HTTP トランスポートを担当し、MSL の暗号化/署名は
WidevineCryptoContext/ApiHandlerImplで行われた後に Cronet に渡される - 新規ログエンドポイント:
android14.logs.netflix.com/log/android/cl/2?TAG=LOG_CLV2— CLv2 (Client Log version 2) 形式。MSL 経由の/logblobとは別系統 - Cronet 設定:
enableHttpCache(0, 0L)(キャッシュ無効),enableHttp2(true)(HTTP/2 有効)
Cronet アーキテクチャ
%%{init:{'theme':'dark'}}%%
graph TB
subgraph "Netflix App"
A[ApiHandlerImpl<br/>MSL API] -->|"MSL 暗号文"| D
B[GraphQL Client] -->|"MSL 暗号文"| D
C[AppbootHandlerImpl] -->|"平文 JSON"| D
E[Log Client<br/>CLv2] -->|"ログデータ"| D
D[Cronet HTTP Client<br/>CronetUrlRequest] -->|"HTTP/2 + TLS"| F[Netflix Server]
end
フック構成 (hook_cronet.js)
| フック対象 | クラス / メソッド | 取得データ |
|---|---|---|
| リクエスト開始 | CronetUrlRequest.start() |
URL, HTTP メソッド, UploadData 有無 |
| レスポンス受信 | VersionSafeCallbacks.i.onResponseStarted() |
ステータスコード, プロトコル, URL |
| リダイレクト | VersionSafeCallbacks.i.onRedirectReceived() |
リダイレクト先 URL |
| 完了 | VersionSafeCallbacks.i.onSucceeded() |
受信バイト数 |
| 失敗 | VersionSafeCallbacks.i.onFailed() |
エラーメッセージ |
| キャンセル | VersionSafeCallbacks.i.onCanceled() |
— |
8. アプリ起動フロー
%%{init:{'theme':'dark'}}%%
sequenceDiagram
participant App as Netflix App
participant AB as appboot.netflix.com
participant MSL as MSL Layer
participant FTL as android14.prod.ftl.netflix.com
participant Cloud as android14.prod.cloud.netflix.com
participant Logs as android14.logs.netflix.com
rect rgba(100,100,200,0.15)
Note over App,AB: Appboot フェーズ
App->>AB: appboot リクエスト
AB->>App: TrustStore, TLS 設定, サーバー時刻
end
rect rgba(200,150,100,0.15)
Note over App,FTL: ALE プロビジョニング
App->>FTL: /aleProvision (RSA-OAEP-256 + A128GCM)
FTL->>App: ALE セッショントークン (JWE)
App->>App: AleService.createSession(token)
end
rect rgba(100,200,100,0.15)
Note over App,Cloud: MSL 鍵交換 + 認証
App->>MSL: Widevine エンティティ認証 (HMAC-SHA256 署名)
MSL->>Cloud: keyrequestdata (Widevine)
Cloud->>MSL: keyresponsedata + mastertoken
MSL->>MSL: WidevineCryptoContext 確立
end
rect rgba(100,100,200,0.15)
Note over App,Cloud: SSO + UI データ取得
App->>Cloud: GraphQL RenewSSOToken
App->>Cloud: GraphQL InterstitialForLolomo
App->>Cloud: GraphQL InterstitialForProfileGate
App->>Cloud: GraphQL AccountQuery
end
rect rgba(200,100,100,0.15)
Note over App,FTL: 再生準備
App->>FTL: /licensedManifest (PRE_FETCH, 3 viewableIds)
App->>FTL: /license (standard, challengeBase64)
App->>FTL: bundle /events (keepAlive × 5)
end
rect rgba(150,150,150,0.15)
Note over App,Logs: ログ送信 (並行)
App->>Logs: /logblob × 150 (telemetry, NRTS, FTL)
end
9. /licensedManifest リクエスト詳細
iOS 版の /manifest + /license が Android 版では /licensedManifest として統合されている。
9.1 サポートするストリームプロファイル (L3 環境)
L3 強制環境でキャプチャされた profiles フィールドから。L1 環境では HEVC HDR10 (hevc-hdr-main10-*) や高解像度 H.264 (playready-h264mpl40-dash, playready-h264hpl40-dash) など追加のプロファイルが含まれる。
動画:
| プロファイル | コーデック |
|---|---|
playready-h264mpl30-dash, playready-h264mpl31-dash, playready-h264mpl40-dash |
H.264 Main Profile |
playready-h264hpl22-dash, playready-h264hpl30-dash, playready-h264hpl31-dash, playready-h264hpl40-dash |
H.264 High Profile |
hevc-hdr-main10-L30-dash-cenc, hevc-hdr-main10-L31-dash-cenc, hevc-hdr-main10-L40-dash-cenc, hevc-hdr-main10-L41-dash-cenc |
HEVC HDR10 |
vp9-profile0-L21-dash-cenc, vp9-profile0-L30-dash-cenc, vp9-profile0-L31-dash-cenc, vp9-profile0-L40-dash-cenc |
VP9 Profile 0 |
音声:
| プロファイル | コーデック |
|---|---|
heaac-2-dash, heaac-2hq-dash |
HE-AAC |
xheaac-dash |
xHE-AAC (低ビットレート) |
字幕・その他:
| プロファイル | 用途 |
|---|---|
imsc1.1, simplesdh, nflx-cmisc |
字幕 (IMSC 1.1, SDH) |
BIF320 |
トリックプレイサムネイル |
9.2 DRM 設定
{
"drmType": "widevine",
"flavor": "PRE_FETCH",
"manifestVersion": "v2",
"licenseType": "limited",
"challenges": {
"primary": [{
"challengeBase64": "<Widevine CDM protobuf (L3)>",
"drmSessionId": 1
}]
}
}
9.3 iOS 版との主な違い
| 項目 | iOS | Android |
|---|---|---|
| API パス | /manifest + /license (別リクエスト) |
/licensedManifest (統合) |
| DRM タイプ | fairplay |
widevine |
| チャレンジ形式 | FairPlay SPC | Widevine CDM protobuf |
| 動画コーデック | H.264, HEVC | H.264, HEVC HDR10, VP9 |
| Manifest バージョン | 不明 | v2 |
| ライセンスタイプ | limited |
limited (PRE_FETCH), standard (再生時) |
10. フックスクリプト構成
hook_netflix_android.js (約 1853 行) は以下の 8 つのフック関数で構成:
| フック関数 | 対象 | フックするクラス/メソッド |
|---|---|---|
hookSSLPinning() |
SSL ピン留め無効化 | X509TrustManager, CertificatePinner |
hookSSL() |
ネイティブ SSL 通信 | SSL_write, SSL_read (libssl.so 等) |
hookMSL() |
MSL プロトコル層 | ApiHandlerImpl.apiRequest, BaseHandler.processRequest, PayloadChunk.$init, MessageInputStream.read, AppbootHandlerImpl.appbootExecute |
hookMSLCrypto() |
MSL 暗号化 | WidevineCryptoContext, HmacSha256Signer, AesCbcEncryptor, SymmetricCryptoContext, JsonWebEncryptionCryptoContext |
hookHTTP() |
OkHttp 通信 | RealCall.getResponseWithInterceptorChain, URL.openConnection |
hookWidevineKeyExchange() |
鍵交換 (列挙のみ) | WidevineKeyExchange, DiffieHellmanExchange 等 |
hookALE() |
ALE 暗号化 | AleService, AleSession, AleCryptoBouncyCastle, JweBase |
hookWidevineDRM() |
Widevine DRM | android.media.MediaDrm |
hook_cronet.js — Cronet HTTP トランスポート専用フック:
| フック関数 | 対象 | フックするクラス/メソッド |
|---|---|---|
hookSSLPinning() |
SSL ピン留め無効化 | TrustManagerImpl, CertificatePinner |
hookCronetStart() |
Cronet リクエスト開始 | CronetUrlRequest.start() |
hookCronetCallbacks() |
Cronet コールバック | VersionSafeCallbacks.i (onResponseStarted, onRedirectReceived, onSucceeded, onFailed, onCanceled) |
11. 復号状況サマリー
11.1 レイヤー別取得状況
| レイヤー | 取得方法 | 状態 | 件数 | 備考 |
|---|---|---|---|---|
| MSL リクエスト平文 | ApiHandlerImpl.apiRequest フック |
取得済み | 160 | 暗号化前のパラメータ JSON を完全捕捉 |
| MSL レスポンス平文 | BaseHandler.processRequest フック |
未取得 | 14 (全て null) | フック発火するがフィールド抽出に失敗 |
| Widevine encrypt 平文 | WidevineCryptoContext.encrypt フック |
取得済み | 322 | CBOR エンコード済み MSL コンテンツ |
| Widevine decrypt 平文 | WidevineCryptoContext.c フック |
取得済み | 36 | CBOR バイナリ (MSL レスポンスデータ) |
| Widevine sign データ | WidevineCryptoContext.b フック |
取得済み | 322 | MSL メッセージバイト |
| HMAC-SHA256 sign | HmacSha256Signer.a フック |
取得済み | 1 | エンティティ認証データ |
| Appboot レスポンス | AppbootHandlerImpl.appbootExecute フック |
取得済み | 1 | TrustStore, TLS 設定を完全捕捉 |
| ALE プロビジョニング | AleService フック |
取得済み | 2+2 | provisionRequest + createSession |
| ALE 暗号化 | AleSession + AleCryptoBouncyCastle フック |
取得済み | 3 | encrypt → aesGcmEncrypt → jwe.encrypt |
| ALE 復号 | AleSession.decrypt 等 |
未取得 | 0 | フック実装済みだがイベント未発生 |
| Widevine DRM | android.media.MediaDrm フック |
取得済み | 17 | 全イベントタイプ捕捉済み |
| HTTP (OkHttp) | RealCall フック |
部分的 | 6 | Netflix 外ドメイン (Bugsnag) のみ |
| SSL (native) | SSL_write フック |
部分的 | 27 | バイナリデータのみ (HTTP/2 フレーム) |
| Cronet HTTP | CronetUrlRequest.start() + VersionSafeCallbacks.i |
取得済み | 28 | 全 HTTP リクエスト/レスポンスの URL, ステータス, プロトコルを捕捉 |
11.2 API パス別取得状況
| API パス | ドメイン | リクエスト | レスポンス |
|---|---|---|---|
/aleProvision |
FTL | 取得済み | 復元済み (L3 decrypt → CBOR → GZIP) — provisionResponse (JWE トークン) |
/licensedManifest |
FTL | 取得済み | 復元済み (L3 decrypt → CBOR → GZIP) — 456KB、CDN URL + ストリーム一覧 + DRM |
/license |
FTL | 取得済み | 復元済み (L3 decrypt → CBOR → GZIP) — licenseResponseBase64 (Widevine protobuf) |
/events (bundle) |
FTL | 取得済み | 未取得 |
config (Falcor) |
FTL | 取得済み | 復元済み (L3 decrypt → CBOR → GZIP) — deviceConfig, streamingConfig2 等 |
/graphql (6 ops) |
Cloud | 取得済み | 復元済み (L3 decrypt → CBOR → GZIP) — AccountQuery 等 |
/logblob |
Logs | 取得済み | 復元済み (L3 decrypt → CBOR → GZIP) — logblob ACK |
| appboot | appboot.netflix.com | N/A (GET) | 取得済み (直接フック) |
12. 復号済みレスポンスの復元可能性
12.1 現在の状況
BaseHandler.processRequest フックによる MSL API レスポンスの直接取得は失敗している (全件 response: null)。これは難読化フィールド名の問題であり、L1/L3 に関係しない。
一方、L3 環境では Widevine decrypt フックで復号後の平文データが取得可能であり、CBOR デコード → GZIP 展開により MSL レスポンスの平文 JSON を完全に復元できることを確認済み。
L1 環境での制約: L1 (TEE) では
WidevineCryptoContext.decryptの平文が Widevine CDM のセキュアメモリ内で処理されるため、Frida からのplaintext_b64取得は不可能と推定される。レスポンス復元にはBaseHandler.processRequestのフィールド抽出修正、またはMessageInputStream.readの蓄積が必要。
12.2 復元パイプライン
%%{init:{'theme':'dark'}}%%
flowchart LR
A["暗号化された<br/>MSL レスポンス"] -->|WidevineCryptoContext.c| B["復号済み平文<br/>(CBOR バイナリ)"]
B -->|"cbor2.loads()"| C["dict<br/>key 62 = GZIP データ"]
C -->|"gzip.decompress()"| D["アプリケーション<br/>JSON レスポンス"]
style B stroke:#0f0,stroke-width:2px
style D stroke:#0f0,stroke-width:2px
12.3 CBOR PayloadChunk の構造
キャプチャされた msl.widevine.decrypt の plaintext_b64 を CBOR デコードすると、数値キー の dict が得られる:
| CBOR キー | 値 | 意味 |
|---|---|---|
62 |
bytes (GZIP 圧縮データ) |
ペイロード本体 — GZIP 展開するとアプリケーション層 JSON |
22 |
int (例: 1214369777020027) |
メッセージ ID |
44 |
"GZIP" |
圧縮アルゴリズム |
63 |
true / false |
endofmsg — 最終チャンクフラグ |
14 |
int (例: 1, 2, ...) |
チャンクシーケンス番号 |
注: フィールド名が文字列 (
"data") ではなく数値 (62) になっている。これは MSL の CBOR エンコーディングが整数キーを使用するため。
12.4 復元の検証結果
実際にキャプチャデータで CBOR → GZIP 復元を実行し、以下のレスポンスを確認:
| URL / API | チャンク数 | 復元サイズ | 復元内容 |
|---|---|---|---|
.../samurai/v1/config |
3 | 26,953B | deviceConfig, streamingConfig2, accountConfig (Falcor JSON Graph) |
.../samurai/~9.0.0/api |
3 | 9,208B | AccountQuery レスポンス — memberSince, countryOfSignUp: JP 等 |
/licensedManifest |
8 | 456KB | マニフェスト完全復元 — 3 viewableIds, video/audio/text トラック, CDN URL, DRM |
/license (standard) |
1 | 1.9KB | Widevine ライセンスレスポンス — licenseResponseBase64, drmGroupId, expiration |
/aleProvision |
1 | 1.3KB | ALE provisionResponse (JWE トークン + セッション鍵) |
.../log/android/logblob/1 |
10 | ~60KB/chunk | logblob ACK (サーバー確認応答) |
復元例 (config レスポンス):
{
"paths": [["deviceConfig", "hendrixConfig", "networkScoreConfig", "accountConfig"]],
"jsonGraph": {
"accountConfig": {
"$type": "atom",
"value": {
"thumbRatingActive": true,
"videoBufferSize": 96468992,
"streamingCodecPrefData": {
"isAVCHighCodecEnabled": true,
"isVP9HWCodecEnabled": true,
"isAV1CodecEnabled": false,
"isXHEAACCodecEnabled": true
}
}
}
}
}
12.5 復元手順
import cbor2, gzip, base64, json
# 1. plaintext_b64 を Base64 デコード
plaintext = base64.b64decode(event["plaintext_b64"])
# 2. CBOR デコード (数値キーの dict)
chunk = cbor2.loads(plaintext)
# 3. キー 62 の GZIP データを展開
payload_json = gzip.decompress(chunk[62])
# 4. アプリケーション層 JSON を取得
response = json.loads(payload_json)
12.6 復元対象の分布
| ドメイン | decrypt 件数 | 復元可能 | 主な内容 |
|---|---|---|---|
android14.prod.ftl.netflix.com |
~8 | 6 件 | config, API レスポンス (AccountQuery 等) |
android14.prod.cloud.netflix.com |
~2 | 要検証 | GraphQL レスポンス |
android14.logs.netflix.com |
~26 | 10 件 | logblob ACK (大量チャンク) |
結論:
BaseHandler.processRequestは失敗しているが、Widevine decrypt の平文データから CBOR (key 62) → GZIP 展開で MSL レスポンスの平文 JSON が完全に復元できる。config, AccountQuery,/licensedManifest(456KB),/license等の全主要 API レスポンスを復元済み。復元結果はlogs/android_20260313/decrypted/に保存。
13. レスポンス復元結果
13.1 復元済みレスポンス一覧
L3 Widevine decrypt → CBOR (key 62) → GZIP 展開で復元。logs/android_20260313/decrypted/ に保存。
| ファイル | API パス | サイズ | 内容 |
|---|---|---|---|
01_config_msgid*.json |
appboot | 1.6KB | MSL TrustStore, TLS cipher suites, サーバー時刻 |
02_config_msgid*.json |
/aleProvision |
1.3KB | ALE provisionResponse (JWE トークン) |
03_config_msgid*.json |
config (Falcor) | 39KB | deviceConfig, streamingConfig2, accountConfig |
04_api_msgid*.json |
/graphql |
15KB | AccountQuery (memberSince, countryOfSignUp 等) |
05_api_msgid*.json |
/aleProvision |
1.3KB | ALE provisionResponse (2nd) |
06_logblob_msgid*.json |
/graphql |
15KB | AccountQuery (logs ドメイン経由) |
07_logblob_msgid*.json |
config | 39KB | config (logs ドメイン経由) |
08_logblob_msgid*.json |
/licensedManifest |
622KB | マニフェスト完全復元 — 3 viewableIds, CDN URL, ストリーム一覧, DRM |
09_api_msgid*.json |
/license |
1.9KB | standard ライセンスレスポンス (licenseResponseBase64) |
13.2 BaseHandler.processRequest の問題 (未解決)
msl.api.response は全 14 件で response: null, size: 0。原因:
BaseHandler.processRequestの戻り値o.jRPクラスからフィールドを抽出しようとしているが:- フィールド
c(byte[] レスポンスボディ) →nullを返す - フィールド
e(Map レスポンスヘッダー) → フィールド未発見
- フィールド
- 難読化により実際のフィールド名が異なる可能性
- 回避策: 方法2 (Widevine decrypt 平文の CBOR デコード) で全主要レスポンスを復元済み。
BaseHandlerの修正は優先度低。
13.3 復元パイプライン
import cbor2, gzip, base64, json
# 1. plaintext_b64 を Base64 デコード
plaintext = base64.b64decode(event["plaintext_b64"])
# 2. CBOR デコード (数値キーの dict)
chunk = cbor2.loads(plaintext)
# 3. キー 62 の GZIP データを展開
payload_json = gzip.decompress(chunk[62])
# 4. アプリケーション層 JSON を取得
response = json.loads(payload_json)
複数チャンクの場合は
messageid(キー 22) でグルーピングし、sequencenumber(キー 14) 順に結合してから GZIP 展開する。
14. iOS 版との主要な違い
| 観点 | iOS | Android |
|---|---|---|
| バイナリ形式 | MslClient.framework (C++, Mach-O) |
Java APK (ProGuard 難読化) |
| 暗号エンジン | C++ aesCbcEncrypt / signHmacSha256 |
Java WidevineCryptoContext (TEE 委譲) |
| DRM | FairPlay (SPC/CKC) | Widevine CDM v17.0.0 (本キャプチャは L3) |
| 鍵交換 | Diffie-Hellman, JWE/JWK Ladder | Widevine ベース |
| ALE | なし | あり (RSA-OAEP-256 + A128GCM) |
| エンコーディング | JSON + CBOR (RapidJSON) | CBOR (CBOR self-described tag) |
| MSL ドメイン | ios.prod.cloud.netflix.com |
android14.prod.cloud.netflix.com |
| FTL ドメイン | ios.prod.ftl.netflix.com |
android14.prod.ftl.netflix.com |
| ログドメイン | (cloud と共有) | android14.logs.netflix.com (専用) |
| マニフェスト API | /manifest + /license (分離) |
/licensedManifest (統合) |
| 動画コーデック | H.264, HEVC | H.264, HEVC HDR10, VP9 |
| Appboot レスポンス | 未取得 | 取得済み |
| MSL レスポンス | 未取得 | L3: decrypt 経由で復元済み (9 API レスポンス) / L1: 未取得 |
| GraphQL | cloud のリクエスト取得済み | cloud のリクエスト取得済み (Persisted Query) |
| SSL ピン留め | ObjC TrustManager | CertificatePinner + X509TrustManager |
| UI フレームワーク | Falcor + FTLProbeClient | Samurai (Tanto) + NRTS |
| クラッシュ分析 | (未検出) | Bugsnag 6.25.0 |
15. キャプチャ統計
セッション: android_20260313
| 項目 | 値 |
|---|---|
| 総イベント数 | ~943 ファイル |
| MSL API リクエスト | 160 |
| MSL API レスポンス | 14 (全て response=null) |
| Widevine encrypt | 322 |
| Widevine sign | 322 |
| Widevine decrypt | 36 |
| DRM イベント | 17 |
| ALE イベント | 7 |
| HTTP イベント | 6 |
| SSL write (raw) | 27 |
| Appboot レスポンス | 1 |
| ドメイン数 | 9 |
ドメイン別イベント数
| ドメイン | イベント数 | 主な通信 |
|---|---|---|
android14.logs.netflix.com |
421 | logblob (150), widevine encrypt/decrypt |
crypto/ (ドメインなし) |
334 | widevine.sign (333), hmacSha256.sign (1) |
_other/ (ドメインなし) |
78 | DRM, ALE, ssl.write |
android14.prod.cloud.netflix.com |
68 | GraphQL (6), widevine encrypt/decrypt |
android14.prod.ftl.netflix.com |
55 | aleProvision, licensedManifest, license, bundle |
msl.netflix.com |
18 | msl.api.response (全て null) |
appboot.netflix.com |
1 | appboot レスポンス |
| その他 | 6 | Bugsnag, push, WebSocket |