2024-07-06 20:01:47 +02:00
import json
import logging
import re
2024-07-18 00:09:04 +02:00
2024-07-06 20:01:47 +02:00
from pathlib import Path
import frida
import xmltodict
2024-07-18 00:09:04 +02:00
2024-10-26 15:21:54 +02:00
from frida.core import Session , Script
2024-07-06 20:01:47 +02:00
2024-10-26 15:21:54 +02:00
from keydive.adb import ADB
2024-07-06 20:01:47 +02: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 15:21:54 +02:00
Core class for managing DRM operations and interactions with Android devices.
2024-07-06 20:01:47 +02:00
"""
2024-10-26 15:21:54 +02:00
def __init__ ( self , adb : ADB , cdm : Cdm , functions : Path = None , skip : bool = False ):
2024-07-06 20:01:47 +02:00
"""
Initializes a Core instance.
2025-01-19 14:13:07 +01:00
Parameters:
2024-10-26 15:21:54 +02:00
adb (ADB): ADB instance for device communication.
2025-01-19 14:13:07 +01: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 20:01:47 +02:00
"""
self . logger = logging . getLogger ( self . __class__ . __name__ )
self . running = True
self . cdm = cdm
2024-10-26 15:21:54 +02:00
self . adb = adb
2024-10-18 20:31:10 +02:00
2024-10-27 19:39:09 +01:00
# Flag to skip predefined functions based on the vendor's API level
2025-01-19 14:13:07 +01:00
# https://github.com/hyugogirubato/KeyDive/issues/38#issuecomment-2411932679
2024-10-18 20:26:41 +02:00
self . skip = skip
2024-07-06 20:01:47 +02:00
2025-01-19 14:13:07 +01:00
# Load the hook script with relevant data and prepare for injection
2024-07-07 10:22:21 +02:00
self . functions = functions
2024-07-06 20:01:47 +02:00
self . script = self . __prepare_hook_script ()
2025-01-19 14:13:07 +01:00
self . logger . info ( "Hook script prepared successfully" )
2024-07-06 20:01:47 +02:00
def __prepare_hook_script ( self ) -> str :
"""
2025-01-19 14:13:07 +01:00
Prepares the hook script by injecting library-specific data.
2024-07-06 20:01:47 +02:00
Returns:
2025-01-19 14:13:07 +01:00
str: The finalized hook script content with placeholders replaced.
2024-07-06 20:01:47 +02:00
"""
2025-01-19 14:13:07 +01: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 10:22:21 +02:00
symbols = self . __prepare_symbols ( self . functions )
2024-07-06 20:01:47 +02:00
2025-01-19 14:13:07 +01:00
# Define the placeholder replacements
2024-07-06 20:01:47 +02:00
replacements = {
2025-01-19 14:13:07 +01: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 20:01:47 +02:00
}
2025-01-19 14:13:07 +01:00
# Replace placeholders in the script content
2024-07-06 20:01:47 +02:00
for placeholder , value in replacements . items ():
2025-01-19 14:13:07 +01:00
content = content . replace ( placeholder , value , 1 )
2024-07-06 20:01:47 +02:00
return content
2024-10-18 20:26:41 +02:00
def __prepare_symbols ( self , path : Path ) -> list :
2024-07-06 20:01:47 +02:00
"""
2025-01-19 14:13:07 +01:00
Extracts relevant functions from a Ghidra XML file.
2024-07-06 20:01:47 +02:00
2025-01-19 14:13:07 +01:00
Parameters:
path (Path): Path to the Ghidra XML functions file.
2024-07-06 20:01:47 +02: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 14:13:07 +01:00
# Return an empty list if no path is provided
2024-07-06 20:01:47 +02:00
if not path :
return []
try :
2025-01-19 14:13:07 +01: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 20:01:47 +02:00
2025-01-19 14:13:07 +01: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 20:01:47 +02:00
2025-01-19 14:13:07 +01:00
# Prepare a dictionary to store selected functions
2024-07-06 20:01:47 +02:00
selected = {}
for func in functions :
2025-01-19 14:13:07 +01: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 20:01:47 +02:00
if name not in selected and (
name == target
2024-10-28 21:29:28 +01:00
or any ( True if self . skip else keyword in name for keyword in CDM_FUNCTION_API )
2025-01-19 14:13:07 +01:00
or ( not target and re . match ( r "^[a-z]+$" , name ) and args >= 6 )
2024-07-06 20:01:47 +02:00
):
selected [ name ] = {
2025-01-19 14:13:07 +01:00
"type" : "function" ,
"name" : name ,
"address" : hex ( int ( func [ "@ENTRY_POINT" ], 16 ) - addr_base ) # Calculate relative address
2024-07-06 20:01:47 +02:00
}
2025-01-19 14:13:07 +01:00
# Return the list of selected functions
2024-07-06 20:01:47 +02:00
return list ( selected . values ())
2025-01-19 14:13:07 +01:00
except FileNotFoundError as e :
raise FileNotFoundError ( f "Functions file not found: { path } " ) from e
2024-07-06 20:01:47 +02:00
except Exception as e :
2025-01-19 14:13:07 +01:00
raise ValueError ( "Failed to extract functions from Ghidra XML file" ) from e
2024-07-06 20:01:47 +02:00
def __process_message ( self , message : dict , data : bytes ) -> None :
"""
Handles messages received from the Frida script.
2025-01-19 14:13:07 +01:00
Parameters:
2024-07-06 20:01:47 +02:00
message (dict): The message payload.
data (bytes): The raw data associated with the message.
"""
2025-01-19 14:13:07 +01:00
logger = logging . getLogger ( "Script" )
level = message . get ( "payload" )
2024-07-06 20:01:47 +02:00
if isinstance ( level , int ):
2025-01-19 14:13:07 +01:00
# Log the message based on its severity level
logger . log ( level = level , msg = data . decode ( "utf-8" ))
2024-07-06 20:01:47 +02:00
if level in ( logging . FATAL , logging . CRITICAL ):
2025-01-19 14:13:07 +01: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 20:01:47 +02:00
self . cdm . set_challenge ( data = data )
2025-01-19 14:13:07 +01:00
elif level == "device_id" :
# Set the device ID in the DRM handler
2024-10-27 19:39:09 +01:00
self . cdm . set_device_id ( data )
2025-01-19 14:13:07 +01:00
elif level == "keybox" :
# Set the keybox data in the DRM handler
2024-10-27 19:39:09 +01:00
self . cdm . set_keybox ( data )
2024-07-06 20:01:47 +02:00
def hook_process ( self , pid : int , vendor : Vendor , timeout : int = 0 ) -> bool :
"""
Hooks into the specified process.
2025-01-19 14:13:07 +01:00
Parameters:
2024-07-06 20:01:47 +02: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 14:13:07 +01: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 15:21:54 +02:00
session : Session = self . adb . device . attach ( pid , persist_timeout = timeout )
2024-07-06 21:41:33 +02:00
except frida . ServerNotRunningError as e :
2025-01-19 14:13:07 +01: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 20:01:47 +02:00
except Exception as e :
2025-01-19 14:13:07 +01:00
# Log other exceptions and return False to indicate failure.
2024-07-06 20:01:47 +02:00
self . logger . error ( e )
return False
2025-01-19 14:13:07 +01:00
# Define a callback to handle when the process is destroyed.
2024-07-06 20:01:47 +02:00
def __process_destroyed () -> None :
session . detach ()
2025-01-19 14:13:07 +01:00
# Create a Frida script object using the prepared script content.
2024-07-06 20:01:47 +02:00
script : Script = session . create_script ( self . script )
2025-01-19 14:13:07 +01:00
script . on ( "message" , self . __process_message )
script . on ( "destroyed" , __process_destroyed )
2024-07-06 20:01:47 +02:00
script . load ()
2025-01-19 14:13:07 +01:00
# Fetch a list of libraries loaded by the target process.
2025-01-11 16:54:27 +01:00
libraries = script . exports_sync . getlibraries ()
2025-01-19 14:13:07 +01:00
library = next (( l for l in libraries if re . match ( vendor . pattern , l [ "name" ])), None )
2025-01-11 16:54:27 +01:00
2024-07-06 20:01:47 +02:00
if library :
2025-01-19 14:13:07 +01: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 20:01:47 +02:00
2025-01-19 14:13:07 +01: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 20:01:47 +02:00
2025-01-19 14:13:07 +01:00
return script . exports_sync . hooklibrary ( library [ "name" ])
2024-07-06 20:01:47 +02:00
2025-01-19 14:13:07 +01:00
# Unload the script if the target library is not found.
2024-07-06 20:01:47 +02:00
script . unload ()
2025-01-19 14:13:07 +01:00
self . logger . warning ( "Library not found: %s " % vendor . pattern )
2024-07-06 20:01:47 +02:00
return False
2025-01-19 14:13:07 +01:00
__all__ = ( "Core" ,)