Files
revkit/tools/decrypt_capture.py
tkgstrator f481f03124 feat(msl-ios): improve iOS CBOR MSL decryption and tweak DH key capture
Enhances iOS MSL CBOR decoder for multi-item parsing, iOS-specific payload
handling, and IV extraction from ciphertext. Updates NetflixCrypto to support
Tweak-format key JSON. Extends the Tweak to capture DH key exchange material
and session keys more reliably, saving full key histories and implementing
better appboot phase tracking. Adds scripts for DH-derived HKDF parameter
analysis and appboot key response investigation.

Improves documentation on iOS CBOR MSL protocol differences, decryption
pipeline, and key extraction workflow.

Relates to iOS MSL traffic analysis and decryption research.
2026-04-08 10:18:44 +00:00

205 lines
6.3 KiB
Python

#!/usr/bin/env python3
"""decrypt_capture.py — MSL キャプチャファイルのオフライン復号 CLI
Frida / mitmproxy でキャプチャしたバイナリファイルと鍵素材を入力に、
AES-CBC 復号した結果を出力する。
使い方:
python tools/decrypt_capture.py --keys keys.json --input capture.bin
python tools/decrypt_capture.py --keys keys.json --input capture.bin --output decrypted.json
python tools/decrypt_capture.py --enc-key <hex> --sign-key <hex> --input capture.bin
python tools/decrypt_capture.py --keys keys.json --input-dir ./captures/
python tools/decrypt_capture.py --keys keys.json --input capture.bin --format raw
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
# src/ をモジュール検索パスに追加
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from netflix_msl.cbor_decoder import CborMslDecoder
from netflix_msl.crypto import NetflixCrypto
def build_crypto(args: argparse.Namespace) -> NetflixCrypto:
"""引数から NetflixCrypto インスタンスを構築して返す."""
crypto = NetflixCrypto()
if args.keys:
crypto.import_keys_from_file(str(args.keys))
elif args.enc_key and args.sign_key:
enc = bytes.fromhex(args.enc_key)
sign = bytes.fromhex(args.sign_key)
crypto.import_session_keys(enc, sign)
else:
print(
"[!] --keys または --enc-key / --sign-key のいずれかを指定してください",
file=sys.stderr,
)
sys.exit(1)
return crypto
def process_file(
decoder: CborMslDecoder,
path: Path,
fmt: str,
output_path: Path | None,
) -> bool:
"""単一ファイルを処理する. 成功時 True を返す."""
raw = path.read_bytes()
try:
result = decoder.process_message(raw)
except Exception as e:
print(f"[E] {path.name}: {e}", file=sys.stderr)
return False
payloads = result.get("payloads", [])
if not payloads:
print(f"[W] {path.name}: ペイロードなし", file=sys.stderr)
return False
# 復号されたペイロードを出力
output_data: dict | list = {
"file": path.name,
"signature_valid": result.get("signature_valid"),
"payloads": payloads,
}
if fmt == "hex":
output_str = json.dumps(output_data, ensure_ascii=False, default=repr)
elif fmt == "raw":
output_str = json.dumps(output_data, ensure_ascii=False, indent=2, default=repr)
else:
output_str = json.dumps(output_data, ensure_ascii=False, indent=2, default=repr)
if output_path is not None:
output_path.write_text(output_str, encoding="utf-8")
print(f"[+] {path.name} -> {output_path}", file=sys.stderr)
else:
print(output_str)
return True
def main() -> None:
parser = argparse.ArgumentParser(
description="MSL キャプチャファイルのオフライン復号",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
# 鍵素材
key_group = parser.add_argument_group("鍵素材 (いずれか必須)")
key_group.add_argument(
"--keys",
type=Path,
metavar="FILE",
help='Frida が出力した鍵 JSON ファイル ({"enc_key": "<hex>", "sign_key": "<hex>"})',
)
key_group.add_argument(
"--enc-key",
metavar="HEX",
help="AES-128 暗号化鍵を hex で直接指定",
)
key_group.add_argument(
"--sign-key",
metavar="HEX",
help="HMAC-SHA256 署名鍵を hex で直接指定",
)
# 入力
input_group = parser.add_argument_group("入力 (いずれか必須)")
input_ex = input_group.add_mutually_exclusive_group(required=True)
input_ex.add_argument(
"--input",
type=Path,
metavar="FILE",
help="単一の .bin ファイル",
)
input_ex.add_argument(
"--input-dir",
type=Path,
metavar="DIR",
help="ディレクトリ内の全 .bin ファイルを処理",
)
# 出力
parser.add_argument(
"--output",
type=Path,
metavar="FILE",
help="出力先ファイル (省略時は stdout)。--input-dir 使用時は無視される",
)
parser.add_argument(
"--format",
choices=["json", "raw", "hex"],
default="json",
help="出力フォーマット (default: json)",
)
args = parser.parse_args()
# 鍵素材の相互排他チェック
if args.keys and (args.enc_key or args.sign_key):
parser.error("--keys と --enc-key/--sign-key は同時に指定できません")
if not args.keys and not (args.enc_key and args.sign_key):
parser.error("--keys か --enc-key と --sign-key の両方を指定してください")
crypto = build_crypto(args)
decoder = CborMslDecoder(crypto)
# 入力ファイルの収集
if args.input is not None:
if not args.input.exists():
print(f"[!] ファイルが見つかりません: {args.input}", file=sys.stderr)
sys.exit(1)
files = [args.input]
output_path = args.output
else:
if not args.input_dir.is_dir():
print(
f"[!] ディレクトリが見つかりません: {args.input_dir}", file=sys.stderr
)
sys.exit(1)
files = sorted(args.input_dir.glob("*.bin"))
if not files:
print(
f"[!] .bin ファイルが見つかりません: {args.input_dir}", file=sys.stderr
)
sys.exit(1)
output_path = None # ディレクトリ処理時は stdout に個別出力
success = 0
failure = 0
for path in files:
if args.input_dir is not None and len(files) > 1:
# 複数ファイル処理: ファイル名をヘッダとして表示
print(f"\n=== {path.name} ===", file=sys.stderr)
ok = process_file(decoder, path, args.format, output_path)
if ok:
success += 1
else:
failure += 1
if len(files) > 1:
print(
f"\n[*] 完了: {success} 成功 / {failure} 失敗 (合計 {len(files)} ファイル)",
file=sys.stderr,
)
if failure > 0:
sys.exit(1)
if __name__ == "__main__":
main()