Skip to main content

Intro to Process Injection

Date: 09/10/25·Author: emryllfoundationevasion

Intro to Process Injection

In this article we will go over the basic concept of process injection; what it consists of and a few examples.

Process Injection is a classic technique used by a large amount of malware. For a long time it has been a go-to evasion trick for malware authors. However with the advancement of EDRs, sophisticated adversaries are increasingly pivoting away from relying on process injection for evasion.

The idea is to make a more trusted, benign program run your malicious code. This makes it more difficult to detect and correlate malicious behavior to the real origin, and can even achieve objectives beyond evasion, such as privilege escalation. Malware abuses legitimate features of the OS to achieve this.

There are countless process injection techniques, but generally process injection consists of 3 primitives:

  1. The allocation primitive. Allocating memory in the remote process. For example VirtualAllocEx.
  2. The write primitive. Writing into the allocated memory. For example WriteProcessMemory.
  3. The execution primitive. Actually executing the payload. EDRs are often most focused on this part for detection.

Some of the more advanced process injection techniques don't require all 3 primitives. It should also be noted that the exact purpose of these primitives differ through out different techniques. There is a 0th one, which is opening a handle to the target process, although it is mostly constant among techniques.

Let's briefly go over two of the most basic injection techniques. I will not go over the implementation in code, as there are already tons of these online. I will link some at the bottom of the article.

Classic shellcode injection

This is the most basic form of process injection. The idea is to write our shellcode to a remote process, and then create a thread within this process to execute said shellcode.

1. Get a handle to the process

First we must get a handle to the remote process. You can do this with OpenProcess. Notice that you must have sufficient rights to opening a handle to said process. Not all processes work for injection, as some are protected.

You should also only get a handle with only the access rights you need. Getting a handle with PROCESS_ALL_ACCESS is quite suspicious and EDRs have increasingly focused on this part.

DWORD desiredAccess = PROCESS_CREATE_THREAD | PROCESS_VM_WRITE | PROCESS_VM_OPERATION;
HANDLE hProcess = OpenProcess(desiredAccess, FALSE, pid);

Make sure to always check the return value to see if the function call succeeded. If the call failed, you can get more information with GetLastError and looking up the code. I won't include error handling in these snippets, as it would add visual clutter.

2. Allocate memory for your shellcode

Next we need to allocate executable memory in the target process. Allocating memory basically makes the OS give us some memory which we can use. We can do this with the VirtualAllocEx API function.

For simplicity's sake, let's allocate RWX memory. In reality this should not be done, because memory should never be writable and executable. RWX memory is heavily monitored by security products and will get you flagged very quickly. In benign applications RWX memory is only used in special contexts like JIT compilation.

Instead of using RWX memory, you can allocate RW memory, write to it and then change it to RX memory.

LPVOID buffer = VirtualAllocEx(hProcess, NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

3. Write your shellcode to memory

Now that we have allocated some memory in our target process, let's actually use that memory. We can write our shellcode to it with WriteProcessMemory. There are many alternative functions for each of these steps, but we will use the most basic ones for this basic example.

BOOL ok = WriteProcessMemory(hProcess, buffer, shellcode, sizeof(shellcode), NULL);
ok = VirtualProtectEx(hProcess, buffer, sizeof(shellcode), PAGE_EXECUTE_READ, &dwOldProtection);

4. Execute your shellcode

Finally we can execute our payload by creating a thread in the target process. This can be done with the CreateRemoteThread API function_. At this point the buffer our shellcode resides in must be executable._

HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)buffer, NULL, 0, NULL);

Basic DLL injection

DLL injection takes another route and instead of using shellcode as the payload, a DLL is used. DLLs are libraries which use the same file format as regular exe files. DLL injection is easier to implement than shellcode injection, as you dont have to bother with shellcode, and it is also arguably stealthier.

DLLs have a unique (optional) main function called DllMain. It allows us to run code when the DLL is loaded. It should be mentioned that when your DLL is loaded and your DllMain runs, something called loader lock is on. This imposes significant limitations to what can be done in DllMain, and generally it should be as short as possible_._ We will not go into details of the loader lock in this article, but there is a long list of things we are not supposed to do in DllMain.

Even though creating a thread inside DllMain is not best practice according to MSDN, we will still do it, as it shouldn't cause trouble without synchronization. If you wait for the new thread while loader lock is held, the program will deadlock.

So to recap, our payload DLL should have a DllMain, which creates a new thread to run our payload.

1. Get a handle to the process

First we must get a handle to the remote process. You can do this with OpenProcess. Notice that you must have sufficient rights to opening a handle to said process. Not all processes work for injection, as some are protected.

You should also only get a handle with only the access rights you need. Getting a handle with PROCESS_ALL_ACCESS is quite suspicious and EDRs have increasingly focused on this part.

DWORD desiredAccess = PROCESS_CREATE_THREAD | PROCESS_VM_WRITE | PROCESS_VM_OPERATION;
HANDLE hProcess = OpenProcess(desiredAccess, FALSE, pid);

Make sure to always check the return value to see if the function call succeeded. If the call failed, you can get more information with GetLastError and looking up the code

2. Get the address of LoadLibraryA

Now we will get the address of the LoadLibraryA function, so we can later call it in the target process. You can get the address by first calling GetModuleHandleA to get the base address/handle of kernel32.dll and then calling GetProcAddress to get the address of LoadLibraryA.

HMODULE moduleBase = GetModuleHandleA("kernel32.dll");
FARPROC pLoadLibrary = GetProcAddress(moduleBase, "LoadLibraryA");

There is an interesting detail hiding in this. Even though ASLR randomizes addresses for each new process, it does not randomize the addresses of so-called Known DLLs, such as kernel32.dll. These Known DLLs get their addresses randomized when you first boot up. All processes are actually using the same memory for these libraries under-the-hood, for efficiency purposes and this is why they have the same addresses across processes, even with ASLR enabled. You can observe this yourself with a tool such as System Informer.

3. Allocate memory for the DLL name

Next we need to allocate memory in the target process to store our DLLs name. This can be done with VirtualAllocEx. Notice that you need to allocate 1 extra byte for the null terminator. This gives us memory that we can use. It does not need to be executable.

LPVOID buffer = VirtualAllocEx(hProcess, NULL, strlen(DLL_NAME)+1, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

4. Write the DLL name

Now that we have allocated some memory, we can write the name or path of our DLL into it. If you only use the name, the default search order will be used to attempt to find it. You can use WriteProcessMemory for this step.

BOOL ok = WriteProcessMemory(hProcess, buffer, DLL_NAME, strlen(DLL_NAME)+1, NULL);

5. Create a thread to load the DLL

Finally we can trigger our payload. We will create a thread in the target process, which will call LoadLibraryA with our newly allocated buffer (the name/path) as the function argument. Now your payload should run.

HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)pLoadLibrary, buffer, 0, NULL);

Out of these two most basic process injection techniques, I would say DLL injection is clearly superior. It has a few advantages; it doesn't allocate executable memory, some benign applications use DLL injection, and it is a bit challenging to differentiate a maliciously loaded DLL from a benign load event.

This basic DLL injection in this form is still not very stealthy. Especially the CreateRemoteThread is suspicious and can be used for quite reliable detection. Creating a thread starting at the address of LoadLibraryA is quite odd to say the least...

With shellcode injection, you are creating a remote thread into suspicious unbacked executable memory. Normally, executable memory should be backed by a file on disk, like an EXE or DLL. If the memory is RWX, that's even worse, but some modern EDRs also look for the RW->RX move, although it is a better choice.

In the next few articles we will go over some more techniques of process injection...

Further reading