This post walks the captured audit records phase-by-phase for both Dirty Frag exploitation paths — Path A (ESP / CVE-2026–43284) and Path B (RxRPC / CVE-2026–43500). Every record below is real, captured on a default Ubuntu 22.04 GCP instance running kernel 6.8.0-1054-gcp (unpatched, auditd loaded). Path A was captured first; Path B was captured separately via ./exp --force-rxrpc -v with the audit backlog raised to 8192.

The main blog covers the structural "why" — kernel mechanism, atomic Sigma rules, and how each fires. This post is the receipts: the actual audit records each phase produces.

A note on what "fires" means below. Every event captured here is class-atomic — the kernel mechanism structurally requires it, so the exploit class produces it as a side-effect of doing what it does. Each phase below flags the corresponding Sigma rule inline.

Telemetry Walkthrough — Path A (ESP)

Phase 1 — Namespace + XFRM arming

The exploit calls unshare(CLONE_NEWUSER | CLONE_NEWNET) to create an isolated sandbox. Inside, an unprivileged process gets CAP_NET_ADMIN — root-level network control scoped to the sandbox.

type=SYSCALL msg=audit(1778417834.601:51031): arch=c000003e syscall=272 success=yes exit=0
  a0=50000000 a1=0 pid=6631 auid=1001 uid=1001 euid=1001
  comm="exp" exe="/home/user/dirtyfrag/exp" key="net_userns_escape"
  SYSCALL=unshare AUID="user" UID="user"

a0=50000000 is CLONE_NEWUSER|CLONE_NEWNET. auid=1001 is the login-session UID — set once at login by PAM, immutable for the rest of the session, visible in every record even after the process acquires root.

The new namespace starts empty. The only interface is lo, initially DOWN. The exploit immediately brings it up — there are no routable interfaces, no external routes, no way to reach anything outside 127.0.0.1. This is structurally unavoidable — a direct consequence of the namespace isolation that granted the capabilities. The attacker cannot route to an external IP without abandoning the namespace model that gives them CAP_NET_ADMIN.

This single fact is the foundation of the most reliable detection signal in the entire kill chain.

48 fake IPsec Security Associations are registered with the kernel — one per four bytes of the 192-byte root-shell payload. The shellcode is stored as the SA's seq_hi metadata field, which the kernel will later write back into packet data during integrity verification. Each registration opens an AF_NETLINK socket to the XFRM subsystem:

type=SYSCALL msg=audit(1778417834.704:51057): arch=c000003e syscall=41 success=yes exit=4
  a0=10 a1=3 a2=6 a3=0 pid=6631 auid=1001 uid=1001 euid=1001
  comm="exp" exe="/home/user/dirtyfrag/exp" key="dirtyfrag_xfrm_netlink"

a0=10 is AF_NETLINK (16 decimal — netlink(7)). a2=6 is NETLINK_XFRM (per the same man page: "NETLINK_XFRM — IPsec"). The kernel then confirms each SA into its database with an automatic record:

Detection note: Rule 2 watches this AF_NETLINK channel — class-atomic. The kernel mechanism requires netlink-XFRM to deliver the attacker-shaped seq_hi that drives the page-cache write. The downstream MAC_IPSEC_EVENT SAD-add (Rule 4) fires regardless.

type=MAC_IPSEC_EVENT msg=audit(1778417834.704:51050):
  op=SAD-add auid=1001 ses=1 src=127.0.0.1 dst=127.0.0.1
  spi=3735928341(0xdeadbe15) res=1

Loopback src/dst — already, because the namespace leaves no other option. Fires 48 times in the public PoC (one per 4-byte chunk of the 192-byte payload — the count scales with payload size, but each individual event is structural). No legitimate unprivileged process registers XFRM SAs to loopback.

Detection note: MAC_IPSEC_EVENT SAD-add is the class-atomic Path A anchor (Rule 4). Kernel-native, no auditd rule required, fires once per registered SA — 48 events per Path A run.

Phase 2 — Splice chain → page cache

/usr/bin/su is opened read-only — forces the binary into the page cache without triggering any VFS write event:

type=SYSCALL msg=audit(1778417835.769:51252): syscall=257 success=yes exit=6
  a2=0 pid=6631 auid=1001 uid=1001 comm="exp" key="setuid_target_open"

a2=0 is O_RDONLY. The exploit then builds a three-call zero-copy pipeline:

vmsplice(pipe_write, &esp_header, 1, 0);                       // ESP header → pipe
splice(su_fd, &offset, pipe_write, NULL, 16, SPLICE_F_MOVE);   // su's page → pipe
splice(pipe_read, NULL, udp_socket, NULL, 40, SPLICE_F_MOVE);  // pipe → UDP socket

Note: SPLICE_F_MOVE has been a no-op in the Linux kernel since 2.6.21 (splice(2)). The zero-copy semantics come from splice()'s pipe buffer reference counting, not from the flag. Its presence in the PoC is vestigial.

After these three calls, the UDP socket's send buffer holds a live, direct memory reference into a root-owned setuid binary's page cache — no filesystem permission check ever ran.

Detection note: Rule 3 watches vmsplice + spliceclass-atomic. The page-cache routing primitive requires the splice chain to land a target file's page-cache page reference inside the kernel's send-side skb such that the recv-side in-place crypto writes back into that same physical page.

type=SYSCALL msg=audit(1778417835.769:51254): syscall=278 a2=1 pid=6631
  comm="exp" key="pagecache_write_primitive" SYSCALL=vmsplice
type=SYSCALL msg=audit(1778417835.769:51255): syscall=275 a2=8 pid=6631
  comm="exp" key="pagecache_write_primitive" SYSCALL=splice

Same millisecond timestamp — a single write iteration captured in real time. Fires on every one of the 48 iterations in the public PoC, advancing 4 bytes deeper into su's page cache each time. (The 48 count is PoC-specific — payload-size driven; the per-iteration vmsplice+splice pair is what Rule 3 captures.)

Phase 3 — In-place decrypt poisons the page

The crafted packet is transmitted to 127.0.0.1. The ESP receive path picks it up. The receive path was optimized in 2017 to operate in-place on whatever memory the packet's data is pointing at, rather than decrypting into a fresh buffer. Faster — and dangerous when the buffer is, courtesy of the splice chain, /usr/bin/su's page-cache page.

In Extended Sequence Number mode, the kernel writes seq_hi from the SA's replay state into the packet data before running ICV verification. That seq_hi — 4 bytes of shellcode loaded in Phase 1 — gets written directly into su's page. The crypto engine has no awareness of memory ownership.

The kernel emits a MAC_IPSEC_EVENT automatically:

type=MAC_IPSEC_EVENT msg=audit(1778417835.769:51256):
  op=SA-icv-failure src=127.0.0.1 dst=127.0.0.1
  spi=3735928343(0xdeadbe17) seqno=200

Why the write succeeds but the ICV fails — pipeline order:

  1. SPI lookup — matching SA found.
  2. seq_hi write — kernel writes seq_hi from the SA's replay state into packet data. This is the write that reaches the page cache.
  3. In-place decryption — fast path decrypts payload directly in the same buffer.
  4. ICV computation — HMAC-SHA256 over decrypted payload.
  5. ICV comparison — fails (the exploit never built a valid ESP packet). Packet dropped. SA-icv-failure fires.

Step 2 happens before step 5. By the time the kernel concludes the packet is malformed, the shellcode bytes are already in the page cache. The packet is gone; the modification is not. SA-icv-failure is not evidence of malfunction — it is evidence that step 2 completed.

Critically, src=127.0.0.1 dst=127.0.0.1 aren't attacker-chosen — they're forced by the namespace isolation. Real SA-icv-failure events come from network misconfigurations or replay attacks across real links — never loopback. The combination of any MAC_IPSEC_EVENT + loopback src/dst + non-IPsec-daemon process has no legitimate explanation outside kernel selftest harnesses. Not a heuristic — a structural impossibility under normal operation.

Detection note: this event corresponds to Rule 9 in the main blog — class-atomic. The exploit's payload is intentionally garbage (memset(0xCC) at exp.c:260) because the write primitive lives at the pre-ICV step of the receive pipeline (seq_hi write at step 2). ICV verification therefore always fails by design — and SA-icv-failure fires once per packet, 48 times per Path A run.

After 48 iterations, the first 192 bytes of /usr/bin/su's in-memory image have been replaced with a functional x86-64 ELF that does setgid(0)setuid(0)setgroups(0, NULL)execve("/bin/sh", NULL, ["TERM=xterm"]). The on-disk file is unchanged. Filesystem integrity tools find nothing wrong. The only evidence is in RAM and in the audit log.

Phase 4 — Privileged execution

execve("/usr/bin/su") is called. The kernel inspects the on-disk file: SUID set, owned by root → elevates EUID to 0. Then checks the page cache for executable content — cache hit (loaded in Phase 2, overwritten in Phase 3). Maps the poisoned pages into the new process's execution space without re-reading from disk. Shellcode runs as root.

type=SYSCALL msg=audit(1778417851.386:51721): arch=c000003e syscall=59 success=yes exit=0
  ppid=6632 pid=6633 auid=1001 uid=0 gid=0 euid=0
  comm="whoami" exe="/usr/bin/whoami" key="exec_all"

This record is from whoami run inside the root shell to confirm escalation. uid=0 euid=0 — root. auid=1001 — original login user, immutable.

Telemetry Walkthrough — Path B (RxRPC)

Captured with ./exp --force-rxrpc -v. Zero MAC_IPSEC_EVENT records appear anywhere in this run.

Phase 1 — Module autoload

int dummy = socket(AF_RXRPC, SOCK_DGRAM, PF_INET);
close(dummy);
type=SYSCALL msg=audit(1747052201.943:51820): arch=c000003e syscall=41 success=yes exit=3
  a0=21 a1=2 a2=2 a3=0 pid=3421 auid=1001 uid=1001 euid=1001
  comm="exp" key="dirtyfrag_rxrpc_socket"
type=KERN_MODULE msg=audit(1747052201.697:51815): name="rxrpc"

a0=21 is hex for AF_RXRPC (33 decimal). No unshare preceded — host namespace, original unprivileged user. The kernel resolves the unfamiliar protocol family via MODULE_ALIAS_NETPROTO(PF_RXRPC), calls request_module(), loads rxrpc.ko. No CAP_SYS_MODULE required — that is why an unprivileged user can trigger it.

Detection note: both events are class-atomicAF_RXRPC is the only socket family that reaches rxkad (Rule 5); the module autoload is forced via MODULE_ALIAS_NETPROTO(PF_RXRPC) (Rule 6). Path B's structural anchors.

Phase 2 — Target file open

type=SYSCALL msg=audit(1747052208.450:51830): syscall=257 a2=0 pid=3421 auid=1001
  comm="exp" key="passwd_access"
type=PATH msg=audit(1747052208.450:51830): name="/etc/passwd" mode=0100644

a2=0 is O_RDONLY. 8 bytes read from offsets 4, 6, 8 to get ciphertext. /etc/passwd is never opened for writing — modification happens through the page cache via the rxkad path, not VFS.

Phase 3 — Offline brute-force (silent in auditd, visible in CPU)

Roughly twenty million pcbc(fcrypt) iterations in user-space, dominated by the K_C search (chars 8-15 = 0:GGGGGG:). Zero kernel events — the longest phase by wall-clock time produces zero detection signal in auditd. The only observable signal is a single-core CPU spike pinned to the exploit process. If you have process-CPU-anomaly telemetry (sysstat, eBPF process accounting, EDR baselines), this is the one place in the entire Path B chain where it earns its keep — pointing the kind of probe most SOCs already deploy at the one stealth window in the attack.

Phase 4 — Three kernel triggers (A → B → C)

Each trigger produces a roughly 12–15 syscall burst (count varies with kernel ack/retry): add_keysocket(AF_INET)socket(AF_RXRPC) → 2× setsockopt(SOL_RXRPC, ...) → 2× bindsendmsgrecvfromsendto (CHALLENGE) → connectpipevmsplicesplice(file→pipe)splice(pipe→socket). Plus a separate AF_ALG pcbc(fcrypt) burst per trigger to compute matching rxkad checksums.

type=SYSCALL msg=audit(1747052206.121:51818): arch=c000003e syscall=248 success=yes exit=...
  a0=5fb1c4812435 pid=3421 auid=1001 comm="exp" key="dirtyfrag_add_key"
  SYSCALL=add_key

syscall=248 is add_key. a0 points to the string "rxrpc" (the key type). Fires three times across the run — once per trigger, registering each brute-forced session key into the process keyring. (The count of 3 is PoC-specific — the algorithm derives 3 keys for offsets 4, 6, 8; a different layout could need fewer or more.)

Detection note: add_key(type="rxrpc") from non-AFS process is class-atomic (Rule 7). rxkad_verify_packet_1() requires the brute-forced session key to be registered in the process keyring before the trigger packet is sent.

Also firing per trigger: AF_ALG bind(salg_name="pcbc(fcrypt)"). Before each kernel trigger can succeed, the exploit computes matching rxkad packet checksums in user-space via AF_ALG (exp.c:559). pcbc(fcrypt) is the AFS Kerberos session-key cipher — outside kafs/aklog/openafs, essentially nothing legitimate uses it.

type=SYSCALL msg=audit(1747052205.984:51816): syscall=41 a0=26 a1=5 pid=3421 auid=1001
  comm="exp" key="dirtyfrag_af_alg_socket"

a0=26 is hex for AF_ALG (38 decimal). a1=5 is SOCK_SEQPACKET. The follow-on bind carries the pcbc(fcrypt) salg_name string but its argument is in userspace memory — capturing the string requires SIEM-side payload inspection (Sysdig's Falco rule for AF_ALG matches the bind payload; pure auditd captures only the socket creation).

Detection note: Rule 8 watches AF_ALG pcbc(fcrypt) bind — class-atomic. The rxkad checksum-prep step requires this AF_ALG socket to compute the per-trigger csum_iv and cksum values the kernel will compare against on the receive side.

Then the page-cache write fires:

type=SYSCALL msg=audit(1747052207.445:51825): syscall=275 success=yes exit=8
  a0=3 pid=3421 auid=1001 comm="exp" key="pagecache_write_primitive" SYSCALL=splice

exit=8 — 8 bytes moved. This routes the /etc/passwd page-cache page into the rxkad receive path. rxkad_verify_packet_1() performs in-place pcbc(fcrypt) decrypt at the splice offset — writing the attacker's chosen 8 bytes into /etc/passwd's page cache.

This is the moment the file is modified — and nothing fires from the kernel to mark it. No rxkad-equivalent of xfrm_audit_state_icvfail(). The only evidence the modification happened is the splice syscall the auditd rule caught — which looks identical to splice from any other process. Without context (AF_RXRPC + add_key + AF_ALG + passwd_access), splice alone is meaningless.

Detection note: same vmsplice + splice chain as Path A — Rule 3 fires here too. Class-atomic for both paths.

After three triggers land, the root entry in /etc/passwd's page cache:

root:x:0:0:root:/root:/bin/bash

becomes:

root::0:0:root:/root:/bin/bash

The password hash field is empty. The on-disk file is unchanged. Stage 5 spawns su and lets PAM's pam_unix.so nullok accept the blank password.

Phase 5 — PAM escalation

type=USER_AUTH msg=audit(1747052208.871:51840): pid=3445 uid=0 auid=1001 ses=4
  msg='op=PAM:authentication acct="root" exe="/usr/bin/su" res=success'

res=success on USER_AUTH for acct="root" from a process running as uid=1001 — with no preceding password prompt visible — is the smoking gun. PAM accepting the empty hash field that the exploit planted.

A handful of priv_esc events fire here too — but unlike Path A, these come from su/passwd/sudo doing normal setuid operations during PAM session setup, not from shellcode. No setuid/setgid event in this run originates from the exp process directly. Escalation flows through PAM-as-itself.

Throughout the entire run, auid remains 1001.

For the structural detection analysis and the Sigma ruleset (8 rules across both paths), see the main post: I'll Catch a Grenade for You — Catching Dirty Frag.