Deep Dive: Process Injection via Code Cave Discovery on Linux #
In the world of low-level systems programming and offensive security research, Process Injection is a surgical technique. While many developers are familiar with LD_PRELOAD or shared library injection, this project explores a more manual approach: finding a “Code Cave” and hijacking the instruction pointer () via the ptrace system call.
This article breaks down a C implementation of a Code Cave Injector designed to hijack a running process without causing a segmentation fault or crash.
The Architecture of the Injection #
The objective of the code is to take control of a “victim” process, execute custom assembly, and then return control to the original execution flow. I have designed the logic to follow a strict four-stage workflow:
- Attachment: The code uses
ptrace(PTRACE_ATTACH)to pause the target process and gain control over its registers. - Reconnaissance: It parses
/proc/[pid]/mapsto find memory segments marked as executable (r-xp). - Cave Hunting: It scans those segments for a “Code Cave”—a sequence of null bytes () large enough to hold the payload.
- Hijack: The code patches the payload with the target’s original , writes it into the cave, and redirects the process.
1. Defining the Payload (The “Guest”) #
The payload is written in raw x86_64 assembly using an __asm__ block. To ensure the target process doesn’t crash, the assembly must save the CPU state.
"push %rax; push %rdi; push %rsi..." // Save volatile registers
// ... Syscall: write(1, msg, 32) ...
"pop %rsi; pop %rdi; pop %rax..." // Restore registers
".byte 0x48, 0xb8\n" // Opcode for MOV RAX, <immediate_64>
"continuation: .quad 0x0\n" // Placeholder for original RIP
"jmp *%rax\n" // Jump back to normal execution
The use of .quad 0x0 is a critical design choice. It acts as a “hot patch” zone. The injector will find the bytes (the movabs instruction) and overwrite the subsequent 8 bytes with the address where the process was interrupted.
2. Hunting for the Cave #
A code cave typically exists because of memory page alignment. When a compiler generates a code section, it might only fill bytes of a -byte page. The remaining bytes are often filled with nulls and remain executable.
The find_code_cave function performs the following:
- It opens
/proc/[target_pid]/maps. - It identifies regions with the ‘x’ (executable) permission bit.
- It uses
PTRACE_PEEKDATAto read the memory of the target 8 bytes at a time. - It iterates through these bytes looking for a continuous stream of values that matches the
payload_size.
3. The Patching Logic #
Before writing to the target, the code must “contextualize” the payload. If the injector simply wrote the raw assembly, the process would have no way to return to its original task.
// Search local_payload for the 0x48 0xb8 signature
unsigned long return_addr_offset = 0;
for(size_t i=0; i<payload_size-8; i++) {
if(local_payload[i] == 0x48 && local_payload[i+1] == 0xb8) {
return_addr_offset = i + 2;
break;
}
}
// Overwrite the placeholder with the actual RIP of the victim
*(unsigned long*)(local_payload + return_addr_offset) = regs->rip;This logic ensures that the injected code acts as a “transparent wrapper.” I am essentially inserting a detour: the CPU goes to the cave, prints the message, and then jumps back to the exact instruction it was supposed to execute next.
4. Modifying the Instruction Pointer #
The final step is the actual redirection. I use PTRACE_SETREGS to update the target’s register state. By changing the to the address of the code cave, the next instruction the CPU fetches for that process will be the start of my payload.
regs->rip = cave_addr;
if (ptrace(PTRACE_SETREGS, target, NULL, ®s) == -1) {
perror("ptrace setregs");
}Once ptrace(PTRACE_DETACH) is called, the kernel resumes the process. The victim process continues running, completely unaware that it just executed an external payload.
Technical Constraints and Stability #
- ASLR (Address Space Layout Randomization): This implementation bypasses ASLR because it queries the dynamic memory map of the process at runtime rather than relying on static offsets.
- PTRACE_SCOPE: On many modern Linux distributions,
/proc/sys/kernel/yama/ptrace_scopeis set to1. This prevents the injector from attaching to processes that are not its children unless it is run withsudo. - Memory Coherency: Since I am writing to an existing executable page, I don’t need to call
mmapormprotect, making the injection stealthier and less likely to trigger security alerts that monitor forRWXmemory allocations.