Introduction

Preparing for an upcoming red teaming competition and “becoming” one with the MSDN documents, I stumbled across something very interesting. You may have noticed that when you create an .NET Framework application with Visual Studio an app.config is created. A typical app.config looks like:

Copy 1 <?xml version="1.0" encoding="utf-8" ?> 2 < configuration > 3 < startup > 4 < supportedRuntime version = " v4.0 " sku = " .NETFramework,Version=v4.7.2 " /> 5 </ startup > 6 </ configuration >

Configuration: Root element in every configuration file used by the Common Language Runtime (CLR) and .NET Framework applications

Startup: Element specifies the CLR startup information

Version: Specifies version of the CLR the app supports

Sku: Specifies which .NET Framework release the app supports

That’s great but why should you care? Remember what was just said, “Root element in every configuration file.” There are more configuration files besides app.config. This is where our journey begins.

Note

This post does not cover the internals of things such as Global Assembly Cache (GAC), CLR, or strong signed .NET assemblies. If you need a primer on those, I highly suggest you check out am0nsec’s post covering a different technique for CLR hooking.

Digging Deeper

Exactly what other config files exist? Before we get to that, we must first understand how the runtime locates assemblies. There are four steps in the process:

Examining the configuration files Checking for previously referenced assemblies Checking the global assembly cache Locating the assembly through codebases or probing

The first step is what we will be focusing on. There are three configuration files.

We already covered the application configuration file. But what about the other two?

The publisher policy file

Distributed by a component publisher as a fix or update to a shared component.

Affects all applications that use a shared component.

And finally:

The machine configuration file

Takes precedence over all other configuration settings

Applied to the entire machine.

Resides on the local computer in the config subdirectory

Can be used by administrators to specify assembly binding restrictions that are local to the computer

Requires administrative permissions to modify

Understanding machine.config

Where is it located?

Simply type this in PowerShell:

Copy 1 [System.Runtime.InteropServices.RuntimeEnvironment]::SystemConfigurationFile

You will most likely get: C:\Windows\Microsoft.NET\Framework64\v4.0.30319\config\machine.config

There is also one here: C:\Windows\Microsoft.NET\Framework64\v2.0.50727\config\machine.config

There are also config files in the 32-bit version of the .NET Framework.

According to Microsoft:

“The configuration system first looks in the machine configuration file for the appSettings element and other configuration sections that a developer might define.”

It’s those other sections that allow us to get persistence. One section in particular stands out, the runtime section!

In total, the runtime section, its parent and child elements total 55 items.

However, we only care about two:

Understanding appDomainManager & appDomainManagerAssembly

To understand why we care about those two elements, we must first understand what an AppDomain is. AppDomains allow untrusted third-party code to run in an existing process, essentially a logical container for a set of assemblies. The appDomainManager class allows a host to override CLR default behavior by using managed code instead of unmanaged code.

In order to achieve this, we need to create a class that derives from AppDomainManager which initializes a new domain. The appDomainManager element can be configured in the runtime section of a configuration file. This is accomplished via the elements appDomainManagerAssembly and appDomainManagerType. The class that derives from AppDomainManager needs to be installed into the GAC because the assembly needs to be granted full-trust. All assemblies in the GAC are always granted full-trust.

TLDR: We can create a class that derives from appDomainManager class that initializes a new domain to run our code. The InitializeNewDomain method is called on the new instance of AppDomainManager. Allowing us to hook all calls to AppDomain.CreateDomain to our AppDomainManager which in turn will run our code.

Putting the Pieces Altogether

Now that we understand what machine.config and appDomains are and how we can leverage them, it’s time put all the pieces together to accomplish persistence. There are a few things we need to perform:

Generate a keyfile. Simply do that with sn -k key.snk, sn standing for strong name tool Create a strong signed .NET assembly using our newly created keyfile with an assembly that derives from AppDomainManager and overrides InitiliazeNewDomain Install that assembly onto the GAC using gacutil, C# via Publish.GacInstall, or PowerShell via this amazing script Modify machine.config’s runtime element attribute with the appDomainType and appDomainManagerAssembly elements:

To get the appDomainManagerAssembly you can

Copy 1 < runtime > 2 < appDomainManagerType value = " Context Name " /> 3 < appDomainManagerAssembly 4 value = " Signed, Version=1.0.0.0, Culture=neutral, PublicKeyToken=token " /> 5 </ runtime >

Luckily machine.config is simply an XML file so it’s very easy to modify. There also exists the System.Configuration namespace, which also could have been utilized. However, the xml method was personally much easier to use.

Demo Time

Putting all those pieces together in a simple program which just requires you to put the keyfile in the correct spot. This automates the process :)

Assemblies installed onto the GAC are located here:

C:\Windows\Microsoft.NET\assembly

You will most likely see three folders:

GAC_32 : For assemblies targeting 32-bit platforms

GAC_64 : For assemblies targeting 64-bit platforms

GAC_MSIL : For assemblies targeting both 32-bit and 64-bit platforms

You will notice our strong-named assembly called test is installed in GAC_MSIL

One of the most important parts is where we create the class that inherits from appDomainManager.

Copy 1 using System; 2 namespace Context { 3 public sealed class ConfigHooking : AppDomainManager { 4 public override void InitializeNewDomain(AppDomainSetup appDomainInfo) { 5 System.Diagnostics.Process.Start(""calc.exe""); 6 return; 7 } 8 } 9 }

This is running our code that modifies machine.config, which means that any .NET assembly that is ran will run our code as well.

The full code can be found here: ConfigPersist

Creating Configs for Arbitrary .NET Assemblies

Thank you to @subTee for the amazing research on this technique. Remember what was said earlier about how the runtime locates assemblies. Specifically number 4:

Locating the assembly through codebases or probing

We can also take any arbitrary .NET assembly for example, FileHistory.exe and create a custom config for it such as FileHistory.exe.config looking like this:

Copy 1 < configuration > 2 < runtime > 3 < assemblyBinding xmlns = " urn:schemas-microsoft-com:asm.v1 " > 4 < probing privatePath = " C:\Windows\System32\com; " /> 5 </ assemblyBinding > 6 < appDomainManagerAssembly value = " ArbConfig, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null " /> 7 < appDomainManagerType value = " MyAppDomainManager " /> 8 </ runtime > 9 </ configuration >

ArbConfig being the name of the unsigned assembly. We can then simply place that config file inside System32 and the next time FileHistory.exe is ran our code will be ran instead.

Create your assembly that derives from AppDomainManager. Place your unsigned assembly in the same directory as the .NET assembly you want to affect. Modify your config with the privatePath being equal to where you placed your assembly. Place the config file in the same directory or a subdirectory as the .NET assembly you want to affect.

This creates an arbitrary config for FileHistory which means when FileHistory is it runs our code in ArbConfig.dll first.

The code for ArbConfig.dll:

Copy 1 using System; 2 using System.Reflection; 3 using System.EnterpriseServices; 4 using System.Runtime.InteropServices; 5 6 public sealed class MyAppDomainManager : AppDomainManager 7 { 8 public override void InitializeNewDomain(AppDomainSetup appDomainInfo) 9 { 10 System.Windows.Forms.MessageBox.Show("Config Files are fun"); 11 return; 12 } 13 }

For more information on this technique read about the probing element.

Mitigation

Monitoring for modifications to machine.config

Monitoring for dynamic compilation of C# code

What’s Next?

Look into automating the creation of configs for any arbitrary .NET assembly

Dig deeper into just how powerful machine.config is

Research .NET Core runtime persistence mechanisms

Closing Thoughts

The concept of leveraging built-in config files for persistence is quite fascinating as with the shift from PowerShell to .NET tradecraft, .NET attacks will become much more common and frequent.

Shout outs to:

am0nsec for the amazing post on CLR hooking and for his awesome suggestions on this post.

subTee for motivating me to work harder and for his amazing research on the topic as well.

References