KeyDive/keydive/core.py

227 lines
9.2 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.
2025-01-19 13:13:07 +00:00
Parameters:
2024-10-26 13:21:54 +00:00
adb (ADB): ADB instance for device communication.
2025-01-19 13:13:07 +00:00
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.
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
2024-10-27 18:39:09 +00:00
# Flag to skip predefined functions based on the vendor's API level
2025-01-19 13:13:07 +00:00
# https://github.com/hyugogirubato/KeyDive/issues/38#issuecomment-2411932679
2024-10-18 18:26:41 +00:00
self.skip = skip
2024-07-06 18:01:47 +00:00
2025-01-19 13:13:07 +00:00
# Load the hook script with relevant data 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()
2025-01-19 13:13:07 +00:00
self.logger.info("Hook script prepared successfully")
2024-07-06 18:01:47 +00:00
def __prepare_hook_script(self) -> str:
"""
2025-01-19 13:13:07 +00:00
Prepares the hook script by injecting library-specific data.
2024-07-06 18:01:47 +00:00
Returns:
2025-01-19 13:13:07 +00:00
str: The finalized hook script content with placeholders replaced.
2024-07-06 18:01:47 +00:00
"""
2025-01-19 13:13:07 +00:00
# Read the base JavaScript template file
content = Path(__file__).with_name("keydive.js").read_text(encoding="utf-8")
# Generate the list of symbols from the functions file
2024-07-07 08:22:21 +00:00
symbols = self.__prepare_symbols(self.functions)
2024-07-06 18:01:47 +00:00
2025-01-19 13:13:07 +00:00
# Define the placeholder replacements
2024-07-06 18:01:47 +00:00
replacements = {
2025-01-19 13:13:07 +00:00
"${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)
2024-07-06 18:01:47 +00:00
}
2025-01-19 13:13:07 +00:00
# Replace placeholders in the script content
2024-07-06 18:01:47 +00:00
for placeholder, value in replacements.items():
2025-01-19 13:13:07 +00:00
content = content.replace(placeholder, value, 1)
2024-07-06 18:01:47 +00:00
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
"""
2025-01-19 13:13:07 +00:00
Extracts relevant functions from a Ghidra XML file.
2024-07-06 18:01:47 +00:00
2025-01-19 13:13:07 +00:00
Parameters:
path (Path): Path to the Ghidra XML functions file.
2024-07-06 18:01:47 +00:00
Returns:
list: List of selected functions as dictionaries.
Raises:
FileNotFoundError: If the functions file is not found.
ValueError: If functions extraction fails.
"""
2025-01-19 13:13:07 +00:00
# Return an empty list if no path is provided
2024-07-06 18:01:47 +00:00
if not path:
return []
try:
2025-01-19 13:13:07 +00:00
# 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
2024-07-06 18:01:47 +00:00
2025-01-19 13:13:07 +00:00
# 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)
2024-07-06 18:01:47 +00:00
2025-01-19 13:13:07 +00:00
# Prepare a dictionary to store selected functions
2024-07-06 18:01:47 +00:00
selected = {}
for func in functions:
2025-01-19 13:13:07 +00:00
name = func["@NAME"] # Function name
args = len(func.get("REGISTER_VAR", [])) # Number of arguments
"""
Add the function if it matches specific criteria
- Match the target function if identified
- Match API keywords
- Match unnamed functions with 6+ args
"""
2024-07-06 18:01:47 +00:00
if name not in selected and (
name == target
2024-10-28 20:29:28 +00:00
or any(True if self.skip else keyword in name for keyword in CDM_FUNCTION_API)
2025-01-19 13:13:07 +00:00
or (not target and re.match(r"^[a-z]+$", name) and args >= 6)
2024-07-06 18:01:47 +00:00
):
selected[name] = {
2025-01-19 13:13:07 +00:00
"type": "function",
"name": name,
"address": hex(int(func["@ENTRY_POINT"], 16) - addr_base) # Calculate relative address
2024-07-06 18:01:47 +00:00
}
2025-01-19 13:13:07 +00:00
# Return the list of selected functions
2024-07-06 18:01:47 +00:00
return list(selected.values())
2025-01-19 13:13:07 +00:00
except FileNotFoundError as e:
raise FileNotFoundError(f"Functions file not found: {path}") from e
2024-07-06 18:01:47 +00:00
except Exception as e:
2025-01-19 13:13:07 +00:00
raise ValueError("Failed to extract functions from Ghidra XML file") from e
2024-07-06 18:01:47 +00:00
def __process_message(self, message: dict, data: bytes) -> None:
"""
Handles messages received from the Frida script.
2025-01-19 13:13:07 +00:00
Parameters:
2024-07-06 18:01:47 +00:00
message (dict): The message payload.
data (bytes): The raw data associated with the message.
"""
2025-01-19 13:13:07 +00:00
logger = logging.getLogger("Script")
level = message.get("payload")
2024-07-06 18:01:47 +00:00
if isinstance(level, int):
2025-01-19 13:13:07 +00:00
# Log the message based on its severity level
logger.log(level=level, msg=data.decode("utf-8"))
2024-07-06 18:01:47 +00:00
if level in (logging.FATAL, logging.CRITICAL):
2025-01-19 13:13:07 +00:00
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
2024-07-06 18:01:47 +00:00
self.cdm.set_challenge(data=data)
2025-01-19 13:13:07 +00:00
elif level == "device_id":
# Set the device ID in the DRM handler
2024-10-27 18:39:09 +00:00
self.cdm.set_device_id(data)
2025-01-19 13:13:07 +00:00
elif level == "keybox":
# Set the keybox data in the DRM handler
2024-10-27 18:39:09 +00:00
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.
2025-01-19 13:13:07 +00:00
Parameters:
2024-07-06 18:01:47 +00:00
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:
2025-01-19 13:13:07 +00:00
# Attach to the target process using the specified PID.
# The 'persist_timeout' parameter ensures the session persists for the given duration.
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:
2025-01-19 13:13:07 +00:00
# Handle the case where the Frida server is not running on the device.
raise EnvironmentError("Frida server is not running") from e
2024-07-06 18:01:47 +00:00
except Exception as e:
2025-01-19 13:13:07 +00:00
# Log other exceptions and return False to indicate failure.
2024-07-06 18:01:47 +00:00
self.logger.error(e)
return False
2025-01-19 13:13:07 +00:00
# Define a callback to handle when the process is destroyed.
2024-07-06 18:01:47 +00:00
def __process_destroyed() -> None:
session.detach()
2025-01-19 13:13:07 +00:00
# Create a Frida script object using the prepared script content.
2024-07-06 18:01:47 +00:00
script: Script = session.create_script(self.script)
2025-01-19 13:13:07 +00:00
script.on("message", self.__process_message)
script.on("destroyed", __process_destroyed)
2024-07-06 18:01:47 +00:00
script.load()
2025-01-19 13:13:07 +00:00
# Fetch a list of libraries loaded by the target process.
2025-01-11 15:54:27 +00:00
libraries = script.exports_sync.getlibraries()
2025-01-19 13:13:07 +00:00
library = next((l for l in libraries if re.match(vendor.pattern, l["name"])), None)
2025-01-11 15:54:27 +00:00
2024-07-06 18:01:47 +00:00
if library:
2025-01-19 13:13:07 +00:00
# Log information about the library if it is found.
self.logger.info("Library: %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)
# Determine if the Frida server version is older than 16.6.0.
code = tuple(map(int, version.split(".")))
minimum = code[0] < 16 or (code == 16 and code[1] < 6)
2024-07-06 18:01:47 +00:00
2025-01-19 13:13:07 +00:00
# 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")
2024-07-06 18:01:47 +00:00
2025-01-19 13:13:07 +00:00
return script.exports_sync.hooklibrary(library["name"])
2024-07-06 18:01:47 +00:00
2025-01-19 13:13:07 +00:00
# Unload the script if the target library is not found.
2024-07-06 18:01:47 +00:00
script.unload()
2025-01-19 13:13:07 +00:00
self.logger.warning("Library not found: %s" % vendor.pattern)
2024-07-06 18:01:47 +00:00
return False
2025-01-19 13:13:07 +00:00
__all__ = ("Core",)