Release v2.0.0

This commit is contained in:
hyugogirubato
2024-07-06 20:01:47 +02:00
parent 687a493d26
commit 84b4689487
23 changed files with 2032 additions and 782 deletions
+5
View File
@@ -0,0 +1,5 @@
from .core import Core
from .cdm import Cdm
from .vendor import Vendor
__version__ = '2.0.0'
+171
View File
@@ -0,0 +1,171 @@
import argparse
import logging
import subprocess
import time
from datetime import datetime
from pathlib import Path
import coloredlogs
import keydive
from keydive.cdm import Cdm
from keydive.constants import CDM_VENDOR_API
from keydive.core import Core
def configure_logging(path: Path, verbose: bool) -> Path:
"""
Configures logging for the application.
Args:
path (Path, optional): The path for log files.
verbose (bool): Whether to enable verbose logging.
Returns:
Path: The path of log file.
"""
# Get the root logger
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG if verbose else logging.INFO)
# Clear any existing handlers (optional, to avoid duplicate logs if reconfiguring)
if root_logger.hasHandlers():
root_logger.handlers.clear()
file_path = None
if path:
if path.is_file():
path = path.parent
path.mkdir(parents=True, exist_ok=True)
# Create a file handler
file_path = path / ('keydive_%s.log' % datetime.now().strftime('%Y-%m-%d_%H-%M-%S'))
file_path = file_path.resolve(strict=False)
file_handler = logging.FileHandler(file_path)
file_handler.setLevel(logging.DEBUG)
formatter = logging.Formatter(
fmt='%(asctime)s [%(levelname).1s] %(name)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
file_handler.setFormatter(formatter)
# Add the file handler to the root logger
root_logger.addHandler(file_handler)
# Configure coloredlogs for console output
coloredlogs.install(
fmt='%(asctime)s [%(levelname).1s] %(name)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
level=logging.DEBUG if verbose else logging.INFO,
logger=root_logger
)
return file_path
def main() -> None:
parser = argparse.ArgumentParser(description='Extract Widevine L3 keys from an Android device.')
# Global options
opt_global = parser.add_argument_group('Global options')
opt_global.add_argument('-d', '--device', required=False, type=str, metavar='<id>', help='Specify the target Android device ID to connect with via ADB.')
opt_global.add_argument('-v', '--verbose', required=False, action='store_true', help='Enable verbose logging for detailed debug output.')
opt_global.add_argument('-l', '--log', required=False, type=Path, metavar='<dir>', help='Directory to store log files.')
opt_global.add_argument('--delay', required=False, type=float, metavar='<delay>', default=1, help='Delay (in seconds) between process checks in the watcher.')
opt_global.add_argument('--version', required=False, action='store_true', help='Display KeyDive version information.')
# Cdm options
opt_cdm = parser.add_argument_group('Cdm options')
opt_cdm.add_argument('-a', '--auto', required=False, action='store_true', help='Automatically open Bitmovin\'s demo.')
opt_cdm.add_argument('-c', '--challenge', required=False, type=Path, metavar='<file>', help='Path to unencrypted challenge for extracting client ID.')
opt_cdm.add_argument('-w', '--wvd', required=False, action='store_true', help='Generate WVD.')
opt_cdm.add_argument('-o', '--output', required=False, type=Path, default=Path('device'), metavar='<dir>', help='Output directory path for extracted data.')
opt_cdm.add_argument('-f', '--functions', required=False, type=Path, metavar='<file>', help='Path to Ghidra XML functions file.')
args = parser.parse_args()
if args.version:
print(f'KeyDive {keydive.__version__}')
exit(0)
# Configure logging
log_path = configure_logging(path=args.log, verbose=args.verbose)
logger = logging.getLogger('KeyDive')
logger.info('Version: %s', keydive.__version__)
try:
# Start the ADB server if not already running
sp = subprocess.run(['adb', 'start-server'], capture_output=True)
if sp.returncode != 0:
raise EnvironmentError('ADB is not recognized as an environment variable, refer to https://github.com/hyugogirubato/KeyDive/blob/main/docs/PACKAGE.md#adb-android-debug-bridge')
# Initialize Cdm instance
cdm = Cdm()
if args.challenge:
cdm.set_challenge(data=args.challenge)
# Initialize Core instance for interacting with the device
core = Core(cdm=cdm, device=args.device, symbols=args.functions)
# Process watcher loop
logger.info('Watcher delay: %ss' % args.delay)
current = None
while core.running:
# https://github.com/hyugogirubato/KeyDive/issues/14#issuecomment-2146788792
processes = {
key: (name, pid)
for name, pid in core.enumerate_processes().items()
for key in CDM_VENDOR_API.keys() if key in name
}
if not processes:
raise EnvironmentError('Unable to detect Widevine, refer to https://github.com/hyugogirubato/KeyDive/blob/main/docs/PACKAGE.md#drm-info')
# Check if the current process has changed
if current:
if current not in [v[1] for v in processes.values()]:
logger.warning('Widevine process has changed')
current = None
elif cdm.export(args.output, args.wvd):
raise KeyboardInterrupt
# If current process not found, attempt to hook into the detected processes
if not current:
logger.debug('Analysing...')
for key, (name, pid) in processes.items():
if current:
break
for vendor in CDM_VENDOR_API[key]:
if core.hook_process(pid=pid, vendor=vendor):
logger.info('Process: %s (%s)', pid, name)
current = pid
break
elif not core.running:
raise KeyboardInterrupt
if current:
logger.info('Successfully hooked.')
if args.auto:
logger.info('Starting DRM player launch process...')
sp = subprocess.run(['adb', '-s', str(core.device.id), 'shell', 'am', 'start', '-a', 'android.intent.action.VIEW', '-d', 'https://bitmovin.com/demos/drm'], capture_output=True)
if sp.returncode != 0:
logger.error('Error launching DRM player: %s' % sp.stdout.decode('utf-8').strip())
else:
logger.warning('Widevine library not found, searching...')
# Delay before next iteration
time.sleep(args.delay)
except KeyboardInterrupt:
pass
except Exception as e:
logger.critical(e, exc_info=args.verbose)
# Final logging and exit
if log_path:
logger.info('Log file: %s' % log_path)
logger.info('Exiting')
if __name__ == '__main__':
main()
+177
View File
@@ -0,0 +1,177 @@
import json
import logging
import re
from pathlib import Path
from typing import Union
from zlib import crc32
from Cryptodome.PublicKey import RSA
from pywidevine import Device
from pywidevine.device import DeviceTypes
from pywidevine.license_protocol_pb2 import SignedMessage, LicenseRequest, ClientIdentification, SignedDrmCertificate, DrmCertificate
from unidecode import unidecode
def sanitize(path: Path) -> Path:
"""
Sanitizes the given path by replacing invalid characters.
Args:
path (Path): The path to sanitize.
Returns:
Path: The sanitized path.
"""
paths = [path.name, *[p.name for p in path.parents if p.name]][::-1]
for i, p in enumerate(paths):
p = p.replace('...', '').strip()
p = re.sub(r'[<>:"/|?*\x00-\x1F]', '_', p)
paths[i] = p
return Path().joinpath(*paths)
class Cdm:
"""
The Cdm class manages CDM-related operations, such as setting challenge data,
extracting and storing private keys, and exporting device information.
"""
def __init__(self):
self.logger = logging.getLogger(self.__class__.__name__)
# https://github.com/devine-dl/pywidevine
self.client_id: dict[int, ClientIdentification] = {}
self.private_key: dict[int, RSA] = {}
def __client_info(self, client_id: ClientIdentification) -> dict:
"""
Converts client identification information to a dictionary.
Args:
client_id (ClientIdentification): The client identification.
Returns:
dict: A dictionary of client information.
"""
return {e.name: e.value for e in client_id.client_info}
def set_challenge(self, data: Union[Path, bytes]) -> None:
"""
Sets the challenge data by extracting device information.
Args:
data (Union[Path, bytes]): The challenge data as a file path or bytes.
"""
try:
if isinstance(data, Path):
if not data.is_file():
raise FileNotFoundError(data)
data = data.read_bytes()
signed_message = SignedMessage()
signed_message.ParseFromString(data)
license_request = LicenseRequest()
license_request.ParseFromString(signed_message.msg)
client_id: ClientIdentification = license_request.client_id
self.set_client_id(data=client_id.SerializeToString())
except Exception as e:
self.logger.error('Error parsing challenge: %s', e)
def set_private_key(self, data: bytes) -> None:
"""
Sets the private key from the provided data.
Args:
data (bytes): The private key data.
"""
try:
key = RSA.import_key(data)
if key.n not in self.private_key:
self.logger.debug('Receive private key: \n\n%s\n', key.exportKey('PEM').decode('utf-8'))
self.private_key[key.n] = key
except Exception as e:
self.logger.error('Error parsing private key: %s', e)
def set_client_id(self, data: Union[ClientIdentification, bytes]) -> None:
"""
Sets the client ID from the provided data.
Args:
data (Union[ClientIdentification, bytes]): The client ID data.
"""
try:
if isinstance(data, ClientIdentification):
client_id = data
else:
client_id = ClientIdentification()
client_id.ParseFromString(data)
signed_drm_certificate = SignedDrmCertificate()
drm_certificate = DrmCertificate()
signed_drm_certificate.ParseFromString(client_id.token)
drm_certificate.ParseFromString(signed_drm_certificate.drm_certificate)
public_key = drm_certificate.public_key
key = RSA.importKey(public_key)
if key.n not in self.client_id:
self.logger.debug('Receive client id: \n\n%s\n', json.dumps(self.__client_info(client_id), indent=2))
self.client_id[key.n] = client_id
except Exception as e:
self.logger.error('Error parsing client ID: %s', e)
def export(self, parent: Path, wvd: bool = False) -> bool:
"""
Exports the client ID and private key to disk.
Args:
parent (Path): The parent directory to export the files to.
wvd (bool): Whether to export WVD files.
Returns:
bool: True if any keys were exported, otherwise False.
"""
keys = set(self.client_id.keys()) & set(self.private_key.keys())
for k in keys:
client_info = self.__client_info(self.client_id[k])
# https://github.com/devine-dl/pywidevine/blob/master/pywidevine/main.py#L211
device = Device(
client_id=self.client_id[k].SerializeToString(),
private_key=self.private_key[k].exportKey('PEM'),
type_=DeviceTypes.ANDROID,
security_level=3,
flags=None
)
# https://github.com/hyugogirubato/KeyDive/issues/14#issuecomment-2146958022
parent = sanitize(parent / client_info['company_name'] / client_info['model_name'] / str(device.system_id) / str(k)[:10])
parent.mkdir(parents=True, exist_ok=True)
path_id_bin = parent / 'client_id.bin'
path_id_bin.write_bytes(data=device.client_id.SerializeToString())
self.logger.info('Exported client ID: %s', path_id_bin)
path_key_bin = parent / 'private_key.pem'
path_key_bin.write_bytes(data=device.private_key.exportKey('PEM'))
self.logger.info('Exported private key: %s', path_key_bin)
if wvd:
wvd_bin = device.dumps()
name = f"{client_info['company_name']} {client_info['model_name']}"
if client_info.get('widevine_cdm_version'):
name += f" {client_info['widevine_cdm_version']}"
name += f" {crc32(wvd_bin).to_bytes(4, 'big').hex()}"
name = unidecode(name.strip().lower().replace(' ', '_'))
path_wvd = parent / f'{name}_{device.system_id}_l{device.security_level}.wvd'
path_wvd.write_bytes(data=wvd_bin)
self.logger.info('Exported WVD: %s', path_wvd)
return len(keys) > 0
__all__ = ('Cdm',)
+119
View File
@@ -0,0 +1,119 @@
from keydive.vendor import Vendor
NATIVE_C_API = {
# STDIO
'fclose', 'fflush', 'fgetc', 'fgetpos', 'fgets', 'fopen', 'fprintf', 'fputc', 'fputs', 'fread', 'freopen',
'fscanf', 'fseek', 'fsetpos', 'ftell', 'fwrite', 'getc', 'getchar', 'gets', 'perror', 'printf', 'putc',
'putchar', 'puts', 'remove', 'rename', 'rewind', 'scanf', 'setbuf', 'setvbuf', 'sprintf', 'sscanf', 'tmpfile',
'tmpnam', 'ungetc', 'vfprintf', 'vprintf', 'vsprintf', 'fileno', 'feof', 'ferror',
# STDLIB
'abort', 'abs', 'atexit', 'atof', 'atoi', 'atol', 'bsearch', 'calloc', 'div', 'exit', 'free', 'getenv', 'labs',
'ldiv', 'malloc', 'mblen', 'mbstowcs', 'mbtowc', 'qsort', 'rand', 'realloc', 'srand', 'strtod', 'strtol',
'strtoul', 'system', 'wcstombs', 'wctomb',
# STRING
'memchr', 'memcmp', 'memcpy', 'memmove', 'memset', 'strcat', 'strchr', 'strcmp', 'strcoll', 'strcpy', 'strcspn',
'strerror', 'strlen', 'strncat', 'strncmp', 'strncpy', 'strpbrk', 'strrchr', 'strspn', 'strstr', 'strtok',
'strxfrm', 'strncasecmp',
# MATH
'acos', 'asin', 'atan', 'atan2', 'cos', 'cosh', 'exp', 'fabs', 'floor', 'fmod', 'frexp', 'ldexp', 'log',
'log10', 'modf', 'pow', 'sin', 'sinh', 'sqrt', 'tan', 'tanh',
# CTYPE
'isalnum', 'isalpha', 'iscntrl', 'isdigit', 'isgraph', 'islower', 'isprint', 'ispunct', 'isspace', 'isupper',
'isxdigit', 'tolower', 'toupper',
# TIME
'asctime', 'clock', 'ctime', 'difftime', 'gmtime', 'localtime', 'mktime', 'strftime', 'time',
# UNISTD
'access', 'alarm', 'chdir', 'chown', 'close', 'dup', 'dup2', 'execle', 'execv', 'execve', 'execvp', 'fork',
'fpathconf', 'getcwd', 'getegid', 'geteuid', 'getgid', 'getgroups', 'getlogin', 'getopt', 'getpgid', 'getpgrp',
'getpid', 'getppid', 'getuid', 'isatty', 'lseek', 'pathconf', 'pause', 'pipe', 'read', 'rmdir', 'setgid',
'setpgid', 'setsid', 'setuid', 'sleep', 'sysconf', 'tcgetpgrp', 'tcsetpgrp', 'ttyname', 'ttyname_r', 'write',
'fsync', 'unlink', 'syscall', 'getpagesize',
# FCNTL
'creat', 'fcntl', 'open',
# SYS_TYPE
'fd_set', 'FD_CLR', 'FD_ISSET', 'FD_SET', 'FD_ZERO',
# SYS_STAT
'chmod', 'fchmod', 'fstat', 'mkdir', 'mkfifo', 'stat', 'umask',
# SYS_TIME
'gettimeofday', 'select', 'settimeofday',
# SIGNAL
'signal', 'raise', 'kill', 'sigaction', 'sigaddset', 'sigdelset', 'sigemptyset', 'sigfillset', 'sigismember',
'sigpending', 'sigprocmask', 'sigsuspend', 'alarm', 'pause',
# SETJMP
'longjmp', 'setjmp',
# ERRNO
'errno', 'strerror', 'perror',
# ASSERT
'assert',
# LOCAL
'localeconv', 'setlocale',
# WCHAR
'btowc', 'fgetwc', 'fgetws', 'fputwc', 'fputws', 'fwide', 'fwprintf', 'fwscanf', 'getwc', 'getwchar', 'mbrlen',
'mbrtowc', 'mbsinit', 'mbsrtowcs', 'putwc', 'putwchar', 'swprintf', 'swscanf', 'ungetwc', 'vfwprintf',
'vfwscanf', 'vwprintf', 'vwscanf', 'wcrtomb', 'wcscat', 'wcschr', 'wcscmp', 'wcscoll', 'wcscpy', 'wcscspn',
'wcsftime', 'wcslen', 'wcsncat', 'wcsncmp', 'wcsncpy', 'wcspbrk', 'wcsrchr', 'wcsrtombs', 'wcsspn', 'wcsstr',
'wcstod', 'wcstok', 'wcstol', 'wcstombs', 'wcstoul', 'wcsxfrm', 'wctob', 'wmemchr', 'wmemcmp', 'wmemcpy',
'wmemmove', 'wmemset', 'wprintf', 'wscanf',
# WCTYPE
'iswalnum', 'iswalpha', 'iswcntrl', 'iswdigit', 'iswgraph', 'iswlower', 'iswprint', 'iswpunct', 'iswspace',
'iswupper', 'iswxdigit', 'towlower', 'towupper', 'iswctype', 'wctype',
# STDDEF
'NULL', 'offsetof', 'ptrdiff_t', 'size_t', 'wchar_t',
# STDARG
'va_arg', 'va_end', 'va_start',
# DLFCN
'dlclose', 'dlerror', 'dlopen', 'dlsym',
# DIRENT
'closedir', 'opendir', 'readdir',
# SYS_SENDFILE
'sendfile',
# SYS_MMAN
'mmap', 'mprotect', 'munmap',
# SYS_UTSNAME
'uname',
# LINK
'dladdr'
}
OEM_CRYPTO_API = {
# Mapping of function names across different API levels (obfuscated names may vary).
'rnmsglvj', 'polorucp', 'kqzqahjq', 'pldrclfq', 'kgaitijd', 'cwkfcplc', 'crhqcdet', 'ulns', 'dnvffnze', 'ygjiljer',
'qbjxtubz', 'qkfrcjtw', 'rbhjspoh', 'zgtjmxko', 'igrqajte', 'ofskesua', 'qllcoacg', 'pukctkiv', 'ehmduqyt'
# Add more as needed for different versions.
}
CDM_VENDOR_API = {
'mediaserver': {
Vendor(11, '1.0', 'libwvdrmengine.so')
},
'mediadrmserver': {
Vendor(11, '1.0', 'libwvdrmengine.so')
},
'android.hardware.drm@1.0-service.widevine': {
Vendor(13, '5.1.0', 'libwvhidl.so')
},
'android.hardware.drm@1.1-service.widevine': {
Vendor(14, '14.0.0', 'libwvhidl.so')
},
'android.hardware.drm@1.2-service.widevine': {
Vendor(15, '15.0.0', 'libwvhidl.so')
},
'android.hardware.drm@1.3-service.widevine': {
Vendor(16, '16.0.0', 'libwvhidl.so')
},
'android.hardware.drm@1.4-service.widevine': {
Vendor(16, '16.1.0', 'libwvhidl.so')
},
'android.hardware.drm-service.widevine': {
Vendor(17, '17.0.0', 'libwvaidl.so'),
Vendor(18, '18.0.0', 'android.hardware.drm-service.widevine')
}
}
# https://developer.android.com/tools/releases/platforms
CDM_FUNCTION_API = {
'UsePrivacyMode',
'GetCdmClientPropertySet',
'PrepareKeyRequest',
'getOemcryptoDeviceId'
}
+235
View File
@@ -0,0 +1,235 @@
import json
import logging
import re
import subprocess
from pathlib import Path
import frida
import xmltodict
from frida.core import Device, Session, Script
from keydive.cdm import Cdm
from keydive.constants import OEM_CRYPTO_API, NATIVE_C_API, CDM_FUNCTION_API
from keydive.vendor import Vendor
class Core:
"""
Core class for handling DRM operations and device interactions.
"""
def __init__(self, cdm: Cdm, device: str = None, symbols: Path = None):
"""
Initializes a Core instance.
Args:
cdm (Cdm): Instance of Cdm for managing DRM related operations.
device (str, optional): ID of the Android device to connect to via ADB. Defaults to None (uses USB device).
symbols (Path, optional): Path to Ghidra XML functions file for symbol extraction. Defaults to None.
"""
self.logger = logging.getLogger(self.__class__.__name__)
self.running = True
self.cdm = cdm
# Select device based on provided ID or default to the first USB device.
self.device: Device = frida.get_device(id=device, timeout=5) if device else frida.get_usb_device(timeout=5)
self.logger.info('Device: %s (%s)', self.device.name, self.device.id)
# Obtain device properties
properties = self.device_properties()
self.logger.info('SDK API: %s', properties['ro.build.version.sdk'])
self.logger.info('ABI CPU: %s', properties['ro.product.cpu.abi'])
# Load the hook script
self.symbols = self.__prepare_symbols(symbols)
self.script = self.__prepare_hook_script()
self.logger.info('Script loaded successfully')
def __prepare_hook_script(self) -> str:
"""
Prepares the hook script content by injecting the library-specific scripts.
Returns:
str: The prepared script content.
"""
content = Path(__file__).with_name('keydive.js').read_text()
# Replace placeholders in script template
replacements = {
'${OEM_CRYPTO_API}': json.dumps(list(OEM_CRYPTO_API)),
'${NATIVE_C_API}': json.dumps(list(NATIVE_C_API)),
'${SYMBOLS}': json.dumps(self.symbols)
}
for placeholder, value in replacements.items():
content = content.replace(placeholder, value)
return content
def __prepare_symbols(self, path: Path) -> list:
"""
Parses the provided XML functions file to select relevant functions.
Args:
path (Path): Path to Ghidra XML functions file.
Returns:
list: List of selected functions as dictionaries.
Raises:
FileNotFoundError: If the functions file is not found.
ValueError: If functions extraction fails.
"""
if not path:
return []
elif not path.is_file():
raise FileNotFoundError('Functions file not found')
try:
program = xmltodict.parse(path.read_bytes())['PROGRAM']
addr_base = int(program['@IMAGE_BASE'], 16)
functions = program['FUNCTIONS']['FUNCTION']
# Find a target function from a predefined list
target = next((f['@NAME'] for f in functions if f['@NAME'] in OEM_CRYPTO_API), None)
# Extract relevant functions
selected = {}
for func in functions:
name = func['@NAME']
args = len(func.get('REGISTER_VAR', []))
# Add function if it matches specific criteria
if name not in selected and (
name == target
or any(keyword in name for keyword in CDM_FUNCTION_API)
or (not target and re.match(r'^[a-z]+$', name) and args >= 6)
):
selected[name] = {
'type': 'function',
'name': name,
'address': hex(int(func['@ENTRY_POINT'], 16) - addr_base)
}
return list(selected.values())
except Exception as e:
raise ValueError('Failed to extract functions from Ghidra') from e
def device_properties(self) -> dict:
"""
Retrieves system properties from the connected device using ADB shell commands.
Returns:
dict: A dictionary of device properties.
"""
# https://source.android.com/docs/core/architecture/configuration/add-system-properties?#shell-commands
properties = {}
sp = subprocess.run(['adb', '-s', str(self.device.id), 'shell', 'getprop'], capture_output=True)
for line in sp.stdout.decode('utf-8').splitlines():
match = re.match(r'\[(.*?)\]: \[(.*?)\]', line)
if match:
key, value = match.groups()
# Attempt to cast numeric and boolean values to appropriate types
try:
value = int(value)
except ValueError:
if value.lower() in ('true', 'false'):
value = value.lower() == 'true'
properties[key] = value
return properties
def enumerate_processes(self) -> dict:
"""
Lists processes running on the device, returning a mapping of process names to PIDs.
Returns:
dict: A dictionary mapping process names to PIDs.
"""
processes = {}
# https://github.com/frida/frida/issues/1225#issuecomment-604181822
prompt = ['adb', '-s', str(self.device.id), 'shell', 'ps']
sp = subprocess.run([*prompt, '-A'], capture_output=True)
if sp.returncode != 0:
sp = subprocess.run(prompt, capture_output=True)
# Iterate through lines starting from the second line (skipping header)
for line in sp.stdout.decode('utf-8').splitlines()[1:]:
try:
line = line.split() # USER,PID,PPID,VSZ,RSS,WCHAN,ADDR,S,NAME
name = ' '.join(line[8:]).strip()
name = name if name.startswith('[') else Path(name).name
processes[name] = int(line[1])
except Exception:
pass
return processes
def __process_message(self, message: dict, data: bytes) -> None:
"""
Handles messages received from the Frida script.
Args:
message (dict): The message payload.
data (bytes): The raw data associated with the message.
"""
logger = logging.getLogger('Script')
level = message.get('payload')
if isinstance(level, int):
# Process logging messages from Frida script
logger.log(level=level, msg=data.decode('utf-8'))
if level in (logging.FATAL, logging.CRITICAL):
self.running = False
elif level == 'challenge':
self.cdm.set_challenge(data=data)
elif level == 'private_key':
self.cdm.set_private_key(data=data)
elif level == 'client_id':
self.cdm.set_client_id(data=data)
else:
self.logger.warning('Malformed message: %s -> %s' % (level, data))
def hook_process(self, pid: int, vendor: Vendor, timeout: int = 0) -> bool:
"""
Hooks into the specified process.
Args:
pid (int): The process ID to hook.
vendor (Vendor): Instance of Vendor class representing the vendor information.
timeout (int, optional): Timeout for attaching to the process. Defaults to 0.
Returns:
bool: True if the process was successfully hooked, otherwise False.
"""
try:
session: Session = self.device.attach(pid, persist_timeout=timeout)
except Exception as e:
self.logger.error(e)
return False
def __process_destroyed() -> None:
session.detach()
script: Script = session.create_script(self.script)
script.on('message', self.__process_message)
script.on('destroyed', __process_destroyed)
script.load()
library = script.exports_sync.getlibrary(vendor.name)
if library:
self.logger.info('Library: %s (%s)', library['name'], library['path'])
# Check if Ghidra XML functions loaded
if vendor.oem > 17 and not self.symbols:
self.logger.warning('For OEM API > 17, specifying "functions" is required, refer to https://github.com/hyugogirubato/KeyDive/blob/main/docs/FUNCTIONS.md')
elif vendor.oem < 18 and self.symbols:
self.logger.warning('The "functions" attribute is deprecated for OEM API < 18')
return script.exports_sync.hooklibrary(vendor.name)
script.unload()
self.logger.warning('Library not found: %s' % vendor.name)
return False
__all__ = ('Core',)
+299
View File
@@ -0,0 +1,299 @@
/**
* Date: 2024-06-30
* Description: DRM key extraction for research and educational purposes.
* Source: https://github.com/hyugogirubato/KeyDive
*/
// Placeholder values dynamically replaced at runtime.
const OEM_CRYPTO_API = JSON.parse('${OEM_CRYPTO_API}');
const NATIVE_C_API = JSON.parse('${NATIVE_C_API}');
const SYMBOLS = JSON.parse('${SYMBOLS}');
// Logging levels to synchronize with Python's logging module.
const Level = {
NOTSET: 0,
DEBUG: 10,
INFO: 20,
// WARN: WARNING,
WARNING: 30,
ERROR: 40,
// FATAL: CRITICAL,
CRITICAL: 50
};
// Utility for encoding strings into byte arrays (UTF-8).
// https://gist.github.com/Yaffle/5458286#file-textencodertextdecoder-js
function TextEncoder() {
}
TextEncoder.prototype.encode = function (string) {
const octets = [];
let i = 0;
while (i < string.length) {
const codePoint = string.codePointAt(i);
let c = 0;
let bits = 0;
if (codePoint <= 0x007F) {
c = 0;
bits = 0x00;
} else if (codePoint <= 0x07FF) {
c = 6;
bits = 0xC0;
} else if (codePoint <= 0xFFFF) {
c = 12;
bits = 0xE0;
} else if (codePoint <= 0x1FFFFF) {
c = 18;
bits = 0xF0;
}
octets.push(bits | (codePoint >> c));
while (c >= 6) {
c -= 6;
octets.push(0x80 | ((codePoint >> c) & 0x3F));
}
i += codePoint >= 0x10000 ? 2 : 1;
}
return octets;
};
// Simplified log function to handle messages and encode them for transport.
const print = (level, message) => {
message = message instanceof Object ? JSON.stringify(message) : message;
message = message ? new TextEncoder().encode(message) : message;
send(level, message);
}
// @Utils
const getLibraries = (name) => {
// https://github.com/hyugogirubato/KeyDive/issues/14#issuecomment-2146788792
try {
const libraries = Process.enumerateModules();
return libraries.filter(l => l.name.includes(name));
} catch (e) {
print(Level.CRITICAL, e.message);
return [];
}
};
const getLibrary = (name) => {
const libraries = getLibraries(name);
return libraries.length === 1 ? libraries[0] : undefined;
}
const getFunctions = (library) => {
try {
return library.enumerateExports();
} catch (e) {
print(Level.CRITICAL, e.message);
return [];
}
}
// @Libraries
const UsePrivacyMode = (address) => {
// wvcdm::Properties::UsePrivacyMode
Interceptor.replace(address, new NativeCallback(function () {
return 0;
}, 'int', []));
Interceptor.attach(address, {
onEnter: function (args) {
print(Level.DEBUG, '[+] onEnter: UsePrivacyMode');
},
onLeave: function (retval) {
print(Level.DEBUG, '[-] onLeave: UsePrivacyMode');
}
});
}
const GetCdmClientPropertySet = (address) => {
// wvcdm::Properties::GetCdmClientPropertySet
Interceptor.replace(address, new NativeCallback(function () {
return 0;
}, 'int', []));
Interceptor.attach(address, {
onEnter: function (args) {
print(Level.DEBUG, '[+] onEnter: GetCdmClientPropertySet');
},
onLeave: function (retval) {
print(Level.DEBUG, '[-] onLeave: GetCdmClientPropertySet');
}
});
}
const PrepareKeyRequest = (address) => {
// wvcdm::CdmLicense::PrepareKeyRequest
Interceptor.attach(address, {
onEnter: function (args) {
print(Level.DEBUG, '[+] onEnter: PrepareKeyRequest');
// https://github.com/hyugogirubato/KeyDive/issues/13#issue-2327487249
this.params = [];
for (let i = 0; i < 6; i++) {
this.params.push(args[i]);
}
},
onLeave: function (retval) {
print(Level.DEBUG, '[-] onLeave: PrepareKeyRequest');
let dumped = false;
for (let i = 0; i < this.params.length; i++) {
try {
const param = ptr(this.params[i]);
const size = Memory.readUInt(param.add(Process.pointerSize));
const data = Memory.readByteArray(param.add(Process.pointerSize * 2).readPointer(), size);
if (data) {
dumped = true;
send('challenge', data);
}
} catch (e) {
// print(Level.WARNING, `Failed to dump data for arg ${i}`);
}
}
!dumped && print(Level.ERROR, 'Failed to dump challenge.');
}
});
}
const GetCertificatePrivateKey = (address, name) => {
// wvcdm::CryptoSession::GetCertificatePrivateKey
Interceptor.attach(address, {
onEnter: function (args) {
if (!args[6].isNull()) {
const size = args[6].toInt32();
if (size >= 1000 && size <= 2000 && !args[5].isNull()) {
const buffer = args[5].readByteArray(size);
const bytes = new Uint8Array(buffer);
// Check for DER encoding markers for the beginning of a private key (MII).
if (bytes[0] === 0x30 && bytes[1] === 0x82) {
/*
let key = bytes;
try {
// Fixing key size
const binaryString = String.fromCharCode.apply(null, bytes);
const keyLength = getKeyLength(binaryString); // ASN.1 DER
key = bytes.slice(0, keyLength);
} catch (e) {
print(Level.ERROR, `${e.message} (${address})`);
}
*/
print(Level.DEBUG, `[*] GetCertificatePrivateKey: ${name}`);
!OEM_CRYPTO_API.includes(name) && print(Level.WARNING, `The function "${name}" does not belong to the referenced functions. Communicate it to the developer to improve the tool.`);
send('private_key', bytes);
}
}
}
},
onLeave: function (retval) {
// print(Level.DEBUG, `[-] onLeave: ${name}`);
}
});
}
const getKeyLength = (key) => {
// Skip the initial tag
let pos = 1;
// Extract length byte, ignoring the long-form indicator bit
let lengthByte = key.charCodeAt(pos++) & 0x7F;
// If lengthByte indicates a short form, return early.
/*
if (lengthByte < 0x80) {
return pos + lengthByte;
}
*/
// For long-form, calculate the length value.
let lengthValue = 0;
while (lengthByte--) {
lengthValue = (lengthValue << 8) + key.charCodeAt(pos++);
}
return pos + lengthValue;
}
const GetDeviceId = (address, name) => {
// wvcdm::Properties::GetCdmClientPropertySet
Interceptor.attach(address, {
onEnter: function (args) {
print(Level.DEBUG, '[+] onEnter: getOemcryptoDeviceId');
this.data = args[0];
this.size = args[1];
},
onLeave: function (retval) {
print(Level.DEBUG, '[-] onLeave: getOemcryptoDeviceId');
try {
const size = Memory.readPointer(this.size).toInt32();
const data = Memory.readByteArray(this.data, size);
data && send('client_id', data);
} catch (e) {
print(Level.ERROR, `Failed to dump device Id.`);
}
}
});
}
// @Hooks
const hookLibrary = (name) => {
// https://github.com/poxyran/misc/blob/master/frida-enumerate-imports.py
const library = getLibrary(name);
if (!library) return false;
let functions;
if (SYMBOLS.length) {
// https://github.com/hyugogirubato/KeyDive/issues/13#issuecomment-2143741896
functions = SYMBOLS.map(s => ({
type: s.type,
name: s.name,
address: library.base.add(s.address)
}));
} else {
functions = getFunctions(library);
}
functions = functions.filter(f => !NATIVE_C_API.includes(f.name));
const targets = functions.filter(f => OEM_CRYPTO_API.includes(f.name)).map(f => f.name);
let hooked = 0;
functions.forEach(func => {
if (func.type !== 'function') return;
const {name: funcName, address: funcAddr} = func;
try {
if (funcName.includes('UsePrivacyMode')) {
UsePrivacyMode(funcAddr);
} else if (funcName.includes('GetCdmClientPropertySet')) {
GetCdmClientPropertySet(funcAddr);
} else if (funcName.includes('PrepareKeyRequest')) {
PrepareKeyRequest(funcAddr);
} else if (funcName.includes('getOemcryptoDeviceId')) {
GetDeviceId(funcAddr);
} else if (targets.includes(funcName) || (!targets.length && funcName.match(/^[a-z]+$/))) {
GetCertificatePrivateKey(funcAddr, funcName);
} else {
return;
}
hooked++;
print(Level.DEBUG, `Hooked (${funcAddr}): ${funcName}`);
} catch (e) {
print(Level.ERROR, `${e.message} for ${funcName}`);
}
});
if (hooked < 3) {
print(Level.CRITICAL, 'Insufficient functions hooked.');
return false;
}
return true;
}
// RPC interfaces exposed to external calls.
rpc.exports = {
getlibrary: getLibrary,
hooklibrary: hookLibrary
};
+32
View File
@@ -0,0 +1,32 @@
class Vendor:
"""
Represents a Vendor with OEM, version, and name attributes.
"""
def __init__(self, oem: int, version: str, name: str):
"""
Initializes a Vendor instance.
Args:
oem (int): The OEM identifier.
version (str): The version of the vendor.
name (str): The name of the vendor.
"""
self.oem = oem
self.version = version
self.name = name
def __repr__(self) -> str:
"""
Returns a string representation of the Vendor instance.
Returns:
str: String representation of the Vendor instance.
"""
return '{name}({items})'.format(
name=self.__class__.__name__,
items=', '.join([f'{k}={repr(v)}' for k, v in self.__dict__.items()])
)
__all__ = ('Vendor',)