Understanding ptrace: A Post-Exploitation Primitive on Linux

󰃭 2022-05-12



Most Linux developers know ptrace as the system call behind tools like gdb and strace.

ptrace gives one process the ability to inspect—and even modify—the execution of another process. That makes it an incredibly useful debugging primitive, but it also makes it a valuable post-exploitation tool once an attacker gains code execution as a user.

In this post we’ll look at:

  • What ptrace actually does
  • Why it is such a powerful attack primitive
  • Two common abuse cases
    • Reading sensitive process memory
    • Injecting code into another process
  • How sudo credential caching can amplify the impact
  • Defensive mitigations

What is ptrace?

The Linux man page describes ptrace as a mechanism that allows one process (the tracer) to observe and control another process (the tracee).

A tracer can:

  • Pause and resume execution
  • Read registers
  • Modify registers
  • Read process memory
  • Write process memory
  • Intercept system calls

These capabilities are exactly what debuggers need. They’re also exactly what an attacker wants after compromising a machine.

If malware can attach to another process running as the same user, it effectively gains visibility into everything that process is doing.


Why this matters

Modern Linux desktops and servers often run many sensitive applications under the same user account:

  • shells
  • SSH sessions
  • browsers
  • password managers
  • GPG agents
  • cloud CLIs

Historically, Linux assumed processes running as the same user trusted one another. Today that assumption rarely holds true.

A compromised application can potentially inspect other processes owned by the same user and steal credentials, authentication tokens, API keys, or other secrets. This is one of the reasons Linux introduced the Yama LSM and kernel.yama.ptrace_scope to restrict who can use ptrace.


Reading Sensitive Data

One of the simplest abuses of ptrace is inspecting another process’s memory.

By attaching to an interactive shell or terminal process, an attacker can observe system calls, inspect register values, and read memory buffers that contain user input or other sensitive information.

For example, I wrote a small proof-of-concept called termspy that demonstrates how a tracer can observe terminal activity by monitoring system calls and inspecting the buffers passed between user space and the kernel.

The goal wasn’t to build a production keylogger—it was simply to demonstrate how much visibility ptrace provides once a process is attached.

Source:


First we attach to the target process with PTRACE_ATTACH which sends a SIGSTOP signal to the target process making it a tracee of the termspy process. Then we use PTRACE_SYSCALL to restart the stopped process and continue running it until the next entry or exit from a system call execution.

To perform key-logging we focus on write syscall since it will be executed when a user types a keystroke into the bash or terminal process. Snippet below shows the registers used for execution of a syscall in x86–64 bit systems. With PTRACE_PEEKUSER we can read the value of these registers and check rax register to compare if the system call being executed is write

orig_eax = ptrace(PTRACE_PEEKUSER, pid, 8 * ORIG_RAX, NULL)
if (orig_eax == SYS_write)

We are interested in the second parameter which is a pointer to the char array containing user’s keystrokes. We can read this address with PTRACE_PEEKUSER and use PTRACE_PEEKDATA to read the data at this address.



params[1] = ptrace(PTRACE_PEEKUSER, pid, 8 * RSI, NULL);
data.val = ptrace(PTRACE_PEEKDATA, child, addr + i * 8, NULL);

This returns lot of non-ASCII text which we don’t care about so we filter out any non-ASCII characters. For the demo, we just print out the ASCII characters to stdout. This works pretty well when attached to a bash or terminal process.


Process Injection

Reading memory is only half the story. ptrace can also modify another process.

The tracer can write data into the target’s address space, modify registers, and redirect execution. This makes process injection possible without exploiting a memory corruption bug in the target application. This technique appears in multiple offensive tools and is documented by the MITRE ATT&CK framework as a Linux process injection technique.

One of the biggest advantages of ptrace is that it doesn’t require dropping a malicious executable into the target process. Instead of creating a new suspicious process, an attacker can abuse an existing trusted process. This reduces the number of obvious indicators compared to launching separate malware.

To explore this, I built another proof-of-concept:


The project demonstrates how code can be injected into an existing process using ptrace. It attaches to the given pid, halts it’s execuation, backs up current instructions, injects reverse-shell shellcode (which forks a child process) and resumes the execution from the backed up instructions.


sudo Credential Caching

One particularly interesting consequence of process injection involves sudo credential caching.

By default, sudo remembers a successfully authenticated session for a configurable timeout (typically five minutes). During that window, subsequent sudo commands from the same terminal session don’t require the password again.

This improves usability, but it also changes the threat model.

Imagine the following sequence:

  1. A user authenticates with sudo.
  2. The credential timestamp becomes valid.
  3. Malware running as that same user attaches to the user’s shell.
  4. The malware injects code into that shell.
  5. The injected code executes a privileged command while the cached credential is still valid.

The attacker never steals the password—they simply abuse the existing authenticated session.

This is conceptually similar to session hijacking in web applications: rather than obtaining the user’s secret, the attacker reuses an already authenticated context.

It’s worth noting that the attacker still needs permission to attach to the target process. Systems configured with Yama restrictions or other hardening measures may prevent this attack entirely.


Mitigations

Fortunately, Linux provides several ways to reduce the attack surface.

Restrict ptrace

The most effective mitigation is enabling Yama restrictions.

sudo sysctl kernel.yama.ptrace_scope=2

This limits ptrace to privileged users and significantly reduces opportunities for process injection between user processes.


PR_SET_DUMPABLE - set the “dumpable” attribute of the calling process

Sensitive applications such as ssh-agent which handle private keys, use PR_SET_DUMPABLE flag to

  • Prevent core dumps
  • Prevent other processes with the same UID from attaching via ptrace.
  • Restrict access to /proc/<pid>/mem
  • Restricts access to some /proc/<pid> files
#include <sys/prctl.h>

prctl(PR_SET_DUMPABLE, 0);

Disable sudo credential caching

If your environment prioritizes security over convenience, disable timestamp caching.

Add this line to your /etc/sudoers file

Defaults timestamp_timeout=0

With caching disabled, every sudo invocation requires fresh authentication, eliminating this particular abuse scenario.


Monitor ptrace Activity

From a detection engineering perspective, ptrace is an interesting signal.

Legitimate uses usually involve tools such as:

  • gdb
  • strace
  • lldb

Unexpected PTRACE_ATTACH operations originating from random user processes are often worth investigating.

Linux auditing, eBPF, Falco, or auditd can all be used to monitor ptrace activity.


References