In this blog post, we will discuss a well-known method, which involves loading a .NET assembly (dll or exe) within C# code using the “Assembly.Load(…)” method. This technique is quite interesting, but it’s worth noting that if we load a potentially harmful .NET assembly, such as a payload from Mythic C2, it will likely be identified as malware. Without any doubt AMSI is responsible for detecting such payload as malicious There are various publicly known techniques for evading AMSI before executing a malicious payload on a system. However, in this blog post, we will showcase a unique approach to bypass the AMSI scan specifically for the Assembly.Load(…) method. Our approach involves writing a single byte to a specific memory location, ultimately enabling us to evade detection by Windows Defender as well.
When it comes to the Assembly.Load(…) function, it triggers the “RuntimeAssembly.nLoadImage(…)” function. However, if we attempt to examine the implementation of “RuntimeAssembly.nLoadImage(…)” in tools like dnspy, we will reach a dead end as it provides no visible code. This function is an internalcall, and its actual implementation can be found in clr.dll as “AssemblyNative::LoadImage(…)“. If we examine the below stack trace, we can observe that the “RawImageLayout::RawImageLayout(…)” function invokes the “AmsiScan(…)” function. However, the root of the call is from “AssemblyNative::LoadImage(…)” if we look in the call stack
Below is the AmsiScan function in the clr.dll library,
If we closely observe the pseudo code in IDA, we can see the first if block where “g_amsiContext” and “byte_107E2CFC” variables are getting checked global structure and initialised variables respectively .
To enter a if block both variables need to be 0, initially both are initialised to 0. If both are initialised to 0 then it’ll enter the if block and load the amsi.dll library and it’ll resolve 2 functions “AmsiInitialize” and “AmsiScanBuffer”. After that it’ll call the AmsiInitialize() function and return AMSICONTEXT in the v19 variable. Then the v19 which is AMSICONTEXT is set to the global structure “g_amsiContext”. Then “byte_107E2CFC” is set to “1” which indicates that Amsi is initialised successfully. As seen in the below image.
After exiting from the if block it’ll check if g_amsiContext is set and if it’s set it’ll call the function AmsiScanBuffer(…) otherwise it’ll just return from the AmsiScan(…).
Now if we overwrite the value of variable “byte_107E2CFC” to 1 before the “AmsiScan” function gets triggered, it’ll never enter the if block and it’ll never initialise the g_amsiContext which ultimately stops execution of AmsiScanBuffer.
To overwrite the value of variable “byte_107E2CFC” we first need to find the location of the variable in the memory. For this we’ll perform a memory pattern search.
After finding the pattern we’ll extract the variable memory address and write a value “1” to that address before executing the Assembly.Load(…) function. This will tell the program that Amsi is already initialised and the global structure “g_amsiContext” will never be initialised. Good part is the memory location is already RW, so we don’t need to change the memory protection before writing a byte into that memory address.
Author : John Sherchan, Red Team Security Researcher at CyberWarFare Labs
Co-Author : Yash Bharadwaj, CTO at CyberWarFare Labs