Add docstrings

This commit is contained in:
Yotam Nachum
2022-01-29 13:05:10 +02:00
committed by Orip
parent 057d901c06
commit af23dd92f2
2 changed files with 438 additions and 7 deletions
+71 -6
View File
@@ -43,6 +43,10 @@ OperationCancelledError = _frida.OperationCancelledError
def query_system_parameters() -> Dict[str, Any]:
"""
Returns a dictionary of information about the host system
"""
return get_local_device().query_system_parameters()
@@ -55,53 +59,114 @@ def spawn(
stdio: Optional[str] = None,
**kwargs: Any,
) -> int:
"""
Spawn a process into an attachable state
"""
return get_local_device().spawn(program=program, argv=argv, envp=envp, env=env, cwd=cwd, stdio=stdio, **kwargs)
def resume(target: core.ProcessTarget) -> None:
"""
Resume a process from the attachable state
:param target: the PID or name of the process
"""
get_local_device().resume(target)
def kill(target: core.ProcessTarget) -> None:
"""
Kill a process
:param target: the PID or name of the process
"""
get_local_device().kill(target)
def attach(target: Union[int, str], realm: Optional[str] = None, persist_timeout: Optional[int] = None) -> core.Session:
def attach(
target: core.ProcessTarget, realm: Optional[str] = None, persist_timeout: Optional[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)
def inject_library_file(target: Union[int, str], path: str, entrypoint: str, data: str) -> int:
def inject_library_file(target: core.ProcessTarget, path: str, entrypoint: str, data: str) -> int:
"""
Inject a library file to a process.
:param target: the PID or name of the process
"""
return get_local_device().inject_library_file(target, path, entrypoint, data)
def inject_library_blob(target: Union[int, str], blob: bytes, entrypoint: str, data: str) -> int:
def inject_library_blob(target: core.ProcessTarget, blob: bytes, entrypoint: str, data: str) -> int:
"""
Inject a library blob to a process
:param target: the PID or name of the process
"""
return get_local_device().inject_library_blob(target, blob, entrypoint, data)
def get_local_device() -> core.Device:
return get_device_matching(lambda d: d.type == "local", timeout=0)
"""
Get the local device
"""
return get_device_manager().get_local_device()
def get_remote_device() -> core.Device:
return get_device_matching(lambda d: d.type == "remote", timeout=0)
"""
Get the first remote device in the devices list
"""
return get_device_manager().get_remote_device()
def get_usb_device(timeout: int = 0) -> core.Device:
return get_device_matching(lambda d: d.type == "usb", timeout)
"""
Get the first device connected over USB in the devices list
"""
return get_device_manager().get_usb_device(timeout)
def get_device(id: Optional[str], timeout: int = 0) -> core.Device:
"""
Get a device by its id
"""
return get_device_manager().get_device(id, timeout)
def get_device_matching(predicate: Callable[[core.Device], bool], timeout: int = 0) -> core.Device:
"""
Get device matching predicate.
:param predicate: a function to filter the devices
:param timeout: operation timeout in seconds
"""
return get_device_manager().get_device_matching(predicate, timeout)
def enumerate_devices() -> List[core.Device]:
"""
Enumerate all the devices from the device manager
"""
return get_device_manager().enumerate_devices()
@core.cancellable
def shutdown() -> None:
"""
Shutdown the main device manager
"""
get_device_manager()._impl.close()
+367 -1
View File
@@ -17,6 +17,10 @@ ProcessTarget = Union[int, str]
def get_device_manager() -> "DeviceManager":
"""
Get or create a singleton DeviceManager that let you manage all the devices
"""
global _device_manager
if _device_manager is None:
_device_manager = DeviceManager(_frida.DeviceManager())
@@ -46,6 +50,10 @@ def cancellable(f: Callable[..., R]) -> Callable[..., R]:
class IOStream:
"""
Frida's own implementation of an input/output stream
"""
def __init__(self, impl) -> None:
self._impl = impl
@@ -54,26 +62,50 @@ class IOStream:
@property
def is_closed(self) -> bool:
"""
Query whether the stream is closed
"""
return self._impl.is_closed()
@cancellable
def close(self) -> None:
"""
Close the stream.
"""
self._impl.close()
@cancellable
def read(self, count: int) -> bytes:
"""
Read up to the specified number of bytes from the stream
"""
return self._impl.read(count)
@cancellable
def read_all(self, count: int) -> bytes:
"""
Read exactly the specified number of bytes from the stream
"""
return self._impl.read_all(count)
@cancellable
def write(self, data: bytes) -> int:
"""
Write as much as possible of the provided data to the stream
"""
return self._impl.write(data)
@cancellable
def write_all(self, data: bytes) -> None:
"""
Write all of the provided data to the stream
"""
self._impl.write_all(data)
@@ -83,10 +115,20 @@ class PortalMembership:
@cancellable
def terminate(self) -> None:
"""
Terminate the membership
"""
self._impl.terminate()
class ScriptExports:
"""
Proxy object that expose all the RPC exports of a script as attributes on this class
A method named exampleMethod in a script will be called with instance.example_method on this object
"""
def __init__(self, script: "Script") -> None:
self._script = script
@@ -124,21 +166,41 @@ class Script:
@property
def is_destroyed(self) -> bool:
"""
Query whether the script has been destroyed
"""
return self._impl.is_destroyed()
@cancellable
def load(self) -> None:
"""
Load the script.
"""
self._impl.load()
@cancellable
def unload(self) -> None:
"""
Unload the script
"""
self._impl.unload()
@cancellable
def eternalize(self) -> None:
"""
Eternalize the script
"""
self._impl.eternalize()
def post(self, message: Any, data: Optional[str] = None) -> None:
"""
Post a JSON-encoded message to the script
"""
raw_message = json.dumps(message)
kwargs = {"data": data}
_filter_missing_kwargs(kwargs)
@@ -146,33 +208,65 @@ class Script:
@cancellable
def enable_debugger(self, port: Optional[int] = None) -> None:
"""
Enable the Node.js compatible script debugger
"""
kwargs = {"port": port}
_filter_missing_kwargs(kwargs)
self._impl.enable_debugger(**kwargs)
@cancellable
def disable_debugger(self) -> None:
"""
Disable the Node.js compatible script debugger
"""
self._impl.disable_debugger()
def on(self, signal: str, callback: Callable[..., Any]) -> None:
"""
Add a signal handler
"""
if signal == "message":
self._on_message_callbacks.append(callback)
else:
self._impl.on(signal, callback)
def off(self, signal: str, callback: Callable[..., Any]) -> None:
"""
Remove a signal handler
"""
if signal == "message":
self._on_message_callbacks.remove(callback)
else:
self._impl.off(signal, callback)
def get_log_handler(self) -> Callable[[str, str], None]:
"""
Get the method that handles the script logs
"""
return self._log_handler
def set_log_handler(self, handler: Callable[[str, str], None]) -> None:
"""
Set the method that handles the script logs
:param handler: a callable that accepts two parameters:
1. the log level name
2. the log message
"""
self._log_handler = handler
def default_log_handler(self, level: str, text: str) -> None:
"""
The default implementation of the log handler, prints the message to stdout
or stderr, depending on the level
"""
if level == "info":
print(text, file=sys.stdout)
else:
@@ -290,28 +384,52 @@ class Session:
@property
def is_detached(self) -> bool:
"""
Query whether the session is detached
"""
return self._impl.is_detached()
@cancellable
def detach(self) -> None:
"""
Detach session from the process
"""
self._impl.detach()
@cancellable
def resume(self) -> None:
"""
Resume session after network error
"""
self._impl.resume()
@cancellable
def enable_child_gating(self) -> None:
"""
Enable child gating
"""
self._impl.enable_child_gating()
@cancellable
def disable_child_gating(self) -> None:
"""
Disable child gating
"""
self._impl.disable_child_gating()
@cancellable
def create_script(
self, source: str, name: Optional[str] = None, snapshot: Optional[bytes] = None, runtime: Optional[str] = None
) -> Script:
"""
Create a new script
"""
kwargs = {"name": name, "snapshot": snapshot, "runtime": runtime}
_filter_missing_kwargs(kwargs)
return Script(self._impl.create_script(source, **kwargs))
@@ -320,12 +438,20 @@ class Session:
def create_script_from_bytes(
self, data: bytes, name: Optional[str] = None, snapshot: Optional[bytes] = None, runtime: Optional[str] = None
) -> Script:
"""
Create a new script from bytecode
"""
kwargs = {"name": name, "snapshot": snapshot, "runtime": runtime}
_filter_missing_kwargs(kwargs)
return Script(self._impl.create_script_from_bytes(data, **kwargs))
@cancellable
def compile_script(self, source: str, name: Optional[str] = None, runtime: Optional[str] = None) -> bytes:
"""
Compile script source code to bytecode
"""
kwargs = {"name": name, "runtime": runtime}
_filter_missing_kwargs(kwargs)
return self._impl.compile_script(source, **kwargs)
@@ -340,6 +466,10 @@ class Session:
def setup_peer_connection(
self, stun_server: Optional[str] = None, relays: Optional[Sequence[_frida.Relay]] = None
) -> None:
"""
Set up a peer connection with the target process
"""
kwargs = {"stun_server": stun_server, "relays": relays}
_filter_missing_kwargs(kwargs)
self._impl.setup_peer_connection(**kwargs)
@@ -352,14 +482,26 @@ class Session:
token: Optional[str] = None,
acl: Union[None, List[str], Tuple[str]] = None,
) -> PortalMembership:
"""
Join a portal
"""
kwargs: Dict[str, Any] = {"certificate": certificate, "token": token, "acl": acl}
_filter_missing_kwargs(kwargs)
return PortalMembership(self._impl.join_portal(address, **kwargs))
def on(self, signal: str, callback: Callable[..., Any]) -> None:
"""
Add a signal handler
"""
self._impl.on(signal, callback)
def off(self, signal: str, callback: Callable[..., Any]) -> None:
"""
Remove a signal handler
"""
self._impl.off(signal, callback)
@@ -372,21 +514,37 @@ class Bus:
@cancellable
def attach(self) -> None:
"""
Attach to the bus
"""
self._impl.attach()
def post(self, message: Any, data: Optional[Union[str, bytes]] = None) -> None:
"""
Post a JSON-encoded message to the bus
"""
raw_message = json.dumps(message)
kwargs = {"data": data}
_filter_missing_kwargs(kwargs)
self._impl.post(raw_message, **kwargs)
def on(self, signal: str, callback: Callable[..., Any]) -> None:
"""
Add a signal handler
"""
if signal == "message":
self._on_message_callbacks.append(callback)
else:
self._impl.on(signal, callback)
def off(self, signal: str, callback: Callable[..., Any]) -> None:
"""
Remove a signal handler
"""
if signal == "message":
self._on_message_callbacks.remove(callback)
else:
@@ -403,6 +561,9 @@ class Bus:
class Device:
"""
Represents a device that Frida connects to
"""
def __init__(self, device) -> None:
self.id = device.id
@@ -418,14 +579,26 @@ class Device:
@property
def is_lost(self) -> bool:
"""
Query whether the device has been lost
"""
return self._impl.is_lost()
@cancellable
def query_system_parameters(self) -> Dict[str, Any]:
"""
Returns a dictionary of information about the host system
"""
return self._impl.query_system_parameters()
@cancellable
def get_frontmost_application(self, scope: Optional[str] = None) -> Optional[_frida.Application]:
"""
Get details about the frontmost application
"""
kwargs = {"scope": scope}
_filter_missing_kwargs(kwargs)
return self._impl.get_frontmost_application(**kwargs)
@@ -434,6 +607,10 @@ class Device:
def enumerate_applications(
self, identifiers: Optional[Sequence[str]] = None, scope: Optional[str] = None
) -> List[_frida.Application]:
"""
Enumerate applications
"""
kwargs = {"identifiers": identifiers, "scope": scope}
_filter_missing_kwargs(kwargs)
return self._impl.enumerate_applications(**kwargs)
@@ -442,12 +619,21 @@ class Device:
def enumerate_processes(
self, pids: Optional[Sequence[int]] = None, scope: Optional[str] = None
) -> List[_frida.Process]:
"""
Enumerate processes
"""
kwargs = {"pids": pids, "scope": scope}
_filter_missing_kwargs(kwargs)
return self._impl.enumerate_processes(**kwargs)
@cancellable
def get_process(self, process_name: str) -> _frida.Process:
"""
Get the process with the given name
:raises ProcessNotFoundError: if the process was not found or there were more than one process with the given name
"""
process_name_lc = process_name.lower()
matching = [
process
@@ -464,18 +650,34 @@ class Device:
@cancellable
def enable_spawn_gating(self) -> None:
"""
Enable spawn gating
"""
self._impl.enable_spawn_gating()
@cancellable
def disable_spawn_gating(self) -> None:
"""
Disable spawn gating
"""
self._impl.disable_spawn_gating()
@cancellable
def enumerate_pending_spawn(self) -> List[_frida.Spawn]:
"""
Enumerate pending spawn
"""
return self._impl.enumerate_pending_spawn()
@cancellable
def enumerate_pending_children(self) -> List[_frida.Child]:
"""
Enumerate pending children
"""
return self._impl.enumerate_pending_children()
@cancellable
@@ -489,6 +691,10 @@ class Device:
stdio: Optional[str] = None,
**kwargs: Any,
) -> int:
"""
Spawn a process into an attachable state
"""
if not isinstance(program, str):
argv = program
if isinstance(argv[0], bytes):
@@ -504,14 +710,28 @@ class Device:
@cancellable
def input(self, target: ProcessTarget, data: bytes) -> None:
"""
Input data on stdin of a spawned process
:param target: the PID or name of the process
"""
self._impl.input(self._pid_of(target), data)
@cancellable
def resume(self, target: ProcessTarget) -> None:
"""
Resume a process from the attachable state
:param target: the PID or name of the process
"""
self._impl.resume(self._pid_of(target))
@cancellable
def kill(self, target: ProcessTarget) -> None:
"""
Kill a process
:param target: the PID or name of the process
"""
self._impl.kill(self._pid_of(target))
@cancellable
@@ -521,30 +741,61 @@ class Device:
realm: Optional[str] = None,
persist_timeout: Optional[int] = None,
) -> Session:
"""
Attach to a process
:param target: the PID or name of the process
"""
kwargs = {"realm": realm, "persist_timeout": persist_timeout}
_filter_missing_kwargs(kwargs)
return Session(self._impl.attach(self._pid_of(target), **kwargs))
@cancellable
def inject_library_file(self, target: ProcessTarget, path: str, entrypoint: str, data: str) -> int:
"""
Inject a library file to a process
:param target: the PID or name of the process
"""
return self._impl.inject_library_file(self._pid_of(target), path, entrypoint, data)
@cancellable
def inject_library_blob(self, target: ProcessTarget, blob: bytes, entrypoint: str, data: str) -> int:
"""
Inject a library blob to a process
:param target: the PID or name of the process
"""
return self._impl.inject_library_blob(self._pid_of(target), blob, entrypoint, data)
@cancellable
def open_channel(self, address: str) -> IOStream:
"""
Open a device-specific communication channel
"""
return IOStream(self._impl.open_channel(address))
@cancellable
def get_bus(self) -> Bus:
"""
Get the message bus of the device
"""
return Bus(self._impl.get_bus())
def on(self, signal: str, callback: Callable[..., Any]) -> None:
"""
Add a signal handler
"""
self._impl.on(signal, callback)
def off(self, signal: str, callback: Callable[..., Any]) -> None:
"""
Remove a signal handler
"""
self._impl.off(signal, callback)
def _pid_of(self, target: ProcessTarget) -> int:
@@ -562,19 +813,41 @@ class DeviceManager:
return repr(self._impl)
def get_local_device(self) -> Device:
"""
Get the local device
"""
return self.get_device_matching(lambda d: d.type == "local", timeout=0)
def get_remote_device(self) -> Device:
"""
Get the first remote device in the devices list
"""
return self.get_device_matching(lambda d: d.type == "remote", timeout=0)
def get_usb_device(self, timeout: int = 0) -> Device:
"""
Get the first device connected over USB in the devices list
"""
return self.get_device_matching(lambda d: d.type == "usb", timeout)
def get_device(self, id: Optional[str], timeout: int = 0) -> Device:
"""
Get a device by its id
"""
return self.get_device_matching(lambda d: d.id == id, timeout)
@cancellable
def get_device_matching(self, predicate: Callable[[Device], bool], timeout: int = 0) -> Device:
"""
Get device matching predicate
:param predicate: a function to filter the devices
:param timeout: operation timeout in seconds
"""
if timeout < 0:
raw_timeout = -1
elif timeout == 0:
@@ -585,6 +858,10 @@ class DeviceManager:
@cancellable
def enumerate_devices(self) -> List[Device]:
"""
Enumerate devices
"""
return [Device(device) for device in self._impl.enumerate_devices()]
@cancellable
@@ -596,6 +873,10 @@ class DeviceManager:
token: Optional[str] = None,
keepalive_interval: Optional[int] = None,
) -> Device:
"""
Add a remote device
"""
kwargs: Dict[str, Any] = {
"certificate": certificate,
"origin": origin,
@@ -607,16 +888,32 @@ class DeviceManager:
@cancellable
def remove_remote_device(self, address: str) -> None:
"""
Remove a remote device
"""
self._impl.remove_remote_device(address=address)
def on(self, signal: str, callback: Callable[..., Any]) -> None:
"""
Add a signal handler
"""
self._impl.on(signal, callback)
def off(self, signal: str, callback: Callable[..., Any]) -> None:
"""
Remove a signal handler
"""
self._impl.off(signal, callback)
class RPCException(Exception):
"""
Wraps remote errors from the script RPC
"""
def __str__(self) -> str:
return str(self.args[2]) if len(self.args) >= 3 else str(self.args[0])
@@ -673,40 +970,79 @@ class PortalService:
@cancellable
def start(self) -> None:
"""
Start listening for incoming connections
:raises InvalidOperationError: if the service isn't stopped
:raises AddressInUseError: if the given address is already in use
"""
self._impl.start()
@cancellable
def stop(self) -> None:
"""
Stop listening for incoming connections, and kick any connected clients
:raises InvalidOperationError: if the service is already stopped
"""
self._impl.stop()
def post(self, connection_id: int, message: Any, data: Optional[Union[str, bytes]] = None) -> None:
"""
Post a message to a specific control channel.
"""
raw_message = json.dumps(message)
kwargs = {"data": data}
_filter_missing_kwargs(kwargs)
self._impl.post(connection_id, raw_message, **kwargs)
def narrowcast(self, tag: str, message: Any, data: Optional[Union[str, bytes]] = None) -> None:
"""
Post a message to control channels with a specific tag
"""
raw_message = json.dumps(message)
kwargs = {"data": data}
_filter_missing_kwargs(kwargs)
self._impl.narrowcast(tag, raw_message, **kwargs)
def broadcast(self, message: Any, data: Optional[Union[str, bytes]] = None) -> None:
"""
Broadcast a message to all control channels
"""
raw_message = json.dumps(message)
kwargs = {"data": data}
_filter_missing_kwargs(kwargs)
self._impl.broadcast(raw_message, **kwargs)
def enumerate_tags(self, connection_id: int) -> List[str]:
"""
Enumerate tags of a specific connection
"""
return self._impl.enumerate_tags(connection_id)
def tag(self, connection_id: int, tag: str) -> None:
"""
Tag a specific control channel
"""
self._impl.tag(connection_id, tag)
def untag(self, connection_id: int, tag: str) -> None:
"""
Untag a specific control channel
"""
self._impl.untag(connection_id, tag)
def on(self, signal: str, callback: Callable[..., Any]) -> None:
"""
Add a signal handler
"""
if signal == "authenticated":
self._on_authenticated_callbacks.append(callback)
elif signal == "message":
@@ -715,6 +1051,10 @@ class PortalService:
self._impl.on(signal, callback)
def off(self, signal: str, callback: Callable[..., Any]) -> None:
"""
Remove a signal handler
"""
if signal == "authenticated":
self._on_authenticated_callbacks.remove(callback)
elif signal == "message":
@@ -818,9 +1158,18 @@ class Cancellable:
@property
def is_cancelled(self) -> bool:
"""
Query whether cancellable has been cancelled
"""
return self._impl.is_cancelled()
def raise_if_cancelled(self) -> None:
"""
Raise an exception if cancelled
:raises OperationCancelledError:
"""
self._impl.raise_if_cancelled()
def get_pollfd(self) -> CancellablePollFD:
@@ -829,7 +1178,7 @@ class Cancellable:
@classmethod
def get_current(cls) -> _frida.Cancellable:
"""
Get the top cancellable from the stack.
Get the top cancellable from the stack
"""
return _Cancellable.get_current()
@@ -846,16 +1195,33 @@ class Cancellable:
self._impl.pop_current()
def connect(self, callback: Callable[..., Any]) -> int:
"""
Register notification callback
:returns: the created handler id
"""
return self._impl.connect(callback)
def disconnect(self, handler_id: int) -> None:
"""
Unregister notification callback.
"""
self._impl.disconnect(handler_id)
def cancel(self) -> None:
"""
Set cancellable to cancelled
"""
self._impl.cancel()
def make_auth_callback(callback: Callable[[str], Any]) -> Callable[[Any], str]:
"""
Wraps authenticated callbacks with JSON marshaling
"""
def authenticate(token: str) -> str:
session_info = callback(token)
return json.dumps(session_info)