Process Hollowing in C#
Now that the process hollowing theory is out of the way, let’s implement it in C#.
The very first step is to create a suspended process.
We have to use the Win32 CreateProcessW API because Process.Start and similar
do not allow us to create a suspended process.
We’ll create a new Console App project in Visual Studio
and name it “Hollow”.
We’ll then find the DllImport for CreateProcessW from pinvoke.net
and add it to our project.
To import CreateProcessW, we must also include the System.Threading namespace.
Some of the argument types are unknown to C#, so we’ll later define them manually.
Let’s examine the function prototype to understand what arguments it accepts.
CreateProcessW accepts a daunting ten parameters
but we’ll only leverage a few of them.
The first parameters include the name of the application to be executed
and the full command line to be executed.
Typically, we’ll set lpApplicationName to “null”
and lpCommandLine to the full path of svchost.exe.
For the next two parameters, we’ll need to specify a security descriptor
but we can submit “null” to obtain the default descriptor.
Next, we must specify if any handles in our current process should be inherited
by the new process, but since we do not care, we can specify “false”.
The dwCreationFlags argument is used to indicate our intention
to launch the new process in a suspended state.
We’ll set this to the numerical representation of
CREATE_SUSPENDED, which is 0x4.
The next two parameters specify the environment variable settings to be used
and the current directory for the new application.
We’ll simply set these to “null”.
Next, we must pass a STARTUPINFO structure,
which can contain a number of values related to how the
window of a new process should be configured.
We’ll find this on pinvoke.net
We’ll add the structure to the source code just prior to the DllImport statements.
The final argument is a PROCESS_INFORMATION structure
that is populated by CreateProcessW
with identification information about the new process,
including the process ID and a handle to the process.
We’ll copy the PInvoke definition of PROCESS_INFORMATION from pinvoke.net.
With all of the arguments understood and the required structures defined,
we can invoke the call by first instantiating a STARTUPINFO
and a PROCESS_INFORMATION object and then supply them to CreateProcessW.
Next, we need to locate the EntryPoint by first disclosing the PEB
through ZwQueryInformationProcess.
We’ll again use P/Invoke to define the DllImport statement.
The ZwQueryInformationProcess API has many uses,
and although most are not officially documented by Microsoft,
an example that fetches the PEB is documented.
Let’s inspect this prototype a bit more closely.
First, notice the function’s prefix.
“Nt” or “Zw” indicates that the API can be called by either a user-mode program
or by a kernel driver respectively.
For our purposes, we do not have to worry about this
as calling the function with either prefix will yield the same results in user-space.
The second item of note is that the return value is given as NTSTATUS.
ZwQueryInformationProcess is a low-level API
located in ntdll.dll and returns a hexadecimal value directly from the kernel
Most of the arguments are relatively simple.
The first is a process handle
that we can obtain from the PROCESS_INFORMATION structure.
The API can perform many actions depending on the second argument,
which is only partially documented.
For our purposes, we’ll set this to ProcessBasicInformation
with a numerical representation of “0”.
When we specify ProcessBasicInformation, the third argument,
ProcessInformation, must be a PROCESS_BASIC_INFORMATION structure
that is populated by the API.
This structure can be found on pinvoke.net.
The remaining two arguments, ProcessInformationLength and ReturnLength,
indicate the size of the input structure
and a variable to hold the size of the fetched data, respectively.
We can now call ZwQueryInformationProcess
and fetch the address of the PEB from the PROCESS_BASIC_INFORMATION structure.
The ptrToImageBase variable now contains a pointer
to the image base of svchost.exe in the suspended process.
Next, we’ll use ReadProcessMemory to fetch the address of the code base
by reading eight bytes of memory.
We must supply five parameters for this function.
They are a process handle, the address to read from,
a buffer to copy the content into, the number of bytes to read,
and a variable to contain the number of bytes actually read.
The DllImport statement for ReadProcessMemory is
also very similar to that of WriteProcessMemory:
Following the DllImport, we can call ReadProcessMemory
by specifying an 8-byte buffer that is then converted to a 64bit integer,
and then cast to a pointer.
The following step is to parse the PE header to locate the EntryPoint.
This is performed by calling ReadProcessMemory again
with a buffer size of 0x200 bytes.
To parse the PE header, we must read the content at offset 0x3C
and use that as a second offset when added to 0x28.
To implement this, we convert four bytes at offset 0x3C,
the “e_lfanew” field, to an unsigned integer.
As stated previously, this is the offset from the
image base to the PE header structure.
Next, we convert the four bytes at offset “e_lfanew”
plus 0x28 into an unsigned integer.
This value is the offset from the image base to the EntryPoint.
The offset from the base address of svchost.exe to the EntryPoint
is also called the relative virtual address.
We must add it to the image base to obtain the full memory address of the EntryPoint.
This is done on the last line of the code.
We have obtained the address of the EntryPoint so we use WriteProcessMemory
to overwrite the existing code with our meterpreter shellcode.
Remember that we must add a DllImport statement
for WriteProcessMemory before using it.
Now that everything is set up correctly, we’ll start the execution of our shellcode
In the previous techniques, we have called CreateRemoteThread to spin up a new thread
but in this case, a thread already exists and is waiting to execute our shellcode.
We can use the Win32 ResumeThread API to let the suspended thread
of a remote process continue its execution.
ResumeThread is an easy API to call
since it only requires the handle of the thread to resume.
When CreateProcessW started svchost.exe
and populated the PROCESS_INFORMATION structure,
it also copied the handle of the main thread into it.
We can then import ResumeThread and call it directly.
We now have all the pieces to create a suspended process,
hollow out its original code,
replace it with our shellcode, and subsequently execute it.
Once we have combined all the code we must remember to specify a 64-bit architecture,
since svchost.exe is a 64-bit process,
and change it from “debug” to “release”,
before compiling.
As always, we make sure our listener is running.
When we execute it,
the compiled code results in a reverse Meterpreter shell
executing inside a svchost.exe process, possibly evading suspicion
since it is a trusted process that also engages in network communications.
Excellent!