04 - Creating Plugins¶
This tutorial demonstrates how the in-memory string finding script from the previous tutorial can be adapted to a plugin.
Files and scripts from this tutorial are available in the examples/inmemory_strings directory.
Plugin Overview¶
Plugins are ways that make sharing additional functionalities for Zelos even easier. Plugins can be used to
Modify how Zelos executes
Provide additional output from zelos
Extend Zelos’s capabilities
In order for Zelos to find plugins, the python module containing the plugin must be located in a path specified by the ZELOS_PLUGIN_DIR
environment variable.
Building a Minimal Plugin¶
Zelos identifies plugins as objects that subclass the zelos.IPlugin
class.
from zelos import IPlugin
class MinimalPlugin(IPlugin):
pass
If we include this in a file /home/kevin/zelos_plugins/MinimalPlugin.py
, let’s just set our environment up appropriately before running zelos with our plugin!
$ ZELOS_PLUGIN_DIR=$ZELOS_PLUGIN_DIR,`/home/kevin/zelos_plugins`
$ zelos target_binary
Plugins: runner, minimalplugin
...
Unfortunately, our plugin doesn’t do much at the moment. We can add some functionality, but first we should have a way to turn our plugin on and off from the command line. This prevents plugins from running costly operations or printing extraneous output when they aren’t being used. The easiest way to do this is by specifying a zelos.CommandLineOption
to add flags to the zelos command line tool. The arguments for creating a zelos.CommandLineOption
are identical to the python argparse
library’s add_argument() function.
The ideal time to activate the plugin is when the plugin is initialized by Zelos through the __init__
function. You can add your own initialization code by creating an __init__
which takes zelos.Zelos
as an input. Remember to begin with a call to the parent __init__
function.
from zelos import IPlugin, Zelos
class MinimalPlugin(IPlugin):
def __init__(self, z:Zelos):
super.__init__(z)
print("Minimal plugin is created.")
Now, we add the zelos.CommandLineOption
to change behavior at run time. The option can then be accessed using zelos.Zelos
’s config
field.
from zelos import IPlugin, CommandLineOption
CommandLineOption('activate_minimal_plugin', action='store_true')
class MinimalPlugin(IPlugin):
def __init__(self, z):
super.__init__(z)
print("Minimal plugin is created.")
if z.config.activate_minimal_plugin:
print("Minimal plugin has been activated!")
Now we can change the behavior of zelos using our MinimalPlugin
!
$ zelos target_binary
Minimal plugin is created.
...
$ zelos --activate_minimal_plugin target_binary
Minimal plugin is created.
Minimal plugin has been activated!
...
Now to do something a bit more complicated.
Creating the In-Memory Strings Plugin.¶
The script from the previous tutorial can be converted into a plugin so that we can easily use it in the future.
The following plugin showing how to collect in-memory strings can be found at examples/inmemory_strings/strings_plugin.py. To invoke the plugin, run zelos --print_strings 4 target_binary
.
from zelos import CommandLineOption, Zelos, HookType, IPlugin
CommandLineOption(
"print_strings",
type=int,
default=None,
help="The minimum size of string to identify",
)
class StringCollectorPlugin(IPlugin):
def __init__(self, z: Zelos):
super().__init__(z)
if z.config.print_strings:
z.hook_memory(
HookType.MEMORY.WRITE,
self.collect_writes,
name="strings_syscall_hook",
)
self._min_len = z.config.print_strings
self._current_string = ""
self._next_addr = 0
def collect_writes(self, zelos, access, address, size, value):
"""
Collects strings that are written to memory. Intended to be used
as a callback in a Zelos HookType.MEMORY hook.
"""
data = zelos.memory.pack(value)
try:
decoded_data = data.decode()
except UnicodeDecodeError:
self._next_addr = 0
self._end_current_string()
return
decoded_data = decoded_data[:size]
first_null_byte = decoded_data.find("\x00")
if first_null_byte != -1:
decoded_data = decoded_data[:first_null_byte]
self._current_string += decoded_data
self._next_addr = 0
self._end_current_string()
return
if address != self._next_addr:
self._end_current_string()
self._next_addr = address + size
self._current_string += decoded_data
return
def _end_current_string(self) -> None:
"""
Ends the currently identified string. May save the string if it
looks legit enough.
"""
if len(self._current_string) >= self._min_len:
print(f'Found string: "{self._current_string}"')
self._current_string = ""