Process hollowing is a useful subtechnique of process injection used by adversaries to execute malicious code in the context of otherwise a legitimate process. It can be used potentially as a privilege escalation method whilst bypassing any process based defenses which may be implemented on the system.
In this article we will take a deeper dive into the topic and create a windows executable which will write our malicious shell code inside a legitimate process.
To understand this technique more thoroughly I suggest to first check this post about Windows processes and how they work.
This technique in essence consists of three main stages,
- creating or finding a process to inject into
- injecting our shellcode inside this process
- resuming the execution of the target process
We can use many different techniques for each of these stages as time goes by I will be expanding this topic to include more topics.
Here I will be developing a native executable using Visual Studio via pinvoke techniques. Initially let’s set up our environment.
We can download Visual Studio from here. While installing make sure you check the box stating .NET Desktop development, this will install the .NET framework.
Once installed, open Visual Studio select “Create New Project” search for “Console Application” then select “Console App (.NET Framework)” with C# then select “Next”.
In the next window you can give a name to this project and select a target framework.
!! Make sure to select a matching .NET framework version. If you are unsure of the .NET version of the target machine you can check it by using this command.
reg query "HKLM\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full"
The version entry in this registry will indicate the framework version we need to choose here.
Select the matching version from the dropdown menu.
Before we can start writing code. We need to configure our project accordingly. On the right side of the screen you will the “Solution Explorer” window. Right click the project name and select properties.
Here on the build section select x64 as your platform target and check the “Allow unsafe code”. Process data structures vary from architecture type, so defining our target architecture before hand will aid us in further steps while debugging.
Allow unsafe code option disables the native bounds checking for pointers and safety precautions of that nature. We will be working with pointers a lot on our code so this will suppress unnecessary warnings.
Now we are ready to start writing our code.
Our first step will be loading our the function we are planning to
use. We will use functions from Windows API and load them from the
system DLLs (kernel32.dll
, ntdll.dll
).
I will be using the platform invoke trick detailed in the Offensive Security Evasion Professional handbook. This trick uses two namespaces System
and System.Runtime.InteropServices
and allows us to access Windows API functions and use their unmanaged
code. For any API calls we wish to make we can search their name in pinvoke platform and use the DLLImport statement provided.
We can go ahead and delete all other using statements and add the ones we need.
Let’s start !
We first need to either create or a find and open a process.
In order to create a process we can utilize many function as also displayed in this figure extracted form Windows Internals.
We can use CreateProcess here as it is the easiest and most common way to create processes in windows. If we just try to call the function inside our main method we will observe that the function is not found within the current context. This is because we have not yet imported our function yet, to import our function we can just search the
pinvoke
site using the keyword CreateProcess
.
Here we can see the relevant page giving us a C# Signature which we can use in our code to import our function.
We can then paste this signature, inside our class. However this will still give raise some errors, this is due to the fact that we still have not defined the data types used in the declaration of this imported function.
So we need to define, SECURITY_ATTRIBUTES
, STARTUPINFO
and PROCESS_INFORMATION
data types. We can find their definition also on the pinvoke.net website. Just repeat the previous steps.
TIP : In, Visual Studio we can also define regions using the appropriately named #region directive. We can enclose these import and definition statements withing a region to have a clean experience by collapsing the regions.
Now let’s create a process using the function we imported. If we write our function name, Visual Studio will kindly give us a list of parameters we should pass to this function.
To know what we have to pass as arguments to this function to achieve what we want we can check out the Microsoft documentation for it.
First two arguments are lpApplicationName
and lpCommandLine
.
These specify the application name and the command the process should
be created from. We can use these interchangeably and leave lpApplicationName
empty while specifying a program path in lpCommandLine
or vice-versa.
Next two arguments are the Process and Thread security attributes lpProcessAttributes
and lpThreadAttributes
. For these arguments we can initialize default SECURITY_ATTRIBUTES
and pass them along.
To initialize default SECURITY _ATTRIBUTES.
SECURITY_ATTRIBUTES pSec = new SECURITY_ATTRIBUTES();
SECURITY_ATTRIBUTES tSec = new SECURITY_ATTRIBUTES();
Next argument is a boolean variable indicating whether we should inherit the parent’s handles in our new process. This feature is not relevant for us in this case so we can just pass false.
Next are the creation flags(dwCreationFlags
). These flags are important for our cause. We can see a list of creation flag here or here.
They all have their own use cases but what interest us most is the
CREATE_SUSPENDED option. This option will create the process in a
suspended state, meaning it will initialize the process but not execute
it until we call ResumeThread
function. However, for now we
can just pass 0 for this argument as well to observe CreateProcess in
action. Later we will change the 0 value to 0x00000004 to indicate this
option.
Next argument lpEnvironment
allows us to pass a custom environment to our newly created process which we don’t need to, so we can pass 0.
Next inline is lpCurrentDirectory this indicated the path for the
current working directory for the process to which we can pass an empty
value. However since this value is of type IntPtr, instead of using null
or 0 we should pass IntPtr.Zero
.
Second to last is the lpStartupInfo
. This argument
specifies the window size and similar options. Like security options we
can initialize default STARTUPINFO structure and pass it along.
STARTUPINFO sInfo = new STARTUPINFO();
Last argument is the most important for our case. It is the PROCESS_INFORMATION
type variable lpProcessInformation
. This is an out
argument meaning we need to declare it ourselves and pass by reference
to the function. Function will write to this variable so we can later
use it.
By passing the above said arguments some by reference (ref) and out keywords we will get this. And if we click the start button highlighted below, we will see that the calculator process is started.
Now that we have validated that our code is working as intended we can pass the CREATE_SUSPENDED argument to our function to halt the process execution even before it starts. Let’s make the changes to our code and run it.
If we look at our process list using Process Explorer or similar utilities. We can see that a calculator process has indeed been started in a suspended state.
For better understanding of what our code does we can get our process
id either by using these tools or just printing it from our code via
the following lines.
Console.WriteLine("Process ID: {0}",pInfo.dwProcessId);
Console.ReadLine();
Now here we see that this pInfo structure which is a type of PROCESS_BASIC_INFORMATION, holds the id of our process. Interesting, what else does it hold?
We can answer this question by referring to the microsoft documentation.