In this post, we’ll review a simple technique that we’ve developed to encrypt Cobalt Strike’s Beacon in memory while executing BOFs to prevent a memory scan from detecting Beacon.

Picture this — you’re on a red team engagement and your phish went through, your initial access payload got past EDR, your beacon is now living in memory and calling back to you. The hard part is over, time to do some post-exploitation. You fire up your trusty BOF toolkit and watch the “last” timer tick up indefinitely.

While an initial beacon can go undetected, performing common post-exploitation activities from a Beacon Object File can trigger a memory scan of your process by EDR. This can result in an EDR product finding your Beacon sitting in memory and killing the process.

Cobalt Strike (somewhat) recently introduced the Sleep Mask functionality, which serves to hide Beacon in memory while it’s sleeping. This helps prevent detection by threat hunting tools or memory scanners that look for Beacon signatures or suspicious artifacts like unbacked executable memory. As of Cobalt Strike 4.7, Sleep Mask is implemented as a BOF, which provides the operator with much more control over how Sleep Mask works. This demonstrates that it is possible to have Beacon encrypted and sleeping during BOF execution. However, during normal BOF execution, Beacon is sitting in memory. Let’s look at how to change that.

Beacon object file basics

If you’re unfamiliar with the internals of Beacon Object Files (BOFs), they’re essentially a way to write position independent code where Beacon handles loading and linking any dependencies. This allows operators to quickly develop post-exploitation tooling without the hassle of writing shellcode or reflective DLLs.

When you execute a BOF, it looks something like this:

  1. Beacon allocates memory according to your Malleable C2 settings and writes the BOF content
  2. The BOF loader handles linking any imported functions and finding the specified entry point of the BOF
  3. Execution is passed to the entry point, your BOF content runs, and Beacon resumes executing
  4. Allocated BOF memory is cleaned up according to your Malleable C2 settings

This blog is not intended to be a reference on BOFs, so you can find more information about BOFs here:

Finding Beacon’s base address

To mask Beacon in memory, we need to know its base address and its size. There are a few ways we could figure out this information, but the method I found to be most reliable uses a bit of assembly and the VirtualQuery API.

When a BOF is executed, and we call a function from our BOF entry point, our stack frame looks like this at the top:

  1. Current function
  2. BOF entry point
  3. Beacon

Below is a snippet of assembly to go back two stack frames. This will get us from our function to our BOF entry point, to the return address of Beacon. This will give us the address that Beacon will resume executing from after our BOF finishes, which is inside Beacon’s .text section.

Now that we have an address for Beacon’s memory range, we need to find its base address. We can accomplish this with two calls to VirtualQuery. The first one will get us the base address of the region that the previous address is in, and the second will give us the base address and size of the allocation that was made for Beacon. These second two values are what we will need for masking.

Accounting for malleable C2 and UDRLs

One of Beacon’s greatest features is how it exposes flexibility to operators at many points. There are two main mechanisms for changing how Beacon is loaded into memory: Malleable C2 settings and User Defined Reflective Loaders. Here are some links diving into both:

We need to account for the fact that Beacon might be loaded into memory in different ways that will break our VirtualQuery logic. When you call VirtualQuery on a region of memory, it will return results for all of the following pages in memory that share the same attributes (memory protection and page state). So if Beacon is allocated entirely with RWX permissions then calling VirtualQuery twice works perfectly. But if you properly set the memory protections for each section of Beacon, then calling VirtualQuery on the base address of Beacon will only return the size of the NT Header section, since it will have a different memory protection setting than the .text section.

Thankfully, compensating for this is pretty straightforward. We can query the next region in memory and verify that it is executable and that the return address we got for Beacon earlier falls within that region. If it does, then we’ve found Beacon’s .text section!

Masking Beacon

Now that we have the base address and size of Beacon’s .text section, we can change the page protection and then apply our mask.

In the snippet above, we call VirtualProtect to change Beacon’s page protection to RW, and then apply a simple XOR mask across our Beacon. We generate this random mask once in our setup function for simplicity. To unmask Beacon, we just reverse the order of these two operations by applying the XOR again and changing Beacon’s page protection to whatever it was before (RWX or RX).

Demonstration

For demonstration purposes, we have a BOF that just calls MessageBoxA to block execution and give us an opportunity to scan the memory of our Beacon process with some common memory analysis tools: Moneta, PESieve, and YARA.

Without Masking

Here is Moneta reporting three unbacked RX regions. This is our Beacon, our Sleep Mask BOF, and the BOF we’re currently executing. This may look different for you depending on your Malleable C2 profile.

Here is PE-Sieve with the /shellc and /data three options dumping out a region that we can see is our Beacon.

Here is a YARA detection from this set of rules published by Elastic to detect Cobalt Strike.

With Masking

Now we can apply our masking code to hide Beacon in memory and try again. Here is Moneta reporting on our Sleep Mask BOF and our currently executing BOF, but not our Beacon.

PE-Sieve output with zero detections.

And no detections from YARA.

As you can see, this technique does yield results and should help prevent memory scanners from detecting our Beacon via signature-based detections or memory artifacts during BOF execution. The presence of our current BOF and our Sleep Mask BOF are not ideal, as they are unbacked RX region IOCs, but EDR may not alert solely on this if no signatures are identified during the memory scan.

If these unbacked memory regions are a concern, it is relatively simple to identify and mask the Sleep Mask BOF in a similar fashion. As for masking the current BOF, it should be possible to mask with some existing ROP techniques, but it gets very difficult for more complex BOFs.

Caveats

The most important thing to note with this technique is that you CANNOT call Beacon APIs from your BOF while Beacon is encrypted — this means any of the internal Beacon APIs like BeaconPrintf, BeaconSpawnInject, etc. Since these functions are located in the .text section of Beacon you will be passing execution to non-executable garbage code and your Beacon will die. If you have output that you need to get from your BOF then you can either send it all back after unmasking, or you can toggle the mask before/after every Beacon API call.

Integration with existing tooling

To allow red teams to simulate more advanced threat actors and allow blue teams to be more familiar with memory scanning evasion techniques, we wanted to implement this technique so that integrating it with your BOF arsenal is as easy as possible. To that end, we’ve published this project as a single C header file that you can include in your existing BOF. You just need to call the GetBeaconBaseAddress function before calling MaskBeacon for the first time, and then you can call the MaskBeacon/UnmaskBeacon functions to toggle the mask. An example BOF entrypoint would look like this.

If you want to call Beacon APIs inside of your code, you can just toggle the mask like this.

Stability is always a concern when operating over a C2 channel, and errors in a BOF are a great way to kill your hard-earned Beacon. We have tried to make this code as reliable and stable as possible, but there is always the chance that something can go wrong. If your BOF relies on any Beacon API calls, you should thoroughly test to ensure that you will not hit any snags during execution as a result of the masking. The same goes for using this code with any complicated loaders — you should ensure that the .text section of Beacon is properly located before executing. Some debugging directives have been included to help with troubleshooting.

Detections

As with any C2 related subject matter, detection is a chief concern. However, detecting BOF-specific execution is not a particularly useful area to focus on. BOFs are ultimately just position independent code being loaded with a handful of benign API calls. Detection efforts are much better spent on detecting Beacon execution and post-exploitation activity. For a BOF to be useful it must generate some activity on the host or the network, and hunting these behaviors is far more fruitful. Some example BOF activities could include enumerating the local host or Active Directory, credential dumping activities, or injecting into another process.

All of that said, this technique does leave the executing BOF and a Sleep Mask BOF in memory as unbacked RX (or RWX) regions. These are generally good indicators of malicious activity for threat hunters and memory scanners alike. However, as mentioned above, there are ways that these artifacts can be hidden by a skilled operator.

Below is a non-comprehensive list of resources that you can use for detecting Cobalt Strike and performing memory analysis.

Conclusion

In this post, we’ve shown how we can apply the same principle as Beacon’s Sleep Mask kit to provide some extra OPSEC to Beacon during BOF execution. We believe that this is a relatively simple technique that can provide big returns against products that utilize memory scanning and static signature detection.

You can find the published header file here.

Learn about adversary simulation services from IBM X-Force here.

More from Adversary Services

Getting “in tune” with an enterprise: Detecting Intune lateral movement

13 min read - Organizations continue to implement cloud-based services, a shift that has led to the wider adoption of hybrid identity environments that connect on-premises Active Directory with Microsoft Entra ID (formerly Azure AD). To manage devices in these hybrid identity environments, Microsoft Intune (Intune) has emerged as one of the most popular device management solutions. Since this trusted enterprise platform can easily be integrated with on-premises Active Directory devices and services, it is a prime target for attackers to abuse for conducting…

Racing Round and Round: The Little Bug That Could

13 min read - The little bug that could: CVE-2024-30089 is a subtle kernel vulnerability I used to exploit a fully updated Windows 11 machine (with all Virtualization Based Security and hardware security mitigations enabled) and scored my first win at Pwn2Own this year. In this article, I outline my straightforward approach to bug hunting: picking a starting point and intuitively following a path until something catches my attention. This bug is interesting because it can be reliably triggered due to a logic error.…

Q&A with Valentina Palmiotti, aka chompie

4 min read - The Pwn2Own computer hacking contest has been around since 2007, and during that time, there has never been a female to score a full win — until now.This milestone was reached at Pwn2Own 2024 in Vancouver, where two women, Valentina Palmiotti and Emma Kirkpatrick, each secured full wins by exploiting kernel vulnerabilities in Microsoft Windows 11. Prior to this year, only Amy Burnett and Alisa Esage had competed in the contest's 17-year history, with Esage achieving a partial win in…

Topic updates

Get email updates and stay ahead of the latest threats to the security landscape, thought leadership and research.
Subscribe today