Catching Linux Kernel Exploits Through Behavior
A new Linux kernel privilege escalation vulnerability gets a name and a logo every few months. Dirty Cow (CVE-2016-5195), Dirty Pipe (CVE-2022-0847), DirtyFrag (CVE-2026-43284 and CVE-2026-43500), Copy Fail (CVE-2026-31431), Fragnesia. The exploit code changes. The hashes change. The kernel subsystems being abused change. What changes much less often is what the attacker has to do on the target host.
Public reports of these exploits usually publish IOCs such as file hashes, command-line snippets, and kernel-version fingerprints, which go stale as the exploit code mutates. Behavioral detection works on what the attacker does on the host instead, and tends to hold up better against unknown variants and against new CVEs in the same family. The behavioral surface varies by exploit class, by the kernel primitive being abused, and by what the attacker does after getting root. Many behavioral signatures lead to viable detections.
Linux identifies users through UID and RUID. The audit loginuid identifies the login session. CapPrm holds the capability state. The kernel exposes these at /proc/[pid]/status and /proc/[pid]/loginuid. RUID, LoginUID, CapPrm, and CapsSet may not be standard names 1 to 1 across telemetry sources. Map each kernel concept to whatever field name the local telemetry uses (for example, auditd's auid for LoginUID).
| Detection | What it detects | Window | Example exploits caught |
|---|---|---|---|
| Behavioral Sequence Consistent with Local Kernel Exploitation | A non-root user compiles a binary, runs it, and the binary spawns a root child (UID 0, RUID 0) within one login session | 10 min | Dirty Cow, Dirty Pipe, eBPF verifier exploits |
| Root Account Verification Followed by Privilege Escalation via su | A process queries the root entry via getent, then the same parent process spawns su to root | 1 min | DirtyFrag, Copy Fail, Dirty Pipe targeting /etc/passwd |
| SUID Binary Executed with No Arguments from Anomalous Parent | A SUID-root binary spawned by an unrecognized parent runs with kernel capabilities but no command line at all | 5 min | Fragnesia, DirtyFrag and Copy Fail SUID-targeting variants |
Behavioral Sequence Consistent with Local Kernel Exploitation
A non-root user compiling a binary, executing it, and producing a root child (UID 0 AND RUID 0) within the same login session and within ten minutes is consistent with local kernel exploit compilation.
The login session is identified by the audit loginuid, set when a user authenticates and inherited by every process the session spawns. A kernel exploit produces a process with both UID 0 and RUID 0 because the kernel itself granted the privilege. Legitimate sudo and su escalations produce UID 0 with a non-zero RUID showing who originally invoked them. When a non-root user's child process ends up with both UID 0 and RUID 0, the combination distinguishes kernel-mediated privilege from user-mediated escalation.
| Correlation specification | |
|---|---|
| Sequence | Strict order. Compile ➤ Execute ➤ Root child. Out-of-order events do not correlate. |
| Maximum window | 10 minutes from Compile to Root child |
| Same across all 3 events | Endpoint (host). Events on different hosts do not correlate together. |
| Same between Compile and Execute | LoginUID. Compile and Execute must share the same login session. Events with different LoginUIDs do not correlate even on the same host. |
| Lineage between Execute and Root child | Root child's ParentProcessId must equal Execute's ProcessId. The Root child must be a direct child of the executed binary, not merely co-occurring in the same session. |
| Step 1 (Compile) match | Process name is in the compiler list. UID is not 0. |
| Step 2 (Execute) match | Process name is NOT in the exclusion list. UID is not 0. |
| Step 3 (Root child) match | Process name is in the post-exploit binary list. UID is 0 AND RUID is 0. |
| Calculated at alert time | Compile-to-Execute delta, Execute-to-Root delta, total elapsed, intent classification of the Root child binary |
Compiler list (Step 1). gcc, cc, cc1, clang, clang++, g++, c++, ld, ld.bfd, ld.gold, ld.lld, as, collect2, cargo, rustc, go, nasm, yasm, make, cmake.
Execute exclusion list (Step 2). su, sudo, logrotate, nm-dispatcher.
Post-exploit binary list (Step 3). bash, sh, dash, zsh, ash, busybox, passwd, useradd, usermod, chpasswd, groupadd, shadow, chmod, chown, setcap, crontab, systemctl, at, insmod, modprobe, kmod, curl, wget, nc, ncat, netcat, socat, python, python3, perl, ruby, node.
Intent classification applied to Step 3 at alert time.
| Category | Binaries |
|---|---|
| Interactive shell | bash, sh, dash, zsh, ash, busybox |
| Credential modification | passwd, useradd, usermod, chpasswd, groupadd, shadow |
| Permission manipulation | chmod, chown, setcap |
| Persistence mechanism | crontab, systemctl, at |
| Kernel module loading | insmod, modprobe, kmod |
| Second-stage callback | curl, wget, nc, ncat, netcat, socat |
| Script interpreter | python, python3, perl, ruby, node |
T+0:00 PID 12345 gcc -o ./exploit ./exploit.c UID=33 (www-data) parent=bash
T+0:23 PID 12378 ./exploit UID=33 (www-data) parent=bash
T+0:25 PID 12379 /bin/bash UID=0 (root) parent=12378
Dirty Cow (CVE-2016-5195), Dirty Pipe (CVE-2022-0847), eBPF verifier exploits, and other Linux privilege escalation exploits follow this workflow.
Root Account Verification Followed by Privilege Escalation via su
A single parent process spawning getent passwd root and then su to root within one minute is consistent with page-cache write exploits that corrupt the in-memory copy of /etc/passwd.
/etc/passwd contains one line per user with the password field normally set to x (pointing to the hash in /etc/shadow). If the field is left empty, PAM's pam_unix.so with nullok authenticates without prompting for a password. Page-cache write vulnerabilities including DirtyFrag and Copy Fail can write arbitrary bytes into the in-memory copy of a read-only file. A page-cache write that empties root's password field means every program reading the root entry sees an empty password, while the on-disk file remains untouched. The exploit verifies the corruption with getent passwd root (which reads through the page cache) and then runs su to log in via nullok.
| Correlation specification | |
|---|---|
| Sequence | Strict order. Verify (getent) ➤ Escalate (su). |
| Maximum window | 1 minute |
| Same across both events | Endpoint (host). |
| Same between Verify and Escalate | ParentProcessId. Both events must be children of the same parent process. Events with different parents do not correlate, even if both happen on the same host within the window. |
| Step 1 (Verify) match | Process name is getent. CommandLine contains "passwd root" or "passwd 0". |
| Step 2 (Escalate) match | Process name is su. CommandLine matches a login-to-root invocation form. |
| Calculated at alert time | Verify-to-Escalate delta, full identity context of the Escalate event, parent compile history within the surrounding window |
su invocation forms matched (Step 2). Six forms, all of which authenticate to root.
| Form | Behavior |
|---|---|
su | Bare invocation, defaults to root |
su - | Login shell to root |
su -l | Short login flag |
su --login | Long login flag |
su root | Explicit target |
su -c <command> | Run command as root |
T+0:00 PID 23456 ./exploit UID=1000 parent=bash
T+0:01 PID 23457 getent passwd root UID=1000 parent=23456
T+0:02 PID 23458 su - UID=0 parent=23456
The shared parent PID is a key discriminating signal. An administrator running getent passwd root at one point and su later in the day does not produce events with the same non-shell parent, because their shell session reuses its same parent across all foreground children and the events are separated by interactive thinking time. The one-minute window enforces machine-speed pacing.
DirtyFrag (CVE-2026-43284 and CVE-2026-43500), Copy Fail (CVE-2026-31431), and Dirty Pipe variants (CVE-2022-0847) that target /etc/passwd produce this signature.
SUID Binary Executed with No Arguments from Anomalous Parent
A SUID-root binary spawned by an unrecognized parent, holding kernel capabilities, with an empty command line is consistent with exploits that replace SUID binary code in memory or on disk.
A SUID-root binary runs with root-level capabilities when the kernel loads it and applies SUID semantics, regardless of whether the on-disk binary or its cached copy is what gets executed. When any program is launched via execve, argv[0] is set to the binary's name by convention. Even invoking su with no user-supplied flags still passes argv[0] = "su" to the kernel, which preserves it in /proc/[pid]/cmdline. A SUID binary invoked through a normal launcher (a shell, systemd, a script interpreter, another program calling fork and execve with a populated argv) records at least argv[0] in /proc/[pid]/cmdline.
A page-cache write exploit replaces the binary's code in the kernel's cache without touching the on-disk file. The kernel still applies SUID semantics based on the file's mode bits and ownership, then transfers control to the bytes in the cached page. The replacement code typically calls setresuid(0, 0, 0) followed by execve("/bin/sh", ...) and doesn't parse argv. Attacker shellcode commonly passes an empty or NULL argv when invoking the SUID binary, because the replacement code doesn't need arguments. The resulting process has no argv[0] at all, and the recorded command line is empty. That is a structural difference between a legitimate invocation (which has at least argv[0]) and an exploit invocation that goes through a NULL argv path.
| Correlation specification | |
|---|---|
| Sequence | Strict order. Anomalous parent ➤ SUID call. |
| Maximum window | 5 minutes |
| Same across both events | Endpoint (host). |
| Lineage between Parent and SUID call | SUID call's ParentProcessId must equal Parent's ProcessId. The SUID call must be a direct child of the anomalous parent, not merely co-occurring on the same host. |
| Step 1 (Anomalous parent) match | Process name is NOT in the system binary exclusion list. UID is not 0. LoginUID is a real user session (not 0, not 4294967295). |
| Step 2 (SUID call) match | Process name is in the SUID binary list. UID is not 0. LoginUID is real. CapPrm is not 0. CapPrm contains at least one high-impact capability. CommandLine is literally empty. |
| Calculated at alert time | CapPrm bitmask decoded into the CapsSet list of capability names, parent process metadata, on-disk vs. loaded binary hash comparison flag |
SUID binary list (Step 2). su, sudo, passwd, chsh, chfn, newgrp, gpasswd, pkexec, mount, umount.
High-impact capabilities (Step 2 requires at least one).
| Capability | What it grants |
|---|---|
| CAP_DAC_OVERRIDE | Bypass file permission checks |
| CAP_DAC_READ_SEARCH | Bypass read and search permission checks |
| CAP_SETPCAP | Modify process capabilities |
| CAP_LINUX_IMMUTABLE | Modify immutable file attributes |
| CAP_SYS_MODULE | Load and unload kernel modules |
| CAP_SYS_RAWIO | Perform raw device I/O |
| CAP_SYS_CHROOT | Change the filesystem root |
| CAP_SYS_PTRACE | Trace any process |
| CAP_SYS_ADMIN | Broad administrative operations |
| CAP_SYS_BOOT | Reboot the system |
| CAP_AUDIT_CONTROL | Configure the audit subsystem |
| CAP_MAC_OVERRIDE | Override SELinux or AppArmor |
| CAP_MAC_ADMIN | Configure mandatory access control |
| CAP_SYSLOG | Read and clear the kernel log |
| CAP_BPF | Use the BPF subsystem |
System binary exclusion categories (Step 1). Shells (bash, zsh, dash, fish, ksh, tcsh, csh). System init and login (systemd, init, sshd, login, sulogin, agetty, getty). Service schedulers (cron, crond, anacron, atd). System bus and policy daemons (dbus-daemon, dbus-broker, polkitd, accounts-daemon). Device and network management (udevd, NetworkManager, wpa_supplicant, dhclient). Time and logging services (chronyd, ntpd, rsyslogd, auditd). File system services (rpcbind, nfsd, smbd, cupsd). Container runtimes (containerd, runc, crun, crio, docker, dockerd, kubelet).
Bitfield decoding (Step 2 prerequisite). CapPrm in raw telemetry is a 41-bit numeric bitmask surfaced as a hexadecimal value (for example 0x1ffffffffff). The deployment decodes this into individual capability flag fields before the high-impact capability filter can fire. CapsSet is the decoded representation, surfaced as a comma-separated list of set capability names.
PID 34568 /usr/bin/su UID=1000 CapPrm=0x1ffffffffff CommandLine="" parent=34567 (./exploit)
A key state anomaly is that UID is not zero. A SUID binary running normally would have UID 0 once it has fully transitioned. What gets caught here is the state where the kernel has applied SUID semantics (CapPrm is non-zero) but the process's UID has not been changed by the real code, because the real code is not running. The replacement code calls setresuid(0, 0, 0) itself rather than relying on the SUID binary's own transition logic.
Fragnesia (which overwrites the first 192 bytes of /usr/bin/su in the page cache with an ELF stub that calls setresuid(0, 0, 0) and execve("/bin/sh")), DirtyFrag and Copy Fail SUID-targeting variants, on-disk modification of SUID binaries, and LD_PRELOAD or runtime linker abuse that hijacks startup before normal argument parsing all produce this anomaly.
Why these patterns generalize
Each pattern generalizes forward to new CVEs whose exploit follows the same workflow, because attacker behavior changes less often than exploit code. Hash and filename rotation does nothing to the workflow. The telemetry needed (process execution, parent-child lineage, user identity, capability state) is collected by most Linux EDRs. Intent classification happens at alert time. The signal used varies by detection. It can be the spawned binary, the held capabilities, or the shared parent.
| Exploit name | CVE | Caught by detection |
|---|---|---|
| Dirty Cow | CVE-2016-5195 | Behavioral Sequence Consistent with Local Kernel Exploitation |
| Dirty Pipe | CVE-2022-0847 | Behavioral Sequence Consistent with Local Kernel Exploitation, and also Root Account Verification Followed by Privilege Escalation via su when targeting /etc/passwd |
| Copy Fail | CVE-2026-31431 | Root Account Verification Followed by Privilege Escalation via su, or SUID Binary Executed with No Arguments from Anomalous Parent, depending on variant |
| DirtyFrag | CVE-2026-43284, CVE-2026-43500 | Root Account Verification Followed by Privilege Escalation via su, or SUID Binary Executed with No Arguments from Anomalous Parent, depending on variant |
| Fragnesia | CVE attribution not confirmed | SUID Binary Executed with No Arguments from Anomalous Parent |