Process Injection in a Nutshell

Nikhil gupta
9 min readSep 7, 2024

--

Disclaimer:
The code and techniques provided in this blog are intended for educational purposes only. They are designed to help individuals understand the underlying principles of cybersecurity, ethical hacking, and software development. Under no circumstances should the information or code be used for unauthorized access, illegal hacking, or any activities that violate the law.

The author and publisher do not endorse or condone any illegal activities, and they will not be held responsible for any misuse of the information provided. By reading this article, you agree to use the information solely for lawful and ethical purposes.

Process injection is a technique used by software (often malware) to execute arbitrary code within the address space of another process. It involves manipulating the memory and execution context of a target process to insert and run malicious or unauthorized code. By doing this, the injected code runs within the context of the legitimate process, potentially bypassing security controls like firewalls, antivirus, and monitoring systems.

Key Techniques for Process Injection:

  1. DLL Injection: Involves forcing a process to load a Dynamic Link Library (DLL) that contains malicious code.
  2. Process Hollowing: A legitimate process is created in a suspended state, its original code is replaced with malicious code, and then the process is resumed.
  3. APC (Asynchronous Procedure Call) Injection: Utilizes APCs to schedule malicious code for execution in the context of another process.
  4. Thread Execution Hijacking: Involves suspending a target process thread, modifying its execution context to point to the injected code, and resuming it.
  5. Reflective DLL Injection: A DLL is loaded into memory and executed without the involvement of the Windows loader, bypassing many security checks.

Process Injection for Legitimate Reasons?

Process injection, while commonly associated with malicious activities, also has legitimate uses:

  1. Debugging and Profiling: Debuggers often inject code to monitor or modify the behavior of processes. For example, breakpoints and watchpoints are set by injecting specific instructions into the target process.
  2. Performance Monitoring: Tools like profilers inject code to track performance metrics of an application, such as CPU usage or memory allocation.
  3. Inter-Process Communication (IPC): Some applications inject shared libraries or code to facilitate communication between processes.
  4. Software Compatibility Layers: Compatibility tools like Wine or virtual machines may inject code to redirect system calls or modify behavior to match another platform or environment.
  5. Hooking API Calls: Security software and other monitoring tools often use injection techniques to hook into sensitive API calls to detect or prevent malicious activity.

How Process Injection Can Be Achieved:

  1. Identifying Target Process: Before injection, the target process must be identified, which can be done using OpenProcess() API in Windows, which requires the process ID (PID) and appropriate access rights.
  2. Memory Manipulation: Allocate memory in the target process to hold the malicious code. VirtualAllocEx()Allocates memory within the address space of the target process. Writing the payload to the allocated memory in the target process is done by using WriteProcessMemory().
  3. Execution Context Manipulation: To execute the injected code, the execution context is modified using CreateRemoteThread(). This Creates a new thread in the target process that executes the injected code. QueueUserAPC()Adds a function to be executed when the target thread enters an alertable state. Hijacking an existing thread can be done using SuspendThread(), GetThreadContext(), SetThreadContext(), and ResumeThread().
  4. Code Execution Techniques:
    DLL Injection: Uses CreateRemoteThread() and LoadLibrary() to load a DLL into the target process. The path to the DLL is written into the target process's memory, and LoadLibrary is executed remotely.
    Reflective DLL Injection: Bypasses the Windows loader by manually resolving function addresses and writing the DLL into the target process’s memory, then creating a thread to execute it.
    Process Hollowing: A legitimate process is started in a suspended state using CreateProcess(). The original process's memory is unmapped (ZwUnmapViewOfSection), and new malicious code is written to it. Then, the process is resumed.

Here are the Windows APIs and syscalls most commonly used for process injection:

  • OpenProcess(): Opens a handle to the target process with necessary privileges (e.g., PROCESS_ALL_ACCESS).
  • VirtualAllocEx(): Allocates memory in the address space of the target process.
  • WriteProcessMemory(): Writes the payload into the allocated memory space of the target process.
  • CreateRemoteThread(): Creates a thread in the target process that points to the payload location.
  • LoadLibraryA/W(): Loads a DLL into the address space of the target process.
  • NtCreateThreadEx() (Syscall): Similar to CreateRemoteThread(), but directly invokes the system call, often used to evade user-mode hooks.
  • NtMapViewOfSection() (Syscall): Maps a view of a section into the virtual address space of a process, used in process hollowing.
  • QueueUserAPC(): Schedules an Asynchronous Procedure Call for a specific thread, allowing the execution of injected code when the thread enters an alertable state.

Example of Process Injection Using Shellcode Injection

To perform process injection by directly inserting code into a target process, we’ll use a technique known as shellcode injection. This involves injecting a block of machine code (often written in assembly language) into the memory space of a target process and then executing it.

Shellcode injection generally involves the following steps:

  • Identify the Target Process: Select a target process and obtain its process ID (PID).
  • Open the Target Process: Open the target process with sufficient privileges to modify its memory and create threads.
  • Allocate Memory in the Target Process: Allocate memory within the target process for the shellcode.
  • Write the Shellcode to the Allocated Memory: Write the machine code (shellcode) to the allocated memory.
  • Create a Remote Thread to Execute the Shellcode: Create a new thread in the target process to execute the shellcode.

Below is the C code to perform the process injection by inserting code (shellcode) directly into the target process. For demonstration purposes, I’ve targeted the notepad.exe process, assuming it is already running. Upon successful injection and execution, this shellcode will display a message box with “Hello” in both the title and message.

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

// Shellcode example: MessageBoxA call
unsigned char shellcode[] = {
0x6A, 0x00, // push 0 (MB_OK)
0x68, 0x65, 0x6C, 0x6C, 0x6F, // push "olleh"
0x68, 0x48, 0x65, 0x6C, 0x6C, // push "lleH"
0x8B, 0xC4, // mov eax, esp
0x6A, 0x00, // push 0 (NULL)
0x50, // push eax (Title)
0x50, // push eax (Text)
0xB8, 0xC3, 0x93, 0xC2, 0x77, // mov eax, 0x77C293C3 (Address of MessageBoxA)
0xFF, 0xD0, // call eax
0xCC // int 3 (debug break)
};

// Function to find a target process ID (PID) by name
DWORD FindTargetProcessID(const char* processName) {
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) return 0;

PROCESSENTRY32 pe;
pe.dwSize = sizeof(PROCESSENTRY32);

if (Process32First(hSnapshot, &pe)) {
do {
if (strcmp(pe.szExeFile, processName) == 0) {
CloseHandle(hSnapshot);
return pe.th32ProcessID;
}
} while (Process32Next(hSnapshot, &pe));
}

CloseHandle(hSnapshot);
return 0;
}

int main() {
// Find the target process ID
DWORD targetProcessID = FindTargetProcessID("notepad.exe");
if (targetProcessID == 0) {
printf("Target process not found.\n");
return 1;
}

// Open the target process
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetProcessID);
if (hProcess == NULL) {
printf("Failed to open target process. Error: %lu\n", GetLastError());
return 1;
}

// Allocate memory in the target process for the shellcode
LPVOID pRemoteShellcode = VirtualAllocEx(hProcess, NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (pRemoteShellcode == NULL) {
printf("Failed to allocate memory in target process. Error: %lu\n", GetLastError());
CloseHandle(hProcess);
return 1;
}

// Write the shellcode to the allocated memory
BOOL writeResult = WriteProcessMemory(hProcess, pRemoteShellcode, shellcode, sizeof(shellcode), NULL);
if (!writeResult) {
printf("Failed to write shellcode to process memory. Error: %lu\n", GetLastError());
VirtualFreeEx(hProcess, pRemoteShellcode, 0, MEM_RELEASE);
CloseHandle(hProcess);
return 1;
}

// Create a remote thread in the target process to execute the shellcode
HANDLE hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pRemoteShellcode, NULL, 0, NULL);
if (hRemoteThread == NULL) {
printf("Failed to create remote thread. Error: %lu\n", GetLastError());
VirtualFreeEx(hProcess, pRemoteShellcode, 0, MEM_RELEASE);
CloseHandle(hProcess);
return 1;
}

// Wait for the remote thread to complete execution
WaitForSingleObject(hRemoteThread, INFINITE);

// Clean up
VirtualFreeEx(hProcess, pRemoteShellcode, 0, MEM_RELEASE);
CloseHandle(hRemoteThread);
CloseHandle(hProcess);

printf("Shellcode injected and executed successfully.\n");
return 0;
}

Explanation of the Code:

  1. Shellcode Definition: The shellcode array contains raw machine code to display a message box (MessageBoxA). This code is written in assembly and compiled into byte form.
  2. Find Target Process ID (PID): The FindTargetProcessID() function searches for a running process named notepad.exe using the Toolhelp32 API to enumerate processes and returns its PID.
  3. Open the Target Process: OpenProcess() opens the target process (notepad.exe in this case) with full access rights (PROCESS_ALL_ACCESS), allowing us to modify its memory.
  4. Allocate Memory in Target Process: VirtualAllocEx() allocates a block of memory in the target process with MEM_COMMIT and PAGE_EXECUTE_READWRITE permissions. This memory will hold the shellcode.
  5. Write Shellcode into Target Process Memory: WriteProcessMemory() writes the shellcode into the allocated memory of the target process. The shellcode contains machine instructions to be executed within the target process.
  6. Create Remote Thread to Execute Shellcode: CreateRemoteThread() creates a new thread in the target process. This thread begins execution at the address where the shellcode is located (pRemoteShellcode), causing the shellcode to run in the context of the target process.
  7. Wait for the Shellcode to Execute and Clean Up: WaitForSingleObject() waits for the remote thread to complete. After execution, the allocated memory is freed, and handles are closed to avoid leaks.

Assembly Code Explanation:

  • The shellcode directly calls the MessageBoxA API from the user32.dll using its hardcoded address. In a real-world scenario, this address might need to be dynamically located to ensure compatibility across different system versions.
  • The 0x6A 0x00 pushes MB_OK (message box type).
  • The series of push instructions push the message text and title onto the stack.
  • mov eax, 0x77C293C3 loads the address of MessageBoxA into the eax register.
  • call eax calls the function at the address stored in eax.

Assumptions made in the code block

The code block provided for injecting shellcode into a target process makes several assumptions to ensure that the process injection will be successful. Here are the key assumptions:

1. Target Process Assumption:

  • The code assumes that the target process (notepad.exe) is running on the system at the time of execution. If notepad.exe is not running, the function FindTargetProcessID() will fail to find the process ID (PID), and the code will terminate with a message stating, "Target process not found."

2. Hardcoded Shellcode Assumption:

  • The shellcode is designed to call the MessageBoxA function with a hardcoded memory address (0x77C293C3). This address corresponds to the location of the MessageBoxA function in user32.dll for a specific version of the Windows operating system (32-bit version). This address may vary across different versions of Windows (such as Windows 10 vs. Windows 7) and between 32-bit and 64-bit architectures. Therefore, the shellcode will only work correctly if it is executed in an environment where user32.dll is loaded at this exact address.

3. Process Permissions Assumption:

  • The code assumes that it has sufficient permissions to open the target process with PROCESS_ALL_ACCESS rights using OpenProcess(). The executing user must have administrative privileges or the necessary rights to access another process's memory. If the permissions are insufficient, the OpenProcess() call will fail.

4. Compatible Architecture Assumption:

  • The code assumes that both the injector (the program executing this code) and the target process (notepad.exe) are running on the same architecture (either both 32-bit or both 64-bit). Mixing architectures (e.g., a 32-bit injector attempting to inject shellcode into a 64-bit target process) will cause the injection to fail. This is because addresses, structures, and calling conventions differ between architectures.

5. Memory Allocation Assumption:

  • The code assumes that the target process has enough free virtual memory to accommodate the injected shellcode. The VirtualAllocEx() function must succeed in allocating memory within the target process's address space. If there is insufficient memory or if memory allocation is otherwise restricted, the injection will fail.

6. Function Pointer Validity Assumption:

  • The code assumes that the cast to (LPTHREAD_START_ROUTINE)pRemoteShellcode is valid, meaning that the starting address for the thread (where the shellcode is injected) points to a correctly aligned and executable region of memory. If this assumption is incorrect, the remote thread creation will fail, or the process may crash.

7. Security Software Interference Assumption:

  • The code assumes that no security software (such as antivirus or anti-malware tools) will block or interfere with the injection process. Many modern security solutions monitor for behaviors such as memory allocation, writing into the memory of another process, and remote thread creation — all of which are flagged as suspicious or malicious activities.

8. Shellcode Execution Assumption:

  • It is assumed that the injected shellcode will execute successfully once injected. This assumes that there are no compatibility issues or other errors in the shellcode logic, that the necessary APIs are present and callable, and that the shellcode’s behavior (calling MessageBoxA and executing int 3) will not be blocked or altered by the operating system or security software.

9. Proper Cleanup Assumption:

  • The code assumes that the cleanup routine (freeing allocated memory, closing handles, etc.) will execute without errors. If any unexpected errors occur, or if the program crashes before cleanup, resources may remain allocated in the target process, which could lead to memory leaks or other issues.

10. Debugging Context Assumption:

  • The shellcode ends with an int 3 instruction (software interrupt), which is a breakpoint commonly used in debugging. It assumes that either no debugger is attached (causing the process to terminate or behave as designed) or that a debugger is attached and will handle the breakpoint appropriately. Without a debugger, this could cause the target process to crash.

#ProcessInjection #ShellCode #WindowsInternals

--

--

Nikhil gupta
Nikhil gupta

Written by Nikhil gupta

Incident Response, Threat Hunting, and Reverse Engineering professional, writing things to learn them better. https://www.linkedin.com/in/nikhilnow/

No responses yet