๐จโ๐Detecting Cobalt Strike Fork&Run
Last updated
Last updated
Cobalt Strike (CS) is one of the most effective post-exploitation frameworks and is popular among red teamers and adversaries alike. One of the things that stood out to me while playing with Cobalt is its fork&run behavior. Detecting the fork&run through the default named pipes is well-established (see example detections on Splunk, Red Canary, and SigmaHQ). However, not much is written on detecting the fork&run behavior itself, hence this blog post.
Operators can change the fork&run behavior by using the PROCESS_INJECT_SPAWN
and PROCESS_INJECT_EXPLICIT
hooks, but detecting the default behavior is still valuable because customizing the hooks is not something that an average adversary is expected to be able to do. In addition, using the injection hooks has its own tradeoffs (not applicable for all post-ex capabilities, does not obfuscate injected payload) and thus won't be always used. Further, while the process injection hooks provide flexibility over the fork&run behavior, not everything is easily customizable as we shall see (spoiler: named pipes behavior).
Cobalt strike provides two methods to execute post-exploitation capabilities inside a remote process: fork&run and explicit injection.
Fork&run: this is where the PROCESS_INJECT_SPAWN
hook is used and is the default when an operator does not provide a target process pid. The beacon will create a new sacrificial process, inject the post-ex capability into it, and start listening for output using the preconfigured named pipe. The sacrificial process and its command line arguments are configurable through the malleable profile. Further, the name of the named pipe is also configurable in the malleable profile, and in some cases, an anonymous pipe is used as pointed out by WithSecure.
Explicit: this is where the PROCESS_INJECT_EXPLICIT
hook is used. CS will use it when the operator selects a process from the process viewer and, for example, click on the screenshot
button, or when the operator provides a pid when executing the post-ex capability. The beacon will inject into an existing process and start listening for output using the preconfigured named pipe.
As an occasional CS user, I must admit that I enjoy and admire its flexibility. Looking at fork&run as an example, everything seems configurable. I can configure the name of the process to be spawned, its command line arguments, and also the named pipe it will create. This makes detection much harder. There is nothing special about creating a process, creating a pipe, or connecting to one. For a blue team to detect this, the attacker must make a mistake like specifying an abnormal child process or not including a command line when a command line is expected. Even the behavior that is not easily customizable like the creation of a named pipe by the child and listening to it by the beacon is now controllable thanks to the process injection hooks. As an attacker, you could hook CreatePipe and CreateFile to change the behavior to, for example, writing and reading from a file or shared memory.
We established that an attacker can easily influence single events in the fork&run attacks using the malleable profile. However, some characteristics of fork&run are not easily changed, and maybe we can rely on these characteristics for detection\hunting. For example, the attacker cannot change the fact that the child process is creating the named pipe, or that the parent process is communicating with an external IP and connecting to the named pipe that the child has created. Looking at the behavior as a whole instead of trying to detect individual events creates some interesting detection opportunities. The following screenshot shows the sequence of events that gets generated when fork&run is used
This sequence of events can be translated into the following detection
Note that the time window and sequence are key factors here. The entire chain occurs in less than five seconds and always occurs in the same sequence. Using our datasets and taking the time window and sequence into consideration, this behavior was unique to CS fork&run and did not generate any FPs. In addition, the fact that these events are captured does not mean that all of them must be used for detection to be accurate. For example, dropping step four from the detection logic and testing against a our datasets produced no FPs. This logic can, and should, be customized depending on the environment and available resources.
If you think that this is a bullet-proof detection for CS, then I am sorry to disappoint you! This detection can be bypassed if the process injection BOFs are modified, and there are also some corner cases to consider. After all, CS is meant to be flexible and its maintainers are doing a great job to ensure it is!
Four challenges that should be mentioned when discussing the application of this detection: pivoting, sysmon configuration, anonymous pipes, and resource consumption. The first three challenges are related to potential blind spots, and the last challenge is more about your SIEM engineers being unhappy because you threw this detection on the SIEM without proper testing. Let's discuss them one by one.
The detection assumes that the beacon process will communicate with an external address. This detection cannot detect fork&run in the later stages of the attack where SMB beacons are used for pivoting since no external connections are made by the SMB beacon. This limitation can be addressed by changing from external to any communication in the logic.
If you rely on sysmon while researching or if your organization is using sysmon in production, your sysmon configuration might affect the results. For example, using the default sysmon-modular configuration and detection logic based on the sequence shown above will not detect the jquery 4.7 profile. This is because the jquery
profile is leveraging Dllhost.exe
as a spawnto
option, and Dllhost.exe
is excluded from the process creation event of the default sysmon-modular configuration. This means that the first event in the sequence will not be captured. In addition, the fourth event in the sequence will also not be captured because the named pipe in the malleable profile is not included in the sysmon-modular configuration. The following screenshot shows the captured sysmon events. As you can see, event IDs 1 and 18 are missing due to the combination of sysmon-modular configuration and the malleable profile.
Validating that your current sysmon config or telemetry provider can capture events from different malleable profiles is worthwhile. Therefore, I am working on a tool that will simulate CS behavior in the event log and hopefully it can be released soon.
As noted by WithSecure, not all post-exploitation jobs create a named pipe. Some jobs like execute-assembly will use anonymous pipes and that will change the sequence of events to the following
Something that stood out to me in this sequence is event ID 18 (Pipe Connected). This event triggered when the parent (P1) connected to the pipe but did not trigger when the child connected. I looked it up and, according to the documentation, event ID 18 should have never triggered because it triggers only on named pipe connections, not anonymous pipes. If you know why the event was generated on the parent connection but not the child, please let me know ;-)
Temporal near real-time detection rules might cause high SIEM resource consumption. Depending on how your SIEM work, you might want to limit this detection to threat-hunting exercises or properly configure the time window.
And that's it for today ;-)