Files
video_extractor/get_client_id.py
T
claudeandClaude Opus 4.8 453ca2f85a Initial commit of video_extractor toolkit
Widevine/DRM research and video download scripts (Apple, CDA, Sky, TVP),
CDM tooling, and helper utilities. Excludes downloads/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WFUhtZEpkGZJjNCGFS9r4Z
2026-08-08 12:35:28 +02:00

164 lines
5.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Capture the Widevine ClientIdentification by getting a license challenge
from MediaDrm and parsing out the embedded client_id.
Run while the emulator is running with frida-server active.
Saves L3/client_id.bin on success.
"""
import frida, sys, time, os
from pathlib import Path
OUT = Path(__file__).parent / 'L3'
OUT.mkdir(exist_ok=True)
ADB = os.path.expanduser('~/Android/Sdk/platform-tools/adb')
PSSH_B64 = 'AAAAOnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABoIARIQ1DG5TL5NQ/2zoimqi0ixiUjj3JWbBg=='
JS = r"""
'use strict';
send({type:'log', msg:'Script loaded in PID ' + Process.id});
var MAX_ATTEMPTS = 30;
var attempt = 0;
function tryJava() {
attempt++;
var avail = (typeof Java !== 'undefined') && Java.available;
if (avail) {
runMediaDrm();
} else if (attempt < MAX_ATTEMPTS) {
setTimeout(tryJava, 200);
} else {
send({type:'error', msg:'Java not available after ' + MAX_ATTEMPTS + ' attempts'});
}
}
function runMediaDrm() {
Java.perform(function() {
try {
var UUID = Java.use('java.util.UUID');
var MediaDrm = Java.use('android.media.MediaDrm');
var Base64 = Java.use('android.util.Base64');
var wvUUID = UUID.fromString('edef8ba9-79d6-4ace-a3c9-27dcd51d21ed');
send({type:'log', msg:'Creating MediaDrm...'});
var drm = MediaDrm.$new(wvUUID);
var session = drm.openSession();
send({type:'log', msg:'Session opened: ' + session.length + ' bytes'});
var pssh = Base64.decode('""" + PSSH_B64 + r"""', 0);
send({type:'log', msg:'Calling getKeyRequest...'});
var req = drm.getKeyRequest(session, pssh, 'video/mp4', 1, null);
var challenge = req.getData();
send({type:'log', msg:'Challenge: ' + challenge.length + ' bytes'});
// Send full bytes
var arr = Array.from(challenge);
send({type:'challenge', data: arr});
drm.closeSession(session);
drm.release();
send({type:'done'});
} catch(e) {
send({type:'error', msg: e.toString()});
}
});
}
setTimeout(tryJava, 100);
"""
def on_message(msg, data):
if msg['type'] == 'send':
p = msg['payload']
t = p.get('type')
if t == 'log':
print(f' {p["msg"]}')
elif t == 'challenge':
raw = bytes(p['data'])
print(f'[+] Challenge: {len(raw)} bytes head={raw[:16].hex()}')
# Save raw challenge
(OUT / 'challenge.bin').write_bytes(raw)
print(f' Saved challenge → L3/challenge.bin')
# Parse to extract ClientIdentification
try:
from pywidevine import license_protocol_pb2 as pb
sm = pb.SignedMessage()
sm.MergeFromString(raw)
print(f' SignedMessage.type={sm.type} msg={len(sm.msg)} bytes sig={len(sm.signature)} bytes')
lr = pb.LicenseRequest()
lr.MergeFromString(sm.msg)
ci = lr.client_id
ci_bytes = ci.SerializeToString()
print(f' ClientIdentification: type={ci.type} token={len(ci.token)} bytes')
print(f' ClientIdentification total: {len(ci_bytes)} bytes head={ci_bytes[:16].hex()}')
if len(ci_bytes) > 50:
(OUT / 'client_id.bin').write_bytes(ci_bytes)
print(f'[+] Saved → L3/client_id.bin')
else:
print(f'[!] ClientIdentification too small ({len(ci_bytes)} bytes) — may be empty')
print(f' Full LicenseRequest:\n{lr}')
except Exception as e:
print(f'[!] Parse error: {e}')
elif t == 'done':
print('[*] MediaDrm complete.')
sys.exit(0)
elif t == 'error':
print(f'[!] Error: {p["msg"]}')
elif msg['type'] == 'error':
print(f'[!] Frida JS error: {msg["description"]}')
dev = frida.get_usb_device()
PREFERRED = [
'com.google.android.gms.persistent',
'com.google.android.gms',
'com.google.android.gms.unstable',
]
print('[*] Finding GMS process...')
procs = {p.name: p.pid for p in dev.enumerate_processes()}
session = None
for name in PREFERRED:
pid = procs.get(name)
if pid:
print(f'[*] Found: {name} PID={pid}')
try:
session = dev.attach(pid)
print(f'[*] Attached.')
break
except Exception as e:
print(f' Attach failed: {e}')
if session is None:
# Fallback: try any process with Java
print('[*] GMS not found by preferred name. Trying other processes...')
for name, pid in procs.items():
if name in ('frida-server', 'adbd', 'zygote', 'zygote64', 'init', 'surfaceflinger'):
continue
if not any(k in name for k in ('google', 'android', 'gms')):
continue
try:
session = dev.attach(pid)
print(f'[*] Attached to {name} (PID {pid})')
break
except Exception:
pass
if session is None:
print('[!] Could not attach to any process. Is frida-server running?')
sys.exit(1)
script = session.create_script(JS)
script.on('message', on_message)
script.load()
print('[*] Waiting for license challenge...\n')
try:
while True:
time.sleep(0.5)
except KeyboardInterrupt:
print('\n[!] Stopped.')