Gargoyle Memory Scanning Evasion for .NET

Posted on 4 December 2018 by Luke Jennings

Gargoyle is a memory scanning evasion technique released as a proof-of-concept in 2017 by Josh Lospinoso. It is focused on ensuring that injected code is non-executable most of the time to make it more difficult for memory scanning techniques to identify it. This is a great technique, which our very own MWR consultants have blogged about before with regard to Cobalt Strike integration. We have also shown how to detect it using both WinDBG and a Volatility plugin we released.

 

Whilst performing some unrelated research into detection of both malicious and dynamic use of .NET, driven by a shift from PowerShell to .NET within the offensive security research community, it occurred to me that a similar approach to Gargoyle might be applicable using .NET execution methods. This was covered briefly in my related Bluehatv18 presentation on memory resident implant techniques and this blog post will look at the detail of how this can be done, along with some detection strategies and proof-of-concept code.

 

Dynamic .NET Code Execution

Many modern offensive techniques avoid “touching disk” where possible, which has traditionally been done to avoid classic antivirus scanning and to avoid leaving evidence that can be found using disk forensics. This has led to the development of memory forensics techniques to help combat it. For native code, this has traditionally been performed using reflective DLL injection techniques, process hollowing, and a host of other similar techniques. However, .NET provides very easy mechanisms to achieve a similar end goal via use of dynamic assembly loading and reflection.

 

For example, from within PowerShell we can see how we can load a .NET assembly dynamically in-memory and use reflection to instantiate a class and call a method on it:

1 call a method on dotnet assembly

This is sufficient background for the purposes of this blog post, but there are more details on this in part 1 of our post on detecting malicious use of .NET. Additionally, the subject is covered in a great post by Joe Desimone at Endgame, where he also demonstrates how to detect in-memory loaded .NET assemblies using the Get-ClrReflection.ps1 PowerShell script he released. 

 

.NET Timers and Callbacks

The original Gargoyle technique makes use of native Windows timer objects in order to execute callbacks on a defined interval. These are then executed using APCs delivered to the target process by the kernel when the timer expires. In the world of .NET, it has its own higher-level implementation of timers that has similar functionality.

2 timer callback

As we can see from the constructor for the Timer() class, we have the ability to specify a .NET callback method and an argument to supply on expiry of the timer. One limitation here is that the callback has to match the TimerCallback delegate, which we can see below:

3 timer callback delegate

Due to this delegate specification, we are limited to calling methods with this signature. In an ideal world, we might want to purely specify a callback to Assembly.Load(), with the byte array for the assembly as the state argument, and ensure that our malicious code executes as a result. Sadly, the delegate does not match and getting code to automatically run on the loading of an assembly is not as trivial as putting code in the DllMain() function, like is the case with native DLLs. Consequently, we need to make a thin wrapper to handle the loading and execution of our malicious assembly in-memory, followed by a clean-up and rescheduling of the timer.  

Implementation of a Loader

There are a few steps to consider now that we have a .NET timer primitive that allows us to define callback:

 

  1. We will need some custom loading code, which will need to be permanently loaded. Therefore, our strategy would be to make this as small and innocuous as possible so as to go with a hide-in-plain-sight approach;
  2. You cannot unload assemblies individually (as seen here) and so to clean-up our fully-featured malicious implant assembly we will need to load it into a new AppDomain and unload that after;
  3. We need to find some method to load our .NET loader assembly and then invoke a method to create the .NET timers to load our malicious implant assembly in future.

 

For the purposes of the rest of the blog post, “AssemblyLoader” will be our minimal innocuous loader assembly and “DemoAssembly” will be a PoC assembly representing what would be a fully featured malicious implant in real-world use.

 

Given below is a snippet of C# code that covers the bulk of what is required for points 1 and 2 above:

public class AssemblyLoaderProxy : MarshalByRefObject, IAssemblyLoader 
{ 
    public void Load(byte[] bytes) 
    { 
        var assembly = AppDomain.CurrentDomain.Load(bytes); 
        var type = assembly.GetType("DemoAssembly.DemoClass"); 
        var method = type.GetMethod("HelloWorld"); 
        var instance = Activator.CreateInstance(type, null); 
        Console.WriteLine("--- Executed from {0}: {1}", AppDomain.CurrentDomain.FriendlyName, method.Invoke(instance, null)); 
    } 
} 
 
public static int StartTimer(string gargoyleDllContentsInBase64) 
{ 
    Console.WriteLine("Start timer function called"); 
    byte[] dllByteArray = Convert.FromBase64String(gargoyleDllContentsInBase64); 
    Timer t = new Timer(new TimerCallback(TimerProcAssemblyLoad), dllByteArray, 0, 0); 
    return 0; 
} 

private static void TimerProcAssemblyLoad(object state) 

{ 
    AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve); 
    Console.WriteLine("Hello from timer!");  

    String appDomainName = "TemporaryApplicationDomain"; 
    AppDomain applicationDomain = System.AppDomain.CreateDomain(appDomainName); 
    var assmblyLoaderType = typeof(AssemblyLoaderProxy); 
    var assemblyLoader = (IAssemblyLoader)applicationDomain.CreateInstanceFromAndUnwrap(assmblyLoaderType.Assembly.Location, assmblyLoaderType.FullName); 
    assemblyLoader.Load((byte[])state); 
    Console.WriteLine("Dynamic assembly has been loaded in new AppDomain " + appDomainName); 

    AppDomain.Unload(applicationDomain); 
    Console.WriteLine("New AppDomain has been unloaded");  

    Timer t = new Timer(new TimerCallback(TimerProcAssemblyLoad), state, 1000, 0); 
}

This is a pretty small amount of code and, with some names and debug statements aside, may pass a casual inspection as it does not necessarily appear inherently malicious. This would be especially true if a much larger innocuous assembly was used and this code was just a small part of it.

 

This works by defining a callback function that will load our malicious assembly (DemoAssembly in this case) using the following process:

 

  1. Load from DemoAssembly from a byte[] array within a new AppDomain
  2. Instantiate a DemoClass object
  3. Execute the HelloWorld() method
  4. Unload the AppDomain to clean-up DemoAssembly in-memory
  5. Re-schedule the timer to repeat the process indefinitely

 

Executing the Assembly Loader

 

To achieve the third point of our requirements we can utilize COM from native code in order to trigger the loading of our Loader assembly and invoke the StartTimer() method, which will setup the .NET timer to then periodically load our “malicious” DemoAssembly on a timer. This following snippet shows the key part of this code: 

// execute managed assembly 
DWORD pReturnValue; 
hr = pClrRuntimeHost->ExecuteInDefaultAppDomain( 
L"AssemblyLoader.dll", 
L"AssemblyLoader", 
L"StartTimer", 
lpcwstr_base64_contents, 
&pReturnValue); 

This gives us a couple of options for triggering the loader in the first place. We can either run a native application that executes this code or we can inject it as a native DLL into a legitimate application and then unload it immediately after. The end result is we then have a .NET loader assembly that appears as innocuous as possible loaded, but with .NET timers that can load a fully featured .NET implant in-memory at regular intervals in the future.

 

If we put all these pieces together we get something that looks like the following:

4 loaded dotnet assemblies

If we then inspect the loaded .NET assemblies with a tool like ProcessHacker, we will see only the loader assembly, but no evidence of the temporary AppDomain or our “malicious” DemoAssembly: 

process hacker

Additionally, if we use a tool like Get-ClrReflection.ps1 then we will not see any results as our “innocuous” assembly loader is loaded from disk and our “malicious” in-memory DemoAssembly is overwhelmingly unlikely to be loaded at the exact moment the scan runs:

6 Get ClrReflection

Detection Strategies

This technique, like the original native Gargoyle technique that inspired it, is based on evading point-in-time memory scanning techniques and general memory forensics. That means that real-time tracing solutions that give visibility of .NET activity under the hood can inspect the activity. In parts one and two of our previous blog posts on this topic, we covered how to trace the loading of .NET assemblies and the activity they engage in. For example, if we use the PoC ETW tracing tool we released with those blog posts to track only high risk module loads, we can clearly see the continual reloading of our “malicious” DemoAssembly:

7 continual reloading of our malicious DemoAssembly

However, the question remains as to if there are any alternative point-in-time memory scanning approaches that can be used to detect this technique in use. One interesting aspect is that .NET does leave some trace of this activity that can be acquired using the SOS debugging extension for WinDBG. An excerpt of the !DumpDomain command can be seen below, showing how there is residual evidence of lots of “TemporaryApplicationDomain” AppDomains and references to dynamic modules being loaded: 

8 dumpdomain command

However, the next question is whether it would be possible to analyse .NET timers system-wide in order to identify potentially suspicious callbacks, as a more direct method of gaining visibility of this technique. Microsoft actually provide a memory diagnostics library for .NET called ClrMD that I have used before and was my first port of call.

 

It turns out that Criteo Labs had actually already released an excellent article about exactly how to do this using ClrMD (for non-security purposes), including releasing code. With some investigation and additional code tweaks to suit our use cases, it was possible to produce a tool that could give visibility of timer callbacks and identify potentially suspicious instances for investigation.

 

If we run this tool on an example system with Visual Studio running, we can find various examples of timer callbacks present in Visual Studio related processes:

9 timer callbacks in Visual Studio

However, these are all very similar and all have callbacks within the System or Microsoft namespace. It could potentially be possible to find malicious use cases making use of callbacks in these namespaces, but the constraints we faced earlier lead us to using custom code to do it. If as a basic first check we look for callbacks in other namespaces, then this narrows our results down to just one result in this case, which shows our malicious callback.

10 malicious callbacks

It would of course be possible for attackers aware of this to aim to have their callback appear as legitimate as possible, but then if detection was adopted as an at-scale technique, hunting for anomalous instances across a network could prove useful despite this, as would baselining common legitimate callbacks more specifically than anything within the System or Microsoft namespace.

 

Conclusion

As part of this post, we have investigated a .NET spiritual equivalent of the Gargoyle memory scanning evasion technique, how to implement it to aid in hiding a memory-resident .NET implant from point-in-time memory scanning techniques and strategies that can be used for detecting the technique. All the relevant code is available at: https://github.com/countercept/dotnet-gargoyle