Linux broken permission and object lifetime handling for ptrace_traceme Vulnerability / Exploit
/
/
/
Exploits / Vulnerability Discovered : 2019-07-17 |
Type : local |
Platform : linux
[+] Code ...
== Summary ==
This bug report describes two issues introduced by commit 64b875f7ac8a ("ptrace:
Capture the ptracer's creds not PT_PTRACE_CAP", introduced in v4.10 but also
stable-backported to older versions). I will send a suggested patch in a minute
("ptrace: Fix ->ptracer_cred handling for PTRACE_TRACEME").
When called for PTRACE_TRACEME, ptrace_link() would obtain an RCU reference
to the parent's objective credentials, then give that pointer to
get_cred(). However, the object lifetime rules for things like struct cred
do not permit unconditionally turning an RCU reference into a stable
reference.
PTRACE_TRACEME records the parent's credentials as if the parent was acting
as the subject, but that's not the case. If a malicious unprivileged child
uses PTRACE_TRACEME and the parent is privileged, and at a later point, the
parent process becomes attacker-controlled (because it drops privileges and
calls execve()), the attacker ends up with control over two processes with
a privileged ptrace relationship, which can be abused to ptrace a suid
binary and obtain root privileges.
== Long bug description ==
While I was trying to refactor the cred_guard_mutex logic, I stumbled over the
following issues:
ptrace relationships can be set up in two ways: Either the tracer attaches to
another process (PTRACE_ATTACH/PTRACE_SEIZE), or the tracee forces its parent to
attach to it (PTRACE_TRACEME).
When a tracee goes through a privilege-gaining execve(), the kernel checks
whether the ptrace relationship is privileged. If it is not, the
privilege-gaining effect of execve is suppressed.
The idea here is that a privileged tracer (e.g. if root runs "strace" on
some process) is allowed to trace through setuid/setcap execution, but an
unprivileged tracer must not be allowed to do that, since it could otherwise
inject arbitrary code into privileged processes.
In the PTRACE_ATTACH/PTRACE_SEIZE case, the tracer's credentials are recorded at
the time it calls PTRACE_ATTACH/PTRACE_SEIZE; later, when the tracee goes
through execve(), it is checked whether the recorded credentials are capable
over the tracee's user namespace.
But in the PTRACE_TRACEME case, the kernel also records _the tracer's_
credentials, even though the tracer is not requesting the operation. There are
two problems with that.
First, there is an object lifetime issue:
ptrace_traceme() -> ptrace_link() grabs __task_cred(new_parent) in an RCU
read-side critical section, then passes the creds to __ptrace_link(), which
calls get_cred() on them. If the parent concurrently switches its creds (e.g.
via setresuid()), the creds' refcount may already be zero, in which case
put_cred_rcu() will already have been scheduled. The kernel usually manages to
panic() before memory corruption occurs here using the following code in
put_cred_rcu(); however, I think memory corruption would also be possible if
this code races exactly the right way.
if (atomic_read(&cred->usage) != 0)
panic("CRED: put_cred_rcu() sees %p with usage %d\n",
cred, atomic_read(&cred->usage));
A simple PoC to trigger this bug:
============================
#define _GNU_SOURCE
#include <unistd.h>
#include <signal.h>
#include <sched.h>
#include <err.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <sys/ptrace.h>
int grandchild_fn(void *dummy) {
if (ptrace(PTRACE_TRACEME, 0, NULL, NULL))
err(1, "traceme");
return 0;
}
int main(void) {
pid_t child = fork();
if (child == -1) err(1, "fork");
/* child */
if (child == 0) {
static char child_stack[0x100000];
prctl(PR_SET_PDEATHSIG, SIGKILL);
while (1) {
if (clone(grandchild_fn, child_stack+sizeof(child_stack), CLONE_FILES|CLONE_FS|CLONE_IO|CLONE_PARENT|CLONE_VM|CLONE_SIGHAND|CLONE_SYSVSEM|CLONE_VFORK, NULL) == -1)
err(1, "clone failed");
}
}
/* parent */
uid_t uid = getuid();
while (1) {
if (setresuid(uid, uid, uid)) err(1, "setresuid");
}
}
============================
The second problem is that, because the PTRACE_TRACEME case grabs the
credentials of a potentially unaware tracer, it can be possible for a normal
user to create and use a ptrace relationship that is marked as privileged even
though no privileged code ever requested or used that ptrace relationship.
This requires the presence of a setuid binary with certain behavior: It has to
drop privileges and then become dumpable again (via prctl() or execve()).
- task A: fork()s a child, task B
- task B: fork()s a child, task C
- task B: execve(/some/special/suid/binary)
- task C: PTRACE_TRACEME (creates privileged ptrace relationship)
- task C: execve(/usr/bin/passwd)
- task B: drop privileges (setresuid(getuid(), getuid(), getuid()))
- task B: become dumpable again (e.g. execve(/some/other/binary))
- task A: PTRACE_ATTACH to task B
- task A: use ptrace to take control of task B
- task B: use ptrace to take control of task C
Polkit's pkexec helper fits this pattern. On a typical desktop system, any
process running under an active local session can invoke some helpers through
pkexec (see configuration in /usr/share/polkit-1/actions, search for <action>s
that specify <allow_active>yes</allow_active> and
<annotate key="org.freedesktop.policykit.exec.path">...</annotate>).
While pkexec is normally used to run programs as root, pkexec actually allows
its caller to specify the user to run a command as with --user, which permits
using pkexec to run a command as the user who executed pkexec. (Which is kinda
weird... why would I want to run pkexec helpers as more than one fixed user?)
I have attached a proof-of-concept that works on Debian 10 running a distro
kernel and the XFCE desktop environment; if you use a different desktop
environment, you may have to add a path to the `helpers` array in the PoC. When
you compile and run it in an active local session, you should get a root shell
within a second.
Proof of Concept:
https://github.com/offensive-security/exploitdb-bin-sploits/raw/master/bin-sploits/47133.zip
Linux broken permission and object lifetime handling for ptrace_traceme