Skip to content

Latest commit

 

History

434 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

matilda

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

Running matilda

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).

Running the agent in a new java process

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")

Running the agent in a new native process

Similarly, calling Matilda.run_in_native_process, one can run the matilda agent in a new isolated native process.

Running the agent on a connected android device

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

Running the agent in a custom way

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 MatildaConnection object, which contains an InputStream and an OutputStream to communicate with the python side
  • Zero or more Logger instances 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 MatildaConnection object, that contains input & output streams connected to the python side
  • The MatildaPlatform the agent is running on
  • A Filesystem used 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")

Using matilda

Matilda supplies several function that can be used to debug the remote process

Finding classes

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)]

Using methods

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.

Using fields

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))

Using constructors

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())

Creating Proxy objects

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.

Creating plugins for Matilda

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 plugin

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 plugin

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:

  1. 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 in matilda.platform.supported_platforms, which contains all the possible platforms. The value will be a PluginEntryPoint object, containing:

    1. entry_point_symbol, for java platforms, it is the full name of the java class that implements the createCommandRegistry() method. For native platforms, it is the full name of the function that creates the command registry
    2. (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
  2. a function called load_plugins() that receives a DependencyContainer object 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))

About

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 debuf the remote JVM

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages