从内存加载.NET程序集(execute-assembly)的利用分析

0x00 前言


Cobalt Strike 3.11中,加入了一个名为”execute-assembly”的命令,能够从内存中加载.NET程序集。这个功能不需要向硬盘写入文件,十分隐蔽,而且现有的Powershell利用脚本能够很容易的转换为C#代码,十分方便。

本文将会对”execute-assembly”的原理进行介绍,结合多个开源代码,介绍实现方法,分析利用思路,最后给出防御建议

0x01 简介


本文将要介绍以下内容:

  • 基础知识
  • 正常的实现方法
  • 开源利用代码分析
  • 利用思路
  • 防御建议

0x02 基础知识


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,所以两个接口都可以使用

0x03 正常的实现方法


使用的实例代码:

https://code.msdn.microsoft.com/windowsdesktop/CppHostCLR-e6581ee0#content

这里将参考实例代码并做补充

通用的实现方法如下:

1.将CLR加载到进程中

(1)调用CLRCreateInstance函数以获取ICLRMetaHost或ICLRMetaHostPolicy接口

(2)调用ICLRMetaHost::EnumerateInstalledRuntimes, ICLRMetaHost::GetRuntime或者ICLRMetaHostPolicy::GetRequestedRuntime方法以获取有效的ICLRRuntimeInfo指针

三个任选一个

(3)使用ICorRuntimeHost或者ICLRRuntimeHost

二者都是调用ICLRRuntimeInfo::GetInterface方法,但是参数不同

ICorRuntimeHost:

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

指定CLSID_CorRuntimeHost为rclsid参数

指定IID_ICorRuntimeHost为RIID参数

ICLRRuntimeHost:

支持v2.0.50727和v4.0.30319

指定CLSID_CLRRuntimeHost为rclsid参数

指定IID_ICLRRuntimeHost为RIID参数

2.加载.NET程序集并调用静态方法

在代码实现上,使用ICLRRuntimeHost会比使用ICorRuntimeHost简单的多

3.清理CLR

释放步骤1中的指针

下面使用ICLRMetaHost::GetRuntime获取有效的ICLRRuntimeInfo指针,使用ICLRRuntimeHost从文件加载.NET程序集并调用静态方法,实现代码如下:

#include "stdafx.h"
#include <metahost.h>
#include <windows.h>
#pragma comment(lib, "MSCorEE.lib")

HRESULT RuntimeHost_GetRuntime_ICLRRuntimeInfo(PCWSTR pszVersion, PCWSTR pszAssemblyName, PCWSTR pszClassName, PCWSTR pszMethodName, PCWSTR pszArgName)
{
	// Call the ICLRMetaHost::GetRuntime to get a valid ICLRRuntimeInfo.
	// Call the ICLRRuntimeInfo:GetInterface method.
	HRESULT hr;
	ICLRMetaHost *pMetaHost = NULL;
	ICLRRuntimeInfo *pRuntimeInfo = NULL;
	ICLRRuntimeHost *pClrRuntimeHost = NULL;
	DWORD dwLengthRet;
	// 
	// Load and start the .NET runtime.
	// 
	wprintf(L"Load and start the .NET runtime %s \n", pszVersion);
	hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&pMetaHost));
	if (FAILED(hr))
	{
		wprintf(L"[!]CLRCreateInstance failed w/hr 0x%08lx\n", hr);
		goto Cleanup;
	}
	// Get the ICLRRuntimeInfo corresponding to a particular CLR version. It 
	// supersedes CorBindToRuntimeEx with STARTUP_LOADER_SAFEMODE.
	hr = pMetaHost->GetRuntime(pszVersion, IID_PPV_ARGS(&pRuntimeInfo));
	if (FAILED(hr))
	{
		wprintf(L"[!]ICLRMetaHost::GetRuntime failed w/hr 0x%08lx\n", hr);
		goto Cleanup;
	}
	// Check if the specified runtime can be loaded into the process. This 
	// method will take into account other runtimes that may already be 
	// loaded into the process and set pbLoadable to TRUE if this runtime can 
	// be loaded in an in-process side-by-side fashion. 
	BOOL fLoadable;
	hr = pRuntimeInfo->IsLoadable(&fLoadable);
	if (FAILED(hr))
	{
		wprintf(L"[!]ICLRRuntimeInfo::IsLoadable failed w/hr 0x%08lx\n", hr);
		goto Cleanup;
	}
	if (!fLoadable)
	{
		wprintf(L"[!].NET runtime %s cannot be loaded\n", pszVersion);
		goto Cleanup;
	}
	// Load the CLR into the current process and return a runtime interface 
	// pointer. ICorRuntimeHost and ICLRRuntimeHost are the two CLR hosting  
	// interfaces supported by CLR 4.0. Here we demo the ICLRRuntimeHost 
	// interface that was provided in .NET v2.0 to support CLR 2.0 new 
	// features. ICLRRuntimeHost does not support loading the .NET v1.x 
	// runtimes.
	hr = pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_PPV_ARGS(&pClrRuntimeHost));
	if (FAILED(hr))
	{
		wprintf(L"[!]ICLRRuntimeInfo::GetInterface failed w/hr 0x%08lx\n", hr);
		goto Cleanup;
	}
	// Start the CLR.
	hr = pClrRuntimeHost->Start();
	if (FAILED(hr))
	{
		wprintf(L"[!]CLR failed to start w/hr 0x%08lx\n", hr);
		goto Cleanup;
	}
	// 
	// Load the NET assembly and call the static method.
	// 
	wprintf(L"[+]Load the assembly %s\n", pszAssemblyName);
	// The invoked method of ExecuteInDefaultAppDomain must have the 
	// following signature: static int pwzMethodName (String pwzArgument)
	// where pwzMethodName represents the name of the invoked method, and 
	// pwzArgument represents the string value passed as a parameter to that 
	// method. If the HRESULT return value of ExecuteInDefaultAppDomain is 
	// set to S_OK, pReturnValue is set to the integer value returned by the 
	// invoked method. Otherwise, pReturnValue is not set.
	hr = pClrRuntimeHost->ExecuteInDefaultAppDomain(pszAssemblyName, pszClassName, pszMethodName, pszArgName, &dwLengthRet);
	if (FAILED(hr))
	{
		wprintf(L"[!]Failed to call %s w/hr 0x%08lx\n", pszMethodName, hr);
		goto Cleanup;
	}
	// Print the call result of the static method.
	wprintf(L"[+]Call %s.%s(\"%s\") => %d\n", pszClassName, pszMethodName, pszArgName, dwLengthRet);

Cleanup:
	if (pMetaHost)
	{
		pMetaHost->Release();
		pMetaHost = NULL;
	}
	if (pRuntimeInfo)
	{
		pRuntimeInfo->Release();
		pRuntimeInfo = NULL;
	}
	if (pClrRuntimeHost)
	{
		// Please note that after a call to Stop, the CLR cannot be 
		// reinitialized into the same process. This step is usually not 
		// necessary. You can leave the .NET runtime loaded in your process.
		//wprintf(L"Stop the .NET runtime\n");
		//pClrRuntimeHost->Stop();
		pClrRuntimeHost->Release();
		pClrRuntimeHost = NULL;
	}
	return hr;
}

int main()
{
	RuntimeHost_GetRuntime_ICLRRuntimeInfo(L"v4.0.30319", L"ClassLibrary1.dll", L"ClassLibrary1.Class1", L"TestMethod", L"argstring");
	return 0;
}

代码将会加载同级目录下.Net4.0开发的ClassLibrary1.dll,类名为Class1,方法为TestMethod,传入的参数为argstring

ClassLibrary1.dll的代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ClassLibrary1
{
    public class Class1
    {
        public static int TestMethod(string str)
        {
            System.Diagnostics.Process p = new System.Diagnostics.Process();
            p.StartInfo.FileName = "c:\\windows\\system32\\calc.exe";
            p.Start();
            return 0;
        }
    }
}

0x04 开源利用代码分析


1、Unmanaged CLR Hosting Assembly loader

https://github.com/caseysmithrc/AssemblyLoader

利用CLR从代码中定义好的数组读取shellcode,加载到内存并执行

实现方法如下:

1.将CLR加载到进程中

(1)调用CLRCreateInstance函数以获取ICLRMetaHost或ICLRMetaHostPolicy接口

(2)调用ICLRMetaHost::GetRuntime方法以获取有效的ICLRRuntimeInfo指针

(3)使用ICorRuntimeHost

注:

在使用ICorRuntimeHost时,需要添加对mscorlib.tlb的引用,c++代码如下:

// Import mscorlib.tlb (Microsoft Common Language Runtime Class Library).
#import "mscorlib.tlb" raw_interfaces_only				\
    high_property_prefixes("_get","_put","_putref")		\
    rename("ReportEvent", "InteropServices_ReportEvent")
using namespace mscorlib;
#pragma endregion

在ICorRuntimeHost中,从文件读取并加载.NET程序集的方法定义如下:

  virtual HRESULT __stdcall Load_2 (
    /*[in]*/ BSTR assemblyString,
    /*[out,retval]*/ struct _Assembly * * pRetVal ) = 0;

从内存中读取并加载.NET程序集的方法定义如下:

  virtual HRESULT __stdcall Load_3 (
    /*[in]*/ SAFEARRAY * rawAssembly,
    /*[out,retval]*/ struct _Assembly * * pRetVal ) = 0;

注:

方法定义来自mscorlib.tlh

这里使用了Load_3(…),先从数组中读取shellcode,再加载.NET程序集

2.加载.NET程序集并调用静态方法

3.清理CLR

2、Executing a .NET Assembly from C++ in Memory (CLR Hosting)

https://github.com/etormadiv/HostingCLR

同caseysmith的方法基本相同,都是调用ICLRMetaHost::GetRuntime方法以获取有效的ICLRRuntimeInfo指针,使用ICorRuntimeHost接口,使用Load_3(…)从内存中读取并加载.NET程序集

3、CLR via native code

https://gist.githubusercontent.com/xpn/e95a62c6afcf06ede52568fcd8187cc2/raw/f3498245c8309d44af38502a2cc7090c318e8adf/clr_via_native.c

值得注意的是这里调用ICLRMetaHost::EnumerateInstalledRuntimes获取有效的ICLRRuntimeInfo指针

接着使用ICLRRuntimeHost从文件加载.NET程序集并调用静态方法

4、metasploit-execute-assembly

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

首先创建进程notepad.exe,然后向notepad.exe注入HostingCLRx64.dll,HostingCLRx64.dll实现内存加载.Net程序集

这里我们只关注内存加载.Net程序集的细节,代码位置:

https://github.com/b4rtik/metasploit-execute-assembly/blob/master/HostingCLR_inject/HostingCLR/HostingCLR.cpp

细节如下:

  • 使用.Net v4.0.30319
  • 调用ICLRMetaHost::GetRuntime方法以获取有效的ICLRRuntimeInfo指针
  • 使用ICorRuntimeHost接口
  • 使用Load_3(…)从内存中读取并加载.NET程序集

同1和2基本相同

0x05 利用思路


综合0x04中的开源代码,execute-assembly通常有以下两种利用思路:

1.从内存中读取shellcode并加载.NET程序集

  • 调用ICLRMetaHost::EnumerateInstalledRuntimes, ICLRMetaHost::GetRuntime或者ICLRMetaHostPolicy::GetRequestedRuntime方法以获取有效的ICLRRuntimeInfo指针
  • 使用ICorRuntimeHost接口
  • 使用Load_3(…)从内存中读取并加载.NET程序集
  • 调用静态方法

2.从硬盘读取并加载.NET程序集

  • 调用ICLRMetaHost::EnumerateInstalledRuntimes, ICLRMetaHost::GetRuntime或者ICLRMetaHostPolicy::GetRequestedRuntime方法以获取有效的ICLRRuntimeInfo指针
  • 使用ICorRuntimeHost(使用Load_2(…))或者ICLRRuntimeHost接口
  • 加载.NET程序集并调用静态方法

第一种利用思路要优于第二种,完整的利用过程如下:

  1. 创建一个正常的进程
  2. 通过Dll反射向进程注入dll
  3. dll实现从内存中读取shellcode并加载最终的.NET程序集

优点如下:

  • 整个过程在内存执行,不写入文件系统
  • Payload以dll形式存在,不会产生可疑的进程
  • 最终的Payload为C#程序,现有的Powershell利用脚本转换为C#代码很方便

0x06 防御建议


整个利用过程必须要用到dll注入,可以对常见的dll注入方法(尤其是Dll反射)进行拦截

而对于dll本身,在使用CLR时,会加载系统的dll,例如:

  • mscoree.dll
  • mscoreei.dll
  • mscorlib.dll

可对此进行监控

0x07 小结


本文结合多个开源代码,总结了”execute-assembly”的实现方法和利用思路,分析优点,最后给出防御建议


LEAVE A REPLY

Written on June 19, 2019