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).

DetectionWhat it detectsWindowExample exploits caught
Behavioral Sequence Consistent with Local Kernel ExploitationA non-root user compiles a binary, runs it, and the binary spawns a root child (UID 0, RUID 0) within one login session10 minDirty Cow, Dirty Pipe, eBPF verifier exploits
Root Account Verification Followed by Privilege Escalation via suA process queries the root entry via getent, then the same parent process spawns su to root1 minDirtyFrag, Copy Fail, Dirty Pipe targeting /etc/passwd
SUID Binary Executed with No Arguments from Anomalous ParentA SUID-root binary spawned by an unrecognized parent runs with kernel capabilities but no command line at all5 minFragnesia, 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
SequenceStrict order. Compile ➤ Execute ➤ Root child. Out-of-order events do not correlate.
Maximum window10 minutes from Compile to Root child
Same across all 3 eventsEndpoint (host). Events on different hosts do not correlate together.
Same between Compile and ExecuteLoginUID. 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 childRoot 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) matchProcess name is in the compiler list. UID is not 0.
Step 2 (Execute) matchProcess name is NOT in the exclusion list. UID is not 0.
Step 3 (Root child) matchProcess name is in the post-exploit binary list. UID is 0 AND RUID is 0.
Calculated at alert timeCompile-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.

CategoryBinaries
Interactive shellbash, sh, dash, zsh, ash, busybox
Credential modificationpasswd, useradd, usermod, chpasswd, groupadd, shadow
Permission manipulationchmod, chown, setcap
Persistence mechanismcrontab, systemctl, at
Kernel module loadinginsmod, modprobe, kmod
Second-stage callbackcurl, wget, nc, ncat, netcat, socat
Script interpreterpython, 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
SequenceStrict order. Verify (getent) ➤ Escalate (su).
Maximum window1 minute
Same across both eventsEndpoint (host).
Same between Verify and EscalateParentProcessId. 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) matchProcess name is getent. CommandLine contains "passwd root" or "passwd 0".
Step 2 (Escalate) matchProcess name is su. CommandLine matches a login-to-root invocation form.
Calculated at alert timeVerify-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.

FormBehavior
suBare invocation, defaults to root
su -Login shell to root
su -lShort login flag
su --loginLong login flag
su rootExplicit 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
SequenceStrict order. Anomalous parent ➤ SUID call.
Maximum window5 minutes
Same across both eventsEndpoint (host).
Lineage between Parent and SUID callSUID 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) matchProcess 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) matchProcess 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 timeCapPrm 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).

CapabilityWhat it grants
CAP_DAC_OVERRIDEBypass file permission checks
CAP_DAC_READ_SEARCHBypass read and search permission checks
CAP_SETPCAPModify process capabilities
CAP_LINUX_IMMUTABLEModify immutable file attributes
CAP_SYS_MODULELoad and unload kernel modules
CAP_SYS_RAWIOPerform raw device I/O
CAP_SYS_CHROOTChange the filesystem root
CAP_SYS_PTRACETrace any process
CAP_SYS_ADMINBroad administrative operations
CAP_SYS_BOOTReboot the system
CAP_AUDIT_CONTROLConfigure the audit subsystem
CAP_MAC_OVERRIDEOverride SELinux or AppArmor
CAP_MAC_ADMINConfigure mandatory access control
CAP_SYSLOGRead and clear the kernel log
CAP_BPFUse 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 nameCVECaught by detection
Dirty CowCVE-2016-5195Behavioral Sequence Consistent with Local Kernel Exploitation
Dirty PipeCVE-2022-0847Behavioral Sequence Consistent with Local Kernel Exploitation, and also Root Account Verification Followed by Privilege Escalation via su when targeting /etc/passwd
Copy FailCVE-2026-31431Root Account Verification Followed by Privilege Escalation via su, or SUID Binary Executed with No Arguments from Anomalous Parent, depending on variant
DirtyFragCVE-2026-43284, CVE-2026-43500Root Account Verification Followed by Privilege Escalation via su, or SUID Binary Executed with No Arguments from Anomalous Parent, depending on variant
FragnesiaCVE attribution not confirmedSUID Binary Executed with No Arguments from Anomalous Parent
Previous
Previous

Understanding How Windows Handles Deleted Files

Next
Next

Understanding Alternate Data Streams