Matilda is a python library that gives an infrastructure for a dynamic java-debugger (in the likes of frida). By loading the java library here and connecting the loaded library and the python library using a couple of I/O streams, the python side can debug the remote JVM
Using matilda's functionality requires running it's java agent and connecting it to the python client. Matilda's python library supplies you some basic ways to run the agent, but you can choose to run the agent yourself in some other way (for example - injecting the agent to a process of your choosing).
By calling Matilda.run_in_java_process, one can run the matilda agent in a new isolated process.
The function returns a MatildaProcess object that can be used to interact with the new process.
When you finish using the MatildaProcess, call its close function, or simply use it
in a context manager
from matilda.matilda import Matilda
with Matilda().run_in_java_process() as matilda_process:
integer_class = matilda_process.plugins.java.find_class("java.lang.Integer")Similarly, calling Matilda.run_in_native_process, one can run the matilda agent in a new isolated native process.
By calling Matilda.run_in_android_java_process, one can run the matilda agent in a new java process in a device
connected through adb
If you want to run the matilda agent in some other way (for example, inject it to a process, in order to debug it),
you can implement the MatildaRunner interface.
In your MatildaRunner's run method, you should load the matilda java library to your chosen location,
create a MatildaAgent object, and call its run method.
The MatildaAgent's constructor receives:
- A
MatildaConnectionobject, which contains anInputStreamand anOutputStreamto communicate with the python side - Zero or more
Loggerinstances that can be used by the agent
MatildaAgent's run function is blocking until the connection to the python side is severed.
From your python MatildaRunner's run function, you should return a MatildaAgentEnvironment object, containing:
- A
MatildaConnectionobject, that contains input & output streams connected to the python side - The
MatildaPlatformthe agent is running on - A
Filesystemused to read files from the agent's environment
Once you have created your matilda runner, you can pass it to Matilda:
from matilda.matilda import Matilda
from matilda.java_process_matilda_runner import JavaProcessMatildaRunner
with Matilda().run(JavaProcessMatildaRunner()) as matilda_process:
integer_class = matilda_process.plugins.java.find_class("java.lang.Integer")Matilda supplies several function that can be used to debug the remote process
use the find_class function to locate classes in the remote jvm. This function returns a JavaClass object
from matilda.matilda import Matilda
from matilda.java_process_matilda_runner import JavaProcessMatildaRunner
with Matilda().run(JavaProcessMatildaRunner()) as matilda_process:
integer_class = matilda_process.plugins.java.find_class("java.lang.Integer")
print(integer_class.name) # java.lang.Integer
print(integer_class.superclass) # java.lang.Number
print(integer_class.interfaces) # [JavaClass(java.lang.Comparable)]JavaClass's get_method function allows you to get a JavaMethod corresponding to a method of that class,
and invoke this method.
The get_method receives the method name and the types of the parameters, which might be JavaClasses,
or values of the JavaPrimitiveType enum (representing each of java's primitive types)
Then, one can invoke the method using invoke_static for static methods, or invoke for instance methods
(here you also need to pass the instance)
from matilda.java.java_primitive_type import JavaPrimitiveType
from matilda.matilda import Matilda
with Matilda().run_in_java_process() as matilda_process:
integer_class = matilda_process.plugins.java.find_class("java.lang.Integer")
integer_object = integer_class.get_method("valueOf", JavaPrimitiveType.INT).invoke_static(12)
print(integer_object.get_class())the invoke functions receive & return either primitives (integers, floats, booleans), or JavaObjects for
non-primitives.
You can also use JavaClass's get_methods to get all the class' methods.
Similarly to using java methods, one can use java fields.
The get_field function returns a JavaField object representing the field.
(and the get_fields function returns a list of all the fields in the class)
On a JavaField object, you can call get (or get_static for static fields) to get the value of the field.
These functions return primitive value or a JavaObject.
Similarly, you can call set or set_static to set the field's value
from matilda.java.java_primitive_type import JavaPrimitiveType
from matilda.matilda import Matilda
with Matilda().run_in_java_process() as matilda_process:
integer_class = matilda_process.plugins.java.find_class("java.lang.Integer")
print(integer_class.get_field("value").get(integer_object))Likewise, you can also use java constructors.
JavaClass's get_constructor function allows you to get a JavaConstructor corresponding to a constructor of that
class. The function receives the types of the parameters of the constructor.
You can also use JavaClass's get_constructors to get all the class' constructors.
Then, one can invoke the constructor using new_instance, which receives the constructor's arguments
and returns a new JavaObject
from matilda.java.java_primitive_type import JavaPrimitiveType
from matilda.matilda import Matilda
with Matilda().run_in_java_process() as matilda_process:
integer_class = matilda_process.plugins.java.find_class("java.lang.Integer")
integer_object = integer_class.get_constructor(JavaPrimitiveType.INT).new_instance(12)
print(integer_object.get_class())Use the new_proxy_instance function to create a new dynamic java object that implements a set of java interfaces,
with a custom implementation given by a callback you pass. (Similar to the java.lang.reflect.Proxy class in java)
from matilda.java.java_primitive_type import JavaPrimitiveType
from matilda.matilda import Matilda
with Matilda().run_in_java_process() as matilda_process:
runnable_class = matilda_process.plugins.java.find_class("java.lang.Runnable")
def handler(method: JavaMethod, args: List[JavaValue]):
if method.name == 'run':
print("Running from runnable!")
proxy = matilda_process.plugins.java.new_proxy_instance([runnable_class], handler)
print(proxy)
print(proxy.get_class())
print(proxy.get_class().superclass)
print(proxy.get_class().interfaces)The callback you pass to new_proxy_instance will be called on every method invocation on the new object, and it
receives as arguments the JavaMethod corresponding to the called method, and the list of arguments to the method.
Matilda supports dynamic plugins that implement additional information.
To create a plugin for matilda, check out the matilda plugin template on GitHub and/or read this documentation.
A plugin for matilda is made out of two parts, a java part and a python part, both need to be implemented
The java part of a matilda plugin implements commands that are called by the python side. It contains a main entry point
function called createCommandRegistry, which returns a CommandRegistry object (that supplies to the python side
all the commands it can call).
Do not create the CommandRegistry yourself! matilda's annotation processor will create the command registry
for you as a Dagger dependency
public class TemplatePlugin {
public static CommandRegistry createCommandRegistry(PluginDependenciesModule pluginDependenciesModule) {
return DaggerTemplatePluginComponent.builder()
.pluginDependenciesModule(pluginDependenciesModule)
.build()
.commandRegistry();
}
}The createCommandRegistry also receives a PluginDependenciesModule which contains all the dependencies matilda's RPC
infrastructure needs in your plugin.
For more information on matilda's RPC infrastructure and annotation processing, see the documentation
The python part of a matilda plugin contains all the code and API that's exported to users of the plugin.
First, your plugin's pyproject.toml file should specify the plugin's entry point file
[project.entry-points.'matilda.plugins']
template = 'template.template_plugin'The key ("template" here) specifies the name of the plugin, and the value specifies the file that implements it.
The file should contain the following:
-
an attribute called
PLUGIN_ENTRY_POINTS, which will be a dict. The dict will contain an entry for each platform that the plugin supports (meaning, each target the plugin was compiled to, such as JVM, linux native with x86_64 architecture, android native with ARM32 architecture, and so on). The entry key will be a value from the one of the values inmatilda.platform.supported_platforms, which contains all the possible platforms. The value will be aPluginEntryPointobject, containing:entry_point_symbol, for java platforms, it is the full name of the java class that implements thecreateCommandRegistry()method. For native platforms, it is the full name of the function that creates the command registry- (optional)
binary_path, the path to the binary file of the java/native part of the plugin. If not specified, the jar will be taken from the "resources" directory, with some default file name
-
a function called
load_plugins()that receives aDependencyContainerobject with all the generated services from matilda's RPC as dependencies. The function return value will then be exported as the plugin's API:process.plugins.[plugin_name]
from maddie.dependency import Dependency
from maddie.dependency_container import DependencyContainer
from matilda.platform.supported_platforms import JVM, LINUX_X64
from matilda.plugins.plugin_entry_point import PluginEntryPoint
from template.generated.commands.math_service import MathService
PLUGIN_ENTRY_POINTS = {
JVM: PluginEntryPoint("org.matilda.template.TemplatePlugin"),
LINUX_X64: PluginEntryPoint("createCommandRegistry"),
}
def load_plugin(dependencies_container: DependencyContainer):
return dependencies_container.get(TemplatePlugin)
class TemplatePlugin(Dependency):
def __init__(self, math_service: MathService):
self.__math_service = math_service
@property
def math(self) -> MathService:
return self.__math_service
@staticmethod
def create(dependency_container: DependencyContainer) -> 'TemplatePlugin':
return TemplatePlugin(dependency_container.get(MathService))