New Plugin (Test Case)

Plugins are classes that inherit from Test base class and implement a specific test case which can be an exploit, analysis, recon, fuzzer etc. To extend the framework one can implement any number of plugins.

Sample Plugin coap.generic.sample

There is already a sample plugin implemented in the framework coap.generic.sample that explains the implementation How tos for a plugin. It would be better to open the code of the plugin now and then follow the below documentation.

Note

The sample plugin does not do any operation. It is only implemented to show the features you can use to implement a plugin.

Plugin Directory

Module Directory: expliot/plugins

This is the home for all the plugins that are implemented in the framework. Any new plugin file must be created in its corresponding directory within the plugins directory. For example, any BLE based plugin will go in the directory expliot/plugins/ble. This is done to keep the directory structure clean and help developers find the relevant plugins.

Plugin sections

The implementation of a plugin can be divided into four parts:

  1. Plugin information

  2. Plugin Arguments

  3. Plugin methods

  4. Plugin output

If you are reading this I’m guessing you want to create a new plugin. Well, it is pretty simple to implement and add a new plugin to the framework. You just need to be ready with the idea, logic and arguments. If you need a specific protocol which is not part of the framework yet, just send us an email describing your requirement and the reason why you think that the protocol is important for IoT and needs to be added and we will add it to the framework, if it looks like an interesting protocol.

Note

  1. The plugin file name and class name must be the same.

  2. The plugin must import functionality only from the framework or standard library. It must not import any functionality from external packages.

Plugin information

Plugin information can be thought of as an About me for the plugin which defines what category the plugin belongs to and what is the target of the plugin among other information. All this information goes in the __init__() method of the plugin as shown in the code below.

from expliot.core.tests.test import Test, TCategory, TTarget, TLog

class Sample(Test):
    def __init__(self):
        super().__init__(
            name="Sample",
            summary="Sample Summary",
            descr="Sample Description",
            author="Sample author",
            email="email@example.com",
            ref=["https://example.com", "https://example.dom"],
            category=TCategory(TCategory.COAP, TCategory.SW, TCategory.EXPLOIT),
            target=TTarget(TTarget.GENERIC, TTarget.GENERIC, TTarget.GENERIC)
        )

Let’s look at each parameter:

  • name: This is the name of the plugin and is part of the plugin ID. Keep it short but descriptive about the action and DO NOT use white space in the name.

  • summary: Summary of the plugin. This is displayed in the list command

  • descr: The description of the plugin. This is displayed when the plugin is run with the -h or –help argument. Please be as descriptive as possible assuming the user does not have much knowledge about the plugin and its features.

  • author: Plugin author’s name

  • email: Plugin author’s email ID

  • ref: A list containing one or more reference or source URLs containing either the exploit details or relevant information used as the basis of the plugin.

  • category: The category of the plugin defined by TCategory’s technology, interface and action. For more details look at TCategory implementation in test.py.

  • target: The (vulnerable) target of the Plugin defined by name, version and vendor as a TTarget object. If it is a generic plugin then use generic for all. We may start defining names and vendors in TTarget class itself for standardization.

Other important parameters:

  • needroot: If the plugin needs root privileges, set this to True. DO NOT specify this if root privilege is not required. As of now, the framework checks if it has root privileges, if not it fails with the relevant message.

Plugin Arguments

The plugin can be configured to run based on the requirements. It is done via plugin arguments. For example, if a plugin needs to send payload to a server it needs to know the server host name/IP and the port. All these parameters can be assed with the arguments to the plugin. It is very simple to add and utilize the arguments within the plugin. The parsing logic for the arguments must be implemented in the execute() method. The argument implementation for EXPLIoT is based on python argparse module which is part of the Python Standard Library.

After we define the plugin information in the __init__() method (as shown above), we have to populate the arguments for the plugin (also in the __init__() method). The Test base calls defines an argparse object and initializes it in its __init__() method as shown below (from expliot/core/tests/test.py):

self.argparser = argparse.ArgumentParser(prog=self.id, description=self.descr)

For the argument parser in the framework the self.id and self.descr of the plugin become the name and the description respectively. Now, let’s look at how to add and use the arguments.

Note

For more details on the API and how to use different methods like add_argument(), please refer to the argparse documentation.

Below is an excerpt from the coap.generic.sample plugin.

from expliot.core.tests.test import Test, TCategory, TTarget, TLog

class Sample(Test):
    def __init__(self):
        super().__init__(
            name="Sample",
            summary="Sample Summary",
            descr="Sample Description",
            author="Sample author",
            email="email@example.com",
            ref=["https://example.com", "https://example.dom"],
            category=TCategory(TCategory.COAP, TCategory.SW, TCategory.EXPLOIT),
            target=TTarget(TTarget.GENERIC, TTarget.GENERIC, TTarget.GENERIC)
        )

        self.argparser.add_argument("-r", "--rhost", required=True, help="IP address of the target")
        self.argparser.add_argument("-p", "--rport", default=80, type=int, help="Port number of the target. Default is 80")
        self.argparser.add_argument("-v", "--verbose", action="store_true", help="show verbose output")

We have already discussed the information parameters above, let’s focus on adding arguments. To add an argument, you need to call the add_argument() method on the argparser object and pass the parameters as per the argument’s requirement. You can add as many arguments as required by the plugin by invoking add_argument() method that many number of times.

  • -r: First parameter is the single hyphen-character representation of

    the argument.

  • --rhost: Second parameter is the double hyphen-word representation of

    the argument.

  • help="IP address of the target": Specify the help string. It is mandatory to fill this. This is displayed along with the argument when plugin is run with -h or –help argument.

  • required=True: Set the required parameter to True the argument is mandatory. If it is optional, no need to specify this option.

  • default=80: If you want you can specify a default value for an argument.

  • type=int: You can specify the data type of the argument value. If not, specified, default data type is string.

  • action="store_true": If an argument does not take any value. You can use this as a toggle to set the value of the argument to true (in this example) and decide your action based on whether the user specified this argument or not. As shown in the sample above it is used with the verbose argument.

  • For any other requirements, not covered here refer to the argparse documentation.

Conventions for arguments:

  • Do not define -h and –help in the plugin as they are internally generated on the fly for help text of a plugin.

  • Prefer to define -r and –rhost for remote server hostname or IP.

  • Prefer to define -p and –rport for remote server port number.

  • Prefer to define -l and -lhost for local server hostname or IP.

  • Prefer to define -q and –lport for local server port number.

  • Prefer to define -v and –verbose when you require verbose output option.

  • Prefer to define -a and –addr for hardware addresses (MAC, BLE etc.).

Plugin methods

There are three methods defined in the Test base calls which the plugins will override for its execution. They are:

  1. pre(self): This is an optional method. Any dependency/setup related logic for the plugin will go here. Please DO NOT add argument parsing logic in this method, that must go in the execute() method. The plugin author does not need to override this otherwise. As of now none of the plugins implement this (well, actually there are a few plugins, but the code will be changed soon).

  2. post(self): This is an optional method. Any dependency/cleanup related logic for the plugin will go here. Please DO NOT add plugin fail/success logic in this method, that must go in the execute() method. The plugin author does not need to override this otherwise. As of now none of the plugins implement this (well, actually there are a few plugins, but the code will be changed soon).

  3. execute(self): This method is mandatory to be overridden by the plugin class. This is where the exploit etc. logic will go. At the end of the method you need to set the status of the test case, if it failed as explained below. Also, the plugin needs to use TLog class methods for logging any output.

Plugin output

The last but not the least is the output format of the plugin. Each plugin needs to define a clear and standard output without any ambiguities. This will solve two major concerns:

  1. Automation: To help the users (devs, testers) to be able to automate the system within their CI/CD and testing phase with a reliable mechanism of parsing the output and deciding the course of action based on that.

  2. Plugin Chaining: To be able to execute multiple plugins using a single plugin. In simple terms, one plugin may have a dependency on other plugin(s). A standardized output format will ensure that a plugin will be able to execute plugins that it is dependent on and get the required information from the output of that plugin(s).

The output format of a plugin is a list of dicts/lists. The main list is created by the Test base class. The contents of the lists/dicts have to be defined by the plugin author. All current plugins define their output and can be referred to understand how to specify the format and create it in the plugin. The coap.generic.sample plugin also shows an example output format. The Test base class method output_handler() does the job of adding the dict/list to the main output list as well as logging(printing) the output. The plugin author needs to call this method whenever there is information to be added to the output. Please read the method documentation as well as other plugin code for information on how to implement it. In case there is a need to specifically call TLog class logging methods, they can still be used

The output format of a plugin must ensure:

  1. It is clearly defined in the docstring of the plugin class

  2. There are comments in front of optional fields of the output, specifying that it is optional.

  3. It is indented clearly to show the hierarchy.

  4. There are comments for repeatable lists/dicts. For example, “one or more”, zero or more”, etc.

  5. String data is specified in double quotes and others without them.

  6. Will use double quotes for specifying dict keys.

The output of a plugin is stored in TResult class object’s self.output member. This must never be updated directly by the plugin even though it is accessible via self.result.output of the plugin class. It is updated by calling output_handler() method as of now.

Example Format

Output Format:
[
    {
        "host": "192.168.12.11",
        "port": 1900,
        "services":
        [
            {
                "name": "foo",
                "type": "bar",
                "actions":
                [
                    {
                        "name": "foo",
                        "arguments":
                        [
                            {
                                "name": "bar",
                                "direction": "in",
                                "return_value": None,
                            }, # Zero or more arguments
                        ]
                    }, # Zero or more actions
                ],
                "state_variables":
                [
                    "foobar",
                    "barfoo",
                ] # Zero or more state variables
            }, # Zero or more services
        ]
    }, # Zero or more device entries
    {
        "final_actions": 2
    }
]

Result

It is implemented by an object of TResult class which is a member of plugin class object called result, created and maintained by the Test base class. The plugin’s failure status (basically a Boolean and a message string) after execution needs to be determined and then set accordingly before returning from execute() method in case of failure. The plugin MUST NOT set anything for successful execution. The complete result including the output and the status can be obtained by calling plugin class object’s self.result.getresult() method. There are two ways to set the status:

  1. Set specific message: When you know the exact reason, you can set it using self.result.setstatus(passed=False, reason="Whatever reason")

  2. Unknown/External Exception: If there was an exception raised by another package and the plugin cannot handle all the cases, it can use self.result.exception() which basically sets the reason to the exception’s message.

Refer to the coap.generic.sample plugin’s execute() method.

Note

It is mandatory to determine the fail criteria and call any of these methods in execute().

Logging

All the logging within the plugin must use TLog class methods based on why/ what is being logged.

Refer to:

  1. TLog class details for methods.

  2. coap.generic.sample plugin’s execute() method for usage.

Note

It is mandatory to use only TLog methods for logging. Please do not use any other Python print style methods.