Linux内核漏洞CVE-2025-39682教学文档
一、漏洞概述
| 项目 | 内容 |
|---|---|
| 漏洞编号 | CVE-2025-39682 |
| 影响版本 | Linux Kernel < v6.17 |
| 涉及模块 | Linux Kernel - TLS / tls_record_content_type |
| 漏洞类型 | Use-After-Free (UAF) |
| 利用效果 | 本地提权 |
该漏洞本质上是另一个漏洞CVE-2024-58239的未修复完全版本。CVE-2024-58239的补丁(commit fdfbaec5923d9359698cbb286bc0deadbb717504)忽略了当非Data类型的record长度为0时的场景,导致copied值为0时出现逻辑短路,绕过了后续的control != TLS_RECORD_TYPE_DATA检查。
二、环境搭建
2.1 复现环境
- 模拟器: QEMU
- 内核版本: Linux Kernel v6.16
- 环境附件: bzImage
2.2 复现步骤
- 使用QEMU启动已patch的内核
- 将PoC编译后传输到虚拟机中(或编译后重新打包文件系统)
- 运行PoC即可触发KASAN报告
注意:作者测试时基于一个已修复版本进行patch,其中6.12.67版本已修复此漏洞。
三、漏洞原理
漏洞本质是Use-After-Free (UAF)。
当kTLS使用零拷贝(Zero Copy, ZC) 时,内核不会额外分配一个新的SKB(Socket Buffer),而是直接复用接收队列上原有的SKB。解密完成后,该SKB会被直接释放。然而,在同一解密流中,当Record的类型不一致时,kTLS的处理逻辑会将SKB放入rx_list中等待下一次接收再读取。这导致已释放的内存仍然保留额外的引用在rx_list中,从而引发UAF。
四、漏洞分析
4.1 关键代码段分析
4.1.1 Record类型不一致时的处理
在tls_sw_recvmsg函数中存在以下逻辑:
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;
}
该代码的作用:当处理多个Record时,如果当前Record的类型与之前不一致,则将当前Record对应的SKB存入rx_list中。
4.1.2 rx_list优先读取逻辑
在tls_sw_recvmsg函数开头存在以下逻辑:当rx_list中有数据时优先读取,如果读取的长度已经满足需求,则直接跳转到end。
结合上述两段代码,整体流程是:当Record类型不一致时,将其存放至rx_list供下次读取。
4.1.3 DEBUG提示的含义
代码中的DEBUG_NET_WARN_ON_ONCE(darg.zc)提示了一个重要信息:如果设置了darg.zc(即启用零拷贝),理论上不应走到这个路径。但实际情况并非如此。
4.2 解密流程中的引用关系
4.2.1 SKB加载流程
在TLS解密之前,调用链如下:
tls_rx_rec_wait -> tls_strp_msg_load -> tls_strp_load_anchor_with_queue
最终将接收队列中的一个SKB加载到strp->anchor这条引用中。
4.2.2 解密缓冲区处理
进行TLS解密时的调用链:
tls_rx_one_record -> tls_decrypt_sw -> tls_decrypt_sg
在tls_decrypt_sg函数中,对解密缓冲区的处理有三个选择:
- 非零拷贝路径:内核会分配新的
clear_skb作为载体,在函数返回前将引用传递给darg->skb - 零拷贝路径:返回的是
strp->anchor这条引用
4.2.3 UAF触发点
tls_rx_rec_done的功能是释放strp->anchor这条引用。然而,在内存释放之后,又通过以下代码将引用附加到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); // 释放 strp->anchor
put_on_rx_list_err:
__skb_queue_tail(&ctx->rx_list, darg.skb); // darg.skb == strp->anchor,已释放
goto recv_end;
}
这就形成了典型的Use-After-Free。
五、PoC分析
5.1 触发条件
触发漏洞的核心在于使两个Record的类型不一致,并且后续的Record走零拷贝路径。
关键判断逻辑在tls_record_content_type函数中:
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; // 类型不一致时返回0
}
return 1;
}
另外,在tls_sw_recvmsg中还有以下逻辑:
if (control != TLS_RECORD_TYPE_DATA)
break;
当Record类型不是DATA时,不再继续处理。
5.2 触发步骤(需三个Record)
第一步:发送两个Record后进行第一次recv
- 处理第一个Data类型的Record,初始化
control为TLS_RECORD_TYPE_DATA - 处理第二个其他类型的Record,触发类型不一致判断,将对应的SKB加入
rx_list
第二步:发送第三个Record,以零拷贝方式进行recv
rx_list中有SKB,优先处理该SKB- 处理完毕后,
control被设置为0x16(非Data类型) - 读取长度不够,继续读取第三个Record
- 触发类型不一致判断,将第三个Record对应的SKB加入
rx_list - 由于零拷贝的原因,该SKB内存已被释放,造成UAF
5.3 为什么需要三个Record
关键在于control变量的赋值逻辑:
- 如果
control未被赋值,则进行赋值 - 赋值后,后续Record与之比较
而control != TLS_RECORD_TYPE_DATA这条检查会导致非Data类型的Record不被继续处理。因此需要三个Record来绕过这一限制,使得第三次recv时能成功触发UAF。
六、利用思路
6.1 总体方向
SKB的frags数组中如果包含Page,可以尝试转化为Page UAF。
6.2 Data-only利用方式
获得Page UAF后,最简单的Data-only利用方式是:
- 堆喷
filp结构体 - 篡改
/etc/passwd文件实现权限提升
七、修复方案
7.1 官方补丁
修复该漏洞的补丁提交为:
commit 62708b9452f8eb77513115b17c4f8d1a22ebf843
7.2 修复原理
补丁修复了CVE-2024-58239补丁中遗留的问题——当非Data类型的Record长度为0时,copied值为0导致的逻辑短路,从而绕过了control != TLS_RECORD_TYPE_DATA这条检查。
7.3 升级建议
建议将Linux内核升级至v6.17及以上版本,或至少升级至包含该补丁的稳定版本(如6.12.67+)。