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
值得注意的是这里调用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程序集的细节,代码位置:
细节如下:
· 使用.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程序集并调用静态方法。
第一种利用思路要优于第二种,完整的利用过程如下:
· 创建一个正常的进程。
· 通过Dll反射向进程注入dll。
· 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"的实现方法和利用思路,分析优点,最后给出防御建议。