Linux Kernel CVE-2024-1086: Deconstructing the nf_tables UAF
Overview & Threat Landscape#
In modern Linux operating systems, packet classification, network address translation (NAT), and stateful firewalling are governed by the Netfilter subsystem. Introduced to succeed legacy frameworks such as iptables, ip6tables, and ebtables, nf_tables provides an in-kernel bytecode-driven state machine capable of executing dynamic packet filtering expressions directly within the networking data plane.
However, complex in-kernel virtual machines and packet-parsing hooks frequently expand the kernel's local attack surface. In early 2024, security researcher Notselwyn disclosed CVE-2024-1086, a high-severity use-after-free (UAF) and double-free vulnerability in the nf_tables component affecting Linux kernel versions from 3.15 through 6.8. The flaw carries a CVSS score of 7.8 and was subsequently cataloged in the CISA Known Exploited Vulnerabilities (KEV) repository due to widespread adoption by ransomware operators and threat actors seeking local root escalation on container hosts and multi-tenant virtualization clusters.
What makes CVE-2024-1086 particularly devastating is the intersection of two architectural primitives:
- Unprivileged Namespace Accessibility: Any unprivileged local user can invoke
clone(CLONE_NEWUSER | CLONE_NEWNET)orunshare(CLONE_NEWUSER | CLONE_NEWNET)to instantiate an isolated user and network namespace. Inside this unprivileged container, the user automatically possesses namespacedCAP_NET_ADMINcapabilities, allowing them to create customnf_tablestables, chains, and rules without host-level administrative access. - The "Flipping Pages" Exploit Primitive: Rather than relying on traditional heap sprays targeting fragile function pointers or
credstructures—which are increasingly mitigated by modern Kernel Address Space Layout Randomization (KASLR), Supervisor Mode Execution Prevention (SMEP), and Supervisor Mode Access Prevention (SMAP)—the vulnerability enabled a novel Page Table Entry (PTE) corruption technique. By converting ansk_buffdouble-free into a physical page table overwrite, an attacker can directly modify page mapping permissions (_PAGE_RWand_PAGE_USER), mapping raw physical kernel memory directly into userspace with full read/write authority.
[!WARNING] In enterprise Linux distributions where unprivileged user namespaces are enabled by default (
kernel.unprivileged_userns_clone = 1), CVE-2024-1086 provides a fully deterministic, 100% reliable local privilege escalation path from an unprivileged shell or restricted container directly to host root.
Vulnerability & Attack Root-Cause Analysis#
To deconstruct the root cause of CVE-2024-1086, we must examine the return code conventions governing Netfilter hook execution and how verdict codes are parsed in net/netfilter/nf_tables_api.c.
The Verdict Parser Flaw in nft_verdict_init()#
In Netfilter, rules process network packets and issue verdicts to dictate the packet's lifecycle. Standard verdicts include NF_ACCEPT, NF_DROP, NF_QUEUE, and internal control-flow codes such as NFT_CONTINUE, NFT_JUMP, and NFT_RETURN.
When a rule specifies NF_DROP, the verdict code can optionally encode a custom drop error in its upper 16 bits:
$$\text{verdict} = \text{NF_DROP} \mid (\text{drop_error} \ll 16)$$
To retrieve the error code from an encoded verdict, the kernel employs the NF_DROP_GETERR() macro:
#define NF_VERDICT_MASK 0x000000ff
#define NF_DROP_GETERR(verdict) (-((int)(verdict) >> 16))
Innet/netfilter/nf_tables_api.c, the nft_verdict_init() function initializes the verdict data structure from netlink attributes sent by userspace:
// Vulnerable logic in net/netfilter/nf_tables_api.c (pre-patch)
int nft_verdict_init(const struct nft_ctx *ctx, struct nft_data *data,
struct nft_data_desc *desc, const struct nlattr *nla)
{
u8 genmask = nft_genmask_cur(ctx->net);
struct nlattr *tb[NFTA_VERDICT_MAX + 1];
int err;
err = nla_parse_nested_deprecated(tb, NFTA_VERDICT_MAX, nla,
nft_verdict_policy, NULL);
if (err < 0)
return err;
if (!tb[NFTA_VERDICT_CODE])
return -EINVAL;
data->verdict.code = ntohl(nla_get_be32(tb[NFTA_VERDICT_CODE]));
switch (data->verdict.code) {
default:
// FLAVOR: Only masks lower 8 bits, completely ignoring positive drop_error values
switch (data->verdict.code & NF_VERDICT_MASK) {
case NF_ACCEPT:
case NF_DROP:
case NF_QUEUE:
break;
default:
return -EINVAL;
}
fallthrough;
case NFT_CONTINUE:
case NFT_BREAK:
case NFT_RETURN:
break;
...
Notice the logical flaw: nft_verdict_init() evaluated only data->verdict.code & NF_VERDICT_MASK. It completely failed to validate whether the upper 16 bits (the encoded drop_error) represented a valid negative error code (e.g., -EPERM, -EACCES). Crucially, it permitted positive integers to be supplied as the drop error.
The Execution Trap in nf_hook_slow()#
When an incoming or outgoing packet traverses a Netfilter hook chain, the kernel calls nf_hook_slow()innet/netfilter/core.c:
// Hook processing loop in net/netfilter/core.c
int nf_hook_slow(struct sk_buff *skb, struct nf_hook_state *state,
const struct nf_hook_entries *e, unsigned int s)
{
unsigned int verdict;
int ret;
for (; s < e->num_hook_entries; s++) {
verdict = nf_hook_entry_hook(e, s, skb, state);
switch (verdict & NF_VERDICT_MASK) {
case NF_ACCEPT:
break;
case NF_DROP:
kfree_skb(skb); // 1. Packet buffer is freed immediately
ret = NF_DROP_GETERR(verdict);
if (ret == 0)
ret = -EPERM;
return ret; // 2. Returns extracted error to caller
case NF_QUEUE:
ret = nf_queue(skb, state, s, verdict);
if (ret == 1)
continue;
return ret;
default:
kfree_skb(skb);
return -EPERM;
}
}
return 1;
}
Now trace the catastrophic control-flow desynchronization:
- An attacker configures an
nftablesrule that issuesNF_DROPwith a carefully chosen positive drop error of-1(which, after negation byNF_DROP_GETERR, evaluates to positive1). - Inside
nf_hook_slow(), the kernel matchescase NF_DROP:. It executeskfree_skb(skb), immediately releasing thesk_buffstructure back to the SLUB allocator. - Next,
ret = NF_DROP_GETERR(verdict)executes, settingret = 1. nf_hook_slow()exits and returns1directly to the calling network stack macro,NF_HOOK().
The Fatal Desynchronization in NF_HOOK()#
The networking subsystem invokes Netfilter hooks using the NF_HOOK() inline wrapper:
static inline int
NF_HOOK(uint8_t pf, unsigned int hook, struct net *net, struct sock *sk, struct sk_buff *skb,
struct net_device *in, struct net_device *out,
int (*okfn)(struct net *, struct sock *, struct sk_buff *))
{
int ret = nf_hook(pf, hook, net, sk, skb, in, out, okfn);
if (ret == 1)
return okfn(net, sk, skb); // 3. Assumes packet was ACCEPTED!
return ret;
}
In the standard Linux network stack contract:
- A return value of
1signifiesNF_ACCEPT. The caller is instructed to continue packet transmission via the completion functionokfn(e.g.,ip_local_deliver_finishorip_output). - A negative return value or
0signifies that the packet was dropped and already freed.
Because nf_hook_slow()returned1, NF_HOOK() assumes the packet was accepted! It promptly invokes okfn(net, sk, skb) using the already freed skb pointer. As okfn processes the packet, it reads and writes to the freed chunk, before eventually invoking kfree_skb(skb) a second time upon transmission completion.
This creates a textbook Use-After-Free (UAF) and Double-Free condition on the sk_buff structure.
Exploit Architecture & Protocol Flow#
The following sequence diagram maps the complete exploitation lifecycle, tracing the path from unprivileged user namespace instantiation to kernel page table manipulation:
sequenceDiagram
autonumber
participant Attacker as Unprivileged Local Process
participant Netlink as Netlink Subsystem (AF_NETLINK)
participant NFTables as nf_tables Kernel Engine
participant Hook as Netfilter Hook (nf_hook_slow)
participant SLUB as Kernel SLUB Allocator
participant MM as Kernel Virtual Memory (PTEs)
Note over Attacker: Step 1: Unshare User & Network Namespaces
Attacker->>Attacker: clone(CLONE_NEWUSER | CLONE_NEWNET)
Note over Attacker: Gains namespaced CAP_NET_ADMIN
Note over Attacker: Step 2: Inject Flawed Verdict Rule
Attacker->>Netlink: Send netlink message creating table & chain
Attacker->>Netlink: Commit rule with NF_DROP and drop_error=1
Netlink->>NFTables: Commit rule to chain via nft_verdict_init()
Note over Attacker: Step 3: Trigger Double-Free via Loopback
Attacker->>Attacker: Transmit UDP packet over loopback (lo)
Attacker->>Hook: Packet enters Netfilter hook evaluation
Hook->>SLUB: kfree_skb(skb) [Free #1]
Hook-->>Hook: ret = NF_DROP_GETERR(verdict) yields 1
Hook->>Hook: NF_HOOK receives ret=1 (assumes NF_ACCEPT)
Hook->>Hook: Invokes okfn() with dangling skb pointer
Hook->>SLUB: okfn completes and calls kfree_skb(skb) [Free #2]
Note over Attacker: Step 4: Flipping Pages (PTE Spraying)
Attacker->>MM: Allocate userspace memory pages via mmap()
Attacker->>SLUB: Reclaim freed slab chunk as Page Table Entry (PTE)
Attacker->>MM: Overwrite PTE bits (_PAGE_RW and _PAGE_USER)
Note over Attacker,MM: Physical kernel memory mapped into userspace (Root Acquired)Attack Path Step-by-Step#
Understanding how CVE-2024-1086 achieves deterministic root escalation requires analyzing the transition from a dangling sk_buff pointer to arbitrary physical memory access.
Step 1: Unprivileged Namespace Creation#
An unprivileged local attacker leverages the Linux clone interface to spawn a new process within isolated user and network namespaces:
#define _GNU_SOURCE
#include
#include
#include
int enter_unprivileged_namespace(void) {
// Unshare user and network namespaces
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) != 0) {
perror("[-] Failed to unshare namespaces");
return -1;
}
printf("[+] Successfully instantiated unprivileged user + network namespace\n");
printf("[+] Acquired namespaced CAP_NET_ADMIN capabilities\n");
return 0;
}
Inside this namespace, the process can communicate with the kernel's Netfilter subsystem over NETLINK_NETFILTER sockets.
Step 2: Netlink Verdict Payload Construction#
The attacker constructs a netlink message defining a new nftables table, a base chain hooked into NF_INET_LOCAL_OUT, and an immediate verdict expression.
The verdict code is specifically assembled such that verdict & NF_VERDICT_MASK == NF_DROPwhileNF_DROP_GETERR(verdict) == 1:
// Encoded verdict: NF_DROP (0) combined with negative drop error shifted
// When processed by NF_DROP_GETERR: -((int)(verdict) >> 16) == 1
#define NF_DROP 0
#define TARGET_DROP_ERR 1
#define MALICIOUS_VERDICT (NF_DROP | ((-TARGET_DROP_ERR) << 16))
void append_verdict_rule(struct nlmsghdr *nlh) {
// Conceptual assembly of NFTA_RULE_EXPRS containing NFT_EXPR_VERDICT
printf("[*] Compiling Netlink rule with encoded verdict: 0x%08X\n", MALICIOUS_VERDICT);
// Transmitted to kernel via libmnl / raw netlink socket
}
Step 3: Triggering the Double-Free#
The attacker transmits a single UDP or raw packet over the loopback interface (127.0.0.1). The packet traverses the LOCAL_OUT hook:
nf_hook_slow()executes, evaluates the rule, frees thesk_buff, and returns1.- The caller invokes the output completion function with the freed
sk_buff. - The buffer is freed a second time, leaving the SLUB allocator's freelist in a corrupted state or releasing a physical page that remains referenced.
Step 4: The "Flipping Pages" Technique (PTE Spraying)#
Traditional kernel exploits target cred structs or function pointers in the slab cache. However, CVE-2024-1086 popularized PTE Spraying:
- Allocating Page Tables: When a process calls
mmap()on anonymous memory, the kernel does not allocate physical page tables until the memory is touched. By touching memory addresses across multiple gigabytes, the kernel allocates Page Table Entries (PTE pages, 4096 bytes each) via the page allocator (alloc_pages()). - Reclaiming the Freed Chunk: The attacker carefully times the allocation so that a newly allocated Page Table page occupies the exact physical memory previously occupied by the freed network buffer.
- PTE Bit Modification: Using the dangling reference to the freed buffer, the attacker overwrites the PTE entry. In x86_64 architecture, page table entries contain permission flags:
- Bit 0 (
Present): Page is present in physical memory. - Bit 1 (
Read/Write): Page is writable. - Bit 2 (
User/Supervisor): Page is accessible from unprivileged rings (ring 3).
- Bit 0 (
- Arbitrary Physical Mapping: By setting the
Read/WriteandUserbits on a PTE pointing to physical address0x0or kernel code pages, the attacker gains direct userspace virtual memory pointers to arbitrary physical RAM.
# Conceptual view of physical memory mapping via modified Page Table Entry
Virtual Address (User) [0x7f0000000000]
|
v
PTE Entry: [ Physical Frame: 0x00100000 | Present=1 | R/W=1 | User=1 ]
|
v
Physical RAM: Kernel Code & Credentials (/proc/sys/kernel/modprobe_path)
Step 5: Overwriting modprobe_path for Host Takeover#
With direct write access to physical memory, the attacker locates the modprobe_path kernel string (commonly defaulting to /sbin/modprobe) and overwrites it with an attacker-controlled script path (e.g., /tmp/pwn.sh).
The attacker then attempts to execute a binary with an invalid header (e.g., \xFF\xFF\xFF\xFF), prompting the kernel to invoke call_usermodehelper(modprobe_path) with full root privileges.
Fast Cyber Defense Morning Takeaways#
CVE-2024-1086 demonstrates that even decade-old, mature subsystems like Netfilter can harbor severe type-handling and verdict-synchronization defects that undermine kernel isolation.
- Unprivileged User Namespaces Are the Primary Vector: The vast majority of modern Linux kernel local privilege escalations require unprivileged user namespaces (
CLONE_NEWUSER) to access restricted networking and filesystem code paths. Disabling unprivileged user namespaces instantly neutralizes this entire class of exploits. - Memory Safety Defeats Bypass Traditional Defenses: Techniques like PTE spraying demonstrate that kernel exploit development has evolved past simple shellcode injection and ROP chains. By directly manipulating virtual memory management structures, attackers render KASLR, SMEP, and SMAP ineffective.
- Defense-in-Depth Is Mandatory: Relying solely on delayed distributor patches leaves systems vulnerable. Enterprise defense requires active kernel sysctl restrictions, container runtime seccomp profiles, and eBPF-based syscall monitoring.
- Bridge to Evening Defense Guide: In tonight's Edition 2 post, we will publish the comprehensive blue team engineering blueprint for mitigating and detecting Netfilter vulnerabilities, including:
- Enforcing kernel sysctl hardening (
kernel.unprivileged_userns_clone = 0and unprivileged eBPF restrictions). - Designing production Docker and Podman seccomp profiles blocking
clone(CLONE_NEWUSER)andunshare(). - Implementing validated Sigma rules to detect suspicious
unsharesyscall executions and rapid memory allocations. - Deploying Falco and Tetragon eBPF telemetry to intercept anomalous Netlink socket operations.
- Enforcing kernel sysctl hardening (
Authoritative Technical References#
- pwning.tech (Notselwyn): Flipping Pages - An Analysis of a New Linux Vulnerability in nf_tables (CVE-2024-1086)
- NIST National Vulnerability Database: CVE-2024-1086 Detail
- Linux Kernel Git Commit f342de4e2f33: netfilter: nf_tables: disallow positive drop error in nft_verdict_init
- CISA Known Exploited Vulnerabilities (KEV) Catalog: CVE-2024-1086
Comments
Post a Comment