Files

215 lines
8.4 KiB
Python
Raw Permalink Normal View History

2026-08-08 12:35:28 +02:00
#!/usr/bin/env python3
"""
Offline Widevine L3 private key extraction — revised.
Validation strategy: HMAC-SHA256(oem_key, ay64_ciphertext) == ay64.dat6
(No 'KBOX' magic exists in this binary; protobuf-format keybox is used instead.)
Steps:
1. Brute-force the OEM key in libwvhidl.so using HMAC-SHA256 validation.
2. Use the OEM key (= HMAC key) to AES-128-CBC decrypt ay64.dat → keybox.
3. Extract device_key from decrypted keybox.
4. Try various IV strategies to decrypt wrapped_privkey.bin → RSA DER key.
"""
import hmac as hmacmod, hashlib, struct, sys
from pathlib import Path
from Crypto.Cipher import AES
BASE = Path(__file__).parent
L3 = BASE / 'L3'
LIBWV = Path('/tmp/libwvhidl.so')
AY64_ENC = L3 / 'fs_read_128b.bin' # 128-byte encrypted keybox
AY64_IV = bytes.fromhex('41a210cb7f65b9856a9b87090982a290') # from ay64.dat5
AY64_DAT6 = bytes.fromhex('04ef4d3dbf0663b5e794520e51f6d4e275946d857b1cc703acdbe4b3583c1d36') # HMAC-SHA256
WRAPPED_KEY = L3 / 'wrapped_privkey.bin'
def hmac_sha256(key: bytes, data: bytes) -> bytes:
return hmacmod.new(key, data, hashlib.sha256).digest()
def aes_cbc_decrypt(key: bytes, iv: bytes, data: bytes) -> bytes:
return AES.new(key, AES.MODE_CBC, iv).decrypt(data)
def find_hmac_key(lib: bytes, ciphertext: bytes, expected_mac: bytes):
"""Try every 16-byte-aligned chunk as HMAC-SHA256 key for validation."""
# Strategy A: HMAC-SHA256(key, ciphertext) == expected
print(f'[*] Strategy A: HMAC-SHA256(key16, ct128) == dat6 ({len(lib)//16} candidates)...')
for off in range(0, len(lib) - 16, 16):
k = lib[off:off + 16]
if len(set(k)) < 5:
continue
if hmac_sha256(k, ciphertext) == expected_mac:
print(f' [+] Found (strategy A, off=0x{off:x}): {k.hex()}')
return k
# Strategy B: HMAC-SHA256(key, iv || ciphertext) == expected
print('[*] Strategy B: HMAC-SHA256(key16, iv||ct) == dat6...')
msg = AY64_IV + ciphertext
for off in range(0, len(lib) - 16, 16):
k = lib[off:off + 16]
if len(set(k)) < 5:
continue
if hmac_sha256(k, msg) == expected_mac:
print(f' [+] Found (strategy B, off=0x{off:x}): {k.hex()}')
return k
# Strategy C: 32-byte candidates
print('[*] Strategy C: HMAC-SHA256(key32, ct128) == dat6...')
for off in range(0, len(lib) - 32, 16):
k = lib[off:off + 32]
if len(set(k)) < 8:
continue
if hmac_sha256(k, ciphertext) == expected_mac:
print(f' [+] Found (strategy C 32-byte, off=0x{off:x}): {k.hex()}')
return k
# Strategy D: HMAC of plaintext (try AES-128-CBC first with each candidate as BOTH key)
print('[*] Strategy D: AES-CBC decrypt then HMAC-SHA256(same_key, plaintext) == dat6...')
for off in range(0, len(lib) - 16, 16):
k = lib[off:off + 16]
if len(set(k)) < 5:
continue
plain = aes_cbc_decrypt(k, AY64_IV, ciphertext)
if hmac_sha256(k, plain) == expected_mac:
print(f' [+] Found (strategy D, off=0x{off:x}): {k.hex()}')
return k
return None
def check_der(data: bytes) -> str | None:
if len(data) < 8:
return None
if data[0:2] == b'\x30\x82' and data[4:7] == b'\x02\x01\x00' and data[7:9] == b'\x02\x82':
return 'pkcs1'
if data[0:2] == b'\x30\x82' and data[4:7] == b'\x02\x01\x00' and data[7:8] == b'\x30':
return 'pkcs8'
return None
def strip_der(data: bytes) -> bytes:
"""Trim to actual DER length (strip PKCS#7 padding if present)."""
pad = data[-1]
if 1 <= pad <= 16 and data[-pad:] == bytes([pad]) * pad:
data = data[:-pad]
if data[0:2] == b'\x30\x82':
inner = (data[2] << 8) | data[3]
return data[:4 + inner]
return data
# ── Main ──────────────────────────────────────────────────────────────────────
print('[*] Widevine L3 offline key extractor (HMAC-SHA256 validation)')
print()
for p in (LIBWV, AY64_ENC, WRAPPED_KEY):
if not p.exists():
print(f'[!] Missing: {p}')
if p == LIBWV:
print(' adb pull /vendor/lib64/libwvhidl.so /tmp/libwvhidl.so')
sys.exit(1)
print(f'[OK] {p} ({p.stat().st_size:,} bytes)')
print(f'[OK] ay64.dat6 (hardcoded): {AY64_DAT6.hex()}')
print()
lib_bytes = LIBWV.read_bytes()
ay64_cipher = AY64_ENC.read_bytes()
wrapped = WRAPPED_KEY.read_bytes()
# ── Step 1: Find OEM key / HMAC key ──────────────────────────────────────────
hmac_key = find_hmac_key(lib_bytes, ay64_cipher, AY64_DAT6)
if hmac_key is None:
print('\n[!] HMAC-SHA256 key not found in libwvhidl.so.')
print('[!] The key may be derived at runtime from device-specific data.')
print('[!] Falling back to Frida runtime hook (GetTokenFromKeybox).')
print(' Run: python3 dump_wvd2.py then trigger DRM in browser')
sys.exit(1)
print(f'\n[+] HMAC key: {hmac_key.hex()}')
# ── Step 2: Decrypt keybox ────────────────────────────────────────────────────
# Use the HMAC key as the AES key too (common in Widevine L3 implementations)
keybox = aes_cbc_decrypt(hmac_key, AY64_IV, ay64_cipher)
print(f'[+] Decrypted keybox (128 bytes): {keybox.hex()}')
# Parse protobuf keybox: find a 16-byte AES key embedded in it
# Protobuf field 2 (wire type 2 = LEN) likely contains the device key
# Also try raw struct layout just in case
possible_device_keys = set()
# Try every 16-byte-aligned block in the decrypted keybox as a device key candidate
for off in range(0, 113, 16):
possible_device_keys.add(keybox[off:off + 16])
# Protobuf parse: look for 2-byte length-prefixed blobs of length 16
for i in range(len(keybox) - 2):
tag = keybox[i]
wire = tag & 0x07
if wire == 2: # length-delimited
length = keybox[i + 1]
if length == 16 and i + 2 + 16 <= len(keybox):
possible_device_keys.add(keybox[i + 2:i + 18])
print(f'[*] {len(possible_device_keys)} device_key candidates from decrypted keybox')
# ── Step 3: Decrypt wrapped_privkey.bin ───────────────────────────────────────
print(f'\n[*] Trying to decrypt wrapped_privkey.bin ({len(wrapped)} bytes)...')
ivs = [
('IV=0x00*16', b'\x00' * 16),
('IV=wrapped[0:16]', wrapped[:16]),
('IV=ay64.dat5', AY64_IV),
('IV=hmac_key', hmac_key if len(hmac_key) == 16 else hmac_key[:16]),
]
cts = [
('ct=all', wrapped),
('ct=wrapped[16:]', wrapped[16:]),
]
found = False
for device_key in possible_device_keys:
for iv_name, iv in ivs:
for ct_name, ct in cts:
if len(ct) % 16 != 0 or len(ct) == 0:
continue
plain = aes_cbc_decrypt(device_key, iv, ct)
kt = check_der(plain)
if kt:
print(f'\n[+] RSA KEY FOUND!')
print(f' device_key: {device_key.hex()}')
print(f' {iv_name}, {ct_name}')
key_bytes = strip_der(plain)
out = L3 / 'private_key.der'
out.write_bytes(key_bytes)
print(f' Saved: {out} ({len(key_bytes)} bytes)')
found = True
break
if found:
break
if found:
break
if not found:
print('\n[!] No RSA key found. Plaintext samples (first 8 bytes per combo):')
first_key = next(iter(possible_device_keys))
for iv_name, iv in ivs:
for ct_name, ct in cts:
if len(ct) % 16 != 0:
continue
plain = aes_cbc_decrypt(first_key, iv, ct)
print(f' [{iv_name} {ct_name}] {plain[:16].hex()}')
print()
print('[*] Next step: use Frida GetTokenFromKeybox hook to get device_key at runtime')
print(' python3 dump_wvd2.py')
if found:
print()
print('[*] Next steps:')
print(' 1. Get client_id.bin — run dump_wvd2.py while triggering DRM,')
print(' then capture from GenerateRsaSignature output')
print(' 2. pywidevine create-device -t android -l 3 \\')
print(f' -k {L3}/private_key.der -c L3/client_id.bin -o L3/')