0x00 Preface

---

In Cobalt Strike 3.11, a command named "execute-assembly" was introduced, capable of loading .NET assemblies from memory. This feature does not require writing files to the hard disk, making it highly stealthy. Moreover, existing PowerShell exploitation scripts can be easily converted to C# code, offering great convenience.

This article will introduce the principles of "execute-assembly", combine multiple open-source codes to explain implementation methods, analyze exploitation approaches, and finally provide defense recommendations.

0x01 Introduction

---

This article will cover the following topics:

  • Basic Knowledge
  • Normal Implementation Methods
  • Analysis of Open-Source Exploitation Code
  • Exploitation Approaches
  • Defense Recommendations

0x02 Basic Knowledge

---

1.CLR

Full name Common Language Runtime, is a runtime environment that can be used by multiple programming languages.

CLR is the main execution engine of the .NET Framework, one of its functions is to monitor the operation of programs:

  • Programs running under the supervision of CLR belong to "managed" code.
  • Applications or components that run directly on bare metal without CLR belong to "unmanaged" code.

2.Unmanaged API

Reference:

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

API for loading .NET assemblies into arbitrary programs.

Supports two interfaces:

  • ICorRuntimeHost Interface
  • ICLRRuntimeHost Interface

3.ICorRuntimeHost Interface

Reference:

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

Supports v1.0.3705, v1.1.4322, v2.0.50727, and v4.0.30319

4. ICLRRuntimeHost Interface

References:

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

Supports v2.0.50727 and v4.0.30319

In .NET Framework 2.0, ICLRRuntimeHost is used to replace ICorRuntimeHost

In actual program development, .NET Framework 1.0 is rarely considered, so both interfaces can be used

0x03 Normal Implementation Method

---

Example code used:

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

Here, we will reference the example code and provide additional details

The general implementation method is as follows:

1. Load the CLR into the process

(1) Call the CLRCreateInstance function to obtain the ICLRMetaHost or ICLRMetaHostPolicy interface

(2) Call ICLRMetaHost::EnumerateInstalledRuntimes, ICLRMetaHost::GetRuntime, or ICLRMetaHostPolicy::GetRequestedRuntime method to obtain a valid ICLRRuntimeInfo pointer

Choose any one of the three

(3) Use ICorRuntimeHost or ICLRRuntimeHost

Both call the ICLRRuntimeInfo::GetInterface method, but with different parameters

ICorRuntimeHost:

Supports v1.0.3705, v1.1.4322, v2.0.50727, and v4.0.30319

Specify CLSID_CorRuntimeHost as the rclsid parameter

Specify IID_ICorRuntimeHost as the RIID parameter

ICLRRuntimeHost:

Supports v2.0.50727 and v4.0.30319

Specify CLSID_CLRRuntimeHost as the rclsid parameter

Specify IID_ICLRRuntimeHost as the RIID parameter

2. Load .NET assembly and call static methods

In code implementation, using ICLRRuntimeHost is much simpler than using ICorRuntimeHost

3. Clean up CLR

Release the pointer from step 1

Below, use ICLRMetaHost::GetRuntime to obtain a valid ICLRRuntimeInfo pointer, then use ICLRRuntimeHost to load a .NET assembly from a file and invoke a static method. The implementation code is as follows:

#include "stdafx.h"
#include
#include
#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;
}

The code will load ClassLibrary1.dll (developed with .NET 4.0) from the same directory, with class name Class1, method TestMethod, and passed parameter argstring.

The code for ClassLibrary1.dll is as follows:

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 Open Source Exploit Code Analysis

---

1. Unmanaged CLR Hosting Assembly Loader

https://github.com/caseysmithrc/AssemblyLoader

Utilizes CLR to read shellcode from a predefined array in the code, loads it into memory, and executes it

Implementation method is as follows:

1. Load CLR into the process

(1) Call the CLRCreateInstance function to obtain the ICLRMetaHost or ICLRMetaHostPolicy interface

(2) Call the ICLRMetaHost::GetRuntime method to obtain a valid ICLRRuntimeInfo pointer

(3) Use ICorRuntimeHost

Note:

When using ICorRuntimeHost, a reference to mscorlib.tlb needs to be added. The C++ code is as follows:

// 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

In ICorRuntimeHost, the method for reading and loading .NET assemblies from files is defined as follows:

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

The method for reading and loading .NET assemblies from memory is defined as follows:

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

Note:

Method definitions are from mscorlib.tlh

Here Load_3(...) is used, first reading shellcode from the array, then loading the .NET assembly

2. Load .NET assembly and invoke static method

3. Clean CLR

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

https://github.com/etormadiv/HostingCLR

The method is essentially the same as caseysmith's: both call the ICLRMetaHost::GetRuntime method to obtain a valid ICLRRuntimeInfo pointer, use the ICorRuntimeHost interface, and use Load_3(...) to read and load the .NET assembly from memory.

3. CLR via native code

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

It is noteworthy that here, ICLRMetaHost::EnumerateInstalledRuntimes is called to obtain a valid ICLRRuntimeInfo pointer.

Then, ICLRRuntimeHost is used to load the .NET assembly from a file and call static methods.

4. metasploit-execute-assembly

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

First, create a notepad.exe process, then inject HostingCLRx64.dll into notepad.exe. HostingCLRx64.dll implements in-memory loading of .NET assemblies.

Here, we only focus on the details of in-memory loading of .NET assemblies. Code location:

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

Details are as follows:

  • Using .Net v4.0.30319
  • Call the ICLRMetaHost::GetRuntime method to obtain a valid ICLRRuntimeInfo pointer
  • Use the ICorRuntimeHost interface
  • Use Load_3(...) to read and load a .NET assembly from memory

Essentially the same as 1 and 2

0x05 Exploitation Approach

---

Based on the open-source code in 0x04, execute-assembly typically has the following two exploitation approaches:

1. Read shellcode from memory and load a .NET assembly

  • Call the ICLRMetaHost::EnumerateInstalledRuntimes, ICLRMetaHost::GetRuntime, or ICLRMetaHostPolicy::GetRequestedRuntime method to obtain a valid ICLRRuntimeInfo pointer
  • Use the ICorRuntimeHost interface
  • Use Load_3(...) to read and load a .NET assembly from memory
  • Call static methods

2. Read and load a .NET assembly from disk

  • Call the ICLRMetaHost::EnumerateInstalledRuntimes, ICLRMetaHost::GetRuntime, or ICLRMetaHostPolicy::GetRequestedRuntime method to obtain a valid ICLRRuntimeInfo pointer
  • Use the ICorRuntimeHost (using Load_2(...)) or ICLRRuntimeHost interface
  • Load .NET assembly and invoke static methods

The first exploitation approach is superior to the second; the complete exploitation process is as follows:

  1. Create a normal process
  2. Inject DLL into the process via DLL reflection
  3. The DLL implements reading shellcode from memory and loading the final .NET assembly

Advantages are as follows:

  • The entire process executes in memory without writing to the file system
  • The payload exists as a DLL, avoiding suspicious processes
  • The final payload is a C# program, making conversion from existing PowerShell exploitation scripts to C# code convenient

0x06 Defense Recommendations

---

The entire exploitation process requires DLL injection; common DLL injection methods (especially DLL reflection) can be intercepted

Regarding the DLL itself, when using CLR, system DLLs such as the following will be loaded:

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

This can be monitored

0x07 Summary

---

This article combines multiple open-source codes to summarize the implementation methods and exploitation ideas of "execute-assembly", analyzes its advantages, and finally provides defense recommendations