This article was published on the 10th of November 2021.
To further develop the m3 framework, one must understand its architecture. This article provides in-depth insight into the design of the framework, along with the design choices that were made along the way. After those two topics, the implementation of a new family is described in detail.
Table of contents
The project is divided into multiple packages, where each package has a specific purpose. The main packages are listed below.
The emulator handles the first line of command-line interface arguments, meaning the emulating, guided bot creation, command-line interface bot creation, and displaying the help menu. A separate class is used to contain all command-line interface objects, splitting the creation and handling of the command-line arguments.
The scheduler maintains a list of all loaded bots. It will then sort the list based on the bot that has to contact its C2 first. Every bot from the list goes through a few checks, to ensure that the bot is active, and if the C2 should be contacted. If it meets all criteria, the bot is emulated.
If the bot is inactive, it is removed from the list. The only change to the removed bot’s files is the boolean that marks the bot as (in)active in the config file. All other files remain untouched, which can then be handled as seen fit by m3’s operator. If the time to connect with the C2 is not there yet, the scheduler will sleep until the bot that is first in line is due.
This package contains several classes, most of which are tied to the Phone class. This class contains many methods that are normally present in the Android system that the Android applications use. The Phone class is used for all Android related functions within the bot. The functions have been implemented in pure Java, only keeping the required functionality for the ported functions.
This implementation requires the creation of a class that contains often used methods, but will never be complete. To add a field or method to the Phone class, one has to implement this change for all families. This can be avoided by implementing the field or method in the bot specific class. Although this technically resolves the problem, it does break the used design pattern.
To add store data without adding fields, one can always use the SharedPreferences class, which is included in the Phone object. The SharedPreferences class is a wrapper around a HashMap, working exactly like the Android version of the SharedPreferences.
Note that the solution above does not work for functions that do more than simply return data. One could put such a function within the bot specific class (or in a dedicated helper class), whilst storing required data in the SharedPreferences instance.
Private instances of the framework can be altered in any desired way, but pull requests to the official m3 repository will need to follow the outlined design patterns.
One example is the permission system on Android. The implementation of the permissions of the bot is simple: a HashSet has been used to store the raw values in. One can use the Permissions class (which resides in the device.enum package) to use the traditional names of the permissions.
The configuration files and the bots objects are linked, as bots are created based upon configuration files. The reason to use two different objects for this, has to do with the chosen persistence method. The configuration objects are flat in structure, meaning data is directly stored in the object, without being contained in other objects. Due to this split, the configuration files can easily be stored and loaded in JSON format on the disk using Google’s GSON library. This makes it easy for external tools to create and/or edit the JSON file.
There is a specific configuration class for each implemented family, which extends the IConfig interface, as well as the abstract Config class that contains all generic fields. As such, a family specific implementation only has to add fields to its own config class that are unique to that family.
Additionally, the config package contains classes that can help with handling configuration files, such as loading them. Family specific code is present in several of these classes, which serve as examples one can use.
The bots within the framework are based upon the IBot interface, and the abstract Bot class. A family specific implementation would implement and extend the interface and abstract class respectively. Family specific actions will need to be added within that implementation.
This class uses several abstracted layers that are also present in the project. The class to send HTTP requests has been abstracted into the Connector class. Common encryption methods are present in the encryption package. Handling files is done via the LocalFileSystemManager, which allows the family specific code to deal with files and (sub)folders that are located in a folder on the disk that is specified at the bot’s creation. The additional abstraction layers make it easier to implement an additional family, as the focus should be on writing the bot specific code, not on handling code related issues.
The requirements that are set by the IBot interface are mostly filled in by the abstract class, barring the functions that are explained below.
The constructor should pass the arguments to the abstract class using the super call. Additionally, family specific fields should be initialised in this function.
This function is called when the bot is not yet registered. It should register the bot at the given C2, after which the registration boolean (named registered) should be set to true if the registration was successful. Changing this boolean should be done with the setRegistration function that resides within the abstract class, as that automatically logs the change.
If the registration is successful and the bot is active, the scheduler will call the poll function. This function should check if there is a command coming from the C2, and should handle any incoming command. To keep the code clean, the commands for the bots have been implemented in a new class per command.
The save function should return an instance of the IConfig interface, based on that specific family’s implementation. An example is given below.
IConfig config = new AnubisConfig(...); saveConfig(config);
This function is used to store the bot on the disk as a JSON file. The abstract bot class contains the saveConfig function, which will handle the logging and saving of the config object. The scheduler will call the save function after changes took place in the bot. Additional calls to this function ensure that a changed value is immediately written to the disk, rather than once the poll function finishes.
To add a new family, one has to create a new package with the name of the family in the root of the project. This package is the place to put the family specific bot and config classes. Additional helper classes can be stored here, or in subpackages. In this example, the bot family will be named Example.
The ExampleBot class should extend the abstract Bot class, and implement the IBot interface. The ExampleConfig class should extend the abstract Config class, and implement the IConfig interface. Any additional fields that the Example family requires, need to be added into both the configuration and the bot.
The ExampleBot class needs to implement the register, poll, and save functions. Command handlers should be implemented in separate classes for each of the commands, as this keeps the code readable, and splits the code into manageable chunks. Needless to say, documentation needs to written for all classes that are added. The above-mentioned abstraction layers should be used whenever they are needed. If something is missing, such as a known encryption cypher, one can add a class for it in the respective package, making it available for others in future updates.
Within the the config package, there are several classes that handle the creation of configuration files. One of them is the ConfigCreator class, which contains two functions, namely cliCreation and manualCreation. Both of these are used to create IConfig instances, depending on the requested creation method. When adding a new family, one has to add all family specific arguments to these functions. The code’s documentation will provide guidance on the specifics to add.
Once all is set and done, it is best to test the code on a local network to ensure that all logic is implemented correctly. Once you are sure it is, m3 is ready to support the new family!