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


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.


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.


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.


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

Threat Sharing Evolution: How Groups Offer Less Risk and Better Intelligence to Members

16 min read - Listen to this podcast on Apple Podcasts, Spotify or wherever you find your favorite audio content. In 2019, the World Economic Forum advocated for increased threat intelligence sharing by arguing that cybersecurity is a “public good.” Meaning, if organizations — both public and private — share threat information across groups, everyone has a clearer picture of the threat landscape and with it, the ability to better defend against increasingly aggressive and sophisticated threat actors. In response, multiple threat-sharing groups have sprung to life,…

16 min read

The Trickbot/Conti Crypters: Where Are They Now?

23 min read - Despite Conti shutdown, operators remain active and collaborative in new factions In IBM Security X-Force, we have been following the crypters used by the Trickbot/Conti syndicate, who we refer to as ITG23, since 2021 and demonstrated the intelligence that can be revealed through tracking their use in a blog we published last May. One year on, ITG23 has experienced many organizational changes, splintering into factions and forging new relationships. Despite these events, ITG23 crypters remain fundamental to tracking post-ITG23 factions…

23 min read

ITG10 Likely Targeting South Korean Entities of Interest to the Democratic People’s Republic of Korea (DPRK)

7 min read - In late April 2023, IBM Security X-Force uncovered documents that are most likely part of a phishing campaign mimicking credible senders, orchestrated by a group X-Force refers to as ITG10, and aimed at delivering RokRAT malware, similar to what has been observed by others. ITG10's tactics, techniques and procedures (TTPs) overlap with APT37 and ScarCruft. The initial delivery method is conducted via a LNK file, which drops two Windows shortcut files containing obfuscated PowerShell scripts in charge of downloading a…

7 min read

Poor Communication During a Data Breach Can Cost You — Here’s How to Avoid It

5 min read - No one needs to tell you that data breaches are costly. That data has been quantified and the numbers are staggering. In fact, the IBM Security Cost of a Data Breach estimates that the average cost of a data breach in 2022 was $4.35 million, with 83% of organizations experiencing one or more security incidents. But what’s talked about less often (and we think should be talked about more) is how communication — both good and bad — factors into…

5 min read