缓存时间:
2026/05/20 20:31
# Qualys 安全公告:Linux 内核 `__ptrace_may_access()` 函数中的逻辑错误 (CVE-2026-46333)
## 目录
- [摘要](#summary)
- [分析](#analysis)
- [案例研究:chage](#case-study-chage)
- [案例研究:ssh-keysign](#case-study-ssh-keysign)
- [案例研究:pkexec](#case-study-pkexec)
- [案例研究:accounts-daemon](#case-study-accounts-daemon)
- [致谢](#acknowledgments)
- [时间线](#timeline)
## 摘要
我们发现 Linux 内核的 `__ptrace_may_access()` 函数中存在一个逻辑错误(授权绕过)。此漏洞可在本地被利用,实现信息泄露并以 root 身份执行任意命令。据我们所知,该漏洞于 2016 年 11 月(v4.10-rc1)由提交 bfedb58("mm: Add a user_ns owner to mm_struct and fix ptrace permission checks")引入。
我们为此漏洞开发了四种不同的利用方法(所有方法都依赖于 pidfd_getfd() 系统调用,该调用于 2020 年 1 月(v5.6-rc1)引入,但可能存在其他利用方式):
- 针对 **chage**(一个设置了 set-uid root 或 set-gid shadow 权限的二进制文件)的漏洞,允许本地攻击者泄露 /etc/shadow 文件的内容(系统密码哈希)。我们在 Debian 13、Ubuntu 24.04 和 26.04、Fedora 43 和 44 的默认安装上成功测试了此漏洞;其他发行版也可能受到影响。
- 针对 **ssh-keysign**(一个设置了 set-uid root 权限的二进制文件)的漏洞,允许本地攻击者泄露主机的私钥(/etc/ssh/*_key)。我们在 Debian 13、Ubuntu 24.04 和 26.04 的默认安装上成功测试了此漏洞;其他发行版也可能受到影响。
- 针对 **pkexec**(一个设置了 set-uid root 权限的二进制文件)的漏洞,允许本地攻击者在真实用户实际坐在计算机前时以 root 身份执行任意命令(攻击者可以远程登录到计算机,例如通过 sshd)。我们在 Debian 13、Ubuntu Desktop 24.04 和 26.04、Fedora Workstation 43 和 44 的默认安装上成功测试了此漏洞;其他发行版也可能受到影响。
- 针对 **accounts-daemon**(一个 root 守护进程)的漏洞,允许本地攻击者以 root 身份执行任意命令。我们在 Debian 13、Fedora Workstation 43 和 44 的默认安装上成功测试了此漏洞;其他发行版也可能受到影响,但 Ubuntu 明显不受影响,因为它默认启用了 Yama ptrace 保护(将 kernel.yama.ptrace_scope 设置为 1)。
请注意,我们没有穷举搜索可利用的用户态程序(set-uid、set-gid、set-capabilities 二进制文件以及 root 守护进程);我们只是从过去的研究项目中记起了这四个,可能存在其他更好利用的程序。
**最后一刻说明**:2026 年 5 月 15 日星期五,我们在 https://www.openwall.com/lists/oss-security/2026/05/15/2 和 https://www.openwall.com/lists/oss-security/2026/05/15/8 上预发布了相关信息。
## 分析
一个非特权用户如果想成功地对一个进程调用 `ptrace()`、`process_vm_readv()`、`process_vm_writev()`、`pidfd_getfd()`,或者访问该进程在 /proc/pid 中的敏感文件,必须首先通过 `__ptrace_may_access()` 中的两项安全检查:
```c
276 static int __ptrace_may_access(struct task_struct *task, unsigned int mode)
277 {
...
316 tcred = __task_cred(task);
317 if (uid_eq(caller_uid, tcred->euid) &&
318 uid_eq(caller_uid, tcred->suid) &&
319 uid_eq(caller_uid, tcred->uid) &&
320 gid_eq(caller_gid, tcred->egid) &&
321 gid_eq(caller_gid, tcred->sgid) &&
322 gid_eq(caller_gid, tcred->gid))
323 goto ok;
...
328 ok:
...
340 mm = task->mm;
341 if (mm &&
342 (get_dumpable(mm) != SUID_DUMP_USER) &&
343 !ptrace_has_cap(mm->user_ns, mode))
344 return -EPERM;
345
346 return security_ptrace_access_check(task, mode);
347 }
```
1. 在第 317-322 行,进程的有效、保存、真实 uid 和 gid 必须等于非特权用户的 uid 和 gid;
2. 在第 341-342 行,进程的 dumpable 标志必须等于 `SUID_DUMP_USER`(1)。默认情况下,如果进程更改了其 uid 或 gid,内核会自动将其 dumpable 标志设置为 `SUID_DUMP_DISABLE`(0),以防止非特权用户从该进程提取敏感信息或资源。例如,sshd-session 将其 root uid 和 gid 更改为已验证用户的 uid 和 gid,但其内存中仍可能包含秘密信息(私钥和密码哈希)。
不幸的是,第 342 行的 dumpable 标志检查可以被完全绕过:如果进程的 `mm` 指针在第 341 行为 NULL,那么非特权用户(其 uid 和 gid 与进程的 uid 和 gid 相同)可以诱使 `__ptrace_may_access()` 在第 346 行成功返回,即使该进程的 dumpable 标志实际上不等于 `SUID_DUMP_USER`(即,即使该进程曾经是特权进程)。
内核在 `do_exit()` 的第 964 行将进程的 mm 指针设置为 NULL,此时该进程正在终止:
```c
896 void __noreturn do_exit(long code)
897 {
...
964 exit_mm();
...
971 exit_files(tsk);
...
1019 do_task_dead();
1020 }
```
那么,问题就变成了:在一个进程完全放弃其权限(变为攻击者的 uid 和 gid)之后,在其 mm 指针于第 964 行设置为 NULL 之后,但在它完全死亡(第 1019 行)之前,攻击者可以从该进程中窃取哪些敏感资源?最终,我们在 `pidfd_getfd()` 系统调用中找到了这个问题的答案:
```c
947 SYSCALL_DEFINE3(pidfd_getfd, int, pidfd, int, fd,
948 unsigned int, flags)
949 {
...
964 return pidfd_getfd(pid, fd);
910 static int pidfd_getfd(struct pid *pid, int fd)
911 {
...
920 file = __pidfd_fget(task, fd);
872 static struct file *__pidfd_fget(struct task_struct *task, int fd)
873 {
...
881 if (ptrace_may_access(task, PTRACE_MODE_ATTACH_REALCREDS))
882 file = fget_task(task, fd);
1123 struct file *fget_task(struct task_struct *task, unsigned int fd)
1124 {
...
1128 if (task->files)
1129 file = __fget_files(task->files, fd, 0);
```
- 如果我们(攻击者)在一个进程刚刚将其权限降为我们自己的 uid 和 gid 后立即向其发送 SIGKILL;
- 并且我们在该进程的 mm 指针被设置为 NULL(在 do_exit() 第 964 行)之后、但 files 指针被设置为 NULL(在 do_exit() 第 971 行)之前调用 pidfd_getfd();
- 那么第 881 行对 ptrace_may_access() 的调用会成功(因为进程的 uid 和 gid 在第 317-322 行与我们自己的非特权 uid 和 gid 相等,并且因为其 mm 指针在第 341 行是 NULL,因此绕过了第 342 行的 dumpable 标志检查);
- 我们就可以在第 1129 行窃取该进程的任何打开的文件描述符,并像使用自己的文件描述符一样使用它。
## 案例研究:chage
chage 是 shadow-utils 中的一个设置了 set-uid root 或 set-gid shadow 权限的二进制文件,默认安装在大多数 Linux 发行版上。如果我们使用 `-l` 选项执行它,那么在第 776 行它以 O_RDONLY 模式打开 /etc/shadow,在第 778-779 行它将权限降为我们自己的 uid 和 gid:
```c
726 int main (int argc, char **argv)
727 {
...
753 ruid = getuid ();
754 rgid = getgid ();
...
776 open_files (lflg, &flags);
777 /* Drop privileges */
778 if (lflg && ( (setregid (rgid, rgid) != 0)
779 || (setreuid (ruid, ruid) != 0))) {
```
因此,如果我们直接在 778-779 行之后向 chage 进程发送 SIGKILL,并在一个紧密循环中调用 pidfd_getfd(),那么最终我们会赢得 do_exit() 中的竞态条件(在第 964 行和第 971 行之间),绕过 ptrace_may_access() 中的 dumpable 标志检查,并可以窃取 chage 的 /etc/shadow 文件描述符并读取其内容:
```
$ cat /etc/os-release
PRETTY_NAME="Ubuntu 26.04 LTS"
$ id
uid=1001(jane) gid=1001(jane) groups=1001(jane)
$ stat /usr/bin/chage
Access: (2755/-rwxr-sr-x) Uid: ( 0/ root) Gid: ( 42/ shadow)
$ ./exploit-chage
root:*:20563:0:99999:7:::
...
john:$6$zejBXeN4uVNvydnA$hwbwcoT24evWSI4SqM1p8YIInVMtqY2CCE.vfudaG1/mIKayCFraqWIbY0tSIiLFl.8ZrBm86owPU.Xa8HauQ0:20585:0:99999:7:::
sshd:!*:20585::::::
jane:$y$j9T$r575buH7G8C84ZHsJRiee/$yyVfFeh/EMowm9GhXXC6TdgUGftwYpB8Uffa/k7VNE9:20585:0:99999:7:::
```
## 案例研究:ssh-keysign
ssh-keysign 是 OpenSSH 中的一个设置了 set-uid root 权限的二进制文件,默认安装在大多数 Linux 发行版上。尽管 EnableSSHKeysign 在 /etc/ssh/ssh_config 中默认禁用,但它在第 203-205 行打开了主机的私钥文件(/etc/ssh/*_key),并在第 211 行放弃了权限:
```c
176 main(int argc, char **argv)
177 {
...
203 key_fd[i++] = open(_PATH_HOST_ECDSA_KEY_FILE, O_RDONLY);
204 key_fd[i++] = open(_PATH_HOST_ED25519_KEY_FILE, O_RDONLY);
205 key_fd[i++] = open(_PATH_HOST_RSA_KEY_FILE, O_RDONLY);
206
207 if ((pw = getpwuid(getuid())) == NULL)
208 fatal("getpwuid failed");
209 pw = pwcopy(pw);
210
211 permanently_set_uid(pw);
...
224 if (options.enable_ssh_keysign != 1)
225 fatal("ssh-keysign not enabled in %s",
226 _PATH_HOST_CONFIG_FILE);
```
因此,如果我们直接在 211 行之后向 ssh-keysign 发送 SIGKILL,并在一个循环中调用 pidfd_getfd(),那么我们可以窃取 ssh-keysign 的任何 /etc/ssh/*_key 文件描述符并读取其内容:
```
$ cat /etc/os-release
PRETTY_NAME="Ubuntu 26.04 LTS"
$ id
uid=1001(jane) gid=1001(jane) groups=1001(jane)
$ stat /usr/lib/openssh/ssh-keysign
Access: (4755/-rwsr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
$ ./exploit-ssh-keysign 3
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
...
$ ./exploit-ssh-keysign 4
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
...
$ ./exploit-ssh-keysign 5
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
...
```
## 案例研究:pkexec
pkexec 是 polkit 包中的一个设置了 set-uid root 权限的二进制文件,默认安装在大多数 Linux 桌面发行版上。例如在 Debian 上,一个 "allow_active" 用户(实际坐在计算机前的用户)可以通过 pkexec --user 以 root 或其他任何用户身份执行 /usr/libexec/gsd-backlight-helper:
```
$ cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
$ cat /usr/share/polkit-1/actions/org.gnome.settings-daemon.plugins.power.policy
...
<allow_active>yes</allow_active>
...
/usr/libexec/gsd-backlight-helper
...
```
```c
469 main (int argc, char *argv[])
470 {
...
585 opt_user = g_strdup (argv[n]);
...
641 rc = getpwnam_r (opt_user, &pwstruct, pwbuf, sizeof pwbuf, &pw);
...
1024 if (!fdwalk_close_on_exec (3))
...
1086 (void) setregid (pw->pw_gid, pw->pw_gid);
1087 (void) setreuid (pw->pw_uid, pw->pw_uid);
...
1109 if (execv (path, exec_argv) != 0)
```
- 在第 641 行和第 1024 行之间,pkexec 连接到系统 dbus,并以 root 身份验证此连接(使用其 SCM_CREDENTIALS);
- 在第 1024 行,pkexec 将所有大于等于 3 的打开文件描述符(包括连接到系统 dbus 的文件描述符)设置关闭-on-exec 标志(即,它将在第 1109 行被关闭);
- 在第 1086-1087 行,pkexec 完全放弃其权限(降为 --user 选项在第 585 行指定的用户)。
因此:
- 如果我们(攻击者)使用我们自己的用户作为 --user 选项执行 pkexec,并在它放弃权限到我们 uid 和 gid 之后(第 1086-1087 行)立即发送 SIGKILL;
- 然后,如果我们在一个紧密循环中调用 pidfd_getfd(),我们可以窃取 pkexec 到系统 dbus 的连接,该连接已经以 root 身份验证;
- 并通过此连接向 systemd(PID 1)发送请求以启动一个瞬时单元(StartTransientUnit),并以完全 root 权限执行任意命令(例如 ExecStart=/bin/sh -c 'id>>/tmp/pwned')。
乍一看,这似乎