Process Injection in C#
To begin implementing process injection, we’ll generate a project.
To start, let’s open the ConsoleApp1 Visual Studio solution,
and create a new
.NET standard Console App project
using the Solution Explorer.
We’ll called this project “Inject”.
Once this is open, we’ll begin to import the
four required APIs we discussed earlier.
Let’s start by retrieving the
P/Invoke OpenProcess DllImport statement on pinvoke.net.
We’ll copy the DllImport statement into the Program class,
and add a “using” statement for the InteropServices namespace.
Now that we have the correct syntax for the import statement,
let’s return to the OpenProcess API’s arguments.
The first argument is the access right we want to obtain for the remote process.
Its value will be checked against the security descriptor.
In our case, we request the PROCESS_ALL_ACCESS right,
which will give us complete access to the explorer.exe process.
In our code, we need to reference
PROCESS_ALL_ACCESS by its hexadecimal representation.
Next, we need to decide whether or not
a created child process can inherit this handle.
In our case, we don’t care
so we’ll simply pass the value false.
The final argument is the process ID of explorer.exe,
which we can easily obtain through Process Explorer.
We can now implement the call to OpenProcess.
Now that we have an open channel from one process to another,
we must allocate memory for our shellcode
using VirtualAllocEx,
which requires us to perform another import.
We’ll again use pinvoke.net to find the import.
To enumerate the VirtualAllocEx arguments,
we’ll again turn to the function prototype from MSDN.
The first argument is the process handle to explorer.exe
that we just obtained from OpenProcess,
and the second is the desired address of the allocation in the remote process.
If the API succeeds, our new buffer will be allocated with a starting address
as supplied in lpAddress.
If the address given with lpAddress is already allocated and in use,
the call will fail.
It is better to pass a null value
and let the API select an unused address.
The last three arguments mirror the VirtualAlloc API parameters
and specify the size of the desired allocation,
the allocation type, and the memory protections.
We'll set these to 0x1000, MEM_COMMIT and MEM_RESERVE
and 0x40 (PAGE_EXECUTE_READWRITE), respectively.
After allocating memory
we’ll generate a 64-bit Meterpreter staged shellcode with msfvenom
in csharp format
and embed it in the code.
Next, we’ll copy the shellcode into the memory space of explorer.exe.
We’ll use WriteProcessMemory for this,
and again copy the import statement from pinvoke.net.
WriteProcessMemory also takes five parameters,
and MSDN lists the following prototype:
We first pass the process handle,
followed by the newly allocated memory address in explorer.exe,
along with the address of the byte array containing the shellcode.
The remaining two arguments are the size of the shellcode to be copied,
and a pointer to a location in memory, to output how much data was copied.
Notice that the out keyword was prepended to the outSize variable
to have it passed by reference instead of value.
This ensures that the argument type aligns with the function prototype.
The input buffer also needs to be a pointer
but this is inherint in the C# array data type.
Next, we’ll import CreateRemoteThread with the statement copied from pinvoke.net:
Once more, we’ll inspect the arguments from MSDN.
This API accepts seven arguments,
but we’ll ignore those that aren’t required.
The first argument is the process handle to explorer.exe,
followed by the desired security descriptor of the new thread
and its allowed stack size.
We’ll set these to “0” to accept the default values.
For the fourth argument, we must specify the starting address of the thread.
In our case, it must be equal to the address of the buffer we allocated
and copied our shellcode into inside the explorer.exe process.
The next argument,
is a pointer to variables that will be passed to the thread function
pointed to by lpStartAddress.
Since our shellcode does not need any parameters,
we can pass a NULL here.
The remaining two arguments include various flags,
and an output variable for a thread ID, both of which we’ll ignore.
Before compiling the project, we need to set the CPU architecture to x64
since we are injecting into a 64-bit process.
After compiling the project,
we’ll configure a Meterpreter listener,
and execute our process, injecting the shellcode.
If all goes well,
we’ll obtain a reverse shell running inside explorer.exe
The process ID indicates that the
Meterpreter shell is indeed running inside explorer.exe.
We were able to launch our Meterpreter shellcode directly inside explorer.exe,
which means that even if the original process is killed,
the shell will live on.
We’ve successfully injected arbitrary shellcode into another process.
Good.