Towards generic .NET assembly obfuscation (Pt. 1)- 4 mins
About 2 years ago when I entered the red teaming field, PowerShell was huge. It was an easy, elegant and clean way to evade anti-malware solutions. But largely due to the efforts from Microsoft to implement defence capabilities such as AMSI and Script Logging into PowerShell (v5), those happy PowerShell days for red teamers are over. Sure, it’s still possible:
[PSObject]Assmebly.GetType('System.Management.Automation'+'Utils'),GetType('amsiIni'+'tFailed', 'nonPublic, static').setValue($null, $true)
but it’s getting more difficult.
So as often, the red team finds other low hanging fruit with which it’s easier to achieve its goal: .NET.
Just like with PowerShell modules, it’s often possible to execute those .NET assemblies in memory without touching disk:
or using Cobalt Strike’s 3.11 beacon functionality
Obfuscating .NET binaries
But sometimes it’s inevitable to drop a .NET assembly to disk, or you want to adhere to general good OpSec practices and want to obfuscate your binaries, just in case. I’d be nice to have an obfuscator for .NET assemblies that can obfuscate any .NET assembly, while leaving its functionality intact.
The idea described here is centred around encapsulation of the .NET assembly and loading the encapsulated assembly via the (not logged or monitored)
Assembly.Load(byte) .NET method at runtime. The output of our obfuscator should be an assembly that loads the original (malicious) assembly into its own process space. Our obfuscator should perform the following steps:
1. Take a .NET assembly as input, obfuscate / encrypt the .NET assembly and encode it to a base64 string:
2. Create C# code that deobfuscates / decrypts the base64 string and loads the output via
srcTemplate variable contains a template for the (outer) assembly output of the obfuscator. Into this template, we copy the obfuscated / encrypted malicious assembly. At runtime, this obfuscated assembly will be deobfuscated and loaded via
Assembly.Load(byte). The tricky bit here is that after loading the assembly, we don’t know which method in the assembly
Main is. We can solve this by matching on its features:
static and arguments
String. If it fails, we’ll move on to find the next method with these features. When we’ve found the method that matches these features, we’ll invoke it and pass it the arguments obtained from the “outer” assembly.
3. Compile a new .NET assembly at runtime:
When the template is filled in, we compile the output assembly:
When implementing this yourself, I encourage you to implement your own obfuscation / encryption routines, as well as some sandbox evasion techniques. While this technique bypasses all traditional AV products, leaving the base64 string as is in the “outer” .NET assembly will trigger some “ML engines”, since the assembly looks at lot like a loader: limited code and a large blob of
String. In a following part, I will describe some evasion methods for these “ML engines”.