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 " , )