Process Injection Series: Part 1 - Introduction to Basic Process Injection
- bacavendish
- Aug 12, 2024
- 5 min read
Disclaimer:
The content in this blog is intended solely for educational purposes. The techniques discussed should only be used in a legal and ethical manner, with proper authorization. The author is not responsible for any misuse of the information provided. Always ensure you have explicit permission before testing or applying these techniques on any systems.
Welcome to the first part of my series on process injection techniques. Process injection is a powerful method often used in both legitimate software development and malicious activities to insert code into another running process. In this series, various injection techniques will be explored, starting with a simple example of injecting a message into a running process on a Linux system. By the end of this series, I hope to give my readers a clear understanding of process injection and the various methods in which it can be carried out.
What is Process Injection?
Process injection allows one process to insert code or data into the memory space of another process. This technique can be used to alter the behavior of the target process, execute additional code, or even hide malicious activities. While often associated with malware, process injection is also used in debugging and other legitimate applications. Understanding how this works is crucial for both defensive and offensive cybersecurity strategies.
Setting Up Your Environment
For this demonstration, a Linux environment with a C compiler is needed. Kali Linux is recommended, but any modern Linux distribution should work. The full code for this example can be found in the GitHub repository at the bottom of the page, but key parts of the code will be walked through here to explain the concepts.
Understanding the Code
In this example, a simple message will be injected into a running process gedit. Here’s a brief overview of how the injection works:
void inject_message(pid_t pid, const char *message) {
// Attach to the process
if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1) {
perror("PTRACE_ATTACH failed");
return;
}
waitpid(pid, NULL, 0);
// Get the current registers
struct user_regs_struct regs;
if (ptrace(PTRACE_GETREGS, pid, NULL, ®s) == -1) {
perror("PTRACE_GETREGS failed");
ptrace(PTRACE_DETACH, pid, NULL, NULL);
return;
}
// Inject the message
void remote_addr = (void )regs.rsp;
size_t len = strlen(message) + 1;
for (size_t i = 0; i < len; i += sizeof(long)) {
long word;
memcpy(&word, message + i, sizeof(long));
if (ptrace(PTRACE_POKETEXT, pid, remote_addr + i, word) == -1) {
perror("PTRACE_POKETEXT failed");
ptrace(PTRACE_DETACH, pid, NULL, NULL);
return;
}
}
// Detach from the process
ptrace(PTRACE_DETACH, pid, NULL, NULL);
printf("Message injected successfully.\n");
}
Code Breakdown:
Attaching to the Process: In this first step, the ptrace system call is used to attach to the target process. The ptrace function is a powerful tool provided by Linux that allows one process in this case, the injector to observe and control the execution of another process.
How it works:
The PTRACE_ATTACH request is sent to the target process using the process ID (pid). When this request is made, the target process is paused, allowing the injector to gain control over it. This pause is crucial because it ensures that the target process does not continue its normal operation while being manipulated, which could lead to inconsistencies or errors.
Once attached, the injector has the ability to read and write the target process's memory, inspect and modify its CPU registers, and control its execution flow.
Accessing Registers: Once the injector is attached to the target process, the next step is to retrieve the current state of the process's CPU registers using PTRACE_GETREGS.
How it works:
The CPU registers are small, fast storage locations within the CPU that hold key information about the process’s execution state, such as the address of the next instruction to execute, the current stack pointer, and the values of temporary variables.
In this example, the code retrieves the full set of CPU registers into a user_regs_struct structure. This structure contains all the relevant registers, including the stack pointer (RSP on x86_64 systems).
The stack pointer (RSP) is of particular interest because it points to the top of the stack, a region of memory used for managing function calls, local variables, and control flow within the process. By knowing the exact location of the stack in memory, the injector can safely write data to a predictable location within the process’s memory space.
Injecting Data: After determining where in the target process's memory to write, the injector proceeds to inject the desired data, in this case, a simple message.
How it works:
The code determines the injection point based on the stack pointer (RSP), which is retrieved from the CPU registers. By targeting the stack, the injector takes advantage of a memory region that is typically writable and often used for temporary storage, making it a convenient location for injecting data.
The ptrace system call is then used with the PTRACE_POKETEXT request to write the message into the target process’s memory. The message is written in chunks, typically the size of a long (which is 4 or 8 bytes, depending on the system architecture). This step-by-step writing process ensures that the data is accurately placed into the target memory without overwriting critical sections.
Detaching: After the injection is complete, the injector must cleanly detach from the target process to allow it to resume normal operation.
How it works:
The ptrace system call is used with the PTRACE_DETACH request to release control of the target process. When this command is issued, the target process is allowed to continue running as if it had never been paused.
During detachment, the injector ensures that no traces of its operation remain visible in its own process space. The target process continues to operate normally, but with the injected data now residing in its memory.
Code Results
When this code was executed, the process reported "Message injected successfully." This indicates that the program was able to attach to the target process, write data into its memory, and detach without any errors. Although the injected data was a simple string and not executable code, it didn’t alter the visible behavior of the target process. However, the true significance lies in understanding the subtle system impact of this technique.
The Subtlety of Process Injection
One of the most intriguing aspects of this method is the minimal footprint on the system. The injector program itself uses very little memory, just enough to run a basic C program. Its primary action is to interact with the target process's memory space, not to consume significant system resources of its own. Once the injector program completes its task and exits, it leaves no trace of its own memory usage or presence in the system. The actual memory modifications such as the injected message reside entirely within the target process. This makes the injector program somewhat "invisible" after execution, as it doesn't leave behind any artifacts or consume ongoing system resources.
Next Steps
This basic example introduces the core concepts of process injection, setting the foundation for more complex techniques. As the series progresses, the focus will shift to exploring how to inject executable code, dynamically allocate memory in the target process, and manipulate the process's behavior more deeply.
To explore the full code and follow along with the series, visit the GitHub repository below. Stay tuned for the next installment, where this foundation will be built upon to tackle more advanced injection techniques.
Github Repository: https://github.com/Brandon33310/ProcessInjection
Sources:
Comments