mirror of
https://github.com/frida/frida-python.git
synced 2026-11-08 01:58:13 -10:00
935 lines
33 KiB
Python
935 lines
33 KiB
Python
import os
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
def free_port():
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.bind(("127.0.0.1", 0))
|
|
return s.getsockname()[1]
|
|
|
|
|
|
REPO = Path(__file__).resolve().parent.parent
|
|
BINDGEN_CORE = REPO / "frida-bindgen"
|
|
GIRDIR = REPO / "build" / "subprojects" / "frida-core" / "src" / "api"
|
|
OUTDIR = REPO / "build" / "bindgen-out"
|
|
BUILT_EXTENSION = REPO / "build" / "frida" / "_frida.abi3.so"
|
|
|
|
|
|
@unittest.skipUnless(
|
|
(GIRDIR / "Frida-1.0.gir").exists(),
|
|
"requires a configured build tree with generated .gir files",
|
|
)
|
|
class TestBindgen(unittest.TestCase):
|
|
def test_generates_all_outputs(self):
|
|
OUTDIR.mkdir(exist_ok=True)
|
|
subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"-m",
|
|
"frida_bindgen",
|
|
f"--frida-gir={GIRDIR / 'Frida-1.0.gir'}",
|
|
f"--glib-gir={GIRDIR / 'GLib-2.0.gir'}",
|
|
f"--gobject-gir={GIRDIR / 'GObject-2.0.gir'}",
|
|
f"--gio-gir={GIRDIR / 'Gio-2.0.gir'}",
|
|
f"--output-py={OUTDIR / '__init__.py'}",
|
|
f"--output-aio={OUTDIR / 'aio.py'}",
|
|
f"--output-pyi={OUTDIR / '_frida.pyi'}",
|
|
f"--output-c={OUTDIR / 'extension.c'}",
|
|
],
|
|
check=True,
|
|
env={"PYTHONPATH": os.pathsep.join([str(REPO / "frida"), str(BINDGEN_CORE)])},
|
|
)
|
|
for name in ("__init__.py", "aio.py", "_frida.pyi", "extension.c"):
|
|
self.assertGreater((OUTDIR / name).stat().st_size, 0)
|
|
|
|
def test_generated_python_compiles(self):
|
|
for name in ("__init__.py", "aio.py"):
|
|
path = OUTDIR / name
|
|
if not path.exists():
|
|
self.skipTest("run test_generates_all_outputs first")
|
|
compile(path.read_text(), str(path), "exec")
|
|
|
|
|
|
@unittest.skipUnless(
|
|
BUILT_EXTENSION.exists(),
|
|
"requires a built _frida extension (run `make`)",
|
|
)
|
|
class TestExtension(unittest.TestCase):
|
|
def setUp(self):
|
|
sys.path.insert(0, str(BUILT_EXTENSION.parent))
|
|
import _frida
|
|
|
|
self._frida = _frida
|
|
|
|
def tearDown(self):
|
|
sys.path.remove(str(BUILT_EXTENSION.parent))
|
|
|
|
def test_exposes_version(self):
|
|
self.assertRegex(self._frida.__version__, r"\d+\.\d+")
|
|
|
|
def test_constructs_object_with_string_and_enum_parameters(self):
|
|
relay = self._frida.Relay("1.2.3.4:5", "user", "pass", "turn-udp")
|
|
self.assertIsInstance(relay, self._frida.Relay)
|
|
|
|
def test_constructs_object_without_parameters(self):
|
|
self.assertIsInstance(self._frida.DeviceManager(), self._frida.DeviceManager)
|
|
|
|
def test_rejects_invalid_enum_value(self):
|
|
with self.assertRaises(ValueError):
|
|
self._frida.Relay("1.2.3.4:5", "user", "pass", "bogus-kind")
|
|
|
|
def test_reads_back_string_and_enum_properties(self):
|
|
relay = self._frida.Relay("1.2.3.4:5", "bob", "secret", "turn-tcp")
|
|
self.assertEqual(relay.address, "1.2.3.4:5")
|
|
self.assertEqual(relay.username, "bob")
|
|
self.assertEqual(relay.password, "secret")
|
|
self.assertEqual(relay.kind, "turn-tcp")
|
|
|
|
def test_invokes_synchronous_methods(self):
|
|
cancellable = self._frida.Cancellable()
|
|
self.assertFalse(cancellable.is_cancelled())
|
|
cancellable.cancel()
|
|
self.assertTrue(cancellable.is_cancelled())
|
|
|
|
def test_registers_frida_exceptions(self):
|
|
for name in (
|
|
"ServerNotRunningError",
|
|
"ProcessNotFoundError",
|
|
"InvalidArgumentError",
|
|
"TransportError",
|
|
"OperationCancelledError",
|
|
):
|
|
exception = getattr(self._frida, name)
|
|
self.assertTrue(issubclass(exception, Exception))
|
|
self.assertEqual(exception.__module__, "frida")
|
|
|
|
def test_throwing_method_raises_mapped_exception(self):
|
|
cancellable = self._frida.Cancellable()
|
|
cancellable.cancel()
|
|
with self.assertRaises(self._frida.OperationCancelledError):
|
|
cancellable.set_error_if_cancelled()
|
|
|
|
def test_instantiates_handle_only_types(self):
|
|
# Types with no .gir constructor are created by the marshalling layer
|
|
# and must accept a bare, argument-free construction.
|
|
self.assertIsInstance(self._frida.Device(), self._frida.Device)
|
|
self.assertIsInstance(self._frida.Session(), self._frida.Session)
|
|
|
|
def test_exposes_object_typed_property(self):
|
|
self.assertTrue(hasattr(self._frida.IOStream, "input_stream"))
|
|
self.assertTrue(hasattr(self._frida.IOStream, "output_stream"))
|
|
|
|
def test_string_array_property_round_trips(self):
|
|
options = self._frida.SpawnOptions()
|
|
self.assertIsNone(options.argv)
|
|
options.argv = ["/bin/ls", "-la"]
|
|
self.assertEqual(options.argv, ["/bin/ls", "-la"])
|
|
options.argv = None
|
|
self.assertIsNone(options.argv)
|
|
|
|
def test_vardict_property_round_trips(self):
|
|
options = self._frida.SpawnOptions()
|
|
options.aux = {"name": "value", "count": 42, "flag": True}
|
|
self.assertEqual(options.aux, {"name": "value", "count": 42, "flag": True})
|
|
|
|
def test_variant_marshalling_types_and_casts(self):
|
|
options = self._frida.SpawnOptions()
|
|
options.aux = {
|
|
"int": 42,
|
|
"uint64": ("uint64", 0xFFFFFFFFFFFFFFFF),
|
|
"float": 3.5,
|
|
"string": "hi",
|
|
"bool": True,
|
|
"bytes": b"\x00\x01\x02",
|
|
"list": [1, 2, 3],
|
|
"nested": {"x": 1, "y": "z"},
|
|
"tuple": ("a", 1),
|
|
}
|
|
aux = options.aux
|
|
self.assertEqual(aux["int"], 42)
|
|
# the ("uint64", ...) cast preserves the value; as int64 it would be -1
|
|
self.assertEqual(aux["uint64"], 0xFFFFFFFFFFFFFFFF)
|
|
self.assertEqual(aux["float"], 3.5)
|
|
self.assertEqual(aux["string"], "hi")
|
|
self.assertIs(aux["bool"], True)
|
|
self.assertEqual(aux["bytes"], b"\x00\x01\x02")
|
|
self.assertEqual(aux["list"], [1, 2, 3])
|
|
self.assertEqual(aux["nested"], {"x": 1, "y": "z"})
|
|
self.assertEqual(aux["tuple"], ("a", 1))
|
|
|
|
def test_async_method_delivers_result_to_callback(self):
|
|
import threading
|
|
|
|
manager = self._frida.DeviceManager()
|
|
done = threading.Event()
|
|
box = {}
|
|
|
|
def on_complete(result, error):
|
|
box["result"] = result
|
|
box["error"] = error
|
|
done.set()
|
|
|
|
manager.close(on_complete)
|
|
self.assertTrue(done.wait(10), "callback was not invoked")
|
|
self.assertIsNone(box["error"])
|
|
self.assertIsNone(box["result"])
|
|
|
|
def test_raising_async_callback_does_not_corrupt_thread(self):
|
|
import contextlib
|
|
import io
|
|
import threading
|
|
|
|
raised = threading.Event()
|
|
|
|
def bad_callback(result, error):
|
|
raised.set()
|
|
raise RuntimeError("handler blew up")
|
|
|
|
with contextlib.redirect_stderr(io.StringIO()):
|
|
self._frida.DeviceManager().close(bad_callback)
|
|
self.assertTrue(raised.wait(10))
|
|
|
|
# A subsequent operation must still complete cleanly.
|
|
box = self._await_async(lambda cb: self._frida.DeviceManager().close(cb))
|
|
self.assertIsNone(box["error"])
|
|
|
|
def _await_async(self, invoke):
|
|
import threading
|
|
|
|
done = threading.Event()
|
|
box = {}
|
|
|
|
def on_complete(result, error):
|
|
box["result"] = result
|
|
box["error"] = error
|
|
done.set()
|
|
|
|
invoke(on_complete)
|
|
self.assertTrue(done.wait(10), "callback was not invoked")
|
|
return box
|
|
|
|
def test_async_method_passes_parameters_and_maps_errors(self):
|
|
manager = self._frida.DeviceManager()
|
|
box = self._await_async(lambda cb: manager.get_device_by_id("no-such-device", 1, cb))
|
|
self.assertIsNone(box["result"])
|
|
self.assertIsInstance(box["error"], self._frida.InvalidArgumentError)
|
|
|
|
def test_async_method_honors_cancellable(self):
|
|
manager = self._frida.DeviceManager()
|
|
cancellable = self._frida.Cancellable()
|
|
cancellable.cancel()
|
|
box = self._await_async(lambda cb: manager.get_device_by_id("anything", 5000, cb, cancellable))
|
|
self.assertIsInstance(box["error"], self._frida.OperationCancelledError)
|
|
|
|
|
|
@unittest.skipUnless(
|
|
(REPO / "build" / "frida" / "__init__.py").exists(),
|
|
"requires a built facade (run `make`)",
|
|
)
|
|
class TestFacade(unittest.TestCase):
|
|
def setUp(self):
|
|
sys.path.insert(0, str(REPO / "build"))
|
|
self._purge_frida()
|
|
import frida
|
|
|
|
self.frida = frida
|
|
|
|
def tearDown(self):
|
|
sys.path.remove(str(REPO / "build"))
|
|
self._purge_frida()
|
|
|
|
@staticmethod
|
|
def _purge_frida():
|
|
# The source tree's frida/ is a namespace package the stale test_core
|
|
# imports; drop it so the built facade under build/ is what loads.
|
|
for name in [n for n in sys.modules if n == "frida" or n.startswith("frida.")]:
|
|
del sys.modules[name]
|
|
|
|
def test_reexports_exceptions(self):
|
|
self.assertTrue(issubclass(self.frida.ServerNotRunningError, Exception))
|
|
|
|
def test_synchronous_method_blocks(self):
|
|
self.frida.DeviceManager().close()
|
|
|
|
def test_synchronous_method_raises_mapped_error(self):
|
|
with self.assertRaises(self.frida.InvalidArgumentError):
|
|
self.frida.DeviceManager().get_device_by_id("no-such-device", 1)
|
|
|
|
def test_aio_awaitable_completes(self):
|
|
import asyncio
|
|
|
|
import frida.aio
|
|
|
|
asyncio.run(frida.aio.DeviceManager().close())
|
|
|
|
def test_aio_awaitable_raises_mapped_error(self):
|
|
import asyncio
|
|
|
|
import frida.aio
|
|
|
|
async def scenario():
|
|
with self.assertRaises(self.frida.InvalidArgumentError):
|
|
await frida.aio.DeviceManager().get_device_by_id("no-such-device", 1)
|
|
|
|
asyncio.run(scenario())
|
|
|
|
def test_aio_cancellation_propagates(self):
|
|
import asyncio
|
|
|
|
import frida.aio
|
|
|
|
async def scenario():
|
|
manager = frida.aio.DeviceManager()
|
|
task = asyncio.ensure_future(manager.get_device_by_id("nope", 60000))
|
|
await asyncio.sleep(0.1)
|
|
task.cancel()
|
|
with self.assertRaises(asyncio.CancelledError):
|
|
await task
|
|
|
|
asyncio.run(scenario())
|
|
|
|
def test_unreferenced_interface_impl_is_collected(self):
|
|
import gc
|
|
import weakref
|
|
|
|
class MyAuth(self.frida.AuthenticationService):
|
|
def authenticate(self, token):
|
|
return "{}"
|
|
|
|
refs = [weakref.ref(MyAuth()) for _ in range(3)]
|
|
gc.collect()
|
|
self.assertTrue(all(ref() is None for ref in refs))
|
|
|
|
def test_interface_impl_survives_gc_while_referenced_by_core(self):
|
|
import gc
|
|
|
|
class MyAuth(self.frida.AuthenticationService):
|
|
def authenticate(self, token):
|
|
return "{}"
|
|
|
|
params = self.frida.EndpointParameters(address="127.0.0.1", port=0, authentication=MyAuth())
|
|
gc.collect()
|
|
gc.collect()
|
|
self.assertIsNotNone(params._impl.auth_service)
|
|
|
|
def test_interface_can_be_implemented(self):
|
|
import json
|
|
|
|
class MyAuth(self.frida.AuthenticationService):
|
|
def authenticate(self, token):
|
|
return json.dumps({"token": token})
|
|
|
|
service = MyAuth()
|
|
self.assertIsInstance(service, self.frida.AuthenticationService)
|
|
self.assertEqual(type(service._impl).__name__, "AuthenticationService")
|
|
|
|
def test_endpoint_parameters_authentication_schemes(self):
|
|
self.frida.EndpointParameters(address="::1", authentication=("token", "secret"))
|
|
self.frida.EndpointParameters(address="::1", authentication=("callback", lambda token: {"token": token}))
|
|
|
|
class MyAuth(self.frida.AuthenticationService):
|
|
def authenticate(self, token):
|
|
return "{}"
|
|
|
|
self.frida.EndpointParameters(address="::1", authentication=MyAuth())
|
|
with self.assertRaises(ValueError):
|
|
self.frida.EndpointParameters(authentication=("bogus", "x"))
|
|
|
|
def test_object_constructor_parameters(self):
|
|
cluster = self.frida.EndpointParameters(address="127.0.0.1", port=0)
|
|
service = self.frida.PortalService(cluster, None)
|
|
self.assertIsInstance(service.device, self.frida.Device)
|
|
|
|
def test_implemented_interface_receives_dispatch(self):
|
|
frida = self.frida
|
|
calls = []
|
|
|
|
def authenticate(token):
|
|
calls.append(token)
|
|
if token != "secret":
|
|
raise ValueError("wrong token")
|
|
return "{}"
|
|
|
|
control_port = free_port()
|
|
control = frida.EndpointParameters(
|
|
address="127.0.0.1", port=control_port, authentication=("callback", authenticate)
|
|
)
|
|
cluster = frida.EndpointParameters(address="127.0.0.1", port=free_port())
|
|
service = frida.PortalService(cluster, control)
|
|
service.start()
|
|
try:
|
|
with self.assertRaises(frida.InvalidArgumentError):
|
|
frida.DeviceManager().add_remote_device(
|
|
f"127.0.0.1:{control_port}", token="wrong"
|
|
).enumerate_processes()
|
|
frida.DeviceManager().add_remote_device(f"127.0.0.1:{control_port}", token="secret").enumerate_processes()
|
|
self.assertEqual(calls, ["wrong", "secret"])
|
|
finally:
|
|
service.stop()
|
|
|
|
def test_aio_implemented_interface_receives_dispatch(self):
|
|
import asyncio
|
|
|
|
import frida.aio
|
|
|
|
calls = []
|
|
|
|
async def scenario():
|
|
class MyAuth(frida.aio.AuthenticationService):
|
|
async def authenticate(self, token):
|
|
calls.append(token)
|
|
await asyncio.sleep(0)
|
|
if token != "secret":
|
|
raise ValueError("wrong token")
|
|
return "{}"
|
|
|
|
control_port = free_port()
|
|
control = frida.aio.EndpointParameters(address="127.0.0.1", port=control_port, authentication=MyAuth())
|
|
cluster = frida.aio.EndpointParameters(address="127.0.0.1", port=free_port())
|
|
service = frida.aio.PortalService(cluster, control)
|
|
await service.start()
|
|
try:
|
|
with self.assertRaises(self.frida.InvalidArgumentError):
|
|
manager = frida.aio.DeviceManager()
|
|
device = await manager.add_remote_device(f"127.0.0.1:{control_port}", token="wrong")
|
|
await device.enumerate_processes()
|
|
manager = frida.aio.DeviceManager()
|
|
device = await manager.add_remote_device(f"127.0.0.1:{control_port}", token="secret")
|
|
await device.enumerate_processes()
|
|
self.assertEqual(calls, ["wrong", "secret"])
|
|
finally:
|
|
await service.stop()
|
|
|
|
asyncio.run(scenario())
|
|
|
|
def test_web_request_handler_serves_response(self):
|
|
import urllib.request
|
|
|
|
frida = self.frida
|
|
seen = []
|
|
|
|
class Handler(frida.WebRequestHandler):
|
|
def handle_request(self, request):
|
|
seen.append(request.path)
|
|
return frida.WebResponse(200, b"pong")
|
|
|
|
control_port = free_port()
|
|
control = frida.EndpointParameters(address="127.0.0.1", port=control_port, request_handler=Handler())
|
|
cluster = frida.EndpointParameters(address="127.0.0.1", port=free_port())
|
|
service = frida.PortalService(cluster, control)
|
|
service.start()
|
|
try:
|
|
with urllib.request.urlopen(f"http://127.0.0.1:{control_port}/ping") as response:
|
|
status = response.status
|
|
body = response.read()
|
|
finally:
|
|
service.stop()
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(body, b"pong")
|
|
self.assertEqual(seen, ["/ping"])
|
|
|
|
def test_web_request_handler_returning_none_yields_not_found(self):
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
frida = self.frida
|
|
|
|
class Handler(frida.WebRequestHandler):
|
|
def handle_request(self, request):
|
|
return None
|
|
|
|
control_port = free_port()
|
|
control = frida.EndpointParameters(address="127.0.0.1", port=control_port, request_handler=Handler())
|
|
cluster = frida.EndpointParameters(address="127.0.0.1", port=free_port())
|
|
service = frida.PortalService(cluster, control)
|
|
service.start()
|
|
try:
|
|
with self.assertRaises(urllib.error.HTTPError) as raised:
|
|
urllib.request.urlopen(f"http://127.0.0.1:{control_port}/ping")
|
|
self.assertEqual(raised.exception.code, 404)
|
|
finally:
|
|
service.stop()
|
|
|
|
def test_web_request_handler_error_propagates(self):
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
frida = self.frida
|
|
|
|
class Handler(frida.WebRequestHandler):
|
|
def handle_request(self, request):
|
|
raise ValueError("boom")
|
|
|
|
control_port = free_port()
|
|
control = frida.EndpointParameters(address="127.0.0.1", port=control_port, request_handler=Handler())
|
|
cluster = frida.EndpointParameters(address="127.0.0.1", port=free_port())
|
|
service = frida.PortalService(cluster, control)
|
|
service.start()
|
|
try:
|
|
with self.assertRaises(urllib.error.HTTPError) as raised:
|
|
urllib.request.urlopen(f"http://127.0.0.1:{control_port}/ping")
|
|
self.assertEqual(raised.exception.code, 500)
|
|
self.assertEqual(raised.exception.read(), b"boom")
|
|
finally:
|
|
service.stop()
|
|
|
|
@unittest.skipIf(sys.platform == "win32", "requires a POSIX shell")
|
|
def test_spawn_accepts_program_as_list(self):
|
|
import os
|
|
import tempfile
|
|
import time
|
|
|
|
device = self.frida.get_local_device()
|
|
fd, path = tempfile.mkstemp()
|
|
os.close(fd)
|
|
try:
|
|
pid = device.spawn(["/bin/sh", "-c", f'printf hi > "{path}"'])
|
|
device.resume(pid)
|
|
|
|
deadline = time.time() + 5
|
|
content = ""
|
|
while time.time() < deadline:
|
|
with open(path) as f:
|
|
content = f.read()
|
|
if content:
|
|
break
|
|
time.sleep(0.05)
|
|
finally:
|
|
os.unlink(path)
|
|
|
|
self.assertEqual(content, "hi")
|
|
|
|
@unittest.skipIf(sys.platform == "win32", "requires a POSIX shell")
|
|
def test_spawn_accepts_env_as_dict(self):
|
|
import os
|
|
import tempfile
|
|
import time
|
|
|
|
device = self.frida.get_local_device()
|
|
fd, path = tempfile.mkstemp()
|
|
os.close(fd)
|
|
try:
|
|
pid = device.spawn(
|
|
"/bin/sh",
|
|
argv=["/bin/sh", "-c", f'printf %s "$FRIDA_ENVP_TEST" > "{path}"'],
|
|
env={"FRIDA_ENVP_TEST": "envp-as-dict"},
|
|
)
|
|
device.resume(pid)
|
|
|
|
deadline = time.time() + 5
|
|
content = ""
|
|
while time.time() < deadline:
|
|
with open(path) as f:
|
|
content = f.read()
|
|
if content:
|
|
break
|
|
time.sleep(0.05)
|
|
finally:
|
|
os.unlink(path)
|
|
|
|
self.assertEqual(content, "envp-as-dict")
|
|
|
|
@unittest.skipIf(sys.platform == "win32", "requires a POSIX shell")
|
|
def test_spawn_accepts_envp_and_bytes_argv(self):
|
|
import os
|
|
import tempfile
|
|
import time
|
|
|
|
device = self.frida.get_local_device()
|
|
fd, path = tempfile.mkstemp()
|
|
os.close(fd)
|
|
try:
|
|
pid = device.spawn(
|
|
[b"/bin/sh", b"-c", f'printf %s "$FRIDA_ENVP2" > "{path}"'],
|
|
envp={"FRIDA_ENVP2": "envp-full"},
|
|
)
|
|
device.resume(pid)
|
|
|
|
deadline = time.time() + 5
|
|
content = ""
|
|
while time.time() < deadline:
|
|
with open(path) as f:
|
|
content = f.read()
|
|
if content:
|
|
break
|
|
time.sleep(0.05)
|
|
finally:
|
|
os.unlink(path)
|
|
|
|
self.assertEqual(content, "envp-full")
|
|
|
|
def test_toplevel_convenience_functions(self):
|
|
frida = self.frida
|
|
self.assertRegex(frida.__version__, r"\d+\.\d+")
|
|
for name in (
|
|
"spawn",
|
|
"resume",
|
|
"kill",
|
|
"attach",
|
|
"inject_library_file",
|
|
"inject_library_blob",
|
|
"enumerate_devices",
|
|
"get_device_matching",
|
|
"shutdown",
|
|
"query_system_parameters",
|
|
):
|
|
self.assertTrue(callable(getattr(frida, name)), name)
|
|
self.assertIn("local", [d.id for d in frida.enumerate_devices()])
|
|
self.assertEqual(frida.get_device_matching(lambda d: d.type == "local").id, "local")
|
|
|
|
@unittest.skipIf(sys.platform == "win32", "requires a POSIX shell")
|
|
def test_toplevel_attach_and_query(self):
|
|
frida = self.frida
|
|
pid = frida.spawn(["/bin/sh", "-c", "sleep 30"])
|
|
try:
|
|
session = frida.attach(pid)
|
|
session.detach()
|
|
finally:
|
|
frida.kill(pid)
|
|
params = frida.query_system_parameters()
|
|
self.assertIn("arch", params)
|
|
|
|
def test_device_type_property(self):
|
|
device = self.frida.get_local_device()
|
|
self.assertEqual(device.type, "local")
|
|
|
|
def test_bool_accessors_are_properties(self):
|
|
device = self.frida.get_local_device()
|
|
self.assertIs(device.is_lost, False)
|
|
session = device.attach(0)
|
|
self.assertIs(session.is_detached, False)
|
|
session.detach()
|
|
self.assertIs(session.is_detached, True)
|
|
|
|
def test_device_get_process_by_name(self):
|
|
device = self.frida.get_local_device()
|
|
with self.assertRaises(self.frida.ProcessNotFoundError):
|
|
device.get_process("frida-nonexistent-process-zzz")
|
|
|
|
@unittest.skipIf(sys.platform == "win32", "requires a POSIX shell")
|
|
def test_attach_accepts_pid(self):
|
|
device = self.frida.get_local_device()
|
|
pid = device.spawn(["/bin/sh", "-c", "sleep 30"])
|
|
try:
|
|
session = device.attach(pid)
|
|
session.detach()
|
|
finally:
|
|
device.kill(pid)
|
|
|
|
def test_facade_repr(self):
|
|
device = self.frida.get_local_device()
|
|
self.assertRegex(repr(device), r"Device\(id='local'")
|
|
process = self.frida.get_local_device().enumerate_processes()[0]
|
|
self.assertRegex(repr(process), r"Process\(pid=\d+")
|
|
|
|
def test_create_script_positional_name(self):
|
|
session = self.frida.get_local_device().attach(0)
|
|
try:
|
|
script = session.create_script("rpc.exports={ping:()=>1};", "my-script")
|
|
script.load()
|
|
self.assertEqual(script.exports_sync.ping(), 1)
|
|
finally:
|
|
session.detach()
|
|
|
|
def test_create_script_from_bytes_positional_name(self):
|
|
session = self.frida.get_local_device().attach(0)
|
|
try:
|
|
bytecode = session.compile_script("rpc.exports={ping:()=>1};", "n")
|
|
script = session.create_script_from_bytes(bytecode, "my-script")
|
|
script.load()
|
|
self.assertEqual(script.exports_sync.ping(), 1)
|
|
finally:
|
|
session.detach()
|
|
|
|
def test_file_monitor_reports_changes(self):
|
|
import os
|
|
import tempfile
|
|
import time
|
|
|
|
directory = tempfile.mkdtemp()
|
|
target = os.path.join(directory, "watched.txt")
|
|
monitor = self.frida.FileMonitor(target)
|
|
events = []
|
|
monitor.on("change", lambda *args: events.append(args))
|
|
monitor.enable()
|
|
try:
|
|
with open(target, "w") as f:
|
|
f.write("hi")
|
|
deadline = time.time() + 3
|
|
while not events and time.time() < deadline:
|
|
time.sleep(0.05)
|
|
finally:
|
|
monitor.disable()
|
|
self.assertTrue(events)
|
|
self.assertEqual(events[0][0], target)
|
|
|
|
def test_frida_core_alias(self):
|
|
import frida.core
|
|
|
|
self.assertIs(frida.core.Session, self.frida.Session)
|
|
self.assertIs(frida.core.Device, self.frida.Device)
|
|
|
|
def test_device_bus_post_accepts_dict(self):
|
|
bus = self.frida.get_local_device().bus
|
|
self.assertTrue(callable(bus.post))
|
|
bus.post({"type": "ping"})
|
|
|
|
def test_script_options_snapshot_setter(self):
|
|
options = self._new_script_options()
|
|
options.snapshot = b"\x00\x01\x02"
|
|
self.assertEqual(options.snapshot, b"\x00\x01\x02")
|
|
|
|
def _new_script_options(self):
|
|
from frida import _frida
|
|
|
|
return _frida.ScriptOptions()
|
|
|
|
def test_iostream_exposes_old_api(self):
|
|
for name in ("is_closed", "close", "read", "read_all", "write", "write_all"):
|
|
self.assertTrue(hasattr(self.frida.IOStream, name))
|
|
|
|
def test_get_local_device_returns_wrapped_device(self):
|
|
device = self.frida.get_local_device()
|
|
self.assertIsInstance(device, self.frida.Device)
|
|
self.assertEqual(device.id, "local")
|
|
|
|
def test_device_manager_is_a_singleton(self):
|
|
import frida._frida as _frida
|
|
import frida.aio
|
|
|
|
underlying = _frida.get_device_manager()
|
|
self.assertIs(_frida.get_device_manager(), underlying)
|
|
self.assertIs(self.frida.get_device_manager()._impl, underlying)
|
|
self.assertIs(frida.aio.get_device_manager()._impl, underlying)
|
|
|
|
def test_aio_get_local_device(self):
|
|
import asyncio
|
|
|
|
import frida.aio
|
|
|
|
device = asyncio.run(frida.aio.get_local_device())
|
|
self.assertIsInstance(device, frida.aio.Device)
|
|
self.assertEqual(device.id, "local")
|
|
|
|
def test_method_with_options_parameter_returns_object(self):
|
|
import os
|
|
|
|
device = self.frida.get_local_device()
|
|
process = device.get_process_by_pid(os.getpid())
|
|
self.assertIsInstance(process, self.frida.Process)
|
|
self.assertEqual(process.pid, os.getpid())
|
|
|
|
def test_method_with_options_parameter_maps_errors(self):
|
|
device = self.frida.get_local_device()
|
|
with self.assertRaises(self.frida.ProcessNotFoundError):
|
|
device.attach(999999)
|
|
|
|
def test_list_return_marshals_to_wrapped_objects(self):
|
|
devices = self.frida.get_device_manager().enumerate_devices()
|
|
self.assertIsInstance(devices, list)
|
|
self.assertTrue(all(isinstance(d, self.frida.Device) for d in devices))
|
|
self.assertIn("local", [d.id for d in devices])
|
|
|
|
def test_vardict_return_marshals_to_dict(self):
|
|
params = self.frida.get_local_device().query_system_parameters()
|
|
self.assertIsInstance(params, dict)
|
|
self.assertIn("arch", params)
|
|
self.assertIsInstance(params["os"], dict)
|
|
|
|
def test_signal_handler_is_invoked(self):
|
|
cancellable = self.frida.Cancellable()
|
|
fired = []
|
|
cancellable.on("cancelled", lambda: fired.append(True))
|
|
cancellable.cancel()
|
|
self.assertEqual(fired, [True])
|
|
|
|
def test_removed_signal_handler_is_not_invoked(self):
|
|
cancellable = self.frida.Cancellable()
|
|
hits = []
|
|
|
|
def handler():
|
|
hits.append(True)
|
|
|
|
cancellable.on("cancelled", handler)
|
|
cancellable.off("cancelled", handler)
|
|
cancellable.cancel()
|
|
self.assertEqual(hits, [])
|
|
|
|
def test_custom_base_type_still_exposes_signals(self):
|
|
device = self.frida.get_local_device()
|
|
callback = lambda *args: None
|
|
device.on("spawn-added", callback)
|
|
device.off("spawn-added", callback)
|
|
|
|
def test_unknown_signal_name_raises(self):
|
|
with self.assertRaises(ValueError):
|
|
self.frida.get_device_manager().on("nonexistent", lambda: None)
|
|
|
|
def test_signal_handler_can_receive_the_emitter(self):
|
|
cancellable = self.frida.Cancellable()
|
|
received = []
|
|
cancellable.on("cancelled", lambda sender: received.append(sender))
|
|
cancellable.cancel()
|
|
self.assertEqual(len(received), 1)
|
|
self.assertIsInstance(received[0], self.frida.Cancellable)
|
|
|
|
def test_signal_handler_with_too_many_arguments_raises(self):
|
|
cancellable = self.frida.Cancellable()
|
|
with self.assertRaises(TypeError):
|
|
cancellable.on("cancelled", lambda a, b, c: None)
|
|
|
|
def test_cancellable_is_cancelled_is_a_property(self):
|
|
cancellable = self.frida.Cancellable()
|
|
self.assertFalse(cancellable.is_cancelled)
|
|
cancellable.cancel()
|
|
self.assertTrue(cancellable.is_cancelled)
|
|
|
|
def test_cancellable_pollfd_round_trips(self):
|
|
cancellable = self.frida.Cancellable()
|
|
pollfd = cancellable.get_pollfd()
|
|
with pollfd as fd:
|
|
self.assertIsInstance(fd, int)
|
|
pollfd.release()
|
|
pollfd.release()
|
|
|
|
def test_rpc_round_trip(self):
|
|
session = self.frida.get_local_device().attach(0)
|
|
try:
|
|
script = session.create_script("rpc.exports = { add: (a, b) => a + b, echo: (x) => x };")
|
|
script.load()
|
|
self.assertEqual(sorted(dir(script.exports_sync)), ["add", "echo"])
|
|
self.assertEqual(script.exports_sync.add(3, 4), 7)
|
|
self.assertEqual(script.exports_sync.echo("hello"), "hello")
|
|
finally:
|
|
session.detach()
|
|
|
|
def test_script_rpc_error_propagates(self):
|
|
session = self.frida.get_local_device().attach(0)
|
|
try:
|
|
script = session.create_script("rpc.exports = { boom: () => { throw new Error('kaboom'); } };")
|
|
script.load()
|
|
with self.assertRaises(self.frida.RPCException):
|
|
script.exports_sync.boom()
|
|
finally:
|
|
session.detach()
|
|
|
|
def test_script_log_handler_receives_console_log(self):
|
|
session = self.frida.get_local_device().attach(0)
|
|
try:
|
|
script = session.create_script("console.log('hello from script');")
|
|
logs = []
|
|
script.set_log_handler(lambda level, text: logs.append((level, text)))
|
|
script.load()
|
|
self.assertIn(("info", "hello from script"), logs)
|
|
finally:
|
|
session.detach()
|
|
|
|
def test_script_list_exports(self):
|
|
session = self.frida.get_local_device().attach(0)
|
|
try:
|
|
script = session.create_script("rpc.exports = { a: () => 1, b: () => 2 };")
|
|
script.load()
|
|
self.assertEqual(sorted(script.list_exports_sync()), ["a", "b"])
|
|
finally:
|
|
session.detach()
|
|
|
|
def test_script_message_round_trip(self):
|
|
session = self.frida.get_local_device().attach(0)
|
|
try:
|
|
script = session.create_script("send({ hi: 1 });")
|
|
received = []
|
|
script.on("message", lambda message, data: received.append(message))
|
|
script.load()
|
|
self.assertEqual(received[0]["type"], "send")
|
|
self.assertEqual(received[0]["payload"], {"hi": 1})
|
|
finally:
|
|
session.detach()
|
|
|
|
def test_script_rpc_raises_after_destroy(self):
|
|
session = self.frida.get_local_device().attach(0)
|
|
try:
|
|
script = session.create_script("rpc.exports = { ping: () => 1 };")
|
|
script.load()
|
|
script.unload()
|
|
with self.assertRaises(self.frida.InvalidOperationError):
|
|
script.exports_sync.ping()
|
|
finally:
|
|
session.detach()
|
|
|
|
def test_rpc_exception_str_surfaces_message(self):
|
|
self.assertEqual(str(self.frida.RPCException("type", "name", "boom")), "boom")
|
|
self.assertEqual(str(self.frida.RPCException("boom")), "boom")
|
|
|
|
def test_sync_cancellable_context_cancels_blocking_call(self):
|
|
import threading
|
|
|
|
device = self.frida.get_local_device()
|
|
cancellable = self.frida.Cancellable()
|
|
threading.Timer(0.2, cancellable.cancel).start()
|
|
with self.assertRaises(self.frida.OperationCancelledError):
|
|
with cancellable:
|
|
device.get_process_by_name("frida-nonexistent-zzz", timeout=5000)
|
|
|
|
def test_aio_cancellable_context_cancels_awaitable(self):
|
|
import asyncio
|
|
|
|
import frida.aio
|
|
|
|
async def scenario():
|
|
device = await frida.aio.get_local_device()
|
|
cancellable = frida.aio.Cancellable()
|
|
|
|
async def cancel_soon():
|
|
await asyncio.sleep(0.2)
|
|
cancellable.cancel()
|
|
|
|
asyncio.ensure_future(cancel_soon())
|
|
with self.assertRaises(self.frida.OperationCancelledError):
|
|
with cancellable:
|
|
await device.get_process_by_name("frida-nonexistent-zzz", timeout=5000)
|
|
|
|
asyncio.run(scenario())
|
|
|
|
def test_attach_accepts_options_as_kwargs(self):
|
|
session = self.frida.get_local_device().attach(0, realm="native", persist_timeout=0)
|
|
session.detach()
|
|
|
|
def test_invalid_option_is_reported(self):
|
|
with self.assertRaises(AttributeError):
|
|
self.frida.get_local_device().attach(0, no_such_option=123)
|
|
|
|
def test_compile_script_returns_bytes(self):
|
|
session = self.frida.get_local_device().attach(0)
|
|
try:
|
|
data = session.compile_script("const x = 42;")
|
|
self.assertIsInstance(data, bytes)
|
|
self.assertGreater(len(data), 0)
|
|
finally:
|
|
session.detach()
|
|
|
|
def test_aio_rpc_round_trip(self):
|
|
import asyncio
|
|
|
|
import frida.aio
|
|
|
|
async def scenario():
|
|
device = await frida.aio.get_local_device()
|
|
session = await device.attach(0)
|
|
try:
|
|
script = await session.create_script("rpc.exports = { add: (a, b) => a + b };")
|
|
await script.load()
|
|
self.assertEqual(await script.exports.add(3, 4), 7)
|
|
finally:
|
|
await session.detach()
|
|
|
|
asyncio.run(scenario())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|