Agent Code

How to design an agent for Apfell

This section covers how to design an Apfell agent to be properly modularized for custom payload creations.

Base Agent

The goal here is that the base agent doesn't care about the implementation of the C2 nor does it care about which commands are actually supported. The base agent should just try to look up and invoke a function based on the tasking it gets, and it should just leverage a basic set of C2 commands (independent of how they're actually implemented). This allows an arbitrary set of commands to be stamped into the agent at creation time and allows for the c2 implementation to be swapped out whenever without having to refactor any code.

So, the main components of base agent include:

  • A tasking loop at some interval with jitter

  • A lookup of commands to find the right function to call

  • Ideally a threaded architecture if the language supports it

    • This means a way to spin off tasks in different threads, track those threads, and potentially kill them if necessary

  • Uses c2 functions to return the results of command execution

You might be wondering how your base agent knows about which commands are loaded, which C2 implementation is being used, or how even get a payload's UUID. To help with this, Apfell does an initial level of "pre-processing" when creating an agent. To understand this, it's important to also understand how everything comes back together into a single agent since everything is split out by function.

Payload Creation

When creating an agent, you select a C2 profile, fill out the associated values, select a payload type, fill out the associated build transforms, select the commands you want in the initial payload, and click submit. At this point, Apfell makes a temporary directory and copies all of the agent code and c2 profile code to the new directory. Apfell then loops through all of the files to check for the pre-processor directives:

Directive Key Word

What it does

C2PROFILE_HERE

Copy the content of the C2 profile code to this spot

C2PROFILE_NAME

Because there can be multiple c2 profiles selected when creating an agent, we have to do something special here. The intended use case here is when the payload has something like:

"#define C2PROFILE_NAME" or

"#include C2PROFILE_NAME"

for dynamic inclusion/definition based on the name of the c2 profile. To adjust this for multiple c2 profiles, the corresponding line will be duplicated for each c2 profile selected. Consider the instance where the agent has "#include C2PROFILE_NAME", but the user selected both the "default" and "smb" profiles. That line will be replaced with:

"#include default

#include smb"

C2PROFILE_EGRESS_NAME

Same as above, but only does this for egress profiles selected

C2PROFILE_P2P_NAME

Same as above, but only for P2P profiles selected

C2PROFILE_STRING_EGRESS_LIST

Writes out a list of strings representing all of the egress c2 profiles selected. for example, this can be used like:

"char** egress_profiles = [C2PROFILE_STRING_EGRESS_LIST]"

that will be turned into:

"char** egress_profiles = ["default", "websocket"]" as an example

C2PROFILE_STRING_P2P_LIST

Same as above, but only for p2p profiles

C2PROFILE_COUNT

Replaces this with the total number of C2 profiles selected

C2PROFILE_EGRESS_COUNT

Same as above, but only the count for the number of egress c2 profiles selected

C2PROFILE_P2P_COUNT

Same as above, but only the count for the number of p2p profiles selected

UUID_HERE

Writes the payload's UUID here. If you want this as a string, be sure to put " " around UUID_HERE. This replaces JUST the string "UUID_HERE" with the UUID, the rest of the line is unchanged.

COMMANDS_HERE

Loops through all of the commands that were selected, for each one read all of the content up until "COMMAND_ENDS_HERE" and write that in this location. This results in a flat list of commands like:

function1(params){

content

}

function2(params){

content

}...

This will overwrite the ENTIRE line that contains the string "COMMANDS_HERE" as it writes out all of the commands back-to-back. This will ONLY look at the file "command_name.payload_extension" for each command as it writes out the content. The use case for this pre-processor directive is to get command code into a single file payload.

COMMAND_COUNT_HERE

Writes the number of commands that are stamped in here. This is useful if you're keeping track of commands in some sort of array, you can pre-populate the size. This replaces JUST the string "COMMAND_COUNT_HERE", the rest of the line is unchanged.

COMMAND_STRING_LIST_HERE

Writes all of the command names that were selected as a list of strings: "command1", "command2", "command3"...

If you want this in an array, you can surround COMMAND_STRING_LIST_HERE with [ ] . This replaces JUST the string "COMMAND_STRING_LIST_HERE", the rest of the line is unchanged.

COMMAND_RAW_LIST_HERE

Writes all of the command names that were selected as a list of functions: command1, command2, command3 ...

This is useful when the function to be called matches the name of the function directly. To use this as an array of function pointers in C for example, just surround with [ ] :

void* myfuncs[COMMAND_COUNT_HERE] = [COMMAND_RAW_LIST_HERE];

This replaces JUST the string "COMMAND_RAW_LIST_HERE", the rest of the line is unchanged.

COMMAND_HEADERS_HERE

Loops through all of the commands that were selected, for each one, read the content after COMMAND_ENDS_HERE to the end of the file and writes that here. This is useful for when commands might need external declarations or functional prototypes elsewhere in the code (like in C).

This will replace the ENTIRE line that contains the string "COMMAND_HEADERS_HERE" as it writes out all of the content after "COMMAND_ENDS_HERE" in the command files. This will ONLY look at the file "command_name.payload_extension" for each command as it writes out the content. The use case for this pre-processor directive is to get command code into a single file payload.

WRAPPED_PAYLOAD_HERE

If you're creating a payload wrapper payload (i.e. a payload that contains another payload within it), then this is where you can specify that payload should go. Right now it's only available as a base64 string.

This replaces JUST the string "WRAPPED_PAYLOAD_HERE", the rest of the line is unchanged.

Additionally, for each file, Apfell checks the line for any C2 Profile Parameters and will replace them as needed. More about that in C2 Profile Code.

If Apfell doesn't find an instance of COMMANDS_HERE in any of the files, then Apfell assumes that the agent needs the commands to stay in their already defined folders and structures, so Apfell just copies them all into the same temp directory.

At this point, all of the code is stamped together, but more processing might need to happen. Apfell zips up the whole temp directory and, via RabbitMQ, sends it to the payload type's container. Inside the container, the code is unzipped to a temp directory, the creation transforms are executed, and the final payload is sent back via RabbitMQ to Apfell.

If you're curious about which file is the final one that Apfell treats as the payload, it's the contents returned by the final creation transform. So, it's up to the creation transforms to know which file is the final payload and return those bytes.

Commands

Commands should be self-containing - not relying on any other commands to be loaded. Commands should be able to expect access to C2 functions though.

Commands are able to send multiple responses back to Apfell throughout their execution by using the same task ID. This allows all of the responses to be appropriately tagged, tracked, and grouped together for the operator.

Last updated