Skip to main content

Modules

Modules are the implant's core capabilities. Implemented as distinct C++ functions, they handle specific host interactions (like directory traversal or file I/O).

These modules serve as the default implementation for standard C2 activities. They are designed with the following principles:

  • Modular & Isolated: Each command is isolated in its own function.
  • Replaceable: Because they utilize standard C++ and WinAPI calls, you can easily modify the logic (e.g., swapping CreateFileW for a lower-level NTAPI call) or replace the module entirely without breaking the core C2 architecture.
  • Standardized: All modules communicate with the core using a unified return structure, ensuring consistent error handling and data parsing.

You are strongly encouraged to implement your own unique behavior within the implant modules. Modifying the default logic and execution flow is an effective way to disrupt static signatures and bypass heuristic detection mechanisms. As stated earlier, the implant ships with little to no offensive capability by default. Feel free to add some!

Return Structure: ModuleResult

To maintain consistency between the implant, the GUI, and the API, every module returns a standardized ModuleResult structure. This loose contract allows the core to handle data and errors uniformly, regardless of what the specific module actually does.

This standardization shifts the burden of logic into the modules (i.e. the modules choose if they were successful or not, and what to return) and keeps the core commandtree.cpp lightweight. Instead of writing custom handling code for every new feature, the core simply serializes whatever ModuleResult it receives. This makes the implant significantly more robust, as there is only one "pipeline" for data to travel through.

The Structure:

struct ModuleResult {
std::string data; // Success output (e.g., file contents, directory listing)
DWORD windows_error_code; // The associated WinAPI Error code. Ex, ERROR_SUCCESS, ERROR_ACCESS_DENIED.
};

Here's a snippet of the CD branch of the command tree, which calls cd.

//Don't worry about this snippet, it's just an example of what the parent caller looks like,
//and how it handles the responses from each module.

//Calling our module
ModuleResult module_result = cd(directory_to_traverse_to);
std::string data = module_result.data;
DWORD windows_error_code = module_result.windows_error_code;

//adding results to the task response message
add_text_result(result, "message", GetErrorMessage(windows_error_code));
add_int_result(result, "windows_error_code", windows_error_code);
add_text_result(result, "data", data);

And the snipped of the cd function that is called. Notice, as mentioned above, the logic for what to return is done by the module itself.

#include <windows.h>

/**
* @brief Changes current directory.
* @param std::string path_name: The path to CD to
* @return ModuleResult
*/
ModuleResult cd(std::string path_name) {
BOOL result = SetCurrentDirectory(
path_name.c_str()
);

//https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setcurrentdirectory
if (result == 0) {
//if 0, it means something failed, so call GetLastError() to see what that is.
// no data to send back, leave it empty
return { "", GetLastError()};
}
//on success return ERROR_SUCCESS
// no data to send back, leave it empty
return { "", ERROR_SUCCESS };
}