前言

Cobalt Strike 从3.11开始增加了一个叫“execute-assembly”的命令,这个命令能够从内存中直接加载.net程序集执行。由于没有文件落地,十分隐蔽,在实战当中应用非常广泛。本文会对Cobalt Strike的execute-assembly命令的执行过程进行分析,并且结合现有的开源项目对此技术的原理进行简单介绍。

基础知识

1.CLR

全称Common Language Runtime(公共语言运行时),是一个可由多种编程语言使用的运行环境

CLR是.NET Framework的主要执行引擎,作用之一是监视程序的运行:

  • 在CLR监视之下运行的程序属于”托管的”(managed)代码
  • 不在CLR之下、直接在裸机上运行的应用或者组件属于”非托管的”(unmanaged)的代码

2.Unmanaged API

参考资料:

https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/

用于将.NET 程序集加载到任意程序中的API

支持两种接口:

  • ICorRuntimeHost Interface
  • ICLRRuntimeHost Interface

3.ICorRuntimeHost Interface

参考资料:

https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/hosting/icorruntimehost-interface

支持v1.0.3705, v1.1.4322, v2.0.50727和v4.0.30319

4.ICLRRuntimeHost Interface

参考资料:

https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/hosting/iclrruntimehost-interface

支持v2.0.50727和v4.0.30319

在.NET Framework 2.0中,ICLRRuntimeHost用于取代ICorRuntimeHost

在实际程序开发中,很少会考虑.NET Framework 1.0,所以两个接口都可以使用

CS内存执行流程分析

在Cobalt Strike的代码中找到BeaconConsole.java文件,定位到“execute-assembly”命令处。通过简单分析这段代码可以知道,当解析到用户执行“execute-assembly”命令后,会先验证”pZ“和”F“关键字来判断要执行的.net程序集是否带有参数(具体如何判断请查看CommandParser类)。判断完成使用CommandParser类的popstring方法将execute-assembly的参数赋值给变量,然后调用ExecuteAssembly方法执行程序集。

image-20220114182430780

我们继续跟进ExecuteAssembly方法,ExecuteAssembly方法有两个参数,第一个参数为待执行的.net程序集路径,第二个参数为.net程序集执行需要的参数。执行这个方法时先将要执行的.net程序集从硬盘读取并加载到PE解析器(PEParser)中,随后判断加载的PE文件是否为.net程序集,如果是.net程序集则创建ExecuteAssemblyJob实例并调用spawn方法。

image-20220114182256752

接下来进入spawn方法,可以看到是通过反射DLL的方法,将invokeassembly.dll注入到进程当中,并且设置任务号为70(x86版本)或者71(x64)。注入的invokeassembly.dll在其内存中创建CLR环境,然后通过管道再将C#可执行文件读取到内存中,最后执行。

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
public void spawn(String var1) {
byte[] var2 = this.getDLLContent();
int var3 = ReflectiveDLL.findReflectiveLoader(var2);
if (var3 <= 0) {
this.tasker.error("Could not find reflective loader in " + this.getDLLName());
} else {
if (ReflectiveDLL.is64(var2)) {
if (this.ignoreToken()) {
this.builder.setCommand(71);
} else {
this.builder.setCommand(88);
}
} else if (this.ignoreToken()) {
this.builder.setCommand(70);
} else {
this.builder.setCommand(87);
}

var2 = this.fix(var2);
if (this.tasker.obfuscatePostEx()) {
var2 = this._obfuscate(var2);
}

var2 = this.setupSmartInject(var2);
byte[] var4 = this.getArgument();
this.builder.addShort(this.getCallbackType());
this.builder.addShort(this.getWaitTime());
this.builder.addInteger(var3);
this.builder.addLengthAndString(this.getShortDescription());
this.builder.addInteger(var4.length);
this.builder.addString(var4);
this.builder.addString(var2);
byte[] var5 = this.builder.build();
this.tasker.task(var1, var5, this.getDescription(), this.getTactic());
}
}

image-20220209135538841

image-20220117192352767

总结一下,Cobalt Strike内存加载执行.net程序集大概的过程就是,首先spawn一个进程并传输invokeassembly.dll注入到该进程,invokeassembly.dll实现了在其内存中创建CLR环境,然后通过管道再将C#可执行文件读取到内存中,最后执行。

.net程序集内存加载执行

内存加载执行流程

  1. 初始化ICLRMetaHost接口。
  2. 通过ICLRMetaHost获取ICLRRuntimeInfo接口。
  3. 通过ICLRRuntimeInfo将 CLR 加载到当前进程并返回运行时接口ICLRRuntimeHost指针。
  4. 通过ICLRRuntimeHost.Start()初始化CLR。
  5. 通过ICLRRuntimeHost获取AppDomain接口指针。
  6. 通过AppDomain接口的QueryInterface方法来查询默认应用程序域的实例指针。
  7. 通过默认应用程序域实例的Load_3方法加载安全.net程序集数组,并返回Assembly的实例对象指针。
  8. 通过Assembly实例对象的get_EntryPoint方法获取描述入口点的MethodInfo实例对象。
  9. 创建参数安全数组
  10. 通过描述入口点的MethodInfo实例对象的Invoke方法执行入口点。

代码实现

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include <stdio.h>
#include <tchar.h>
#include <metahost.h>
#pragma comment(lib, "mscoree.lib")

#import <mscorlib.tlb> raw_interfaces_only \
high_property_prefixes("_get","_put","_putref") \
rename("ReportEvent", "InteropServices_ReportEvent") \
rename("or", "InteropServices_or")

using namespace mscorlib;
#define ASSEMBLY_LENGTH 8192


unsigned char dotnetRaw[ASSEMBLY_LENGTH] =
"\x4d\x5a\x90\x00\x03\x00\x00\x00\x04\x00\x00\x00\xff\xff\x00...";//.net程序集字节数组



int _tmain(int argc, _TCHAR* argv[])
{

ICLRMetaHost* iMetaHost = NULL;
ICLRRuntimeInfo* iRuntimeInfo = NULL;
ICorRuntimeHost* iRuntimeHost = NULL;
IUnknownPtr pAppDomain = NULL;
_AppDomainPtr pDefaultAppDomain = NULL;
_AssemblyPtr pAssembly = NULL;
_MethodInfoPtr pMethodInfo = NULL;
// 定义SAFEARRAYBOUND结构体,设置维度为1
SAFEARRAYBOUND saBound[1];
void* pData = NULL;
VARIANT vRet;
VARIANT vObj;
VARIANT vPsa;
SAFEARRAY* args = NULL;
// 初始化ICLRMetaHost接口。
CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (VOID**)&iMetaHost);
// 通过ICLRMetaHost获取ICLRRuntimeInfo接口。
iMetaHost->GetRuntime(L"v4.0.30319", IID_ICLRRuntimeInfo, (VOID**)&iRuntimeInfo);
// 通过ICLRRuntimeInfo将 CLR 加载到当前进程并返回运行时接口ICorRuntimeHost指针
iRuntimeInfo->GetInterface(CLSID_CorRuntimeHost, IID_ICorRuntimeHost, (VOID**)&iRuntimeHost);
// 通过ICLRRuntimeHost.Start()初始化CLR。
iRuntimeHost->Start();

// 通过ICLRRuntimeHost获取AppDomain接口指针。
iRuntimeHost->GetDefaultDomain(&pAppDomain);
// 然后通过AppDomain接口的QueryInterface方法来查询默认应用程序域的实例指针。
pAppDomain->QueryInterface(__uuidof(_AppDomain), (VOID**)&pDefaultAppDomain);

// 数组的长度
saBound[0].cElements = ASSEMBLY_LENGTH;
// 设置第一维的起始下标
saBound[0].lLbound = 0;
// 创建一个新的数组描述符,分配和初始化数组的数据,并返回一个指向新数组描述符的指针。
SAFEARRAY* pSafeArray = SafeArrayCreate(VT_UI1, 1, saBound);
// 增加数组的锁计数,并返回数组的指针。
SafeArrayAccessData(pSafeArray, &pData);
// 在缓冲区之间复制字节。
memcpy(pData, dotnetRaw, ASSEMBLY_LENGTH);
// 减少数组的锁计数,并释放通过SafeArrayAccessData返回的指针。
SafeArrayUnaccessData(pSafeArray);
// 通过默认应用程序域实例的Load_3方法加载安全.net程序集数组,并返回Assembly的实例对象指针。
pDefaultAppDomain->Load_3(pSafeArray, &pAssembly);
// 通过Assembly实例对象的get_EntryPoint方法获取描述入口点的MethodInfo实例对象。
pAssembly->get_EntryPoint(&pMethodInfo);

// 创建参数安全数组
vPsa.vt = (VT_ARRAY | VT_BSTR);
args = SafeArrayCreateVector(VT_VARIANT, 0, 1);
if (argc > 1)
{
vPsa.parray = SafeArrayCreateVector(VT_BSTR, 0, argc);
for (long i = 0; i < argc; i++)
{
SafeArrayPutElement(vPsa.parray, &i, SysAllocString(argv[i]));
}
long idx[1] = { 0 };
SafeArrayPutElement(args, idx, &vPsa);
}

ZeroMemory(&vRet, sizeof(VARIANT));
ZeroMemory(&vObj, sizeof(VARIANT));
vObj.vt = VT_NULL;
// 通过描述入口点的MethodInfo实例对象的Invoke方法执行入口点。
HRESULT hr = pMethodInfo->Invoke_3(vObj, args, &vRet);
pMethodInfo->Release();
pAssembly->Release();
pDefaultAppDomain->Release();
iRuntimeInfo->Release();
iMetaHost->Release();
CoUninitialize();

return 0;
};

其他开源实现

https://github.com/caseysmithrc/AssemblyLoader

https://github.com/etormadiv/HostingCLR

https://github.com/b4rtik/metasploit-execute-assembly

参考链接

https://3gstudent.github.io/%E4%BB%8E%E5%86%85%E5%AD%98%E5%8A%A0%E8%BD%BD.NET%E7%A8%8B%E5%BA%8F%E9%9B%86(execute-assembly)%E7%9A%84%E5%88%A9%E7%94%A8%E5%88%86%E6%9E%90

https://b4rtik.github.io/posts/execute-assembly-via-meterpreter-session-part-2/

https://idiotc4t.com/defense-evasion/cobaltstrike-executeassembly-realization#liu-chengbnei-cun-jia-zai

https://github.com/b4rtik/metasploit-execute-assembly

https://blog.csdn.net/jisuanjixu/article/details/5959186

2022-02-09

⬆︎TOP