概述
在本篇文章中,我们主要讨论一般的点对点(P2P)命令与控制协议的设计与实现,以及在Covenant(一种开源的命令与控制框架)中实现的点对点具体示例。
命令与控制
命令与控制(C2)是指在单个或一组目标受害者主机上建立和维持对植入工具的控制的过程。C2框架通常提供借助某个通信协议与植入工具进行通信的能力,向受害者系统发出命令,并且在C2服务器上接收这些命令的输出,使攻击者实现物理访问或直接的虚拟访问。
P2P命令与控制
命令与控制(C2)协议依赖于每个受控植入工具和C2服务器之间的同步或异步通信信道。点对点(P2P)C2协议允许单个植入工具维持与C2服务器的通信信道,所有其他的植入工具都是通过“网状”的网络进行通信,通过单个植入工具的出口汇集通信。
优点
对于攻击者来说,这种方式有许多优点。其中之一是,较少的出口通信信道为可能识别恶意流量的防御者提供较少的潜在指示。防御者倾向于优先考虑组织网络边界出口的流量检查,而不是内部流量。如果存在足够大量的植入工具,增加的出口通信可能会使防御者检测到恶意活动。
这种方式的另一个优点是访问。受到良好保护的网络可能会在内部处理某些类别的流量,而不是在网络边界进行处理。一个常见的例子是,允许HTTP流量通过其网络边界出站的组织,允许员工浏览互联网,但可能限制源自服务器或其他重要子网的HTTP流量。这种类型的限制,可以防止依赖于每个植入工具的公共C2框架建立出口通信信道,以维持对这些首先服务器或高价值网络子网的访问。
C2框架可以利用不同类型的通信信道作为出口流量,以此来取代网状网络,从而解决这一问题。
在将第一个植入工具传播到目标网络,并对该协议进行测试之前,我们通常无法确定能够确保成功的协议。但遗憾的是,除非我们有大量的时间和资源,否则要将新协议实施到C2框架中,往往是没有时间的。作为C2框架的开发人员,我们能做的事情就是尽可能多的实施协议,以及提前判断出有把握的协议类型,从而尽最大可能取得成功。
选择协议
在SpecterOps,我们发现有许多相同的协议在我们自行测试的大多数网络上取得成功。这样的规则当然存在例外,但我们可以合理地确定一组协议的合法业务用途,并对这些合法的使用采取放行的措施。例如,如果组织希望员工能够浏览互联网,就需要允许HTTP在网络边界出站。这也是HTTP成为C2框架的公共出口通信协议的几大原因之一。但是,如前所述,通常不会允许从高价值区域的网络使用HTTP协议。
对于排在其后的内部通信协议,我们必须要依靠受害者系统的技术来实现通信信道的两个端点,这将会影响我们决定选择哪一种协议。理想情况下,我们希望协议能与网络的正常流量相结合,这在我们植入的工具中很容易实现。
在SpecterOps,我们发现组织通常主要依赖于Windows栈来进行业务操作,而Windows系统内部一个非常常见的协议就是SMB。命名管道(Named Pipe)是一种本地Windows技术,允许通过SMB访问协议跨远程系统实现进程间通信,并且非常容易实现Windows植入。
由于上述原因,Covenant利用HTTP作为出口协议,并将SMB命名管道用于网状协议,这是这类行为的一组通用协议。
但是,有许多不同的协议可以用于点对点协议。借助成功的点对点协议,我们需要考虑的就仅仅是一种能在两个系统之间读写数据的方法。在Windows网络上,存在许多进程间通信的方法,包括WMI、Mailslot、活动目录(AD),甚至是原始TCP或UDP套接字。但是,现在,我们通过命名管道来使用SMB协议。
扩展后的图示
现在,我们已经定义了想要使用的协议,我们需要扩展P2P网状结构(图示),使该过程超过1跳(Hop)。由于我们已经具有能够连接被攻陷终端的协议,我们可以将这一行为扩展到完整的P2P网状结构中,该网状接口在几个嵌套的被攻陷终端之间跳跃。
这些连接的终端,形成一个被攻陷终端的有向无环图(DAG)。随着这些信道复杂性的增加,植入工具现在必须跟踪连接的“子”边界,并且知道哪个边界最终能通向C2服务器。攻击者必须再次列出图示,并进行思考。
理论上,我们还可以为C2创建一个有向循环图。这意味着,植入工具的消息可以重新流经它已经经过的节点(即:A->B->G->A)。我们认为,大家会尽量避免这一方法,因为这样会增加植入工具的复杂性,因此我们也忽略了这一选项,并将该图示作为DAG的方法参考,用于本文的后续部分。
具体实现
可能存在多种方法可以实现上述目标,我们将特定于Covenant,分析具体的实现细节,但我认为这些方法会在许多不同的框架中使用。在Covenant中,每个植入工具(在此称之为“Grunt”)仅仅负责了解每个边界在哪里,以及哪个边界最终通向C2服务器。Grunt不负责了解服务器的完整路径,或嵌套子Grunt的完整路径。
Covenant将维护一个图示结构,它必须使用这一图示来制作消息,所有中间Grunt可以沿着图示中的路径方向来发送消息。通过这些路径发送到出口Grunt的Covenant消息将会被混淆,并填充到HTTP消息之中,但最终这些消息会被转换为GruntEncryptedMessage JSON结构,如下所示:
{ "GUID" : "3630b87066", "Type" : 1, "Meta" : "", "IV" : "srwjK0WYY/XvFBPXGD7MOg==", "EncryptedMessage" : "bU86lKwVJn1sb5m7U1Kekr3qg2KjKyNZeOP1rVoshjGpVK5LURcfYN6sWWGs+20NJUfhz7Jj6o7geAymVwJknRV2s4+Y8uDnRndGxZZKCjiiBGVWMVCPWewRzA93U+SLM55hqdmuLWLR2SPjxGzR5A==", "HMAC" : "n+whkeAvDV+RcLPvazmkakrX8oPwBCCSqRQ8Pte9Ayo=" }
Grunt接收上述结构,并以某种方式确定发送消息的方向。举例来说,我们使用更加简单的名称来标记所有Grunt,并逐步完成整个过程:
C2服务器将GruntEncryptedMessage(图中称为G(message))传递给A出口Grunt,该出口Grunt已被精心设计为传递给G连接的Grunt。GruntEncryptedMessage如下所示:
{ "GUID" : "G", "Type" : 1, "Meta" : "", "IV" : "srwjK0WYY/XvFBPXGD7MOg==", "EncryptedMessage" : "bU86lKwVJn1sb5m7U1Kekr3qg2KjKyNZeOP1rVoshjGpVK5LURcfYN6sWWGs+20NJUfhz7Jj6o7geAymVwJknRV2s4+Y8uDnRndGxZZKCjiiBGVWMVCPWewRzA93U+SLM55hqdmuLWLR2SPjxGzR5A==", "HMAC" : "n+whkeAvDV+RcLPvazmkakrX8oPwBCCSqRQ8Pte9Ayo=" }
A Grunt并不清楚解密此消息所需的密钥,也不知道如何将此消息路由到Grunt G。那么,如何解决这一问题呢?
路径方法
要解决这一问题,有一种选择是,可以将整个路径作为GruntEncryptedMessage结构中的新字段包括在内,如下所示:
{ "GUID" : "G", "Path" : ["A", "B", "G"] "Type" : 1, "Meta" : "", "IV" : "srwjK0WYY/XvFBPXGD7MOg==", "EncryptedMessage" : "bU86lKwVJn1sb5m7U1Kekr3qg2KjKyNZeOP1rVoshjGpVK5LURcfYN6sWWGs+20NJUfhz7Jj6o7geAymVwJknRV2s4+Y8uDnRndGxZZKCjiiBGVWMVCPWewRzA93U+SLM55hqdmuLWLR2SPjxGzR5A==", "HMAC" : "n+whkeAvDV+RcLPvazmkakrX8oPwBCCSqRQ8Pte9Ayo=" }
Grunt A将检查此消息,会发现Grunt B是路径中的下一个Grunt,并且知道如何路由到Grunt B,因为它与图中的直接边界相连接。Grunt B将执行类似的操作,并将消息传递给Grunt G,Grunt G能够解密并解析来自服务器的消息。
局限性
然而,这种方法存在着一些缺点。首先,其实现方式违反了我们的前提,即:Grunt应该只知道通过直接边界连接的Grunt。通过查看单个消息,Grunt可以了解整个图示中的全部路径。类似地,如果防御者拦截了某条消息,或者以其他方式获得对GruntEncryptedMessage的访问权限,则他们可以获得对P2P图示的深入了解,并且可能发现与实际尝试传递的消息无关的植入工具网状结构的大部分内容。更糟糕的是,这部分信息是未经加密的。我们通过使用加密密钥交换协议,得到了来之不易的加密属性,都因为这一条纯文本(明文)路径而功亏一篑,这使得防御者更容易识别整个网络中的活动。尽管这些Grunt标签可能与内部主机名不直接相关吗,但它仍然能为防御者提供有关运营活动范围的不必要信息。
递归方法
Covenant通过使用递归GruntEncryptedMessage结构来避免此问题。他们没有将整个路径以明文字段的形式放在消息中,而是递归了加密路径。例如,我们将最后的消息传递给Grunt G:
{ "GUID" : "G", "Type" : 1, "Meta" : "", "IV" : "srwjK0WYY/XvFBPXGD7MOg==", "EncryptedMessage" : "bU86lKwVJn1sb5m7U1Kekr3qg2KjKyNZeOP1rVoshjGpVK5LURcfYN6sWWGs+20NJUfhz7Jj6o7geAymVwJknRV2s4+Y8uDnRndGxZZKCjiiBGVWMVCPWewRzA93U+SLM55hqdmuLWLR2SPjxGzR5A==", "HMAC" : "n+whkeAvDV+RcLPvazmkakrX8oPwBCCSqRQ8Pte9Ayo=" }
我们将整个JSON结构视为一条消息,并使用Grunt B的对称密钥对其进行加密,并将结果值填充到为Grunt B制作的消息的EncryptedMessage字段中:
{ "GUID" : "B", "Type" : 0, "Meta" : "", "IV" : "jKbU80WYY/XvFBPwVJnPWd==", "EncryptedMessage" : "kr3qg2KjKbU86lKwVJn1sb5m7U1Kekr3qg2KjKyNZeOP1rVoshjGpVK5LURcfYN6sWWGs+20NJUfhz7Jj6o7geAymVwJknRV2s4+Y8uDnRnd6lKwVJnGxZZKCjiiBGVWMVC6lKwVJnPWewRzA93U+SLm7U1Kekr3M55hqdmuLWL20NJUfhz7=", "HMAC" : "KwVJnPAvDV+RcLPva93U+krX8oPwBCCSqRQ8PtLPvaz=" }
我们再一次采用这样的实现方式,通过整个JSON结构,使用Grunt A的对称密钥对其进行加密,并将结果值填充到为Grunt A制作的消息的EncryptedMessage字段中:
{ "GUID" : "A", "Type" : 0, "Meta" : "", "IV" : "geAymV0WYY/XvFBqg2KjKyNZ==", "EncryptedMessage" : "6lKwVJn1sb5m7U1Kekr3qg2KjKyNZeOP1rVoshAymVwJknRV2sjGpVK5LURcKbU86lKwVJn1sb5m7U1Kekr3qg2KjKyNZeOP1rVoshjGpVK5LURcfYN6sWWGs+20NJUfhz7Jj6o7geAymVwJknRV2s4+Y8uDnRnd6lKwVJnGxZZKCjiiBGVWMVCb5m7U1Kekr36lKwVJnPWewRzA93U+SLm7U1Kekrb5m7U1Kekr3=", "HMAC" : "Rnd6lKwVJV+RcLPva93U+krX8oPwBCCSqkr3qg2KjKy=" }
使用此方法,Grunt解密GruntEncryptedMessage中的EncryptedMessage字段,并检查解密结构的GUID字段,以确定将消息继续路由到哪一个Grunt。重要的是,“Routing”(路由)消息的类型字段设置为0,“Delivery”(传递)消息的类型字段设置为1。这一变量用于指示应查看消息,还是应该将消息沿路径继续传递。
这种递归方法,消除了先前提出的中间人可以观察到整个路径的问题。我们使用现有的逐Grunt的对称密钥,通过加密密钥交换协商的方式,使用密码来加密观察者和Grunt的路径。由于采用了递归加密操作,导致最终的GruntEncryptedMessage将会更大。需要注意的是,Grunt也不应该按顺序标记,如上面的例子所示。但幸运的是,这仅仅是一个例子,Covenant在实践中不会使用这样的行为。
C2的命名管道
至此,我们已经详细介绍了路由实现,但没有详细介绍访问协议本身的实现细节。尽管通常被称为“SMB命名管道”,但我们真正要做的是通过SMB访问协议远程访问命名管道。命名管道连接由NamedPipeServer和NamedPipeClient组成。在.NET中,可以使用NamedPipeServerStream和NamedPipeClientStream对象访问这些对象。
每个端点Grunt(即:除了出口Grunt之外的所有Grunt)将为每个直接连接的子Grunt提供一个NamedPipeServer和一个NamedPipeClient。NamedPipeServer监听管道名称,并等待客户端的连接。NamedPipeClient连接到特定主机名和管道名称的服务器,从而创建命名管道。管道的每一个终端都有从另一个终端读取和写入的能力。
这一过程有时被称为“绑定”(Bind)连接,这意味着NamedPipeServer在子Grunt上启动,父Grunt创建与NamedPipeClient的连接。也可以创建“反向”连接,这意味着NamedPipeServer在父Grunt上启动,而子Grunt创建与NamedPipeClient的连接。
无论选择哪种方式,最终的结果都是我们有一个命名管道连接,具有从任一端点读取和写入的能力。但是,Covenant目前只实现了“绑定”(Bind)命名管道连接。
在打开新的命名管道时,我们需要考虑以下几个选项:
1. PipeName(管道名称):管道的两端必须就管道名称达成一致。默认情况下,Grunt使用gruntsvc作为管道名称,这一名称比较显眼。我们强烈建议自定义此名称,Covenant甚至允许我们为NamedPipeServer使用自定义的管道名称。
2. Direction(方向,可以是In、Out、InOut):命名管道流可以是只读、只写或读写。对于P2P通信,我们总是需要使用读写(InOut)。
3. TransmissionMode(传输类型,可以是Byte或Message):命名管道可以作为字节流或序列化消息进行通信,二者都可以在这里工作,但由于一些原因,Covenant使用了字节(Byte)模式,并且必须自行对消息进行序列化。管道的读取端将字节作为连续流进行读取,并且必须具有一些确定它是否已经到达消息结束的方法。具体而言,有两种普遍采用的方法,一种是分隔字符(即:空终止字符串),另一种是预先设定消息的长度,并读取字节到这个长度。Covenant选择了后一种方法,以避免对消息中可用的字符集进行限制。
4. SecurityDescriptor(安全描述符):命名管道是安全对象,这意味着它们受到一个或多个安全描述符的保护。我们可以应用仅允许HOST01和HOST02上当前用户访问管道的安全描述符,但是植入工具通常需要执行一些非典型的凭证管理。例如,令牌操作可能会使我们的命名管道无法访问新用户,并将图示的一部分“孤立”出来。出于这个原因,Covenant应用了一个允许FullControl到Everyone组的安全描述符,以确保我们不会无意中使管道无法访问。
在Covenant中使用命名管道时,有一个重要的注意事项,就是两个Grunt植入工具不能在同一主机上共享相同的管道名称。但是,可以在具有不同管道名称的同一主机上启动新的Grunt植入工具。
检测
检测一般的点对点恶意流量和恶意命名管道可能非常困难,但并非不可能。通常,检测植入工具自身的恶意行为,比检测点对点协议要更为容易。
但是,有一些指标可以被发现。其中,许多检测都需要对某些指标进行基线测试,并将新行为与此基线进行比较,以便进一步了解事件发生的异常情况。
主机检测
1. 已知的恶意命名管道名称:这种检测方式最为明显,也最容易得出结果。C2框架可能会使用容易被检测到的默认命名管道名称。例如,Covenant使用gruntsvc作为默认管道名称,而Cobalt Strike使用msagent作为默认管道名称。这两个管道名称都可以在两个框架内轻松更改,但使用这一指标来检测未更改默认值的攻击者可能会非常有效。
Sysmon可以用于收集有关命名管道的信息。创建命名管道时,会生成Sysmon事件ID 17。我们可以根据已知恶意管道名称列表来检查PipeName字段。
2. 与命名管道相关的异常进程:为了正确利用这种检测方式,组织中应该使用一种方法来收集与命名管道相关的进程,并对其进行基线化。同样,Sysmon事件ID 17具有此检测所需的所有信息。我们应该在一段时间内收集这些事件,并将新事件与其基线进行比较。
例如,Chrome有一组通用的命名管道,它们通常采用“mojo.#####.#####.##################”的格式,其中#代表一个数字。例如:
如果针对与命名管道相关的进程进行基线化的结果显示,C:\Program Files (x86)\Google\Chrome\Application\chrome.exe从不或者几乎没有创建不匹配“mojo.####.#####.####################”格式的命名管道,那么如果存在一个新事件,显示C:\Program Files (x86)\Google\Chrome\Application\chrome.exe创建了一个不符合上述格式的新命名管道(例如:pipe123),这就是异常的:
像这样的异常命名管道可能值得我们的研究。在理想情况下,该检测应该与其他主机指标共同使用,以便为潜在的攻陷判断添加额外的上下文。
3. 异常命名管道安全描述符:如本文前面所述,Covenant将单个安全描述符应用于其命名管道,允许Everyone组进行FullControl。这不是绝对必要的,但可以提高运营者的可用性。例如,对父Grunt上的令牌操作这样的操作,可以改变用户与命名管道交互的方式,并且如果安全描述符仅限于特定用户,可以实现限制使用命名管道。例如,我们可以看到Grunt命名管道使用James Forshaw的NtObjectManager项目来应用这个安全描述符:
组织可以利用这一方法,与可用性进行权衡,通过搜索安全描述符来识别潜在的恶意命名管道。如果单独使用这一检测方法,可能会导致误报。理想情况下,这应该被用作增强其他相关指标严重性的补充指标。
网络检测
用于识别点对点流量的基于网络的检测,必须依赖于受影响组织的内部网络上捕获地数据。在许多组织中,都不会收集这些数据。但我们认为,组织至少应该在内部网络的某些关键节点收集内部网络流量。具体而言,这一收集的位置和收集的数量,应该始终针对于特定的组织。
网络流量模式通常可以从基于主机的指标推断出来。例如,从一个系统到另一个系统的的Windows事件4624(登录事件)意味着从第一个系统到第二个系统的网络连接。但是,从主机收集的数据存在一个缺点,就是它更容易受到攻击者对受损终端的操纵。
下面提到的一些检测方式,一些可能依赖于从网络明确收集的数据(即:“网络流”数据),同时还包含一些可以基于主机日志推断的网络活动,并且还有一些可以通过任一方式来收集。
1. 远程连接到命名管道:点对点命令与控制依赖于远程访问的命名管道。我们可以收集从远程系统访问的命名管道基线。要收集这些数据,可以通过两种方法。一种方法是纯粹的网络方式,使用某种网络镜像或流量收集工具,可以检查SMB网络流量,以查找被访问的命名管道。举例来说,我们可以使用Wireshark看到访问命名管道的流量:
第二种方法是基于主机的方法,我们可以使用Sysmon 事件ID 18,它负责记录到命名管道的新连接,并识别并非来自当前主机的连接。事实证明,当远程访问管道时,Image字段设置为System。但遗憾的是,我们没有得到原始主机名,但至少我们可以确定远程访问管道:
2. 异常的主机到主机流量:这种最终的检测方式比检测命名管道更为有效。我们可以通过将正常的内部主机到主机的流量进行基线化,来尝试识别主机到主机的流量。在对这类活动进行基线化处理之后,会发现某些高价值服务器可能永远不会或几乎不会与主要包含用户工作站的某些子网建立网络连接。如果突然发现从这些高价值服务器到工作站子网的大量网络连接,那么就可以说这一行为是异常的,并值得后续调查。这种检测方式同样依赖于合适的基线,并且可能会发生误报。理想情况下,这种检测方式应该作为补充指标,以增加其他相关指标的严重性。
结论
对于命令和控制来说,植入工具之间的点对点通信非常有用,特别是对于健壮性较强的平台来说,这是必不可少的。在SMB协议上使用命名管道,是一种有效的、通用的点对点通信机制,也是Covenant中实现的机制。但是,存在许多支持该通信方式的不同协议,并且我们后续可能还会看到一些其他协议添加到Covenant中。
要检测这些行为,可能比较困难,但并非不可能。通常,检测植入工具自身的行为与检测点对点协议的行为相比,前者会更为简单。但是,我们也可以收集一些有用的数据,特别是通过使用Sysmon和NtObjectManager创建和使用命名管道的相关数据。
点对点的方式已经在新发布的v0.2版本正式添加到Covenant中,大家可以自行使用。下面的视频展示了在Covenant中使用点对点的基本方法:
https://youtu.be/Wsy_yvxZI4s
参考