KeyDive/keydive/core.py

189 lines
6.8 KiB
Python
Raw Normal View History

2024-07-06 18:01:47 +00:00
import json
import logging
import re
2024-07-17 22:09:04 +00:00
2024-07-06 18:01:47 +00:00
from pathlib import Path
import frida
import xmltodict
2024-07-17 22:09:04 +00:00
2024-10-26 13:21:54 +00:00
from frida.core import Session, Script
2024-07-06 18:01:47 +00:00
2024-10-26 13:21:54 +00:00
from keydive.adb import ADB
2024-07-06 18:01:47 +00:00
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:
"""
2024-10-26 13:21:54 +00:00
Core class for managing DRM operations and interactions with Android devices.
2024-07-06 18:01:47 +00:00
"""
2024-10-26 13:21:54 +00:00
def __init__(self, adb: ADB, cdm: Cdm, functions: Path = None, skip: bool = False):
2024-07-06 18:01:47 +00:00
"""
Initializes a Core instance.
Args:
2024-10-26 13:21:54 +00:00
adb (ADB): ADB instance for device communication.
2024-07-06 18:01:47 +00:00
cdm (Cdm): Instance of Cdm for managing DRM related operations.
2024-07-07 08:22:21 +00:00
functions (Path, optional): Path to Ghidra XML functions file for symbol extraction. Defaults to None.
2024-10-18 18:26:41 +00:00
skip (bool, optional): Flag to determine whether to skip predefined functions (e.g., OEM_CRYPTO_API).
2024-07-06 18:01:47 +00:00
"""
self.logger = logging.getLogger(self.__class__.__name__)
self.running = True
self.cdm = cdm
2024-10-26 13:21:54 +00:00
self.adb = adb
2024-10-18 18:31:10 +00:00
# https://github.com/hyugogirubato/KeyDive/issues/38#issuecomment-2411932679
2024-10-27 18:39:09 +00:00
# Flag to skip predefined functions based on the vendor's API level
2024-10-18 18:26:41 +00:00
self.skip = skip
2024-07-06 18:01:47 +00:00
2024-10-27 18:39:09 +00:00
# Load the hook script and prepare for injection
2024-07-07 08:22:21 +00:00
self.functions = functions
2024-07-06 18:01:47 +00:00
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.
"""
2024-07-07 08:22:55 +00:00
content = Path(__file__).with_name('keydive.js').read_text(encoding='utf-8')
2024-07-07 08:22:21 +00:00
symbols = self.__prepare_symbols(self.functions)
2024-07-06 18:01:47 +00:00
# Replace placeholders in script template
replacements = {
'${OEM_CRYPTO_API}': json.dumps(list(OEM_CRYPTO_API)),
'${NATIVE_C_API}': json.dumps(list(NATIVE_C_API)),
2024-10-18 18:26:41 +00:00
'${SYMBOLS}': json.dumps(symbols),
'${SKIP}': str(self.skip)
2024-07-06 18:01:47 +00:00
}
for placeholder, value in replacements.items():
content = content.replace(placeholder, value)
return content
2024-10-18 18:26:41 +00:00
def __prepare_symbols(self, path: Path) -> list:
2024-07-06 18:01:47 +00:00
"""
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
2024-10-18 18:26:41 +00:00
target = None if self.skip else next((f['@NAME'] for f in functions if f['@NAME'] in OEM_CRYPTO_API), None)
2024-07-06 18:01:47 +00:00
# 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
2024-10-18 18:26:41 +00:00
or any(None if self.skip else keyword in name for keyword in CDM_FUNCTION_API)
2024-07-06 18:01:47 +00:00
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 __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
2024-10-20 14:07:23 +00:00
elif isinstance(level, dict) and 'private_key' in level:
self.cdm.set_private_key(data=data, name=level['private_key'])
2024-07-06 18:01:47 +00:00
elif level == 'challenge':
self.cdm.set_challenge(data=data)
2024-10-27 18:39:09 +00:00
elif level == 'device_id':
self.cdm.set_device_id(data)
elif level == 'keybox':
self.cdm.set_keybox(data)
2024-07-06 18:01:47 +00:00
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:
2024-10-26 13:21:54 +00:00
session: Session = self.adb.device.attach(pid, persist_timeout=timeout)
2024-07-06 19:41:33 +00:00
except frida.ServerNotRunningError as e:
raise EnvironmentError('Frida server is not running') from e
2024-07-06 18:01:47 +00:00
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
2024-07-07 08:22:21 +00:00
if vendor.oem > 17 and not self.functions:
2024-07-06 18:01:47 +00:00
self.logger.warning('For OEM API > 17, specifying "functions" is required, refer to https://github.com/hyugogirubato/KeyDive/blob/main/docs/FUNCTIONS.md')
2024-07-07 08:22:21 +00:00
elif vendor.oem < 18 and self.functions:
2024-07-06 18:01:47 +00:00
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',)