85 lines
2.7 KiB
Python
85 lines
2.7 KiB
Python
#!/usr/bin/env python3
|
|||
|
|
"""
|
||
|
|
Brute-force search for the AES root key in libwvdrmengine.so
|
||
|
|
that decrypts the ay64.dat keybox.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from Crypto.Cipher import AES
|
||
|
|
import struct
|
||
|
|
|
||
|
|
CDM_LIB = "libwvdrmengine.so"
|
||
|
|
KEYBOX = "L3/ay64.dat"
|
||
|
|
|
||
|
|
keybox = open(KEYBOX, "rb").read() # 128 bytes
|
||
|
|
cdm = open(CDM_LIB, "rb").read()
|
||
|
|
|
||
|
|
# The decrypted keybox plaintext should look like structured data.
|
||
|
|
# Known Widevine keybox magic signatures to check for.
|
||
|
|
MAGIC_PATTERNS = [
|
||
|
|
b"KBPI", b"KEYB", b"kbpi",
|
||
|
|
b"\x01\x00\x00\x00",
|
||
|
|
b"\x00\x00\x00\x01",
|
||
|
|
]
|
||
|
|
|
||
|
|
# Also check if decrypted bytes look like a DER-encoded structure
|
||
|
|
def looks_like_der(data):
|
||
|
|
return data[0] == 0x30 and data[1] in (0x81, 0x82)
|
||
|
|
|
||
|
|
def looks_like_rsa_key(data):
|
||
|
|
# SEQUENCE { INTEGER 0, INTEGER (modulus)... }
|
||
|
|
return data[:4] == b"\x30\x82" or data[:2] == b"\x30\x81"
|
||
|
|
|
||
|
|
hits = []
|
||
|
|
step = 1 # slide byte by byte
|
||
|
|
|
||
|
|
print(f"Scanning {len(cdm):,} bytes of libwvdrmengine.so for AES root key...")
|
||
|
|
print(f"Keybox first block: {keybox[:16].hex()}")
|
||
|
|
print()
|
||
|
|
|
||
|
|
for i in range(0, len(cdm) - 16, step):
|
||
|
|
candidate_key = cdm[i:i+16]
|
||
|
|
|
||
|
|
# Skip keys that are all zeros, all same byte, or look like code
|
||
|
|
if len(set(candidate_key)) < 4:
|
||
|
|
continue
|
||
|
|
|
||
|
|
try:
|
||
|
|
# Try ECB first (no IV needed)
|
||
|
|
cipher = AES.new(candidate_key, AES.MODE_ECB)
|
||
|
|
decrypted = cipher.decrypt(keybox)
|
||
|
|
|
||
|
|
for magic in MAGIC_PATTERNS:
|
||
|
|
if decrypted.startswith(magic):
|
||
|
|
print(f"[!] MAGIC HIT at offset {i:#010x}: key={candidate_key.hex()}")
|
||
|
|
print(f" Decrypted: {decrypted.hex()}")
|
||
|
|
hits.append(("ECB", i, candidate_key, decrypted))
|
||
|
|
|
||
|
|
if looks_like_der(decrypted):
|
||
|
|
print(f"[!] DER HIT (ECB) at offset {i:#010x}: key={candidate_key.hex()}")
|
||
|
|
hits.append(("ECB-DER", i, candidate_key, decrypted))
|
||
|
|
|
||
|
|
# Try CBC with first block as IV
|
||
|
|
iv = keybox[:16]
|
||
|
|
cipher2 = AES.new(candidate_key, AES.MODE_CBC, iv)
|
||
|
|
dec2 = cipher2.decrypt(keybox[16:])
|
||
|
|
for magic in MAGIC_PATTERNS:
|
||
|
|
if dec2.startswith(magic):
|
||
|
|
print(f"[!] CBC MAGIC HIT at offset {i:#010x}: key={candidate_key.hex()}")
|
||
|
|
hits.append(("CBC", i, candidate_key, dec2))
|
||
|
|
|
||
|
|
if looks_like_der(dec2):
|
||
|
|
print(f"[!] DER HIT (CBC) at offset {i:#010x}: key={candidate_key.hex()}")
|
||
|
|
hits.append(("CBC-DER", i, candidate_key, dec2))
|
||
|
|
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
|
||
|
|
if i % 500000 == 0:
|
||
|
|
print(f" Progress: {i:,}/{len(cdm):,} ({100*i/len(cdm):.1f}%)")
|
||
|
|
|
||
|
|
if hits:
|
||
|
|
print(f"\nFound {len(hits)} candidate(s)!")
|
||
|
|
else:
|
||
|
|
print("\nNo direct AES magic hits found.")
|
||
|
|
print("The root key may be derived at runtime (not stored as a raw 16-byte value).")
|