This post demonstrates the use of seccomp user notifications to inject a shared library into a Linux process. I haven’t seen this combination documented as a process injection technique before, and it has some benefits over alternatives. In summary, seccomp user notifications enable user-space injection from parent to child without any LD_* environment variables or privileged capabilities, regardless of the ptrace_scope configuration. However, seccomp user notifications have some notable limitations:
SECCOMP_ADDFD_FLAG_SEND (Linux 5.14+) to avoid TOCTOU issues when hooking openat.My specific implementation also requires the target executable to be dynamically linked, though there may be alternative implementations that do not.
While I don’t want to focus too much on other techniques, I will briefly describe previous research for comparison to my PoC. For a comprehensive overview of alternative injection options, I recommend Ori David‘s Linux process injection guide.
Injection into a running process typically requires (at least) one of ptrace, procfs, or process_vm_writev primitives.
ptrace system call enables a debugger to read/write memory and registers, as well as pause or single-step through the process. It also ignores the page permissions of virtual memory, which makes process injection straightforward./proc. The process directory contains various files that include information such as environment variables and even virtual memory. By accessing these pseudo-files under /proc, a remote process can read and write the target’s virtual memory.process_vm_writev system call is similar to WriteProcessMemory on Windows. It allows a process to write data directly to the address space of a remote process. Unlike ptrace and procfs, process_vm_writev respects the target’s page protections, so you can’t directly write to read-only or read-execute memory.On distributions that include the Yama LSM by default (e.g., most Debian and RHEL variants), all three primitives can be constrained using ptrace_scope: a system-wide value ranging from 0 to 3. The default values on many distributions, 0 or 1, allow a process to attach to any of its descendants or, if it has CAP_SYS_PTRACE, to any process. This value also affects access to /proc/<pid>/mem and the process_vm_writev syscall. Increasing the ptrace_scope value further restricts access; ptrace_scope=3 disables remote process attachment entirely.
For concrete examples of these primitives, see the companion PoC repository from Ori’s post.
There are a couple of common ways to inject a shared library into an existing process. First, any of the techniques from the previous section could force a running process to execute dlopen. If you are creating a new process, you can also set the LD_PRELOAD environment variable. In my Linux EDR research, I observed that many products heavily instrument process creation, so I prefer to avoid LD_PRELOAD-style injection.
You can place a shared library on disk for either option, or you can create an in-memory file using memfd_create or /dev/shm. Loading a library from memory may make forensics more difficult since a memfd-backed .so doesn’t have a persistent on-disk path, though it can still be discovered via /proc/<pid>/fd and memory inspection.
The primary strength of seccomp notify injection is that it enables parent-to-child injection at any ptrace_scope level, without requiring elevated privileges.
Seccomp is a Linux kernel feature that restricts the system calls a process can make. It was introduced in Linux 2.6.12 for attack surface reduction, and later extended with seccomp-bpf in kernel version 3.5, which is commonly used in limited-privilege environments such as containers and web browsers.
A seccomp filter can take various actions on system calls, such as allowing the call, returning an error code, or even killing the process. Newer kernels (Linux 5.0+) support a “user-notification” action that notifies a user-mode process to inspect the system call and respond with a decision. One option allows the user-mode process to emulate the system call, enabling more complex sandboxing without kernel-mode code.
For example, consider this simplified example in which a parent process hooks the openat system call:

The initial setup only requires a few steps:
fork.execve, the child process installs a seccomp filter using seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...).execve.Each filtered system call will follow a process like this:
openat.openat, the kernel notifies the parent process and waits for its response. The parent has a couple of options:
SECCOMP_IOCTL_NOTIF_SEND(id, error=-EPERM).openat itself and then returning the result with SECCOMP_IOCTL_NOTIF_ADDFD(id, srcfd=<alt_fd>, flags=SECCOMP_ADDFD_FLAG_SEND) where “<alt_fd>” is replaced with an arbitrary file descriptor.By default, seccomp filters are per-thread, but since the child installs a filter right after fork, no other threads should exist. Threads created later will inherit the seccomp filter as well.
For more details on seccomp user notifications, see The Seccomp Notifier – New Frontiers in Unprivileged Container Development.
My specific implementation is conceptually similar to LD_PRELOAD injection. However, it does not set any environment variables for the target process. This was my initial approach; since seccomp can filter many system calls, alternative implementations may differ significantly.
The proof-of-concept follows these steps to spawn a new process and inject shellcode into it:
.so shellcode loader into memory using memfd_create.forks a child process.openat and sends the listener FD back to the injector process.execve.openat call and returns a file descriptor for the shellcode loader.It should be clear from this sequence why the target process must be dynamically linked. My implementation hijacks the dynamic linker’s openat calls to make the target process load a malicious library instead.
You may have noticed an issue with this strategy: replacing a shared library during startup likely causes problems with dynamic linking. To solve this issue, I took inspiration from the 2024 XZ Utils backdoor. Similar to the infamous backdoor, my PoC uses an IFUNC resolver to execute code before normal symbol resolution. These resolvers execute during relocation, which happens early in dynamic loading. In my testing, the resolver reliably executed before normal symbol resolution, though the exact order may vary depending on how the dynamic linker processes relocations. This gives the library time to execute shellcode and block the process before it crashes.
As shown below, the PoC can spawn and inject shellcode into the memory of a process running a specified executable, regardless of the ptrace_scope value.

curlYou can find the proof-of-concept code here: https://github.com/outflanknl/seccomp-notify-injection.
Other Linux process injection methods can often inject into an arbitrary process, similar to Windows injection, but may be limited on hardened systems. Seccomp user notifications offer a complementary alternative with different constraints.
This technique doesn’t violate Yama’s ptrace_scope restrictions because the injector never directly interacts with another process. From the kernel’s perspective, this technique implements seccomp-notify’s intended use case: a user-space supervisor emulating syscalls for a child process. There are likely additional opportunities for abuse with seccomp user notifications, as it can easily hook any system call that returns a simple scalar value without additional memory access.
Outflank continually expands the tools and techniques available in Outflank Security Tooling (OST), a broad set of evasive tools that allow users to safely and easily perform complex tasks. This toolset includes a C2 implant for Linux operating systems. Consider scheduling an expert-led demo to learn more about the diverse offerings in OST.