mirror of
https://github.com/frida/frida-python
synced 2026-06-08 14:16:17 +00:00
713 lines
19 KiB
Python
713 lines
19 KiB
Python
# -*- coding: utf-8 -*-
|
|
from __future__ import unicode_literals, print_function
|
|
|
|
import fnmatch
|
|
from functools import wraps
|
|
import json
|
|
import numbers
|
|
import sys
|
|
import threading
|
|
import traceback
|
|
|
|
import _frida
|
|
|
|
|
|
_Cancellable = _frida.Cancellable
|
|
|
|
|
|
def cancellable(f):
|
|
@wraps(f)
|
|
def wrapper(*args, **kwargs):
|
|
cancellable = kwargs.pop('cancellable', None)
|
|
if cancellable is not None:
|
|
with cancellable:
|
|
return f(*args, **kwargs)
|
|
|
|
return f(*args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
|
|
class DeviceManager(object):
|
|
def __init__(self, impl):
|
|
self._impl = impl
|
|
|
|
def __repr__(self):
|
|
return repr(self._impl)
|
|
|
|
def get_local_device(self, **kwargs):
|
|
return self.get_device_matching(lambda d: d.type == 'local', timeout=0, **kwargs)
|
|
|
|
def get_remote_device(self, **kwargs):
|
|
return self.get_device_matching(lambda d: d.type == 'remote', timeout=0, **kwargs)
|
|
|
|
def get_usb_device(self, timeout=0, **kwargs):
|
|
return self.get_device_matching(lambda d: d.type == 'usb', timeout, **kwargs)
|
|
|
|
def get_device(self, id, timeout=0, **kwargs):
|
|
return self.get_device_matching(lambda d: d.id == id, timeout, **kwargs)
|
|
|
|
@cancellable
|
|
def get_device_matching(self, predicate, timeout=0):
|
|
if timeout < 0:
|
|
raw_timeout = -1
|
|
elif timeout == 0:
|
|
raw_timeout = 0
|
|
else:
|
|
raw_timeout = int(timeout * 1000.0)
|
|
return Device(self._impl.get_device_matching(lambda d: predicate(Device(d)), raw_timeout))
|
|
|
|
@cancellable
|
|
def enumerate_devices(self):
|
|
return [Device(device) for device in self._impl.enumerate_devices()]
|
|
|
|
@cancellable
|
|
def add_remote_device(self, *args, **kwargs):
|
|
return Device(self._impl.add_remote_device(*args, **kwargs))
|
|
|
|
@cancellable
|
|
def remove_remote_device(self, *args, **kwargs):
|
|
self._impl.remove_remote_device(*args, **kwargs)
|
|
|
|
def on(self, signal, callback):
|
|
self._impl.on(signal, callback)
|
|
|
|
def off(self, signal, callback):
|
|
self._impl.off(signal, callback)
|
|
|
|
|
|
class Device(object):
|
|
def __init__(self, device):
|
|
self.id = device.id
|
|
self.name = device.name
|
|
self.icon = device.icon
|
|
self.type = device.type
|
|
self.bus = Bus(device.bus)
|
|
|
|
self._impl = device
|
|
|
|
def __repr__(self):
|
|
return repr(self._impl)
|
|
|
|
@property
|
|
def is_lost(self):
|
|
return self._impl.is_lost()
|
|
|
|
@cancellable
|
|
def query_system_parameters(self):
|
|
return self._impl.query_system_parameters()
|
|
|
|
@cancellable
|
|
def get_frontmost_application(self, *args, **kwargs):
|
|
return self._impl.get_frontmost_application(*args, **kwargs)
|
|
|
|
@cancellable
|
|
def enumerate_applications(self, *args, **kwargs):
|
|
return self._impl.enumerate_applications(*args, **kwargs)
|
|
|
|
@cancellable
|
|
def enumerate_processes(self, *args, **kwargs):
|
|
return self._impl.enumerate_processes(*args, **kwargs)
|
|
|
|
@cancellable
|
|
def get_process(self, process_name):
|
|
process_name_lc = process_name.lower()
|
|
matching = [process for process in self._impl.enumerate_processes() if fnmatch.fnmatchcase(process.name.lower(), process_name_lc)]
|
|
if len(matching) == 1:
|
|
return matching[0]
|
|
elif len(matching) > 1:
|
|
raise _frida.ProcessNotFoundError("ambiguous name; it matches: %s" % ", ".join(["%s (pid: %d)" % (process.name, process.pid) for process in matching]))
|
|
else:
|
|
raise _frida.ProcessNotFoundError("unable to find process with name '%s'" % process_name)
|
|
|
|
@cancellable
|
|
def enable_spawn_gating(self):
|
|
return self._impl.enable_spawn_gating()
|
|
|
|
@cancellable
|
|
def disable_spawn_gating(self):
|
|
return self._impl.disable_spawn_gating()
|
|
|
|
@cancellable
|
|
def enumerate_pending_spawn(self):
|
|
return self._impl.enumerate_pending_spawn()
|
|
|
|
@cancellable
|
|
def enumerate_pending_children(self):
|
|
return self._impl.enumerate_pending_children()
|
|
|
|
@cancellable
|
|
def spawn(self, program, argv=None, envp=None, env=None, cwd=None, stdio=None, **kwargs):
|
|
if not isinstance(program, string_types):
|
|
argv = program
|
|
program = argv[0]
|
|
if len(argv) == 1:
|
|
argv = None
|
|
|
|
aux_options = kwargs
|
|
|
|
return self._impl.spawn(program, argv, envp, env, cwd, stdio, aux_options)
|
|
|
|
@cancellable
|
|
def input(self, target, data):
|
|
self._impl.input(self._pid_of(target), data)
|
|
|
|
@cancellable
|
|
def resume(self, target):
|
|
self._impl.resume(self._pid_of(target))
|
|
|
|
@cancellable
|
|
def kill(self, target):
|
|
self._impl.kill(self._pid_of(target))
|
|
|
|
@cancellable
|
|
def attach(self, target, *args, **kwargs):
|
|
return Session(self._impl.attach(self._pid_of(target), *args, **kwargs))
|
|
|
|
@cancellable
|
|
def inject_library_file(self, target, path, entrypoint, data):
|
|
return self._impl.inject_library_file(self._pid_of(target), path, entrypoint, data)
|
|
|
|
@cancellable
|
|
def inject_library_blob(self, target, blob, entrypoint, data):
|
|
return self._impl.inject_library_blob(self._pid_of(target), blob, entrypoint, data)
|
|
|
|
@cancellable
|
|
def open_channel(self, address):
|
|
return IOStream(self._impl.open_channel(address))
|
|
|
|
@cancellable
|
|
def get_bus(self):
|
|
return Bus(self._impl.get_bus())
|
|
|
|
def on(self, signal, callback):
|
|
self._impl.on(signal, callback)
|
|
|
|
def off(self, signal, callback):
|
|
self._impl.off(signal, callback)
|
|
|
|
def _pid_of(self, target):
|
|
if isinstance(target, numbers.Number):
|
|
return target
|
|
else:
|
|
return self.get_process(target).pid
|
|
|
|
|
|
class Bus(object):
|
|
def __init__(self, impl):
|
|
self._impl = impl
|
|
self._on_message_callbacks = []
|
|
|
|
impl.on('message', self._on_message)
|
|
|
|
@cancellable
|
|
def attach(self):
|
|
self._impl.attach()
|
|
|
|
def post(self, message, **kwargs):
|
|
raw_message = json.dumps(message)
|
|
self._impl.post(raw_message, **kwargs)
|
|
|
|
def on(self, signal, callback):
|
|
if signal == 'message':
|
|
self._on_message_callbacks.append(callback)
|
|
else:
|
|
self._impl.on(signal, callback)
|
|
|
|
def off(self, signal, callback):
|
|
if signal == 'message':
|
|
self._on_message_callbacks.remove(callback)
|
|
else:
|
|
self._impl.off(signal, callback)
|
|
|
|
def _on_message(self, raw_message, data):
|
|
message = json.loads(raw_message)
|
|
|
|
for callback in self._on_message_callbacks[:]:
|
|
try:
|
|
callback(message, data)
|
|
except:
|
|
traceback.print_exc()
|
|
|
|
|
|
class Session(object):
|
|
def __init__(self, impl):
|
|
self._impl = impl
|
|
|
|
def __repr__(self):
|
|
return repr(self._impl)
|
|
|
|
@property
|
|
def is_detached(self):
|
|
return self._impl.is_detached()
|
|
|
|
@cancellable
|
|
def detach(self):
|
|
self._impl.detach()
|
|
|
|
@cancellable
|
|
def resume(self):
|
|
self._impl.resume()
|
|
|
|
@cancellable
|
|
def enable_child_gating(self):
|
|
self._impl.enable_child_gating()
|
|
|
|
@cancellable
|
|
def disable_child_gating(self):
|
|
self._impl.disable_child_gating()
|
|
|
|
@cancellable
|
|
def create_script(self, *args, **kwargs):
|
|
return Script(self._impl.create_script(*args, **kwargs))
|
|
|
|
@cancellable
|
|
def create_script_from_bytes(self, *args, **kwargs):
|
|
return Script(self._impl.create_script_from_bytes(*args, **kwargs))
|
|
|
|
@cancellable
|
|
def compile_script(self, *args, **kwargs):
|
|
return self._impl.compile_script(*args, **kwargs)
|
|
|
|
@cancellable
|
|
def enable_debugger(self, *args, **kwargs):
|
|
self._impl.enable_debugger(*args, **kwargs)
|
|
|
|
@cancellable
|
|
def disable_debugger(self):
|
|
self._impl.disable_debugger()
|
|
|
|
@cancellable
|
|
def setup_peer_connection(self, *args, **kwargs):
|
|
self._impl.setup_peer_connection(*args, **kwargs)
|
|
|
|
@cancellable
|
|
def join_portal(self, *args, **kwargs):
|
|
return PortalMembership(self._impl.join_portal(*args, **kwargs))
|
|
|
|
def on(self, signal, callback):
|
|
self._impl.on(signal, callback)
|
|
|
|
def off(self, signal, callback):
|
|
self._impl.off(signal, callback)
|
|
|
|
|
|
class Script(object):
|
|
def __init__(self, impl):
|
|
self.exports = ScriptExports(self)
|
|
|
|
self._impl = impl
|
|
|
|
self._on_message_callbacks = []
|
|
self._log_handler = self.default_log_handler
|
|
|
|
self._pending = {}
|
|
self._next_request_id = 1
|
|
self._cond = threading.Condition()
|
|
|
|
impl.on('destroyed', self._on_destroyed)
|
|
impl.on('message', self._on_message)
|
|
|
|
def __repr__(self):
|
|
return repr(self._impl)
|
|
|
|
@property
|
|
def is_destroyed(self):
|
|
return self._impl.is_destroyed()
|
|
|
|
@cancellable
|
|
def load(self):
|
|
self._impl.load()
|
|
|
|
@cancellable
|
|
def unload(self):
|
|
self._impl.unload()
|
|
|
|
@cancellable
|
|
def eternalize(self):
|
|
self._impl.eternalize()
|
|
|
|
def post(self, message, **kwargs):
|
|
raw_message = json.dumps(message)
|
|
self._impl.post(raw_message, **kwargs)
|
|
|
|
def on(self, signal, callback):
|
|
if signal == 'message':
|
|
self._on_message_callbacks.append(callback)
|
|
else:
|
|
self._impl.on(signal, callback)
|
|
|
|
def off(self, signal, callback):
|
|
if signal == 'message':
|
|
self._on_message_callbacks.remove(callback)
|
|
else:
|
|
self._impl.off(signal, callback)
|
|
|
|
def get_log_handler(self):
|
|
return self._log_handler
|
|
|
|
def set_log_handler(self, handler):
|
|
self._log_handler = handler
|
|
|
|
def default_log_handler(self, level, text):
|
|
if level == 'info':
|
|
print(text, file=sys.stdout)
|
|
else:
|
|
print(text, file=sys.stderr)
|
|
|
|
def list_exports(self):
|
|
return self._rpc_request('list')
|
|
|
|
@cancellable
|
|
def _rpc_request(self, *args):
|
|
result = [False, None, None]
|
|
|
|
def on_complete(value, error):
|
|
with self._cond:
|
|
result[0] = True
|
|
result[1] = value
|
|
result[2] = error
|
|
self._cond.notify_all()
|
|
|
|
def on_cancelled():
|
|
self._pending.pop(request_id, None)
|
|
on_complete(None, None)
|
|
|
|
with self._cond:
|
|
request_id = self._next_request_id
|
|
self._next_request_id += 1
|
|
self._pending[request_id] = on_complete
|
|
|
|
if not self.is_destroyed:
|
|
message = ['frida:rpc', request_id]
|
|
message.extend(args)
|
|
self.post(message)
|
|
|
|
cancellable = Cancellable.get_current()
|
|
cancel_handler = cancellable.connect(on_cancelled)
|
|
try:
|
|
with self._cond:
|
|
while not result[0]:
|
|
self._cond.wait()
|
|
finally:
|
|
cancellable.disconnect(cancel_handler)
|
|
|
|
cancellable.raise_if_cancelled()
|
|
else:
|
|
self._on_destroyed()
|
|
|
|
if result[2] is not None:
|
|
raise result[2]
|
|
|
|
return result[1]
|
|
|
|
def _on_rpc_message(self, request_id, operation, params, data):
|
|
if operation in ('ok', 'error'):
|
|
callback = self._pending.pop(request_id, None)
|
|
if callback is None:
|
|
return
|
|
|
|
value = None
|
|
error = None
|
|
if operation == 'ok':
|
|
value = params[0] if data is None else data
|
|
else:
|
|
error = RPCException(*params[0:3])
|
|
|
|
callback(value, error)
|
|
|
|
def _on_destroyed(self):
|
|
while True:
|
|
next_pending = None
|
|
|
|
with self._cond:
|
|
pending_ids = list(self._pending.keys())
|
|
if len(pending_ids) > 0:
|
|
next_pending = self._pending.pop(pending_ids[0])
|
|
|
|
if next_pending is None:
|
|
break
|
|
|
|
next_pending(None, _frida.InvalidOperationError('script has been destroyed'))
|
|
|
|
def _on_message(self, raw_message, data):
|
|
message = json.loads(raw_message)
|
|
|
|
mtype = message['type']
|
|
payload = message.get('payload', None)
|
|
if mtype == 'log':
|
|
level = message['level']
|
|
text = payload
|
|
self._log_handler(level, text)
|
|
elif mtype == 'send' and isinstance(payload, list) and len(payload) > 0 and payload[0] == 'frida:rpc':
|
|
request_id = payload[1]
|
|
operation = payload[2]
|
|
params = payload[3:]
|
|
self._on_rpc_message(request_id, operation, params, data)
|
|
else:
|
|
for callback in self._on_message_callbacks[:]:
|
|
try:
|
|
callback(message, data)
|
|
except:
|
|
traceback.print_exc()
|
|
|
|
|
|
class RPCException(Exception):
|
|
def __str__(self):
|
|
return self.args[2] if len(self.args) >= 3 else self.args[0]
|
|
|
|
|
|
class ScriptExports(object):
|
|
def __init__(self, script):
|
|
self._script = script
|
|
|
|
def __getattr__(self, name):
|
|
script = self._script
|
|
js_name = _to_camel_case(name)
|
|
def method(*args, **kwargs):
|
|
return script._rpc_request('call', js_name, args, **kwargs)
|
|
return method
|
|
|
|
def __dir__(self):
|
|
return self._script.list_exports()
|
|
|
|
|
|
class PortalMembership(object):
|
|
def __init__(self, impl):
|
|
self._impl = impl
|
|
|
|
@cancellable
|
|
def terminate(self):
|
|
self._impl.terminate()
|
|
|
|
|
|
class EndpointParameters(object):
|
|
def __init__(self, address=None, port=None, certificate=None, origin=None, authentication=None, asset_root=None):
|
|
kw = {}
|
|
|
|
if address is not None:
|
|
kw['address'] = address
|
|
|
|
if port is not None:
|
|
kw['port'] = port
|
|
|
|
if certificate is not None:
|
|
kw['certificate'] = certificate
|
|
|
|
if origin is not None:
|
|
kw['origin'] = origin
|
|
|
|
if authentication is not None:
|
|
(auth_scheme, auth_data) = authentication
|
|
if auth_scheme == 'token':
|
|
kw['auth_token'] = auth_data
|
|
elif auth_scheme == 'callback':
|
|
kw['auth_callback'] = make_auth_callback(auth_data)
|
|
else:
|
|
raise ValueError("invalid authentication scheme")
|
|
|
|
if asset_root is not None:
|
|
kw['asset_root'] = str(asset_root)
|
|
|
|
self._impl = _frida.EndpointParameters(**kw)
|
|
|
|
|
|
class PortalService(object):
|
|
def __init__(self, cluster_params=EndpointParameters(), control_params=None):
|
|
args = [cluster_params._impl]
|
|
if control_params is not None:
|
|
args.append(control_params._impl)
|
|
impl = _frida.PortalService(*args)
|
|
|
|
self.device = impl.device
|
|
self._impl = impl
|
|
self._on_authenticated_callbacks = []
|
|
self._on_message_callbacks = []
|
|
|
|
impl.on('authenticated', self._on_authenticated)
|
|
impl.on('message', self._on_message)
|
|
|
|
@cancellable
|
|
def start(self):
|
|
self._impl.start()
|
|
|
|
@cancellable
|
|
def stop(self):
|
|
self._impl.stop()
|
|
|
|
def post(self, connection_id, message, **kwargs):
|
|
raw_message = json.dumps(message)
|
|
self._impl.post(connection_id, raw_message, **kwargs)
|
|
|
|
def narrowcast(self, tag, message, **kwargs):
|
|
raw_message = json.dumps(message)
|
|
self._impl.narrowcast(tag, raw_message, **kwargs)
|
|
|
|
def broadcast(self, message, **kwargs):
|
|
raw_message = json.dumps(message)
|
|
self._impl.broadcast(raw_message, **kwargs)
|
|
|
|
def enumerate_tags(self, connection_id):
|
|
return self._impl.enumerate_tags(connection_id)
|
|
|
|
def tag(self, connection_id, tag):
|
|
self._impl.tag(connection_id, tag)
|
|
|
|
def untag(self, connection_id, tag):
|
|
self._impl.untag(connection_id, tag)
|
|
|
|
def on(self, signal, callback):
|
|
if signal == 'authenticated':
|
|
self._on_authenticated_callbacks.append(callback)
|
|
elif signal == 'message':
|
|
self._on_message_callbacks.append(callback)
|
|
else:
|
|
self._impl.on(signal, callback)
|
|
|
|
def off(self, signal, callback):
|
|
if signal == 'authenticated':
|
|
self._on_authenticated_callbacks.remove(callback)
|
|
elif signal == 'message':
|
|
self._on_message_callbacks.remove(callback)
|
|
else:
|
|
self._impl.off(signal, callback)
|
|
|
|
def _on_authenticated(self, connection_id, raw_session_info):
|
|
session_info = json.loads(raw_session_info)
|
|
|
|
for callback in self._on_authenticated_callbacks[:]:
|
|
try:
|
|
callback(connection_id, session_info)
|
|
except:
|
|
traceback.print_exc()
|
|
|
|
def _on_message(self, connection_id, raw_message, data):
|
|
message = json.loads(raw_message)
|
|
|
|
for callback in self._on_message_callbacks[:]:
|
|
try:
|
|
callback(connection_id, message, data)
|
|
except:
|
|
traceback.print_exc()
|
|
|
|
|
|
class IOStream(object):
|
|
def __init__(self, impl):
|
|
self._impl = impl
|
|
|
|
def __repr__(self):
|
|
return repr(self._impl)
|
|
|
|
@property
|
|
def is_closed(self):
|
|
return self._impl.is_closed()
|
|
|
|
@cancellable
|
|
def close(self):
|
|
self._impl.close()
|
|
|
|
@cancellable
|
|
def read(self, count):
|
|
return self._impl.read(count)
|
|
|
|
@cancellable
|
|
def read_all(self, count):
|
|
return self._impl.read_all(count)
|
|
|
|
@cancellable
|
|
def write(self, data):
|
|
return self._impl.write(data)
|
|
|
|
@cancellable
|
|
def write_all(self, data):
|
|
self._impl.write_all(data)
|
|
|
|
|
|
class Cancellable(object):
|
|
def __init__(self):
|
|
self._impl = _Cancellable()
|
|
|
|
def __repr__(self):
|
|
return repr(self._impl)
|
|
|
|
@property
|
|
def is_cancelled(self):
|
|
return self._impl.is_cancelled()
|
|
|
|
def raise_if_cancelled(self):
|
|
self._impl.raise_if_cancelled()
|
|
|
|
def get_pollfd(self):
|
|
return CancellablePollFD(self._impl)
|
|
|
|
@classmethod
|
|
def get_current(cls):
|
|
return _Cancellable.get_current()
|
|
|
|
def __enter__(self):
|
|
self._impl.push_current()
|
|
|
|
def __exit__(self, *args):
|
|
self._impl.pop_current()
|
|
|
|
def connect(self, callback):
|
|
return self._impl.connect(callback)
|
|
|
|
def disconnect(self, handler_id):
|
|
self._impl.disconnect(handler_id)
|
|
|
|
def cancel(self):
|
|
self._impl.cancel()
|
|
|
|
|
|
class CancellablePollFD(object):
|
|
def __init__(self, cancellable):
|
|
self.handle = cancellable.get_fd()
|
|
self._cancellable = cancellable
|
|
|
|
def __del__(self):
|
|
self.release()
|
|
|
|
def release(self):
|
|
if self._cancellable is not None:
|
|
if self.handle != -1:
|
|
self._cancellable.release_fd()
|
|
self.handle = -1
|
|
self._cancellable = None
|
|
|
|
def __repr__(self):
|
|
return repr(self.handle)
|
|
|
|
def __enter__(self):
|
|
return self.handle
|
|
|
|
def __exit__(self, *args):
|
|
self.release()
|
|
|
|
|
|
def make_auth_callback(callback):
|
|
def authenticate(token):
|
|
session_info = callback(token)
|
|
return json.dumps(session_info)
|
|
return authenticate
|
|
|
|
|
|
def _to_camel_case(name):
|
|
result = ""
|
|
uppercase_next = False
|
|
for c in name:
|
|
if c == '_':
|
|
uppercase_next = True
|
|
elif uppercase_next:
|
|
result += c.upper()
|
|
uppercase_next = False
|
|
else:
|
|
result += c.lower()
|
|
return result
|
|
|
|
|
|
if sys.version_info[0] >= 3:
|
|
string_types = str,
|
|
else:
|
|
string_types = basestring,
|