前言
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方法执行程序集。

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

接下来进入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());       }    }
  | 
 


总结一下,Cobalt Strike内存加载执行.net程序集大概的过程就是,首先spawn一个进程并传输invokeassembly.dll注入到该进程,invokeassembly.dll实现了在其内存中创建CLR环境,然后通过管道再将C#可执行文件读取到内存中,最后执行。
.net程序集内存加载执行
内存加载执行流程
- 初始化ICLRMetaHost接口。
 
- 通过ICLRMetaHost获取ICLRRuntimeInfo接口。
 
- 通过ICLRRuntimeInfo将 CLR 加载到当前进程并返回运行时接口ICLRRuntimeHost指针。
 
- 通过ICLRRuntimeHost.Start()初始化CLR。
 
- 通过ICLRRuntimeHost获取AppDomain接口指针。
 
- 通过AppDomain接口的QueryInterface方法来查询默认应用程序域的实例指针。
 
- 通过默认应用程序域实例的Load_3方法加载安全.net程序集数组,并返回Assembly的实例对象指针。
 
- 通过Assembly实例对象的get_EntryPoint方法获取描述入口点的MethodInfo实例对象。
 
- 创建参数安全数组
 
- 通过描述入口点的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...";
 
 
  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 saBound[1]; 	void* pData = NULL; 	VARIANT vRet; 	VARIANT vObj; 	VARIANT vPsa; 	SAFEARRAY* args = NULL; 	 	CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (VOID**)&iMetaHost);      	iMetaHost->GetRuntime(L"v4.0.30319", IID_ICLRRuntimeInfo, (VOID**)&iRuntimeInfo);      	iRuntimeInfo->GetInterface(CLSID_CorRuntimeHost, IID_ICorRuntimeHost, (VOID**)&iRuntimeHost);      	iRuntimeHost->Start();
  	 	iRuntimeHost->GetDefaultDomain(&pAppDomain);      	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);      	SafeArrayUnaccessData(pSafeArray); 	 	pDefaultAppDomain->Load_3(pSafeArray, &pAssembly);      	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;      	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