Skip to content

Commit 23ec8cf

Browse files
committed
kernelCTF: add CVE-2026-23231_cos
1 parent 6226291 commit 23ec8cf

9 files changed

Lines changed: 4872 additions & 0 deletions

File tree

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
# Exploit: cos-113-18244.521.88
2+
3+
## Overview
4+
5+
Local privilege escalation exploit targeting Linux 6.1.155+ (COS-113, PREEMPT_NONE, KVM, 2 CPUs, 3.5 GB RAM). The exploit leverages a packet-path use-after-free in nf_tables `nf_tables_addchain()` to hijack RIP via a fake rule blob, overwrite `core_pattern`, and exfiltrate the flag from `/dev/vdb` via `call_usermodehelper`.
6+
7+
This exploit targets the packet-path manifestation of CVE-2026-23231. The CVE also covers a control-plane UAF reachable from `nf_tables_dump_chains()`, but the privilege-escalation path here relies on the transient IPv4 hook installed during `NFPROTO_INET` rollback.
8+
9+
For the public PR, the `exploit` target builds the kernelXDK port in `exploit_xdk.cpp`. The 0-day submission archive of the original non-XDK exploit is carried separately as `original.tar.gz`.
10+
11+
**Stability**: Probabilistic race. Verified with the kernelCTF repro harness without the separate KASLR leak path (`requires_separate_kaslr_leak=false`); may require multiple attempts.
12+
13+
**KASLR handling**: The submitted kernelCTF repro leaks both the kernel text base (`kbase`) and direct-map base (`phbase`) internally using a prefetch side channel. It does not rely on the harness-provided KASLR leak path.
14+
15+
---
16+
17+
## Step-by-step Exploitation
18+
19+
### Step 1: KASLR Bypass via Prefetch Side Channel
20+
21+
<!-- @step(name="KASLR Bypass via Prefetch Side Channel") -->
22+
23+
The exploit leaks the kernel text base (`kbase`) and direct-map base (`phbase`) using a prefetch side channel. It times `prefetchnta` and `prefetcht2` with `rdtscp`.
24+
25+
- **Intel kbase scan**: Scans `entry_SYSCALL_64` (`kbase + 0x1400080`) with 16 MB KASLR steps. Each vote primes the syscall path, records the lowest prefetch timing over 16 samples per candidate, then majority-votes 7 scans.
26+
27+
- **Intel phbase scan**: Scans the direct map from `0xffff888000000000` to `0xffffc88000000000` using lowest-timing votes with a 1 GB coarse step, then refines around the first hit with a 64 MB step.
28+
29+
- **Fallback scan**: On non-Intel hosts, scans mapped ranges by treating averaged timings above the calibrated threshold as mapped kernel addresses. The threshold is capped at 190 cycles by default and 130 cycles on Xeon hosts, then adjusted down to the measured unmapped baseline plus a 24-cycle margin when the host timing is lower.
30+
31+
- **Vulnerability verification**: The verifier invokes the exploit with `--vuln-trigger` on a `nokaslr` kernel. In that mode the exploit uses fixed `kbase` and `phbase` values (`0xffffffff81000000`, `0xffff888000000000`) so reachability does not depend on the prefetch leak.
32+
33+
- **Candidate arrays**: The race uses the verified primary `kbase`. The direct-map candidate list includes the primary `phbase` and +/- 1 GB neighbors to absorb small direct-map boundary variance.
34+
35+
**Objects**: None (pure side-channel, no kernel objects involved).
36+
37+
### Step 2: Physmap Spray with ROP Blob
38+
39+
<!-- @step(name="Physmap Spray") -->
40+
41+
A 2 GB region is allocated via `mmap(MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE)`. Every 4 KB page is filled with:
42+
43+
- **Bytes 0x000-0x097**: A fake `nft_rule_blob` containing a ROP chain (4 fake `nft_immediate_eval` expressions). See [ROP Chain Details](#rop-chain-details) below.
44+
- **Byte 0x100** (`PATH_OFFSET`): The payload string `|/bin/dd if=/dev/vdb of=/dev/ttyS0` (for `core_pattern` overwrite).
45+
46+
The direct-map base (`phbase`) provides a 1:1 virtual mapping of physical RAM, so these user pages are accessible from the kernel at predictable KVAs (`phbase + pfn * 0x1000` when pagemap is readable, or `phbase + guessed_offset` otherwise).
47+
48+
If `/proc/self/pagemap` is readable, the exact physical frame number is read to compute the precise KVA. Otherwise, the exploit guesses KVA offsets in the `[1.0, 2.0] GB` range from `phbase` (avoiding the QEMU MMIO hole at 3-4 GB physical).
49+
50+
**Objects**: Physical pages via physmap (direct map).
51+
**Cache**: N/A (physmap pages, not slab objects).
52+
53+
### Step 3: Namespace Setup
54+
55+
<!-- @step(name="Namespace Setup") -->
56+
57+
The exploit creates user and network namespaces (`unshare(CLONE_NEWUSER | CLONE_NEWNET)`) to gain `CAP_NET_ADMIN` for nf_tables access. UID/GID mappings are written to `/proc/self/uid_map` and `/proc/self/gid_map`. The loopback interface is brought up (required for UDP flood packets to traverse LOCAL_OUT hooks).
58+
59+
### Step 4: Saturate IPv6 LOCAL_OUT Hooks
60+
61+
<!-- @step(name="Saturate IPv6 Hooks") -->
62+
63+
An IPv6 nf_tables table (`t6`) is created, then up to 1024 base chains are added on the `NF_INET_LOCAL_OUT` hookpoint. Once `MAX_HOOK_COUNT` (1024) is reached, any subsequent hook registration for IPv6 LOCAL_OUT returns `-E2BIG`.
64+
65+
This sets up the precondition for triggering the vulnerability: when creating an `NFPROTO_INET` chain, the IPv4 hook succeeds but the IPv6 hook fails with `-E2BIG`.
66+
67+
An INET table (`ti`) is also created for the race loop.
68+
69+
**Syscalls**: `sendto()` on `AF_NETLINK` / `NETLINK_NETFILTER` socket (nfnetlink batch messages with `NFT_MSG_NEWTABLE` and `NFT_MSG_NEWCHAIN`).
70+
71+
### Step 5: Race — Trigger UAF and Reclaim with msg_msg
72+
73+
<!-- @step(name="Race: Trigger UAF and Reclaim") -->
74+
75+
Three threads run concurrently on 2 CPUs:
76+
77+
#### Thread 1: UDP Flood (CPU 1)
78+
79+
Sends UDP packets to `127.0.0.1:12345` via `sendmmsg()` in batches of 128. Each packet traverses `NF_INET_LOCAL_OUT`, calling `nft_do_chain_inet()``nft_do_chain()` on any registered IPv4 hook. Yields every 8th batch (`sched_yield()`) to report RCU quiescent states, ensuring `synchronize_rcu()` on CPU 0 completes in ~1 ms instead of blocking for 10+ seconds on a PREEMPT_NONE kernel.
80+
81+
**Purpose**: Provide a continuous stream of packets that hit the transiently-installed IPv4 hook during the race window.
82+
83+
#### Thread 2: INET NEWCHAIN Loop (CPU 0)
84+
85+
Sends nfnetlink batch messages creating `NFPROTO_INET` base chains. Each attempt:
86+
87+
1. Chain is allocated (`nft_base_chain`, kmalloc-256) and published to `table->chains` via `list_add_tail_rcu()`.
88+
2. IPv4 hook registers successfully — packets can now reach `nft_do_chain()` on this chain.
89+
3. IPv6 hook registration fails with `-E2BIG` (hooks saturated).
90+
4. Error path: `nft_chain_del()` removes chain from list, then `nf_tables_chain_destroy()` frees the `nft_base_chain` and its rule blob **without `synchronize_rcu()`**.
91+
5. The nfnetlink batch abort path calls `synchronize_rcu()` unconditionally in `__nf_tables_abort()`, which **blocks** this thread — allowing the spray thread to run.
92+
93+
**Vulnerable window**: ~2 us between IPv4 hook going live and chain free. On KVM, VMEXITs (5-20 us each) can delay a packet's `nft_do_chain()` blob read past the kfree point.
94+
95+
**Vulnerable object**: `struct nft_base_chain` (kmalloc-cg-256).
96+
**Vulnerable field**: `chain.blob_gen_0` / `chain.blob_gen_1` (pointers to rule blob, read by `nft_do_chain()` after free).
97+
98+
#### Thread 3: Dedicated msg_msg Spray (CPU 0)
99+
100+
Runs on the **same CPU** as the race thread. When the race thread blocks in `synchronize_rcu()`, the scheduler switches to this thread. It performs:
101+
102+
1. Burst of 4 `msgsnd()` calls: allocates `msg_msg` objects from CPU 0's `kmalloc-cg-256` per-CPU SLUB freelist. The **first** `msgsnd` picks up the just-freed `nft_base_chain` slot (SLUB LIFO order), overwriting it with controlled data.
103+
2. `sched_yield()`: forces context switch → `rcu_note_context_switch()` → quiescent state for CPU 0, unblocking the pending `synchronize_rcu()`.
104+
105+
**Reclaim object**: `struct msg_msg` (kmalloc-cg-256, 256-byte allocation: 48-byte msg_msg header + 208-byte mtext).
106+
**Overwritten fields in reclaimed nft_base_chain**:
107+
| Allocation Offset | Field | Value | mtext Offset |
108+
|---|---|---|---|
109+
| +64 | `basechain.policy` | `1` (NF_ACCEPT) | mtext[16] |
110+
| +80 | `chain.blob_gen_0` | physmap KVA (points to ROP blob page) | mtext[32] |
111+
| +88 | `chain.blob_gen_1` | physmap KVA (same) | mtext[40] |
112+
| +164 | `chain.flags` | `0x01` (NFT_CHAIN_BASE) | mtext[116] |
113+
114+
When a racing packet on CPU 1 calls `nft_do_chain()` on the freed-and-reclaimed chain, it reads `blob_gen_X` which now points to our physmap ROP blob page.
115+
116+
#### Synchronization
117+
118+
- **Race thread ↔ Spray thread**: Implicit via `synchronize_rcu()` blocking. When the race thread blocks, the OS scheduler switches to the spray thread on the same CPU. No explicit synchronization primitives.
119+
- **Flood thread ↔ Race thread**: The flood thread's `sched_yield()` every 8th batch provides RCU quiescent states on CPU 1, allowing `synchronize_rcu()` on CPU 0 to complete quickly.
120+
- **Termination**: Shared `volatile int race_running` and `race_won` flags. The parent process polls `core_pattern` every 2 seconds.
121+
122+
### Step 6: ROP Chain Execution via nft_immediate_eval
123+
124+
<!-- @step(name="ROP Chain Execution") -->
125+
126+
When the racing packet dereferences the reclaimed `blob_gen_X` pointer, it reaches our physmap page containing a fake `nft_rule_blob`. The blob is structured as:
127+
128+
```
129+
+0x000: blob_size = 0x90 (144 bytes)
130+
+0x008: rule_descriptor_0: dlen=128 (4 expressions x 32 bytes), is_last=0
131+
+0x010: expression 0 (32 bytes)
132+
+0x030: expression 1 (32 bytes)
133+
+0x050: expression 2 (32 bytes)
134+
+0x070: expression 3 (32 bytes)
135+
+0x090: rule_descriptor_1: is_last=1 (end marker)
136+
```
137+
138+
Each expression has `ops` set to `nft_immediate_ops` (`kbase + 0x1d433e0`). The `nft_immediate_eval()` function copies `expr->data` into `regs->data[dreg]`. By targeting the `dreg` corresponding to `nft_do_chain()`'s return address on the stack, we hijack RIP.
139+
140+
#### ROP Chain Details
141+
142+
The ROP chain is written into `nft_do_chain()`'s stack frame via the `dreg` mechanism:
143+
144+
| Expr | dreg | Stack Offset | Gadget / Value | Purpose |
145+
|------|------|-------------|----------------|---------|
146+
| 0 | 130 (ret addr) | rsp+0x250 | `pop rsi; pop rdi; ret` (`kbase + 0xafc2d1`) | Load args for strcpy |
147+
| 0 | (data) | | `rsi` = physmap page + 0x100 (path string), value popped by gadget | Source string |
148+
| 1 | 134 (ret+16) | rsp+0x260 | `rdi` = `kbase + 0x2bb9ec0` (`core_pattern`), then `strcpy` (`kbase + 0x12b59f0`) | Overwrite core_pattern |
149+
| 2 | 138 (ret+32) | rsp+0x270 | `pop rdi; ret` (`kbase + 0x195d8c`), `rdi` = 0x7FFFFFFF | Load msleep argument |
150+
| 3 | 142 (ret+48) | rsp+0x280 | `msleep` (`kbase + 0x243850`), `return_thunk` (`kbase + 0x16054b0`) | Sleep ~25 days (keeps system alive) |
151+
152+
**Constant explanations**:
153+
- `DREG_RET = 130`: `(0x250 - 0x48) / 4`. `nft_do_chain()` has `sub $0x220, %rsp` + 6 pushes (48 bytes). `regs` is at `rsp+0x48`, return address at `rsp+0x250`. Register file is `uint32_t regs.data[]`, so offset `(0x250 - 0x48) / 4 = 130`.
154+
- `PATH_OFFSET = 0x100`: Offset within each physmap page where the payload string is stored, chosen to be past the 0x98-byte blob.
155+
156+
#### Kernel Symbol Offsets (cos-113-18244.521.88 vmlinux)
157+
158+
| Define | Offset | Symbol |
159+
|--------|--------|--------|
160+
| `OFF_CORE_PATTERN` | `0x2bb9ec0` | `core_pattern` (global variable) |
161+
| `OFF_MODPROBE` | `0x2a76a20` | `modprobe_path` (unused, fallback) |
162+
| `OFF_NFT_IMM_OPS` | `0x1d433e0` | `nft_immediate_ops` (expression ops vtable) |
163+
| `OFF_POP_RSI_RDI` | `0xafc2d1` | `pop rsi; pop rdi; ret` gadget |
164+
| `OFF_POP_RDI` | `0x195d8c` | `pop rdi; ret` gadget |
165+
| `OFF_STRCPY` | `0x12b59f0` | `strcpy` |
166+
| `OFF_MSLEEP` | `0x243850` | `msleep` |
167+
| `OFF_RETURN_THUNK` | `0x16054b0` | `__x86_return_thunk` |
168+
169+
### Step 7: core_pattern Overwrite and Flag Exfiltration
170+
171+
<!-- @step(name="core_pattern Overwrite and Flag Exfiltration") -->
172+
173+
After the ROP chain executes `strcpy(core_pattern, "|/bin/dd if=/dev/vdb of=/dev/ttyS0")`, the `msleep(0x7FFFFFFF)` keeps the system alive (BUGs with "scheduling while atomic" but `schedule()` still executes, keeping the system up for ~120 seconds until `hung_task` watchdog panics).
174+
175+
The parent process (forked before entering the exploit child) polls `/proc/sys/kernel/core_pattern` every 2 seconds. Once it detects the overwrite:
176+
177+
1. Sets `RLIMIT_CORE` to unlimited.
178+
2. Sets `PR_SET_DUMPABLE` to 1.
179+
3. Forks a child that calls `raise(SIGSEGV)`.
180+
4. The kernel's core dump handler invokes `call_usermodehelper` with the pipe command from `core_pattern`.
181+
5. `/bin/dd if=/dev/vdb of=/dev/ttyS0` copies the flag from the virtio block device to the serial console.
182+
6. The kctf infrastructure captures the flag from serial output.
183+
184+
**Why `core_pattern` instead of `modprobe_path`**: Inside kctf's nsjail container, `modprobe_path` resolution happens in the init mount namespace. The exploit process's memfd (`/proc/PID/fd/N`) is not visible from the init PID namespace (different PID ns), and `/tmp/pw` is on a separate tmpfs. `core_pattern` with `|/bin/dd` uses only init-namespace-visible paths (`/bin/dd` on rootfs, `/dev/vdb` and `/dev/ttyS0` on devtmpfs).
185+
186+
**Why `msleep()` instead of `do_exit()`**: `do_exit()` from softirq context kills the current task and closes all file descriptors (including memfd FDs). `msleep()` BUGs with "scheduling while atomic" but `schedule()` still executes, keeping the system alive long enough for the parent to detect the overwrite and trigger core dumps.
187+
188+
---
189+
190+
## Environmental Requirements
191+
192+
- **CPU affinity**: The race thread and spray thread are pinned to CPU 0; the flood thread is pinned to CPU 1. This is critical because:
193+
- SLUB uses per-CPU freelists. The spray thread must run on the same CPU as the race thread to pick up the freed `nft_base_chain` slot (LIFO order).
194+
- The flood thread on a separate CPU ensures packets traverse the hook path concurrently with chain allocation/free.
195+
196+
- **Namespaces**: User namespace + network namespace are required to gain `CAP_NET_ADMIN` for nf_tables access. The exploit calls `unshare(CLONE_NEWUSER)` then `unshare(CLONE_NEWNET)`.
197+
198+
- **Memory**: The exploit uses 2 GB for physmap spray (auto-scaled to 75% of available RAM if less than 2 GB). The kctf VM has 3.5 GB RAM.
199+
200+
- **SMAP detection**: If SMAP is not present (e.g., TCG mode), the exploit uses a userspace page as the fake blob instead of physmap spray, eliminating the need for phbase scanning entirely.
201+
202+
## Separation of Concerns
203+
204+
| Code Section | Purpose |
205+
|---|---|
206+
| `do_entrybleed()` / `leak_entrybleed()`, `leak_kernel_text_prefetch()`, `leak_direct_mapping_prefetch()` | KASLR bypass (information leak) |
207+
| `setup_ns()` | Environment setup (namespace creation) |
208+
| `saturate_ipv6_hooks()` | Vulnerability setup (creating precondition for -E2BIG failure) |
209+
| `inet_race_thread()` | Vulnerability trigger (INET NEWCHAIN creating the UAF) |
210+
| `udp_flood_thread()` | Exploitation support (providing racing packets) |
211+
| `dedicated_spray_thread()`, `spray_msgs()`, `build_spray_msg()` | Heap spraying (msg_msg reclaim of freed nft_base_chain) |
212+
| `physmap_spray()`, `physmap_fill_rop()`, `build_blob()` | ROP payload construction |
213+
| `try_core_dump()`, `try_modprobe()` | Post-exploitation (flag exfiltration) |
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Vulnerability: Use-After-Free in nf_tables_addchain() Error Path (CVE-2026-23231)
2+
3+
## Summary
4+
5+
A use-after-free vulnerability exists in the netfilter `nf_tables` subsystem, specifically in the error path of `nf_tables_addchain()`. If hook registration fails after the chain has been published, the error path calls `nft_chain_del()` and then immediately calls `nf_tables_chain_destroy()` without an intervening `synchronize_rcu()`.
6+
7+
This violates the RCU publish/grace-period/free contract and creates two separate UAF conditions covered by CVE-2026-23231:
8+
9+
- **Control-plane UAF**: `nf_tables_dump_chains()` traverses `table->chains` under `rcu_read_lock()`, so a concurrent dump can still walk the chain after it has been removed with `list_del_rcu()` but before readers quiesce.
10+
- **Packet-path UAF**: for `NFPROTO_INET`, `nf_register_net_hook()` can briefly install the IPv4 hook before IPv6 registration fails. Packets entering `nft_do_chain()` through that transient IPv4 hook can still dereference `chain->blob_gen_X` after the chain has been freed.
11+
12+
The exploit in this submission uses the second condition: partial `NFPROTO_INET` hook registration failure caused by exhausting IPv6 `LOCAL_OUT` hooks.
13+
14+
## Affected Component
15+
16+
- **Subsystem**: netfilter / nf_tables
17+
- **Source file**: `net/netfilter/nf_tables_api.c`
18+
- **Function**: `nf_tables_addchain()`
19+
20+
## Vulnerability Type
21+
22+
- **Cause**: Use-After-Free (UAF) due to missing RCU grace period
23+
- **Root cause**: `nf_tables_addchain()` frees the chain immediately after `list_del_rcu()` on the hook-registration failure path, allowing concurrent RCU readers to observe freed memory
24+
25+
## Requirements to Trigger
26+
27+
- **User namespaces**: Not required for the bug in general, but required for this exploit so an unprivileged user can create a network namespace and gain `CAP_NET_ADMIN` inside it.
28+
- **Capabilities**: `CAP_NET_ADMIN` (inside the attacker's network namespace for this exploit)
29+
- **Kernel configuration**: `CONFIG_NF_TABLES`, `CONFIG_NF_TABLES_INET`
30+
- **Other**: Ability to saturate IPv6 hooks to `MAX_HOOK_COUNT` (1024) to force the IPv6 hook registration failure
31+
32+
## Commit Which Introduced the Vulnerability
33+
34+
- **Commit**: [`91c7b38dc9f0`](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=91c7b38dc9f0de4f7f444b796d14476bc12df7bc) ("netfilter: nf_tables: use new transaction infrastructure to handle chain")
35+
- This commit introduced the transaction-based chain handling and the vulnerable teardown path that removes a chain with `list_del_rcu()` and then frees it without waiting for an RCU grace period when hook registration fails.
36+
37+
## Commit Which Fixed the Vulnerability
38+
39+
- **Fix**: Add `synchronize_rcu()` between `nft_chain_del()` and `nf_tables_chain_destroy()` in the `err_register_hook` error path of `nf_tables_addchain()`.
40+
- **Patch commit**: [`71e99ee20fc3`](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=71e99ee20fc3f662555118cf1159443250647533) ("netfilter: nf_tables: fix use-after-free in nf_tables_addchain()")
41+
42+
## Affected Kernel Versions
43+
44+
- **Introduced in**: 3.16 (when `91c7b38dc9f0` first shipped)
45+
- **Affected stable ranges**: 3.16 - 6.1.164, 6.2 - 6.6.127, 6.7 - 6.12.74, 6.13 - 6.18.13, and 6.19 - 6.19.3
46+
- **Fixed in**: 6.1.165, 6.6.128, 6.12.75, 6.18.14, 6.19.4, and mainline 7.0-rc1
47+
48+
## Blocking the Vulnerability
49+
50+
The vulnerability can be prevented by any of the following:
51+
52+
- **Disabling user namespaces** (`kernel.unprivileged_userns_clone=0` or equivalent) prevents this exploit path from obtaining `CAP_NET_ADMIN` in a private network namespace.
53+
- **Blocking `NETLINK_NETFILTER` socket creation** from unprivileged contexts.
54+
- **Disabling nf_tables support** (`CONFIG_NF_TABLES=n`) or, more narrowly, disabling INET chains (`CONFIG_NF_TABLES_INET=n`).
55+
- **Limiting `nf_tables` access** via security modules (e.g., SELinux, AppArmor policies denying netfilter configuration).
56+
57+
## KASAN Report
58+
59+
```
60+
BUG: KASAN: use-after-free in nft_do_chain+0x1214/0x12a0
61+
Read of size 8 at addr ffff888103ef0b00 by task init/76
62+
...
63+
Allocated by task 77:
64+
nf_tables_addchain.constprop.0+0x8f9/0x1f80
65+
Freed by task 77:
66+
nf_tables_chain_destroy+0xb8/0x530
67+
nf_tables_addchain.constprop.0+0xe6f/0x1f80
68+
```

0 commit comments

Comments
 (0)