diff --git a/examples/child_gating.py b/examples/child_gating.py index 0e90ac5..4c7ed13 100644 --- a/examples/child_gating.py +++ b/examples/child_gating.py @@ -44,11 +44,11 @@ def _instrument(self, pid): print("✔ create_script()") script = session.create_script( """\ -Interceptor.attach(Module.getExportByName(null, 'open'), { - onEnter: function (args) { +Interceptor.attach(Module.getGlobalExportByName('open'), { + onEnter(args) { send({ type: 'open', - path: Memory.readUtf8String(args[0]) + path: args[0].readUtf8String() }); } }); diff --git a/examples/spawn_gating.py b/examples/spawn_gating.py new file mode 100644 index 0000000..297dcec --- /dev/null +++ b/examples/spawn_gating.py @@ -0,0 +1,80 @@ +import threading + +from frida_tools.application import Reactor + +import frida + + +class Application: + def __init__(self): + self._stop_requested = threading.Event() + self._reactor = Reactor(run_until_return=lambda reactor: self._stop_requested.wait()) + + self._device = frida.get_usb_device() + self._sessions = set() + self._sessions_lock = threading.Lock() + + self._device.on("spawn-added", lambda spawn: self._reactor.schedule(lambda: self._on_spawn_added(spawn))) + self._device.on("spawn-removed", lambda spawn: self._reactor.schedule(lambda: self._on_spawn_removed(spawn))) + + def run(self): + self._reactor.schedule(lambda: self._start()) + self._reactor.run() + + def _start(self): + self._device.enable_spawn_gating() + + def _instrument(self, pid): + print(f"✔ attach(pid={pid})") + session = self._device.attach(pid) + session.on("detached", lambda reason: self._reactor.schedule(lambda: self._on_detached(pid, session, reason))) + print("✔ create_script()") + script = session.create_script( + """\ +const puts = new NativeFunction(Module.getGlobalExportByName('puts'), 'int', ['pointer']); +puts(Memory.allocUtf8String('Hello from Frida agent')); +Interceptor.attach(Module.getGlobalExportByName('open'), { + onEnter(args) { + send({ + type: 'open', + path: args[0].readUtf8String() + }); + } +}); +""" + ) + script.on("message", lambda message, data: self._reactor.schedule(lambda: self._on_message(pid, message))) + print("✔ load()") + script.load() + print(f"✔ resume(pid={pid})") + self._device.resume(pid) + with self._sessions_lock: + self._sessions.add(session) + + def _on_spawn_added(self, spawn): + print(f"⚡ spawn_added: {spawn}") + t = threading.Thread(target=self._handle_spawn, args=(spawn,)) + t.start() + + def _handle_spawn(self, spawn): + if "/bin/ls" in spawn.identifier: + self._instrument(spawn.pid) + else: + pid = spawn.pid + print(f"✔ resume(pid={pid})") + self._device.resume(pid) + + def _on_spawn_removed(self, spawn): + print(f"⚡ spawn_removed: {spawn}") + + def _on_detached(self, pid, session, reason): + print(f"⚡ detached: pid={pid}, reason='{reason}'") + with self._sessions_lock: + self._sessions.remove(session) + + def _on_message(self, pid, message): + print(f"⚡ message: pid={pid}, payload={message['payload']}") + + +app = Application() +app.run() diff --git a/frida/__init__.py b/frida/__init__.py index 483a51b..46cf817 100644 --- a/frida/__init__.py +++ b/frida/__init__.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, Dict, List, Optional, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union try: from . import _frida @@ -86,14 +86,30 @@ def kill(target: core.ProcessTarget) -> None: def attach( - target: core.ProcessTarget, realm: Optional[str] = None, persist_timeout: Optional[int] = None + target: core.ProcessTarget, + realm: Optional[str] = None, + persist_timeout: Optional[int] = None, + exceptor: Optional[str] = None, + unwind_broker: Optional[bool] = None, + exit_monitor: Optional[bool] = None, + thread_suspend_monitor: Optional[bool] = None, + linker_notifier_offsets: Optional[Sequence[int]] = None, ) -> core.Session: """ Attach to a process :param target: the PID or name of the process """ - return get_local_device().attach(target, realm=realm, persist_timeout=persist_timeout) + return get_local_device().attach( + target, + realm=realm, + persist_timeout=persist_timeout, + exceptor=exceptor, + unwind_broker=unwind_broker, + exit_monitor=exit_monitor, + thread_suspend_monitor=thread_suspend_monitor, + linker_notifier_offsets=linker_notifier_offsets, + ) def inject_library_file(target: core.ProcessTarget, path: str, entrypoint: str, data: str) -> int: diff --git a/frida/_frida/__init__.pyi b/frida/_frida/__init__.pyi index 86cefc5..618e2e1 100644 --- a/frida/_frida/__init__.pyi +++ b/frida/_frida/__init__.pyi @@ -256,7 +256,17 @@ class Device(Object): """ ... - def attach(self, pid: int, realm: Optional[str] = None, persist_timeout: Optional[int] = None) -> "Session": + def attach( + self, + pid: int, + realm: Optional[str] = None, + persist_timeout: Optional[int] = None, + exceptor: Optional[str] = None, + unwind_broker: Optional[bool] = None, + exit_monitor: Optional[bool] = None, + thread_suspend_monitor: Optional[bool] = None, + linker_notifier_offsets: Optional[Sequence[int]] = None, + ) -> "Session": """ Attach to a PID. """ @@ -354,6 +364,12 @@ class Device(Object): """ ... + def override_option(self, name: str, value: Any) -> None: + """ + Override a backend-specific option. + """ + ... + def query_system_parameters(self) -> Dict[str, Any]: """ Returns a dictionary of information about the host system. @@ -611,6 +627,12 @@ class Script(Object): """ ... + def interrupt(self) -> None: + """ + Interrupt any JavaScript currently executing in the script. + """ + ... + def post(self, message: str, data: Optional[Union[str, bytes]] = None) -> None: """ Post a JSON-encoded message to the script. @@ -623,6 +645,12 @@ class Script(Object): """ ... + def terminate(self) -> None: + """ + Interrupt execution and unload the script. + """ + ... + def enable_debugger(self, port: Optional[int]) -> None: """ Enable the Node.js compatible script debugger diff --git a/frida/_frida/extension.c b/frida/_frida/extension.c index f3ef308..58ca6b0 100644 --- a/frida/_frida/extension.c +++ b/frida/_frida/extension.c @@ -404,6 +404,7 @@ static void PyDevice_init_from_handle (PyDevice * self, FridaDevice * handle); static void PyDevice_dealloc (PyDevice * self); static PyObject * PyDevice_repr (PyDevice * self); static PyObject * PyDevice_is_lost (PyDevice * self); +static PyObject * PyDevice_override_option (PyDevice * self, PyObject * args, PyObject * kw); static PyObject * PyDevice_query_system_parameters (PyDevice * self); static PyObject * PyDevice_get_frontmost_application (PyDevice * self, PyObject * args, PyObject * kw); static PyObject * PyDevice_enumerate_applications (PyDevice * self, PyObject * args, PyObject * kw); @@ -419,7 +420,7 @@ static PyObject * PyDevice_input (PyDevice * self, PyObject * args); static PyObject * PyDevice_resume (PyDevice * self, PyObject * args); static PyObject * PyDevice_kill (PyDevice * self, PyObject * args); static PyObject * PyDevice_attach (PyDevice * self, PyObject * args, PyObject * kw); -static FridaSessionOptions * PyDevice_parse_session_options (const gchar * realm_value, guint persist_timeout); +static FridaSessionOptions * PyDevice_parse_session_options (const gchar * realm_value, guint persist_timeout, const gchar * exceptor_value, gint unwind_broker, gint exit_monitor, gint thread_suspend_monitor, PyObject * linker_notifier_offsets_value); static PyObject * PyDevice_inject_library_file (PyDevice * self, PyObject * args); static PyObject * PyDevice_inject_library_blob (PyDevice * self, PyObject * args); static PyObject * PyDevice_open_channel (PyDevice * self, PyObject * args); @@ -491,7 +492,9 @@ static FridaPortalOptions * PySession_parse_portal_options (const gchar * certif static PyObject * PyScript_new_take_handle (FridaScript * handle); static PyObject * PyScript_is_destroyed (PyScript * self); static PyObject * PyScript_load (PyScript * self); +static PyObject * PyScript_interrupt (PyScript * self); static PyObject * PyScript_unload (PyScript * self); +static PyObject * PyScript_terminate (PyScript * self); static PyObject * PyScript_eternalize (PyScript * self); static PyObject * PyScript_post (PyScript * self, PyObject * args, PyObject * kw); static PyObject * PyScript_enable_debugger (PyScript * self, PyObject * args, PyObject * kw); @@ -620,6 +623,7 @@ static PyMethodDef PyDeviceManager_methods[] = static PyMethodDef PyDevice_methods[] = { { "is_lost", (PyCFunction) PyDevice_is_lost, METH_NOARGS, "Query whether the device has been lost." }, + { "override_option", (PyCFunction) PyDevice_override_option, METH_VARARGS | METH_KEYWORDS, "Override a backend-specific option." }, { "query_system_parameters", (PyCFunction) PyDevice_query_system_parameters, METH_NOARGS, "Returns a dictionary of information about the host system." }, { "get_frontmost_application", (PyCFunction) PyDevice_get_frontmost_application, METH_VARARGS | METH_KEYWORDS, "Get details about the frontmost application." }, { "enumerate_applications", (PyCFunction) PyDevice_enumerate_applications, METH_VARARGS | METH_KEYWORDS, "Enumerate applications." }, @@ -738,7 +742,9 @@ static PyMethodDef PyScript_methods[] = { { "is_destroyed", (PyCFunction) PyScript_is_destroyed, METH_NOARGS, "Query whether the script has been destroyed." }, { "load", (PyCFunction) PyScript_load, METH_NOARGS, "Load the script." }, + { "interrupt", (PyCFunction) PyScript_interrupt, METH_NOARGS, "Interrupt any JavaScript currently executing in the script." }, { "unload", (PyCFunction) PyScript_unload, METH_NOARGS, "Unload the script." }, + { "terminate", (PyCFunction) PyScript_terminate, METH_NOARGS, "Interrupt execution and unload the script." }, { "eternalize", (PyCFunction) PyScript_eternalize, METH_NOARGS, "Eternalize the script." }, { "post", (PyCFunction) PyScript_post, METH_VARARGS | METH_KEYWORDS, "Post a JSON-encoded message to the script." }, { "enable_debugger", (PyCFunction) PyScript_enable_debugger, METH_VARARGS | METH_KEYWORDS, "Enable the Node.js compatible script debugger." }, @@ -2496,6 +2502,33 @@ PyDevice_is_lost (PyDevice * self) return PyBool_FromLong (is_lost); } +static PyObject * +PyDevice_override_option (PyDevice * self, PyObject * args, PyObject * kw) +{ + static char * keywords[] = { "name", "value", NULL }; + const char * name; + PyObject * value; + GVariant * raw_value; + GError * error = NULL; + + if (!PyArg_ParseTupleAndKeywords (args, kw, "sO", keywords, &name, &value)) + return NULL; + + if (!PyGObject_unmarshal_variant (value, &raw_value)) + return NULL; + + Py_BEGIN_ALLOW_THREADS + frida_device_override_option (PY_GOBJECT_HANDLE (self), name, raw_value, &error); + Py_END_ALLOW_THREADS + + g_variant_unref (raw_value); + + if (error != NULL) + return PyFrida_raise (error); + + PyFrida_RETURN_NONE; +} + static PyObject * PyDevice_query_system_parameters (PyDevice * self) { @@ -2749,10 +2782,16 @@ static PyObject * PyDevice_enable_spawn_gating (PyDevice * self) { GError * error = NULL; + FridaSpawnGatingOptions * options; + + options = frida_spawn_gating_options_new (); Py_BEGIN_ALLOW_THREADS - frida_device_enable_spawn_gating_sync (PY_GOBJECT_HANDLE (self), g_cancellable_get_current (), &error); + frida_device_enable_spawn_gating_sync (PY_GOBJECT_HANDLE (self), options, g_cancellable_get_current (), &error); Py_END_ALLOW_THREADS + + g_object_unref (options); + if (error != NULL) return PyFrida_raise (error); @@ -3027,21 +3066,43 @@ static PyObject * PyDevice_attach (PyDevice * self, PyObject * args, PyObject * kw) { PyObject * result = NULL; - static char * keywords[] = { "pid", "realm", "persist_timeout", NULL }; + static char * keywords[] = { + "pid", + "realm", + "persist_timeout", + "exceptor", + "unwind_broker", + "exit_monitor", + "thread_suspend_monitor", + "linker_notifier_offsets", + NULL + }; long pid; char * realm_value = NULL; unsigned int persist_timeout = 0; + char * exceptor_value = NULL; + int unwind_broker = -1; + int exit_monitor = -1; + int thread_suspend_monitor = -1; + PyObject * linker_notifier_offsets_value = NULL; FridaSessionOptions * options = NULL; GError * error = NULL; FridaSession * handle; - if (!PyArg_ParseTupleAndKeywords (args, kw, "l|esI", keywords, + if (!PyArg_ParseTupleAndKeywords (args, kw, "l|esIespppO", keywords, &pid, "utf-8", &realm_value, - &persist_timeout)) + &persist_timeout, + "utf-8", &exceptor_value, + &unwind_broker, + &exit_monitor, + &thread_suspend_monitor, + &linker_notifier_offsets_value)) return NULL; - options = PyDevice_parse_session_options (realm_value, persist_timeout); + options = PyDevice_parse_session_options (realm_value, persist_timeout, + exceptor_value, unwind_broker, exit_monitor, thread_suspend_monitor, + linker_notifier_offsets_value); if (options == NULL) goto beach; @@ -3056,6 +3117,7 @@ PyDevice_attach (PyDevice * self, PyObject * args, PyObject * kw) beach: g_clear_object (&options); + PyMem_Free (exceptor_value); PyMem_Free (realm_value); return result; @@ -3063,7 +3125,12 @@ PyDevice_attach (PyDevice * self, PyObject * args, PyObject * kw) static FridaSessionOptions * PyDevice_parse_session_options (const gchar * realm_value, - guint persist_timeout) + guint persist_timeout, + const gchar * exceptor_value, + gint unwind_broker, + gint exit_monitor, + gint thread_suspend_monitor, + PyObject * linker_notifier_offsets_value) { FridaSessionOptions * options; @@ -3081,6 +3148,50 @@ PyDevice_parse_session_options (const gchar * realm_value, frida_session_options_set_persist_timeout (options, persist_timeout); + if (exceptor_value != NULL) + { + FridaExceptor exceptor; + + if (!PyGObject_unmarshal_enum (exceptor_value, FRIDA_TYPE_EXCEPTOR, &exceptor)) + goto propagate_error; + + frida_session_options_set_exceptor (options, exceptor); + } + + if (unwind_broker != -1) + frida_session_options_set_unwind_broker (options, unwind_broker); + + if (exit_monitor != -1) + frida_session_options_set_exit_monitor (options, exit_monitor); + + if (thread_suspend_monitor != -1) + frida_session_options_set_thread_suspend_monitor (options, thread_suspend_monitor); + + if (linker_notifier_offsets_value != NULL) + { + gint n, i; + + n = PySequence_Size (linker_notifier_offsets_value); + if (n == -1) + goto propagate_error; + + for (i = 0; i != n; i++) + { + PyObject * element; + unsigned long offset; + + element = PySequence_GetItem (linker_notifier_offsets_value, i); + if (element == NULL) + goto propagate_error; + offset = PyLong_AsUnsignedLong (element); + Py_DecRef (element); + if (offset == (unsigned long) -1 && PyErr_Occurred () != NULL) + goto propagate_error; + + frida_session_options_add_linker_notifier_offset (options, offset); + } + } + return options; propagate_error: @@ -4286,6 +4397,20 @@ PyScript_load (PyScript * self) PyFrida_RETURN_NONE; } +static PyObject * +PyScript_interrupt (PyScript * self) +{ + GError * error = NULL; + + Py_BEGIN_ALLOW_THREADS + frida_script_interrupt_sync (PY_GOBJECT_HANDLE (self), g_cancellable_get_current (), &error); + Py_END_ALLOW_THREADS + if (error != NULL) + return PyFrida_raise (error); + + PyFrida_RETURN_NONE; +} + static PyObject * PyScript_unload (PyScript * self) { @@ -4300,6 +4425,20 @@ PyScript_unload (PyScript * self) PyFrida_RETURN_NONE; } +static PyObject * +PyScript_terminate (PyScript * self) +{ + GError * error = NULL; + + Py_BEGIN_ALLOW_THREADS + frida_script_terminate_sync (PY_GOBJECT_HANDLE (self), g_cancellable_get_current (), &error); + Py_END_ALLOW_THREADS + if (error != NULL) + return PyFrida_raise (error); + + PyFrida_RETURN_NONE; +} + static PyObject * PyScript_eternalize (PyScript * self) { diff --git a/frida/core.py b/frida/core.py index 1f1c99c..026c572 100644 --- a/frida/core.py +++ b/frida/core.py @@ -309,6 +309,15 @@ def load(self) -> None: self._impl.load() + @cancellable + def interrupt(self) -> None: + """ + Interrupt any JavaScript currently executing in the script, leaving it + loaded and able to run again + """ + + self._impl.interrupt() + @cancellable def unload(self) -> None: """ @@ -317,6 +326,15 @@ def unload(self) -> None: self._impl.unload() + @cancellable + def terminate(self) -> None: + """ + Interrupt execution and unload the script, even if it is stuck in a + long-running or infinite operation + """ + + self._impl.terminate() + @cancellable def eternalize(self) -> None: """ @@ -883,6 +901,14 @@ def is_lost(self) -> bool: return self._impl.is_lost() + @cancellable + def override_option(self, name: str, value: Any) -> None: + """ + Override a backend-specific option + """ + + self._impl.override_option(name, value) + @cancellable def query_system_parameters(self) -> Dict[str, Any]: """ @@ -1038,13 +1064,26 @@ def attach( target: ProcessTarget, realm: Optional[str] = None, persist_timeout: Optional[int] = None, + exceptor: Optional[str] = None, + unwind_broker: Optional[bool] = None, + exit_monitor: Optional[bool] = None, + thread_suspend_monitor: Optional[bool] = None, + linker_notifier_offsets: Optional[Sequence[int]] = None, ) -> Session: """ Attach to a process :param target: the PID or name of the process """ - kwargs = {"realm": realm, "persist_timeout": persist_timeout} + kwargs = { + "realm": realm, + "persist_timeout": persist_timeout, + "exceptor": exceptor, + "unwind_broker": unwind_broker, + "exit_monitor": exit_monitor, + "thread_suspend_monitor": thread_suspend_monitor, + "linker_notifier_offsets": linker_notifier_offsets, + } _filter_missing_kwargs(kwargs) return Session(self._impl.attach(self._pid_of(target), **kwargs)) # type: ignore