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 |
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