mirror of
https://github.com/hyugogirubato/KeyDive.git
synced 2026-07-15 18:40:02 +02:00
release v3.0.0
This commit is contained in:
+395
-157
@@ -1,228 +1,466 @@
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
|
||||
from typing import Optional, Tuple, Literal
|
||||
from pathlib import Path
|
||||
|
||||
import frida
|
||||
import xmltodict
|
||||
|
||||
from frida import ServerNotRunningError
|
||||
from frida.core import Session, Script
|
||||
from pathvalidate import sanitize_filepath
|
||||
|
||||
from keydive.adb import ADB
|
||||
from keydive.cdm import Cdm
|
||||
from keydive.constants import OEM_CRYPTO_API, NATIVE_C_API, CDM_FUNCTION_API
|
||||
from keydive.vendor import Vendor
|
||||
from keydive.adb import DRM_PLAYER, DRM_WEB, NATIVE_C_API
|
||||
from keydive.adb.remote import Remote
|
||||
from keydive.adb.vendor import Vendor
|
||||
from keydive.drm import CDM_VENDOR_API, OEM_CRYPTO_API
|
||||
from keydive.drm.cdm import Cdm
|
||||
from keydive.utils import xmldec, dumps
|
||||
|
||||
|
||||
class Core:
|
||||
"""
|
||||
Core class for managing DRM operations and interactions with Android devices.
|
||||
"""
|
||||
class Server:
|
||||
|
||||
def __init__(self, adb: ADB, cdm: Cdm, functions: Path = None, skip: bool = False):
|
||||
def __init__(self, version: str):
|
||||
"""
|
||||
Initializes a Core instance.
|
||||
Initialize the Server instance with the provided Frida version string.
|
||||
|
||||
Parameters:
|
||||
adb (ADB): ADB instance for device communication.
|
||||
cdm (Cdm): Instance for handling DRM-related operations.
|
||||
functions (Path, optional): Path to Ghidra XML file for symbol extraction. Defaults to None.
|
||||
skip (bool, optional): Whether to skip predefined functions (e.g., OEM_CRYPTO_API). Defaults to False.
|
||||
This constructor sets internal flags based on the Frida version,
|
||||
particularly to determine support for features introduced in Frida 16.6.0+.
|
||||
|
||||
Args:
|
||||
version (str): The version string of the Frida server (e.g., "16.5.3").
|
||||
"""
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.running = True
|
||||
self.cdm = cdm
|
||||
self.adb = adb
|
||||
self.logger = logging.getLogger('Server')
|
||||
|
||||
# Flag to skip predefined functions based on the vendor's API level
|
||||
# https://github.com/hyugogirubato/KeyDive/issues/38#issuecomment-2411932679
|
||||
self.skip = skip
|
||||
self.version = version
|
||||
self.logger.debug('Frida version: %s', self.version)
|
||||
|
||||
# Load the hook script with relevant data and prepare for injection
|
||||
self.functions = functions
|
||||
self.script = self.__prepare_hook_script()
|
||||
self.logger.info("Hook script prepared successfully")
|
||||
# Parse version string and determine feature support
|
||||
code = tuple(map(int, version.split('.')))
|
||||
self.features = code[0] > 16 or (code[0] == 16 and code[1] >= 6)
|
||||
self.logger.debug('Feature support: %s', self.features)
|
||||
|
||||
def __prepare_hook_script(self) -> str:
|
||||
# Used to prevent repeated user warnings in logs or dialogs
|
||||
self.dialog = False
|
||||
|
||||
|
||||
class Core(Remote):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
serial: Optional[str] = None,
|
||||
timeout: int = 5,
|
||||
symbols: Optional[Path] = None,
|
||||
detect: bool = True,
|
||||
disabler: bool = True,
|
||||
unencrypt: bool = False
|
||||
):
|
||||
"""
|
||||
Prepares the hook script by injecting library-specific data.
|
||||
Initialize the Core object, which manages interaction with a remote device
|
||||
and handles Widevine CDM (Content Decryption Module) processes.
|
||||
|
||||
This constructor also prepares a Frida script for hooking and sets up
|
||||
internal state for later process attachment and monitoring.
|
||||
|
||||
Args:
|
||||
serial (Optional[str]): The serial number of the connected Android device.
|
||||
If None, the first available device will be used.
|
||||
timeout (int): Timeout in seconds for establishing the device connection.
|
||||
symbols (Optional[Path]): Path to an XML file containing symbol definitions.
|
||||
detect (bool): Whether to enable detection logic in the injected Frida script.
|
||||
disabler (bool): Whether to enable anti-tamper or anti-debugging disablers
|
||||
in the Frida script.
|
||||
unencrypt (bool): If True, forces license challenge data to be sent unencrypted.
|
||||
"""
|
||||
# Initialize base Remote object with optional serial and connection timeout
|
||||
super().__init__(serial=serial, timeout=timeout)
|
||||
self.logger = logging.getLogger('Core')
|
||||
|
||||
# Indicates whether the watchdog process is actively running
|
||||
self._running = False
|
||||
|
||||
# Server instance (initialized later when Frida is attached)
|
||||
self._server: Optional[Server] = None
|
||||
|
||||
# Load and prepare the Frida script with optional symbol resolution
|
||||
self._script, self._resolved = self.__hook_script(
|
||||
detect=detect,
|
||||
disabler=disabler,
|
||||
path=symbols,
|
||||
unencrypt=unencrypt
|
||||
)
|
||||
|
||||
# Initialize CDM (Content Decryption Module) interface with SDK and disabler flag
|
||||
self.cdm = Cdm(sdk=self.sdk, disabler=disabler)
|
||||
|
||||
def __hook_script(
|
||||
self, detect: bool = True,
|
||||
disabler: bool = True,
|
||||
path: Optional[Path] = None,
|
||||
unencrypt: bool = False
|
||||
) -> Tuple[str, bool]:
|
||||
"""
|
||||
Prepares and customizes a JavaScript hook script with runtime configurations and optional symbol resolution.
|
||||
|
||||
This function reads a base JavaScript file (`keydive.js`), injects configurations and API stubs into it,
|
||||
and optionally replaces placeholders with resolved symbol names extracted from an XML file describing
|
||||
native functions.
|
||||
|
||||
Args:
|
||||
detect (bool): Whether to enable the detection logic in the hook script.
|
||||
disabler (bool): Whether to enable the disabler logic in the hook script.
|
||||
path (Optional[Path]): An optional path to an XML file containing function metadata (e.g., Ghidra-exported data).
|
||||
If provided, function symbols are parsed and injected into the script.
|
||||
unencrypt (bool): If True, configures the hook script to force client ID and related license
|
||||
challenge data to remain unencrypted.
|
||||
|
||||
Returns:
|
||||
str: The finalized hook script content with placeholders replaced.
|
||||
Tuple[str, bool]: A tuple where:
|
||||
- str is the fully prepared hook script as a string.
|
||||
- bool indicates whether symbol data was successfully loaded and injected.
|
||||
|
||||
Exception:
|
||||
Exceptions raised while reading or parsing the symbol file are caught and logged.
|
||||
"""
|
||||
# Read the base JavaScript template file
|
||||
content = Path(__file__).with_name("keydive.js").read_text(encoding="utf-8")
|
||||
# Load the base hook JavaScript file
|
||||
script = Path(__file__).with_name('keydive.js').read_text(encoding='utf-8')
|
||||
|
||||
# Generate the list of symbols from the functions file
|
||||
symbols = self.__prepare_symbols(self.functions)
|
||||
symbols = {}
|
||||
if path:
|
||||
try:
|
||||
# Parse the XML file containing function metadata (e.g., Ghidra export)
|
||||
content = xmldec(path.read_bytes(), force_list=['FUNCTION', 'STACK_VAR', 'REGISTER_VAR'])
|
||||
program = content['PROGRAM']
|
||||
addr_base = int(program['@IMAGE_BASE'], 16)
|
||||
functions = program['FUNCTIONS']['FUNCTION']
|
||||
|
||||
# Define the placeholder replacements
|
||||
replacements = {
|
||||
"${OEM_CRYPTO_API}": json.dumps(list(OEM_CRYPTO_API)),
|
||||
"${NATIVE_C_API}": json.dumps(list(NATIVE_C_API)),
|
||||
"${SYMBOLS}": json.dumps(symbols),
|
||||
"${SKIP}": str(self.skip)
|
||||
# Build a dictionary of function address to name mappings
|
||||
for f in functions:
|
||||
address = hex(int(f['@ENTRY_POINT'], 16) - addr_base)
|
||||
name = f['@NAME']
|
||||
|
||||
# Avoid duplicate entries by checking existing symbol names
|
||||
if name not in symbols.values():
|
||||
symbols[address] = name
|
||||
|
||||
self.logger.info('Successfully loaded %d symbol(s) from: %s', len(symbols), path.as_posix())
|
||||
except Exception as e:
|
||||
# Log the error if symbol import fails, but continue script generation
|
||||
self.logger.error('Unable to import symbols from XML: %s', e)
|
||||
|
||||
# Create the placeholder-to-value mapping
|
||||
placeholders = {
|
||||
'${OEM_CRYPTO_API}': dumps(OEM_CRYPTO_API),
|
||||
'${NATIVE_C_API}': dumps(NATIVE_C_API),
|
||||
'${SYMBOLS}': dumps(symbols),
|
||||
'${DETECT}': str(detect),
|
||||
'${DISABLER}': str(disabler),
|
||||
'${UNENCRYPT}': str(unencrypt)
|
||||
}
|
||||
|
||||
# Replace placeholders in the script content
|
||||
for placeholder, value in replacements.items():
|
||||
content = content.replace(placeholder, value, 1)
|
||||
# Replace placeholders in the script with actual data
|
||||
for placeholder, value in placeholders.items():
|
||||
script = script.replace(placeholder, value, 1)
|
||||
|
||||
return content
|
||||
# Return the modified script and a flag indicating if symbol injection was successful
|
||||
return script, bool(symbols)
|
||||
|
||||
def __prepare_symbols(self, path: Path) -> list:
|
||||
def launch(self, action: Literal['web', 'player'] = 'player') -> None:
|
||||
"""
|
||||
Extracts relevant functions from a Ghidra XML file.
|
||||
Launches a DRM-enabled player either as a native application or a web-based player.
|
||||
|
||||
Parameters:
|
||||
path (Path): Path to the Ghidra XML functions file.
|
||||
Depending on the selected action, this method ensures that the DRM player app is installed and running,
|
||||
or opens a predefined DRM test page in the system browser. It performs basic checks for installation,
|
||||
process existence, and handles optional installation if needed.
|
||||
|
||||
Returns:
|
||||
list: List of selected functions as dictionaries.
|
||||
Args:
|
||||
action (Literal['web', 'player']):
|
||||
- 'player': Launch the native DRM player app.
|
||||
- 'web': Open the web-based DRM player in the default browser.
|
||||
"""
|
||||
if action == 'player':
|
||||
# Retrieve the package name and human-readable name of the DRM player
|
||||
player_package = DRM_PLAYER['package']
|
||||
player_name = DRM_PLAYER['name']
|
||||
|
||||
self.logger.info('Preparing DRM player: %s (%s)', player_name, player_package)
|
||||
|
||||
# Check if the application is already installed
|
||||
installed = player_package in self.enumerate_applications(user=True, system=False)
|
||||
if installed:
|
||||
self.logger.debug('Application is already installed: %s', player_package)
|
||||
else:
|
||||
self.logger.debug('Application not found: %s. Attempting to install...', player_package)
|
||||
|
||||
# Try to install the app from the specified path or URL
|
||||
if not self.install_application(path=DRM_PLAYER['path'], url=DRM_PLAYER['url']):
|
||||
return # Stop if installation fails
|
||||
self.logger.debug('Application installed successfully: %s', player_package)
|
||||
|
||||
# Check if the application is already running
|
||||
player_pid = self.enumerate_processes(names=[player_package])
|
||||
if player_pid:
|
||||
self.logger.warning('Application is already running: %s (%s)', player_name, player_package)
|
||||
else:
|
||||
# Attempt to start the application
|
||||
self.logger.info('Starting application: %s (%s)', player_name, player_package)
|
||||
if not self.start_application(player_package):
|
||||
return # Abort if unable to launch
|
||||
|
||||
# Give the system a moment to register the new process
|
||||
time.sleep(1)
|
||||
player_pid = self.enumerate_processes(names=[player_package])
|
||||
|
||||
# Log running process info or fallback if PID is unknown
|
||||
if player_pid:
|
||||
self.logger.debug('Running process: %s (%s)', list(player_pid.keys())[-1], player_package)
|
||||
else:
|
||||
self.logger.debug('Unable to determine PID (%s).', player_package)
|
||||
|
||||
elif action == 'web':
|
||||
# Attempt to open the DRM test URL in the default browser
|
||||
self.logger.info('Opening DRM web player in default browser...')
|
||||
if not self.open_url(DRM_WEB):
|
||||
return
|
||||
|
||||
# Attempt to detect a running browser process
|
||||
player_pid = self.enumerate_processes(names=[
|
||||
'com.android.chrome', # Google Chrome
|
||||
'com.sec.android.app.sbrowser', # Samsung Internet
|
||||
'org.mozilla.firefox' # Mozilla Firefox
|
||||
])
|
||||
|
||||
if player_pid:
|
||||
self.logger.debug('Running process: %s (%s)', list(player_pid.keys())[-1], list(player_pid.values())[-1])
|
||||
else:
|
||||
self.logger.debug('No known browser process detected. Consider adding support for your browser.')
|
||||
|
||||
def watchdog(
|
||||
self,
|
||||
output: Path,
|
||||
delay: int = 1,
|
||||
auto_stop: bool = True,
|
||||
wvd: bool = False,
|
||||
keybox: bool = False
|
||||
) -> None:
|
||||
"""
|
||||
Continuously monitor Widevine DRM processes, export device credentials, and save to disk.
|
||||
|
||||
This method acts as a watchdog that periodically scans for Widevine-capable processes,
|
||||
attaches hooks to them, and exports relevant DRM credential files to a specified output directory.
|
||||
It supports optional exporting in .wvd format and OEM keyboxes.
|
||||
|
||||
Args:
|
||||
output (Path): Directory path where extracted files will be saved.
|
||||
delay (int, optional): Delay in seconds between each monitoring iteration. Defaults to 1.
|
||||
auto_stop (bool, optional): Automatically stop after successful export of client_id.bin. Defaults to True.
|
||||
wvd (bool, optional): Enable exporting credentials in .wvd format compatible with pywidevine. Defaults to False.
|
||||
keybox (bool, optional): Enable exporting OEM certificates and keyboxes if available. Defaults to False.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the functions file is not found.
|
||||
ValueError: If functions extraction fails.
|
||||
EnvironmentError: Raised if no Widevine DRM process is detected during scanning.
|
||||
"""
|
||||
# Return an empty list if no path is provided
|
||||
if not path:
|
||||
return []
|
||||
self.logger.info('Watcher delay: %ss' % delay)
|
||||
self._running = True
|
||||
|
||||
try:
|
||||
# Parse the XML file and extract program data
|
||||
program = xmltodict.parse(path.read_bytes())["PROGRAM"]
|
||||
addr_base = int(program["@IMAGE_BASE"], 16) # Base address for function addresses
|
||||
functions = program["FUNCTIONS"]["FUNCTION"] # List of functions in the XML
|
||||
extracted = [] # Tracks already exported files to avoid duplicates
|
||||
current = None # Holds the PID of the currently hooked Widevine process
|
||||
|
||||
# Identify a target function from the predefined OEM_CRYPTO_API list (if not skipped)
|
||||
target = next((f["@NAME"] for f in functions if f["@NAME"] in OEM_CRYPTO_API and not self.skip), None)
|
||||
while self._running:
|
||||
# Export device credentials and DRM data files
|
||||
files = self.cdm.export(wvd=wvd, keybox=keybox)
|
||||
if files:
|
||||
for name, data in files.items():
|
||||
# Sanitize file path to ensure valid filesystem names and create directories
|
||||
# https://github.com/hyugogirubato/KeyDive/issues/14#issuecomment-2146958022
|
||||
path = output / sanitize_filepath(name)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Prepare a dictionary to store selected functions
|
||||
selected = {}
|
||||
for func in functions:
|
||||
name = func["@NAME"] # Function name
|
||||
args = len(func.get("REGISTER_VAR", [])) # Number of arguments
|
||||
# If the file already exists and is identical, log a warning (only once) and skip
|
||||
if path.is_file() and path.read_bytes() == data:
|
||||
if path not in extracted:
|
||||
self.logger.warning('File already exists: %s', path)
|
||||
extracted.append(path)
|
||||
continue
|
||||
|
||||
"""
|
||||
Add the function if it matches specific criteria
|
||||
- Match the target function if identified
|
||||
- Match API keywords
|
||||
- Match unnamed functions with 6+ args
|
||||
"""
|
||||
if name not in selected and (
|
||||
name == target
|
||||
or any(True if self.skip else 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) # Calculate relative address
|
||||
}
|
||||
# If exporting a .wvd file, remove any other .wvd files in the same directory
|
||||
if path.suffix == '.wvd' and path.parent.exists():
|
||||
for file in path.parent.glob('*.wvd'):
|
||||
file.unlink()
|
||||
|
||||
# Return the list of selected functions
|
||||
return list(selected.values())
|
||||
except FileNotFoundError as e:
|
||||
raise FileNotFoundError(f"Functions file not found: {path}") from e
|
||||
except Exception as e:
|
||||
raise ValueError("Failed to extract functions from Ghidra XML file") from e
|
||||
# Write the file
|
||||
self.logger.info('Exporting file: %s', path)
|
||||
path.write_bytes(data)
|
||||
extracted.append(path)
|
||||
|
||||
# If the client ID has been exported and auto-stop is enabled, stop monitoring
|
||||
if 'client_id.bin' in dumps(extracted):
|
||||
self._running = not auto_stop
|
||||
|
||||
if not self._running:
|
||||
self.logger.info('Required files exported successfully.')
|
||||
continue
|
||||
|
||||
# Detect Widevine-capable processes running on the device
|
||||
processes = sorted(
|
||||
[
|
||||
(pid, (name, vendor))
|
||||
for pid, name in self.enumerate_processes().items()
|
||||
for vendor in CDM_VENDOR_API
|
||||
if vendor.min_sdk <= self.sdk and vendor.is_process(name)
|
||||
],
|
||||
key=lambda item: item[1][1].min_sdk,
|
||||
reverse=True
|
||||
)
|
||||
|
||||
if not processes:
|
||||
# No DRM process detected, raise error with guidance for the user
|
||||
raise EnvironmentError(
|
||||
'No Widevine DRM process detected. '
|
||||
'Refer to: https://github.com/hyugogirubato/KeyDive/blob/main/docs/PACKAGE.md#drm-info'
|
||||
)
|
||||
|
||||
# If previously hooked process is no longer running, clear current hook
|
||||
if current and current not in [pid for pid, _ in processes]:
|
||||
self.logger.warning('Widevine process terminated or replaced')
|
||||
current = None
|
||||
|
||||
# If no current hooked process, attempt to hook one from detected processes
|
||||
if not current:
|
||||
self.logger.debug('Scanning for Widevine processes...')
|
||||
for pid, (name, vendor) in processes:
|
||||
self.logger.info('Detected process: %s (%s)', pid, name)
|
||||
if self.__hook_process(pid, vendor):
|
||||
current = pid
|
||||
break
|
||||
|
||||
if current:
|
||||
self.logger.info('Successfully attached hook to process: %s', current)
|
||||
else:
|
||||
self.logger.warning('Widevine library not located yet. Retrying...')
|
||||
|
||||
# Sleep before next monitoring iteration to reduce resource usage
|
||||
time.sleep(delay)
|
||||
|
||||
def __process_message(self, message: dict, data: bytes) -> None:
|
||||
"""
|
||||
Handles messages received from the Frida script.
|
||||
Callback handler for messages sent from the Frida-injected script.
|
||||
|
||||
Parameters:
|
||||
message (dict): The message payload.
|
||||
data (bytes): The raw data associated with the message.
|
||||
This function processes various types of messages and data received from the Frida instrumentation layer.
|
||||
It routes them to appropriate CDM (Content Decryption Module) handlers or logs them accordingly.
|
||||
|
||||
Args:
|
||||
message (dict): The message dictionary sent from the Frida script.
|
||||
data (bytes): The associated binary payload, if any.
|
||||
"""
|
||||
logger = logging.getLogger("Script")
|
||||
level = message.get("payload")
|
||||
logger = logging.getLogger('Script')
|
||||
level = message.get('payload')
|
||||
|
||||
if isinstance(level, int):
|
||||
# Log the message based on its severity level
|
||||
logger.log(level=level, msg=data.decode("utf-8"))
|
||||
# Log text message from script with given severity level
|
||||
logger.log(level=level, msg=data.decode('utf-8'))
|
||||
|
||||
# Stop the runner if a critical or fatal error is reported
|
||||
if level in (logging.FATAL, logging.CRITICAL):
|
||||
self.running = False # Stop the process on critical errors
|
||||
elif isinstance(level, dict) and "private_key" in level:
|
||||
# Set the private key in the DRM handler
|
||||
self.cdm.set_private_key(data=data, name=level["private_key"])
|
||||
elif level == "challenge":
|
||||
# Set the challenge data in the DRM handler
|
||||
self.cdm.set_challenge(data=data)
|
||||
elif level == "device_id":
|
||||
# Set the device ID in the DRM handler
|
||||
self.cdm.set_device_id(data)
|
||||
elif level == "keybox":
|
||||
# Set the keybox data in the DRM handler
|
||||
self.running = False
|
||||
|
||||
elif isinstance(level, dict) and 'private_key' in level:
|
||||
self.cdm.set_private_key(data, level['private_key'])
|
||||
elif level == 'challenge':
|
||||
self.cdm.set_challenge(data)
|
||||
elif level == 'client_id':
|
||||
self.cdm.set_client_id(data)
|
||||
elif level == 'keybox':
|
||||
self.cdm.set_keybox(data)
|
||||
elif level == 'stable_id':
|
||||
self.cdm.set_stable_id(data)
|
||||
elif level == 'device_id':
|
||||
self.cdm.set_device_id(data)
|
||||
elif level == 'provisioning_method':
|
||||
self.cdm.set_provisioning_method(data)
|
||||
elif level == 'provisioning_response':
|
||||
self.cdm.set_provisioning_response(data)
|
||||
elif message.get('type') == 'error':
|
||||
# Log any script-side errors
|
||||
logger.error(message['description'])
|
||||
else:
|
||||
# Fallback logging for unrecognized messages
|
||||
logger.warning(message, data)
|
||||
|
||||
def hook_process(self, pid: int, vendor: Vendor, timeout: int = 0) -> bool:
|
||||
def __hook_process(self, pid: int, vendor: Vendor, timeout: int = 0) -> bool:
|
||||
"""
|
||||
Hooks into the specified process.
|
||||
Hooks into a running process using Frida to enable dynamic analysis and instrumentation.
|
||||
|
||||
Parameters:
|
||||
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.
|
||||
This method attaches to a process identified by its PID, loads a Frida script for hooking into
|
||||
a vendor-specific shared library, and optionally enables symbolic analysis depending on device
|
||||
and server capabilities.
|
||||
|
||||
Args:
|
||||
pid (int): Process ID of the target application to attach to.
|
||||
vendor (Vendor): A Vendor object containing metadata about the expected shared library.
|
||||
timeout (int, optional): Time (in seconds) to persist the Frida session. Defaults to 0.
|
||||
|
||||
Returns:
|
||||
bool: True if the process was successfully hooked, otherwise False.
|
||||
bool: True if the hook was successful, False otherwise.
|
||||
|
||||
Raises:
|
||||
EnvironmentError: If the Frida server is not running on the target device.
|
||||
"""
|
||||
try:
|
||||
# Attach to the target process using the specified PID.
|
||||
# The 'persist_timeout' parameter ensures the session persists for the given duration.
|
||||
session: Session = self.adb.device.attach(pid, persist_timeout=timeout)
|
||||
except frida.ServerNotRunningError as e:
|
||||
# Handle the case where the Frida server is not running on the device.
|
||||
raise EnvironmentError("Frida server is not running") from e
|
||||
# Attach to the target process using the specified PID
|
||||
# The 'persist_timeout' parameter ensures the session persists for the given duration
|
||||
session: Session = self.socket.attach(pid, persist_timeout=timeout)
|
||||
except ServerNotRunningError as e:
|
||||
# Handle the case where the Frida server is not running on the device
|
||||
raise EnvironmentError('Frida server is not running on the device.') from e
|
||||
except Exception as e:
|
||||
# Log other exceptions and return False to indicate failure.
|
||||
self.logger.error(e)
|
||||
# Catch and log all other errors that occur during session attachment
|
||||
self.logger.error('Could not attach to process %s: %s', pid, e)
|
||||
return False
|
||||
|
||||
# Define a callback to handle when the process is destroyed.
|
||||
# Define cleanup behavior for when the Frida script is destroyed
|
||||
def __process_destroyed() -> None:
|
||||
session.detach()
|
||||
|
||||
# Create a Frida script object using the prepared script content.
|
||||
script: Script = session.create_script(self.script)
|
||||
script.on("message", self.__process_message)
|
||||
script.on("destroyed", __process_destroyed)
|
||||
# Create and load the Frida script for dynamic analysis
|
||||
script: Script = session.create_script(self._script)
|
||||
script.on('message', self.__process_message) # Set message handler
|
||||
script.on('destroyed', __process_destroyed) # Set destruction handler
|
||||
script.load()
|
||||
|
||||
# Fetch a list of libraries loaded by the target process.
|
||||
# Fetch Frida server metadata (e.g., version) and store it if not already set
|
||||
if not self._server:
|
||||
self._server = Server(script.exports_sync.getversion())
|
||||
|
||||
# Retrieve the list of loaded libraries in the target process
|
||||
libraries = script.exports_sync.getlibraries()
|
||||
library = next((l for l in libraries if re.match(vendor.pattern, l["name"])), None)
|
||||
library = vendor.get_library(libraries)
|
||||
|
||||
if library:
|
||||
# Log information about the library if it is found.
|
||||
self.logger.info("Library: %s (%s)", library["name"], library["path"])
|
||||
# Log details about the matching library
|
||||
self.logger.info('Library found: %s (%s)', library['name'], library['path'])
|
||||
|
||||
# Retrieve and log the version of the Frida server.
|
||||
version = script.exports_sync.getversion()
|
||||
self.logger.debug(f"Server: %s", version)
|
||||
# Provide context-aware warnings about symbol resolution options
|
||||
if not self._server.dialog:
|
||||
if self._server.features and self._resolved:
|
||||
self.logger.warning('The "--symbols" option is deprecated in Frida 16.6.0 and newer.')
|
||||
elif not self._server.features and vendor.min_oem[0] < 18 and self._resolved:
|
||||
self.logger.warning('The "--symbols" option is deprecated for OEM API versions below 18.')
|
||||
elif not self._server.features and vendor.min_oem[0] > 17 and not self._resolved:
|
||||
self.logger.warning(
|
||||
'For OEM API > 17, the "--symbols" option is required. '
|
||||
'Refer to: https://github.com/hyugogirubato/KeyDive/blob/main/docs/FUNCTIONS.md'
|
||||
)
|
||||
self._server.dialog = True
|
||||
|
||||
# Determine if the Frida server version is older than 16.6.0.
|
||||
code = tuple(map(int, version.split(".")))
|
||||
minimum = code[0] > 16 or (code[0] == 16 and code[1] >= 6)
|
||||
# Determine whether to enable dynamic symbol resolution based on server and vendor context
|
||||
dynamic = self._server.features and vendor.min_oem[0] > 17 and not self._resolved
|
||||
|
||||
# Warn the user if certain conditions related to the functions option are met.
|
||||
if minimum and self.functions:
|
||||
self.logger.warning("The '--functions' option is deprecated starting from Frida 16.6.0")
|
||||
elif not minimum and vendor.oem < 18 and self.functions:
|
||||
self.logger.warning("The '--functions' option is deprecated for OEM API < 18")
|
||||
elif not minimum and vendor.oem > 17 and not self.functions:
|
||||
self.logger.warning("For OEM API > 17, specifying '--functions' is required. Refer to https://github.com/hyugogirubato/KeyDive/blob/main/docs/FUNCTIONS.md")
|
||||
# Attempt to hook into the identified library with or without dynamic symbols
|
||||
return script.exports_sync.hooklibrary(library['name'], dynamic)
|
||||
|
||||
# Enable dynamic analysis (symbols) only when necessary
|
||||
dynamic = minimum and vendor.oem > 17 and not self.functions
|
||||
return script.exports_sync.hooklibrary(library["name"], dynamic)
|
||||
|
||||
# Unload the script if the target library is not found.
|
||||
# If the expected library was not found, clean up and notify the user
|
||||
script.unload()
|
||||
self.logger.warning("Library not found: %s" % vendor.pattern)
|
||||
self.logger.warning('Expected library not found: %s' % vendor.library)
|
||||
return False
|
||||
|
||||
|
||||
__all__ = ("Core",)
|
||||
__all__ = ('Core',)
|
||||
|
||||
Reference in New Issue
Block a user