164 lines
5.4 KiB
Python
164 lines
5.4 KiB
Python
#!/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.')
|