Postex Kit
The Postex Kit opens up Cobalt Strike’s existing job architecture to allow users to write their own long running Postex tasks. The term “Postex Kit” encapsulates several different features that provide a huge amount of flexibility. The following sections detail these features, but for simplicity we have kept them under the “Postex Kit” umbrella.
The Postex Kit itself can be found in the Arsenal Kit. It is a Visual Studio project that makes it easy to develop and debug Postex DLLs. The kit includes a library of functions and abstracts the details of different aspects of the job architecture so that users can focus exclusively on their tooling. An example has been provided, to demonstrate the features of the kit.
Postex Kit vs Malleable C2
The settings found in the postex{} block in the malleable C2 profile apply to Cobalt Strike’s built-in postex tasks. These settings also apply to Postex kit Dlls except for amsi_disable and keylogger as they modify specific aspects of some of the Dlls. See Controlling Post Exploitation for more information on this functionality.
Execute-DLL
The execute-dll command can be used to execute Postex Kit DLLs. This command retrieves the DLL from the path provided, prepends a reflective loader and executes it in memory. The Postex DLL can be used with either fork/run or injected into a process. The PID of the current process will cause Beacon to use local injection (not remote injection).
Arguments can be passed to the DLL, however, it is important to note that this method creates an additional allocation of memory and simply copies the arguments into it (see Bi-Directional Comms for a nicer alternative).
execute-dll [pid] /path/to/postex.dll [args]
Output is returned to the user via a named pipe. The Postex job itself can also be seen in the jobs output and killed via 'jobkill [jid]'.
Aggressor Script and Functions
beacon_execute_postex_job() is an aggressor function which provides a lot more flexibility and provides a similar experience to Beacon Object Files. For example, it provides an opportunity to use Beacon Object File (BOF) style arguments via bof_pack(). These can then be used with the familiar Beacon Data Parsing/Format APIs that can be found in beacon.h.
In addition to the above, it is also possible to utilize Aggressor Script to "stomp" arguments into the Postex DLL itself instead of passing them directly. An example of these approaches can be seen in the Postex example DLL provided in the kit.
The following example shows how to pack some arguments and pass them to the Postex dll:
# pack static arguments $argument_string = "example static argument string"; $packed_arguments = bof_pack($beacon_id, "iz", 4444, $argument_string); # run the postex task... beacon_execute_postex_job($beacon_id, $pid, $postex_dll, $packed_arguments);
More details can be found in Aggressor Script and Functions.
There is also an optional 5th argument for beacon_execute_postex_job(). This is to provide advanced users with the ability to modify the default message ID of a given postex task. This functionality allows users to write custom Dlls without the postex kit and take control of all job output and processing.
Bi-Directional Comms
It is possible to communicate with Postex Kit DLLs. The bjob_send_data() function can be used to send data to the DLL over the named pipe. This offers a huge amount of flexibility, however, it’s important to note that the Postex Kit DLL must read any data on the pipe before Beacon is able to write any additional data to it. This design choice puts more responsibility on the developer, however, it ensures that Beacon’s checkins are not blocked by a Postex task. An example of bjob_send_data can be seen below.
bjob_send_data($beacon_id, $job_id, $data);
In addition, to read the data sent via the pipe, we have create the following APIs:
Check whether data is available
DWORD BeaconInputAvailable()
Read data from the named pipe (this is a blocking call, it will wait until X bytes are read)
BOOL BeaconInputRead(char* buffer, DWORD len)
The above APIs can be seen in the following example:
// Check for data on the pipe bytesRead = BeaconInputAvailable(); [...SNIP...] if (bytesRead > 0) { [...SNIP...] // Read the pipe and save the output to the buffer BeaconInputRead(pipeData, bytesRead); [...SNIP...] }
A working example can be found in the Postex Kit example.
Callbacks
Callbacks were originally introduced in CS 4.9. As part of 4.10, these were extended to provide a simpler user interface that provides better interoperability with the Postex Kit. For example, it is possible to dynamically retrieve a Postex tasks’ job id using callbacks which allows users to send data to registered jobs. Callbacks use the %infomap structure to provide relevant information to the user. This structure can be seen below.
-
type: A string value to represent the reason for the call. Possible values are output, error, job_registered, and job_completed.
-
type_id: The integer value of the output type: CALLBACK_OUTPUT, CALLBACK_OUTPUT_OEM, etc.
-
jid: The job id.
-
chunk_num: The id number of the output chunk.
-
is_final: The flag to identify the final chunk.
In the following example, we use a callback to determine when the job is registered, and then send arguments to it via bjob_send_data():
# define custom callback function $callback = lambda({ local('$bid $result %info $type'); this('$jid'); # get all arguments passed to lambda ($bid, $result, %info) = @_; # check the status/type of the job $type = %info["type"]; # if the job is registered, send data via the pipe if ($type eq 'job_registered') { $jid = %info['jid']; # send the pipe argument string to the DLL bjob_send_data($bid, $jid, $arguments); } # if the job is complete, null the job id else if ($type eq 'job_completed') { $jid = $null; } # print output to the console else { bjoblog($1, $jid, "received output:\n". $result); } }, $arguments => $arguments); # run the postex task... beacon_execute_postex_job($beacon_id, $pid, $postex_dll, $packed_arguments, $callback);
Beacon imposes a limitation to the size of Postex kit output. This limit is set by the amount of data Beacon can send back in one request. It is possible to send larger amounts of data, however, this output will be sent in chunks. The info map provided to the callback contains an id (chunk_num) for each chunk. In addition, the final chunk will be marked as is_final. This provides a mechanism to reconstruct large amounts of data.
sub read_file { local('$handle $data'); $handle = openf($1); $data = readb($handle, -1); closef($handle); return $data; } alias cs_postex_chunk_example { local('$bid $dll_path $dll_content') ($bid, $dll_path) = @_; $dll_content = read_file($dll_path); beacon_execute_postex_job($1, $null, $dll_content, $null, { local('$bid $result %info $type $jid'); this('$output'); ($bid, $result, %info) = @_; $type = %info["type"]; $jid = %info["jid"]; if ($type eq "job_registered") { # initialize the output buffer $output = ""; } else if ($type eq "output") { # append the chunk to the output buffer $output = $output . $result; if (%info['is_final'] == 1) { bjoblog($bid, $jid, "Received the final chunk. " . strlen($output) . " bytes in total"); # reset the output buffer $output = ""; } } }); }