execute-assembly原理

简介

"execute-assembly"是Cobalt Strike的一项功能,它允许你在Cobalt Strike的Beacon控制的目标机器上直接执行.NET程序集(Assembly)。

具体来说,这个功能通过在目标机器的内存中加载.NET程序集并执行它,而不需要将程序集写入到磁盘。这种在内存中执行程序的方法也被称为"文件无(fileless)"攻击,因为它能避免在磁盘上留下可疑的文件,从而绕过某些基于文件扫描的防御手段。

使用"execute-assembly"功能时,你需要提供要执行的.NET程序集的本地路径,然后Cobalt Strike会将这个程序集上传到目标机器的内存中并执行它。你还可以为这个程序集提供命令行参数。

这个功能的一个强大之处在于,.NET程序集可以使用C#编写,而C#提供了对Windows API的广泛访问,这使得你可以在目标机器上执行一系列复杂的操作

不过使用execute-assembly时,CobaltStrike创建一个新的进程来加载并执行.NET程序集,这其实可以看作是一种fork&run模式,因为它会“分叉”出一个新的进程来运行.NET程序集,且这种行为十分容易被杀软检测到

这是一个针对execute-assembly改良的项目:https://github.com/anthemtotheego/InlineExecute-Assembly

该项目的优点是,它会在beacon的当前进程中加载并执行.NET程序集,而没有创建新的进程,这种方法更加的隐蔽,但是如果.NET程序集出现问题,那么可能会导致整个beacon进程崩溃

实现原理

execute-assembly功能的实现,必须使用一些来自.NET Framework的核心接口来执行.NET程序集口,分别是分别是ICLRRuntimeHostICLRRuntimeInfo 以及ICLRMetaHost ,以下是这三个接口的简要描述

  • ICLRMetaHost: 这个接口用于在托管代码中获取关于加载的CLR(Common Language Runtime,.NET Framework的核心组件)的信息。基本上,它提供了一个入口点,允许我们枚举加载到进程中的所有CLR版本,并为特定版本的CLR获取ICLRRuntimeInfo接口。

  • ICLRRuntimeInfo: 一旦你有了表示特定CLR版本的ICLRRuntimeInfo接口,你可以用它来获取CLR运行时的其他接口,例如ICLRRuntimeHost。这个接口还允许你判断这个特定版本的CLR是否已经被加载到进程中。

  • ICLRRuntimeHost: 这是执行.NET程序集所必需的主要接口。通过这个接口,你可以启动托管代码的执行环境,加载.NET程序集,并执行它。具体来说,它的ExecuteInDefaultAppDomain方法可以用来加载和执行.NET程序集。

综上所述,要在非托管代码(如C++)中执行.NET程序集,你需要首先使用ICLRMetaHost来确定哪个CLR版本已加载或可用。然后,你可以使用ICLRRuntimeInfo来为这个CLR版本获取ICLRRuntimeHost。最后,使用ICLRRuntimeHost来加载和执行.NET程序集。

如下代码是一个使用C++编写的Windows程序,用于在内存中加载并执行一个名为CSharp.exe的.NET程序集,主要分为以下四个步骤:

  1. 加载CLR环境: 这是整个过程的第一步,因为要运行.NET代码,你需要.NET运行时环境。CLR(Common Language Runtime)是.NET Framework的心脏,它提供了执行.NET代码所需的所有功能。Cobalt Strike首先需要初始化或加载CLR环境以便在其上运行程序集。这通常涉及到确定哪个CLR版本(例如.NET 2.0、4.0等)可用或已加载,并准备它以供使用。

  2. 获取程序域: 在.NET中,应用程序域(AppDomains)是一个轻量级的进程,它提供了运行应用程序的隔离环境。每个.NET应用程序至少有一个默认的应用程序域,但可以创建更多的应用程序域以隔离执行的代码。通过获取程序域,Cobalt Strike可以确保在安全的、隔离的环境中加载并执行程序集。

  3. 装载程序集: 一旦设置了适当的环境并获取了程序域,Cobalt Strike将需要加载(或注入)所需的.NET程序集到这个程序域。这一步涉及到在内存中创建.NET程序集的实例,这样它就可以被执行了。

  4. 执行程序集: 加载程序集后,Cobalt Strike将触发其执行。这通常涉及到调用程序集中的一个特定方法或函数,这个方法可能是程序集的入口点或某个特定的功能函数

简单演示

首先使用VisualStudio创建一个控制台应用(.Net Framework)

image-20230816112328030

如下C#代码用于在控制台输出参数:

上述代码生成可执行程序后,可在CobaltStrike的beacon命令行使用execute-assembly来将其执行

image-20230816112706744

检测分析

使用ProcessHacker查看powershell进程可以发现其加载了CLR环境,这是因为PowerShell是基于.NET Framework构建的。

image-20230816163731355

查看Powershell的模块调用可发现,它不仅调用了clr.dll,还调用了amsi.dll。

amsi是微软提供的一个接口,旨在允许应用程序和服务与已安装的杀毒或反恶意软件解决方案进行互动,从而提供更好的保护。其中最关键的函数当属AmisiScanBuffer,AmsiScanBuffer函数可以检测内存中的恶意内容,这对于检测那些可能在运行时生成或修改其代码的恶意软件特别有用,例如某些脚本或文件less的恶意软件

不过现在国内大多数杀软都没有集成amsi的功能,而且现在amsi还是相对容易绕过的

image-20230816163914780

当我们使用上述github项目的来加载.net程序集时,再通过processexp查看beacon的进程,可以发现在.NET Assemblies处还是会有一些敏感信息的,而这些敏感信息被ETW检测出来的

image-20230821010832445 image-20230821010917168

patch etw后就查看不到任何信息了

image-20230821011237986
image-20230821011314787

bypass etw后还是有可能会被amsi检测到的, 这是因为你的beacon进程加载了CLR环境, 这使得要加载进去的.NET程序集更容易受到amsi的检测,也就是说,你还得将amsi也bypass掉

image-20230821115211340

Amsi和Etw的bypass

如下bof代码用于实现绕过amsi检测,实现原理是将amsi.dll里的关键函数AmsiScanBuffer给ret掉

上述代码提到的硬编码 { 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3 } 对应的汇编指令如下:

他将立即数值0x80070057移动到eax寄存器中。这个值是一个常见的Windows错误代码,表示“参数错误”

总的来说, 这段代码的目的是将一个特定的错误代码放入eax寄存器,并立即返回。这在绕过AMSI的上下文中为了使AMSI的扫描函数始终返回一个表示扫描失败的值

image-20230820114836005

以下代码实现绕过ETW的检测,和bypassAmsi原理一样,这里将ETW的关键函数EtwEventWrite给ret掉了

image-20230820114842176

参考链接

  • https://www.secpulse.com/archives/198531.html

  • https://www.anquanke.com/post/id/220456

最后更新于