Files
revkit/docs/msl_ios.md
T

23 KiB

Netflix Message Security Layer (MSL) — iOS 動的解析レポート

Netflix iOS アプリ (Netflix-15.48.1.ipa / Argo.app) のバイナリ MslClient.framework を静的解析し、Frida による動的フックで得られた通信データと突合した結果をまとめる。


1. MSL プロトコル概要

MSL (Message Security Layer) は Netflix が独自開発した TLS 上のアプリケーション層セキュリティプロトコル。エンドポイント間の認証・鍵交換・メッセージの暗号化と完全性検証を担う。

プロトコルの目的

  • エンティティ認証: デバイス (ESN) をサーバーに証明する
  • ユーザー認証: Netflix アカウントの認証 (メール/パスワード, Cookie, SSO, UserIdToken)
  • 鍵交換: セッション鍵の安全な確立 (DH, JWE/JWK Ladder 等)
  • メッセージ保護: ペイロードの暗号化 (AES-CBC) と署名 (HMAC-SHA256)
  • トークン管理: MasterToken / UserIdToken / ServiceToken によるセッション維持

2. アーキテクチャ

2.1 フレームワーク構成

%%{init:{'theme':'dark'}}%%
graph TB
    subgraph "Argo.app (Netflix iOS)"
        A[Argo<br/>メインバイナリ] --> B[MslClient.framework<br/>MSL プロトコルコア]
        A --> C[NFWebCrypto.framework<br/>Web Crypto API]
        A --> D[NFURLSession.framework<br/>HTTP トランスポート]
        A --> E[NFContentKey.framework<br/>コンテンツ鍵管理]
        A --> F[NFLicenseFetch.framework<br/>DRM ライセンス]
        A --> G[Falcor.framework<br/>データモデル]
        A --> H[FTLProbeClient.framework<br/>低遅延プローブ]
        B --> C
        B --> D
        F --> B
    end

2.2 MslClient 内部クラス構成

%%{init:{'theme':'dark'}}%%
classDiagram
    class IosMslClient {
        -MslContext clientMslContext
        -IosMslStore mslStore
        -MslTrustStore mslTrustStore
        -EncoderFactory encoderFactory
        -MslControl mslControl
        +sendAPIRequest(url, headers, params, userAuthData, options, callback)
        +appboot(params)
        +initializeMslControl()
        +makeUserAuthData()
        +makeClientRequestMessageContext()
    }

    class IosMdxCryptoContext {
        -AppleWebCrypto crypto
        -MslEncoderFactory encoderFactory
        +encrypt(data) NSData
        +decrypt(data) NSData
        +hmac(data) NSData
    }

    class MslHttpTransaction {
        -NSURLRequest request
        -MslHttpRequestStream reqStream
        -MslHttpResponseStream respStream
        +write(data, length)
        +read(data, length)
        +flush()
        +close()
    }

    class MslUserAuthenticationData {
        -NSString GUID
        +isValid() bool
    }

    MslEmailPasswordAuthenticationData --|> MslUserAuthenticationData
    MslNetflixIdAuthenticationData --|> MslUserAuthenticationData
    MslSsoTokenAuthenticationData --|> MslUserAuthenticationData
    MslUserIdTokenAuthenticationData --|> MslUserAuthenticationData
    MslSwitchProfileAuthenticationData --|> MslUserAuthenticationData

    IosMslClient --> IosMdxCryptoContext
    IosMslClient --> MslHttpTransaction
    IosMslClient --> MslUserAuthenticationData
    IosMslClient --> MslContextHolder
    IosMslClient --> UserIdTokenHolder
    IosMslClient --> IosMslStoreTerminationListener

3. MSL メッセージ構造

3.1 全体構造

%%{init:{'theme':'dark'}}%%
graph LR
    subgraph "MSL メッセージ (JSON or CBOR)"
        H[Header] --> P1[PayloadChunk 1]
        P1 --> P2[PayloadChunk 2]
        P2 --> PN[PayloadChunk N<br/>endofmsg=true]
    end

Header は MessageHeader (正常時) または ErrorHeader (エラー時) のいずれか。

3.2 MessageHeader

{
  "headerdata": "<Base64 暗号文>",       // AES-CBC で暗号化された JSON
  "signature": "<Base64 HMAC-SHA256>",
  "mastertoken": { ... }                 // または "entityauthdata": { ... }
}

headerdata を復号すると以下の JSON が得られる:

フィールド 型 説明
messageid number メッセージ一意ID
sender string 送信者 (ESN)
recipient string 受信先
renewable boolean トークン更新要求
handshake boolean ハンドシェイクメッセージか
nonreplayableid number リプレイ防止ID
timestamp number Unix タイムスタンプ
capabilities object 対応する圧縮アルゴリズム等
keyrequestdata array 鍵交換リクエスト
keyresponsedata object 鍵交換レスポンス
userauthdata object ユーザー認証データ
useridtoken object UserIdToken

3.3 MasterToken

{
  "mastertoken": {
    "tokendata": "<Base64>",       // サーバー鍵で暗号化
    "signature": "<Base64>"
  }
}

tokendata (サーバーのみ復号可能) の内部:

フィールド 説明
renewalwindow 更新可能開始時刻
expiration 有効期限
sequencenumber シーケンス番号
serialnumber シリアル番号
sessiondata 暗号化されたセッションデータ (encryptionkey, hmackey, identity)

3.4 PayloadChunk

{
  "payloadchunk": {
    "payload": "<Base64 暗号文>",         // AES-CBC
    "signature": "<Base64 HMAC-SHA256>"
  }
}

payload を復号すると:

フィールド 説明
messageid メッセージID (Header と一致)
sequencenumber チャンク番号
endofmsg 最終チャンクか
compressionalgo GZIP (圧縮時)
data Base64 エンコードされたアプリケーションデータ (圧縮時は展開が必要)

3.5 ErrorHeader

{
  "errordata": "<Base64>",
  "signature": "<Base64>"
}

4. 暗号スタック

4.1 鍵交換フロー

%%{init:{'theme':'dark'}}%%
sequenceDiagram
    participant C as Client (iOS)
    participant S as Server (netflix.com)

    C->>S: MessageHeader + keyrequestdata<br/>(scheme: DIFFIE_HELLMAN)
    Note over C: DH 公開鍵を生成して送信
    S->>C: MessageHeader + keyresponsedata<br/>(mastertoken 付き)
    Note over C: dhComputeSharedSecret() で共有鍵を導出
    Note over C: aesKwUnwrap() でセッション鍵を展開
    Note over C: encryptionkey + hmackey を取得
    C->>S: 以降のメッセージは<br/>aesCbcEncrypt + signHmacSha256 で保護

4.2 鍵交換スキーム

バイナリから抽出された keyrequestdata.scheme の値:

スキーム 説明
DIFFIE_HELLMAN DH 鍵交換。dhComputeSharedSecret() で共有鍵を導出
JWE_LADDER JWE ベースのラダー方式鍵交換
JWK_LADDER JWK ベースのラダー方式鍵交換
ASYMMETRIC_WRAPPED RSA 等の非対称鍵でセッション鍵をラップ
SYMMETRIC_WRAPPED 対称鍵でセッション鍵をラップ

4.3 メッセージ暗号化/署名

%%{init:{'theme':'dark'}}%%
flowchart TD
    A[平文 headerdata / payload] -->|AES-128-CBC| B[暗号文]
    B -->|HMAC-SHA256| C[署名]

    subgraph "使用する鍵"
        EK[encryptionkey<br/>AES-128-CBC 用]
        HK[hmackey<br/>HMAC-SHA256 用]
    end

    EK --> B
    HK --> C

    subgraph "鍵の由来"
        DH[DH 共有鍵 or<br/>JWE/JWK Ladder] --> KW[AES Key Unwrap]
        KW --> EK
        KW --> HK
    end

4.4 C++ 暗号関数 (namespace netflix::msl::crypto)

関数 シグネチャ 用途
aesCbcEncrypt (key, iv, plaintext) → ciphertext ペイロード暗号化
aesCbcDecrypt (key, iv, ciphertext) → plaintext ペイロード復号
signHmacSha256 (key, data) → signature メッセージ署名
verifyHmacSha256 (key, data, signature) → bool 署名検証
aesKwWrap (kek, key) → wrapped セッション鍵ラップ
aesKwUnwrap (kek, wrapped) → unwrapped セッション鍵アンラップ
dhComputeSharedSecret (priv, pub, prime) → shared DH 共有鍵導出
rsaEncrypt (EVP_PKEY*, input) → output RSA 暗号化
rsaDecrypt (EVP_PKEY*, input) → output RSA 復号
hmacSha (alg, key, data) → mac 汎用 HMAC

引数はすべて std::vector<uint8_t> (arm64 レイアウト: +0x00 begin ptr, +0x08 end ptr)。


5. 認証スキーム

5.1 エンティティ認証

デバイスをサーバーに証明する。バイナリからは NONE スキームのみ確認。

5.2 ユーザー認証

%%{init:{'theme':'dark'}}%%
graph LR
    subgraph "ユーザー認証スキーム"
        EP[EMAIL_PASSWORD<br/>メール + パスワード]
        NID[NETFLIXID<br/>NetflixId + SecureNetflixId]
        UIT[USER_ID_TOKEN<br/>既存トークンで再認証]
        SSO[SSO_TOKEN<br/>SSO トークン]
        SP[SWITCH_PROFILE<br/>プロフィール切替]
    end
ObjC クラス スキーム 保持データ
MslEmailPasswordAuthenticationData EMAIL_PASSWORD email, password
MslNetflixIdAuthenticationData NETFLIXID netflixId, secureNetflixId
MslUserIdTokenAuthenticationData USER_ID_TOKEN UIT (UserIdToken)
MslSsoTokenAuthenticationData SSO_TOKEN ssoToken
MslSwitchProfileAuthenticationData SWITCH_PROFILE switchGUID, originalGUID

6. 通信先ドメイン

6.1 ライセンス・MSL 関連ドメイン

appboot.netflix.com — 初回起動・エンティティ認証

項目 内容
プロトコル HTTPS (非 MSL)
メソッド GET
パス /appboot/{ESN}?keyVersion=1
通信内容 アプリ起動時にデバイスの ESN (例: NFAPPL-02-IPHONE9=1-) をパスに含めてリクエスト。レスポンスで MSL TrustStore、サーバー設定、FTL ターゲットホスト一覧を受け取る。MSL 鍵交換の前段階として必須
タイミング アプリ起動時 (coldstart)、および定期的な再取得
復号状況 URL のみ取得済み (NSURL URLWithString フック)。リクエストボディは無い (GET)。レスポンスボディは未取得 — _handleAppbootResponse のフックが未実装のため、TrustStore・設定の中身は見えていない

ios.prod.cloud.netflix.com — MSL API メインエンドポイント

項目 内容
プロトコル HTTPS + MSL (アプリケーション層で MSL 暗号化)
メソッド POST
通信内容 MSL で暗号化されたペイロードを送受信。以下の主要 API を確認:

MSL API (IosMslClient.sendAPIRequest 経由):

MSL パス 用途 詳細 復号状況
/manifest ストリーミングマニフェスト取得 viewableId (コンテンツID) を指定し、利用可能なストリームプロファイル (H.264/HEVC/Audio/字幕) の一覧と CDN URL を取得。flavor は PRE_FETCH (プリフェッチ) と standard (再生開始時) の2種。drmType: "fairplay" で DRM 方式を指定 リクエスト平文: 取得済み — sendAPIRequest フックで暗号化前の全パラメータ (viewableId, profiles, drmType 等) を捕捉。レスポンス: 未取得 — サーバー返却の manifest JSON (CDN URL、ストリーム一覧) は aesCbcDecrypt で復号されるが、アプリ層での復号後データの捕捉フックが無い
/license DRM ライセンス取得 FairPlay の challengeBase64 (SPC: Server Playback Context) を送信し、CKC (Content Key Context) を受け取る。licenseType=limited、playbackContextId、drmContextId、esn をクエリパラメータで指定 リクエスト平文: 取得済み — challengeBase64 (SPC)、drmSessionId、xid 等を捕捉。SPC 自体は FairPlay が生成したバイナリの Base64 であり、中の鍵素材は取り出せていない。レスポンス (CKC): 未取得
/logblob ログ送信 クライアントログ (再生状態、エラー、ネットワーク統計等) をサーバーに送信 リクエスト平文: 取得済み — ログ内容 (type, msg, ftlstatus, ネットワーク統計等) を完全に捕捉
/syncDeactivateLinks デバイスリンク同期 デバイスの無効化リンクを同期 リクエスト平文: 取得済み

HTTP API (非 MSL、GraphQL/Falcor 経由):

HTTP パス 用途 詳細 復号状況
/graphql GraphQL API ブラウズデータ、マイリスト操作等の UI データ取得。operationName でクエリを指定 (例: myListActions) リクエスト: 取得済み — setHTTPBody フックで URL エンコードされたクエリ全文 (operationName, query, variables, esn 等) を捕捉。レスポンス: 未取得
/msl/playapi/ios/logblob 再生ログ送信 MSL ラップされた再生関連ログ (FTL ステータス、プローブ結果等) リクエスト: 部分的 — URL とメタデータ (size, content_type) は取得済みだが、ボディは MSL 暗号化済みのため body: null。平文は /logblob MSL API 側で捕捉済み
/nq/iosplatform/pbo_license/~1.0.0/router ライセンスルーター DRM ライセンス取得のルーティングエンドポイント リクエスト: 部分的 — URL とメタデータ (size: 2321) は取得済みだが、ボディは MSL 暗号化済みのため body: null。平文は /license MSL API 側で捕捉済み

ios.prod.ftl.netflix.com — FTL (低遅延) エンドポイント

項目 内容
プロトコル HTTPS
メソッド POST
パス /graphql
通信内容 ios.prod.cloud.netflix.com と同等の GraphQL API を提供する低遅延パス。FTL (Faster Than Light) はNetflix の独自エッジネットワーク (Open Connect) 上のプロキシで、AWS 経由より近いエッジから応答する。フォールバック先として ios.prod.cloud.netflix.com (AWS) と ios-anycast.prod.ftl.netflix.com (FTL Anycast) が設定されている
タイミング UI 操作時のデータ取得全般
復号状況 リクエスト: 部分的 — URL と HTTP メタデータ (method, content_type) は取得済みだが body: null (size: 0)。FTL 経由の GraphQL ボディは setHTTPBody フックで捕捉できていない (FTL 専用のリクエスト構築パスを使用している可能性)。レスポンス: 未取得

6.2 コンテンツ配信ドメイン

occ-*.nflxso.net — Open Connect CDN (画像)

項目 内容
プロトコル HTTPS
メソッド GET
パス /dnm/api/v6/{apiKey}/{imageHash}.{png,jpg,webp}?r={resolution}
通信内容 ボックスアート、サムネイル等の画像アセットをCDN から取得。occ-0-7146-8934.1.nflxso.net のようなパターンで、数字部分は CDN ノードを示す。大量の並列リクエストが発生 (1セッションで50件以上)
復号状況 URL: 取得済み — 画像 URL のフルパスとクエリパラメータを捕捉 (50件以上)。画像自体は暗号化されていない通常の HTTPS のため復号不要。レスポンス (画像バイナリ): 未取得 — SSL_read フックは無効化中

Note

: 動画・音声ストリーム本体の CDN ドメイン (ipv4-*.1.oca.nflxvideo.net 等) は /manifest レスポンスに含まれる URL から配信されるが、manifest レスポンスが未取得のためストリーム URL 自体を把握できていない。また DASH セグメントは DRM (FairPlay) で暗号化されており、コンテンツ鍵は /license レスポンス (CKC) から取得される。

6.3 その他

ドメイン 用途 通信内容 復号状況
ichnaea-web.netflix.com 地域判定 POST /cl2 (application/json) でクライアントの地理的位置を判定。コンテンツのリージョン制限に使用 URL のみ。body: null、レスポンスも未取得

6.4 復号状況サマリー

レイヤー 取得方法 状態 備考
MSL リクエスト平文 IosMslClient.sendAPIRequest フック 取得済み 暗号化前のパラメータ JSON を完全捕捉 (/manifest, /license, /logblob 等)
MSL レスポンス平文 aesCbcDecrypt フック (実装済み) 未取得 hookMslCrypto() に実装済みだが crypto/ ディレクトリにログが無い。MslClient モジュールがロードされていないか、C++ シンボルの解決に失敗した可能性
HTTP リクエストボディ setHTTPBody フック 部分的 GraphQL (cloud) は取得済み。FTL 経由・MSL ラップ済みリクエストは body: null
HTTP レスポンスボディ SSL_read フック (無効化中) 未取得 hookSSL() がコメントアウトされているため全レスポンスが未捕捉
MSL 暗号鍵素材 aesKwUnwrap / dhComputeSharedSecret フック 未取得 MslClient C++ フックからのログが出力されていない
FairPlay SPC/CKC /license MSL API フック SPC のみ challengeBase64 (SPC) は Base64 で取得済みだが中身は FairPlay バイナリ。CKC (レスポンス) は未取得
コンテンツ鍵 NFContentKey.framework (未フック) 未取得 DRM コンテンツ鍵の取得にはさらなるフックが必要

7. アプリ起動フロー

%%{init:{'theme':'dark'}}%%
sequenceDiagram
    participant App as Argo.app
    participant MSL as MslClient
    participant AB as appboot.netflix.com
    participant API as ios.prod.cloud.netflix.com

    App->>MSL: initWithDevice:storage:urlSession:...
    MSL->>MSL: initializeMslControl()

    rect rgba(100, 100, 200, 0.15)
        Note over MSL,AB: Appboot フェーズ
        MSL->>AB: appboot リクエスト (ESN, 設定)
        AB->>MSL: appboot レスポンス (TrustStore, 設定)
        MSL->>MSL: _handleAppbootResponse
        MSL->>MSL: _updateEntityAuthDeviceIdToken
    end

    rect rgba(100, 200, 100, 0.15)
        Note over MSL,API: 鍵交換フェーズ
        MSL->>API: MessageHeader (entityauthdata + keyrequestdata)
        API->>MSL: MessageHeader (mastertoken + keyresponsedata)
        MSL->>MSL: dhComputeSharedSecret → aesKwUnwrap
        MSL->>MSL: encryptionkey + hmackey 確立
    end

    rect rgba(200, 100, 100, 0.15)
        Note over MSL,API: API 通信フェーズ
        App->>MSL: sendAPIRequest(url, params, userAuthData, ...)
        MSL->>MSL: aesCbcEncrypt(headerdata) + signHmacSha256
        MSL->>MSL: aesCbcEncrypt(payloadchunk) + signHmacSha256
        MSL->>API: 暗号化済み MSL メッセージ (HTTP POST)
        API->>MSL: 暗号化済みレスポンス
        MSL->>MSL: aesCbcDecrypt + verifyHmacSha256
        MSL->>App: 平文レスポンス
    end

8. エンコード形式

バイナリ内に mslcbor_decode_ シンボルが存在することから、JSON と CBOR の両方をサポート:

  • JSON: デフォルトのエンコード形式。MslEncoderFactory + JsonMslArray で処理
  • CBOR: バイナリエンコード形式。mslcbor_decode_negint 等のシンボルあり。libcbor (configuration.h.in) を同梱
  • JSON パーサー: RapidJSON (Tencent 製, MIT ライセンス) を使用 (license.txt から確認)

9. 解析に使ったツール

ツール 用途
unzip IPA (ZIP) の展開
strings バイナリからの文字列抽出
nm --demangle C++ シンボルのデマングル・一覧取得
Frida + Gadget 動的フックによる実行時データ取得
hook_netflix.js MSL 暗号関数・API 呼び出しのインターセプト
run.py Frida ログの JSONL パース・ドメイン別ファイル保存

10. 未取得レスポンス一覧

以下は MSL 暗号化されているため現在のフック構成では平文を捕捉できていないレスポンス。いずれも ios.prod.cloud.netflix.com から MSL (AES-CBC + HMAC-SHA256) で暗号化されて返される。

API パス レスポンス内容 重要度 取得に必要なアプローチ
/manifest ストリーミングマニフェスト JSON — CDN URL 一覧 (urls)、ストリーム情報 (video_tracks, audio_tracks, timedtexttracks)、DRM 初期化データ (drmHeader)、セッション情報 (playbackContextId) 等 高 MSL レスポンス復号の捕捉が必要
/license FairPlay CKC (Content Key Context) — licenseResponseBase64 として返される。コンテンツ復号鍵が含まれる 高 MSL レスポンス復号 + FairPlay CKC パース
/logblob ログ送信の ACK — ステータスコード程度 低 —
/syncDeactivateLinks デバイス無効化リンクの同期結果 低 —
/nq/iosplatform/pbo_license/~1.0.0/router ライセンスルーティング結果 — 実際の /license エンドポイントへのリダイレクト情報 中 MSL レスポンス復号
/graphql (cloud) GraphQL レスポンス JSON — UI データ (lolomo、ビデオ詳細、マイリスト等) 中 SSL_read フック有効化 or レスポンスコールバックのフック
/graphql (ftl) 同上 (FTL 経由) 中 同上
/appboot/{ESN} Appboot 設定 JSON — TrustStore、FTL ターゲット、機能フラグ等 中 SSL_read フック有効化 or _handleAppbootResponse のフック

復号に向けたアプローチ

方法1: hookSSL() を有効化して SSL_read をフック
  → HTTP レスポンスを丸ごと取得
  → MSL 暗号化されたレスポンスはバイナリ (Base64 JSON) として見える
  → 暗号化された headerdata / payload をそのまま保存

方法2: MslClient C++ の aesCbcDecrypt フックを修正
  → hookMslCrypto() は実装済みだが MslClient モジュールのロード検知に問題あり
  → Module.findModuleByName("MslClient") の戻り値を確認
  → MslClient.framework 内の aesCbcDecrypt で復号後の平文を直接取得

方法3: ObjC コールバック層でレスポンスを捕捉
  → IosMslClient の sendAPIRequest の callback 引数をフック
  → MSL 復号後の平文 JSON がコールバックに渡されるため最も確実
  → 実装: ObjC.Block(args[7]) の invocation をラップして引数をログ

最も効果的な手法: 方法3 (コールバックフック) が MSL 復号済みの平文を直接取得でき、暗号鍵の管理が不要。/manifest と /license のレスポンスを取得できれば、CDN URL 一覧と FairPlay CKC を入手できる。