chore: prepare for commit message generation without code changes

This commit is contained in:
tkgstrator
2026-03-14 18:29:30 +00:00
commit bd016ec00a
45 changed files with 9389 additions and 0 deletions
+136
View File
@@ -0,0 +1,136 @@
# プロジェクト概要
Netflix iOS/Androidアプリのリバースエンジニアリングプロジェクト。
対象アプリが送信するHTTPリクエストの安全性を検証するため、暗号化されたリクエストボディの復号に必要な鍵・アルゴリズムを特定する。
## 対象アプリ
- アプリ名: Netflix
- Bundle ID (iOS): `com.netflix.Netflix`
- Bundle ID (Android): `com.netflix.mediaclient`
## 環境構成
| 項目 | バージョン / 値 |
|---|---|
| Python | 3.12(パッケージ管理: uv) |
| Frida | 17.8.0 |
| Objection | 1.12.3 |
### 対象デバイス
| デバイス | OS | ホスト |
|---|---|---|
| iPhone (Jailbroken) | iOS 15.8.3 | `192.168.0.34` |
| Pixel 4a (5G) / `bramble` | Android 14 (API 34) | `192.168.0.37` |
# ドキュメント運用
## 保存先
ドキュメント作成を指示された場合、以下の2種類を必ず作成すること:
| 種別 | 保存先 | 形式 |
|---|---|---|
| LLM向け | `.github/instructions/{名前}.instructions.md` | frontmatterに `applyTo` を記述し、関連ファイルパターンを指定。構造化された簡潔な形式 |
| 人間向け | `docs/{名前}.md` | 散文形式。図解にはMermaid記法を使用し、テーマは `%%{init: {'theme': 'dark'}}%%` を指定すること |
## 制約
- 1ファイルが400行を超える場合は、指示がなくてもトピック単位で分割すること
- ファイル名はスネークケースを使用すること
## 主要ドキュメント一覧
LLM向けの詳細コンテキストは以下を参照すること:
- `.github/instructions/*.instructions.md` — ファイルスコープ付きLLM向け指示(`applyTo` で適用先を指定)
- `docs/` — 人間向けドキュメント
| パス | 内容 |
|---|---|
| `docs/msl_ios.md` | iOS MSLプロトコルの構造・暗号スタック |
| `docs/msl_android.md` | Android MSLプロトコルの構造 |
| `docs/manifest_ios.md` | iOSマニフェスト仕様 |
| `docs/manifest_android.md` | Androidマニフェスト仕様 |
| `docs/auth_flow_android.md` | Android認証フロー |
| `docs/esn_android.md` | Android ESN仕様 |
| `docs/pxa_esn.md` | PXA ESN仕様 |
# ツール使用ルール
## Objection
> **重要**: 以下の非推奨コマンド・オプションをコード生成やコマンド提案で絶対に使用しないこと。
| 禁止 | 代替 | 説明 |
|---|---|---|
| `explore` | `start` | アプリへの接続コマンド |
| `--gadget` / `-g` | `--name` / `-n` | 対象アプリ指定オプション |
正しい使用例:
```bash
objection -n "com.netflix.Netflix" start
```
## Frida
- コマンド実行時は必ず `-H <ホストIP>` を指定して対象デバイスに接続すること
- iOS: `-H 192.168.0.34`
- Android: `-H 192.168.0.37`
- 実行中アプリ一覧の確認: `frida-ps -H <ホストIP> -a`
# アーキテクチャ
## 目的
対象アプリの暗号化通信(MSL等)をFridaでフックし、暗号化前の平文データ・復号済みデータ・暗号鍵・IVなどを取得する。
## JS / Python の役割分担
| レイヤー | ファイル | 責務 |
|---|---|---|
| JavaScript (Frida) | `hook_*.js` | 対象アプリのプロセス内で動作。暗号関数・API呼び出しをフックし、引数や戻り値(鍵・IV・平文・暗号文など)を `@@LOG@@{json}` 形式で `console.log` に出力する。**データの加工・保存はしない。** |
| Python (ホスト) | `run.py` | Fridaプロセスを起動・管理する。stdout から `@@LOG@@` プレフィクス付きの行をパースし、ドメイン別・イベント別にJSON/バイナリファイルとして `logs/` に保存する。MSL平文とHTTPリクエストの紐付け、Cookie/ヘッダの自動エクスポートも行う。 |
### ログプロトコル
JavaScript側は以下の形式で標準出力にログを送る:
```
@@LOG@@{"event":"msl.aesCbcEncrypt.key","key_b64":"...","ts":"..."}
```
- プレフィクス `@@LOG@@` に続くJSON文字列をPython側がパースする
- `event` フィールドでイベント種別を識別する
- バイナリデータは `_b64`(Base64)または `_hex`(16進数)サフィックスのフィールドで渡す
### シグナルハンドリング
`run.py` は `SIGINT` / `SIGTERM` を捕捉し、Fridaサブプロセスを安全に終了させる(`terminate` → 3秒待機 → `kill`)。Ctrl+C での中断時もログの集計とCookie/ヘッダのエクスポートが `finally` ブロックで実行される。フックスクリプト作成時は、JS側での終了処理は不要(Python側が管理する)。
## コード構成
| パス | 役割 |
|---|---|
| `hook_netflix.js` | iOS用フックスクリプト(ObjC/C++関数のフック) |
| `hook_netflix_android.js` | Android用フックスクリプト(Java/JNIメソッドのフック) |
| `hook_msl.js` | MSL暗号関数の個別フック |
| `hook_cronet.js` | Cronetネットワーク層のフック |
| `hook_esn.js` | ESN取得用フック |
| `hook_headers.js` | HTTPヘッダ/Cookie取得用フック |
| `run.py` | メインランナー(iOS/Android両対応、`--android` フラグで切替) |
| `run_android.sh` | Android用シェルラッパー |
| `run_cronet.py` | Cronet用ランナー |
| `__handlers__/` | Objectionハンドラ |
## 新しいフックを書くときの方針
1. **静的解析で対象関数を特定する**: フックを書く前に、アプリのバイナリを静的解析して目的の関数・メソッドを探すこと。推測でフック対象を決めてはならない
- iOS: `strings`, `nm --demangle`, `class-dump` 等でMach-Oバイナリからシンボル・クラス・セレクタを抽出する
- Android: `jadx` でAPK/DEXを逆コンパイルし、クラス名・メソッドシグネチャを確認する。ProGuardで難読化されている場合は既存フックのコメントにあるマッピングを参照する
2. **JS側**: 対象関数をフックし、取得したデータを `@@LOG@@{json}` で出力するだけにする
3. **Python側**: ログのパース・整形・保存・紐付けロジックは `run.py` に集約する
4. バイナリデータはJS側でBase64または16進数に変換してからログに含める
5. 終了処理はPython側が担うため、JS側で `Script.on('unload')` 等の後処理は基本不要
@@ -0,0 +1,44 @@
---
applyTo: 'hook_netflix_android.js,hook_msl.js,run_android.sh,docs/auth_flow_android*.md'
---
## Android 認証フロー (LLM 向け)
### ドキュメント構成
| ファイル | 内容 |
|---|---|
| `docs/auth_flow_android.md` | フロー概要・Mermaid 図・鍵交換・Cookie フロー・ProxyESN ライフサイクル・セキュリティ観察 |
| `docs/auth_flow_android_api.md` | 全 API のリクエスト/レスポンス詳細 (Header/Cookie/Body 表)・Persisted Query 一覧・完全タイムライン |
### 認証フロー要約
1. **Phase 0:** `ProxyEsn.$init` で PXA ESN の TTL チェック。期限切れなら再取得フラグ ON
2. **Phase 1:** 7 リクエストを並列送信 (appboot, getProxyEsn, aleProvision#1, RenewSSOToken, CurrentCountryQuery, Interstitial×2)
3. **Phase 2:** レスポンス受信。CurrentCountryQuery で `NetflixId`/`SecureNetflixId` Cookie 発行、appboot で `nfvdid` Cookie 更新
4. **Phase 3:** AccountQuery, aleProvision#2 (→PXA ESN 確定), PromoProfileGateVideoDataQuery
5. **Phase 4:** 約 30 秒後に FetchConfigData, AccountQuery#2
### エンドポイント
| 略称 | ホスト | 用途 |
|---|---|---|
| appboot | `android14.appboot.netflix.com` | デバイス登録 |
| prod.ftl | `android14.prod.ftl.netflix.com` | MSL API + non-MSL GraphQL |
| prod.cloud | `android14.prod.cloud.netflix.com` | MSL GraphQL |
### 認証方式の二重構造
- **MSL API** (`prod.cloud`, `prod.ftl` の MSL エンドポイント): Master Token + User Auth Data で認証。Cookie 不要
- **Non-MSL GraphQL** (`prod.ftl/graphql`): `NetflixId` / `SecureNetflixId` Cookie で認証
### 鍵交換
- アルゴリズム: RSA-OAEP-256 (鍵交換) + A128GCM (セッション暗号化)
- aleProvision は 2 回呼ばれる。#1 は起動直後、#2 は getProxyEsn レスポンス後
- 同一 RSA-2048 公開鍵を使用。サーバーが新セッション鍵を発行
### Frida フック固有の注意
- `hook_msl.js` が `ProxyEsn.$init` で `expired=true` を強制。通常は TTL が有効ならキャッシュ使用
- キャプチャ内の PXA ESN 再取得は Frida による強制失効の結果
@@ -0,0 +1,54 @@
---
applyTo: 'ipa_extracted/**'
---
## MSL 解析手順 (LLM 向け)
### 静的解析
1. IPA を展開: `unzip -o Netflix-15.48.1.ipa -d ipa_extracted/`
2. MSL バイナリ: `ipa_extracted/Payload/Argo.app/Frameworks/MslClient.framework/MslClient` (arm64 Mach-O)
3. ObjC クラス抽出: `strings MslClient | grep -E '^\+\[|^\-\[' | sed 's/\[//;s/ .*//' | sort -u`
4. C++ シンボル抽出: `nm --demangle MslClient | grep 'netflix.*msl'`
5. JSON フィールド名・定数: `strings MslClient | grep -E '^(mastertoken|headerdata|payloadchunk|scheme|...)$'`
### 動的解析 (Frida)
`hook_netflix.js` を Frida Gadget 注入済み IPA で実行。`run.py` が `@@LOG@@{json}` をパースし `logs/{date}/{domain}/` に保存。
**Hook 関数と目的:**
| 関数 | Hook 対象 | 取得データ |
|---|---|---|
| `hookSSLPinning()` | `-[* URLSession:didReceiveChallenge:completionHandler:]` (NF/Netflix/Osprey) | SSL pinning 回避 |
| `hookSSL()` (無効) | `SSL_write` / `SSL_read` (libboringssl) | TLS 平文 (MSL 暗号文のまま) |
| `hookMSL()` | `IosMslClient -sendAPIRequest:extraHeaders:params:userAuthData:requestOptions:callback:` | **暗号化前の API パス (args[2]) とパラメータ (args[4])** |
| `hookMSL()` | `IosMdxCryptoContext -encrypt:` / `-decrypt:` | MDX 暗号化前後のデータ |
| `hookObjCTrace()` | `+[NSURL URLWithString:]` / `-[NSMutableURLRequest setHTTPBody:]` | URL と HTTP ボディ |
| `hookCrypto()` (無効) | `CCCrypt` / `SecKeyEncrypt` / `SecKeyRawSign` | CommonCrypto/Security 暗号処理 |
| `hookMslCrypto()` | `aesCbcEncrypt` / `aesCbcDecrypt` (MslClient) | AES-CBC 鍵・IV・平文・暗号文 |
| `hookMslCrypto()` | `signHmacSha256` (MslClient) | HMAC 鍵・署名対象・署名値 |
| `hookMslCrypto()` | `aesKwUnwrap` (MslClient) | KEK・ラップ鍵 → セッション鍵 |
| `hookMslCrypto()` | `dhComputeSharedSecret` (MslClient) | DH 秘密鍵・公開鍵・素数 → 共有鍵 |
| `hookMslCrypto()` | `rsaEncrypt` / `rsaDecrypt` (MslClient) | RSA 入出力 |
**C++ 引数の読み方:** `std::vector<uint8_t>` は `+0x00: __begin_` (ptr), `+0x08: __end_` (ptr)。size = end - begin。
**ObjC args オフセット:** args[0]=self, args[1]=_cmd, args[2]~ が実引数。
### ログイベント種別
| event | 内容 |
|---|---|
| `msl.api` | MSL API リクエスト (domain, url, params) |
| `msl.encrypt.input` / `msl.decrypt.output` | MDX 暗号化/復号データ |
| `msl.aesCbcEncrypt.*` / `msl.aesCbcDecrypt.*` | AES-CBC の key, iv, plaintext, ciphertext |
| `msl.hmacSha256.*` | HMAC の key, data, signature |
| `msl.aesKwUnwrap.*` | AES-KW の kek, wrappedKey, unwrappedKey |
| `msl.dh.*` | DH の privKey, pubKey, prime, sharedSecret |
| `url` | URL アクセス |
| `http.request` | HTTP リクエスト (method, url, content_type, body) |
### 詳細ドキュメント
MSL プロトコルの構造・暗号スタック・クラス構成の詳細は `docs/msl.md` を参照。