Hello, and welcome back to CS631 "Advanced Programming in the UNIX Environment". In this video, we'll continue where we left off the last time in our efforts to consider the various ways in which we can restrict a process. In the last video, we saw ways to lock down files using file flags, file systems using mount options, and preventing even root from undoing such changes by raising the securelevel. Today, we'll return to a more process-focused view, although you'll find that we can make use of what we've learned so far and combine many of the methods for increased benefits and security. Anyway, so previously, we saw how we can restrict what commands a user can execute via file permissions, for example, but it gets quite difficult to really restrict a regular, interactive Unix account with full access to the normal commands. --- So in addition to tightening the user- or group permissions or granting ACLs, we can further lock down just what commands are available to a user through the use of a restricted shell. A restricted shell is, generally speaking, any shell that limits the user's ability to execute commands. Having taken a look at how a shell is implemented, we should already have an idea of how this might be accomplished. Just as with a regular shell, there are different variations of the concept of a restricted shell, and many of the popular shells such as bash or the korn shell, support this mode. When running in restricted mode, these shells prohibit, amongst other things: - changing the current working directory; that is, you can't run "cd" - changing the ENV, PATH, and SHELL environment variables - specifying commands containing a '/'. This has the intended effect of only allowing commands that are found in the default directories in the PATH. - prohibit redirecting output into files With these restriction, you can reasonably control any of the commands the user could invoke by only providing the executables you want them to run into a specific location and setting the PATH prior to invoking the restricted shell accordingly. However, as before when we looked at the configuration of sudo(8), it is up to the administrator to know and understand the commands they let the user execute in a restricted environment. - Many commands can be used to shell out, to run other programs or to be used to overcome some or all of the restrictions of the restricted shell. Here, let's take a look at an example. --- The korn shell offers a restricted mode that is enabled if the shell is invoked as, for example, _r_ksh, and the protections we just mentioned are then enforced. Let's create such a restricted korn shell... ...and change the login shell of user 'fred' to use this shell. If we now log in as 'fred', we can run the usual commands. Let's look for the setuid shell we left here from the last video. There it is. Ah, but now we can't run it, because it is not located in our PATH. We can try to be clever and use a relative path, but that doesn't work, either. We can't even change our directory! Let's try to set our PATH to include /tmp, then. Blast - foiled again! Let's try to invoke an unrestricted shell instead! Oh, right, that also doesn't work. But... wait... 'sh' is in our PATH. Can we just run that... like that? Oh, look at that, that works! And yes, now we're in an entirely unrestricted shell again, so we can change directories and invoke any command we like. OK·, so we've seen that a restricted shell seems useful initially, but if we can so trivially break out of it - well, that's not quite so useful. So let's see if we can restrict the restricted shell a bit more. Let's be more careful about which commands we allow in the restricted shell. Let's create a new directory... ...and then place in it all the usual binaries from /bin, but _without_ any shells. There. Now let's force fred's PATH to point to only this directory, meaning with the restricted shell, fred will only be able to invoke these commands. As we saw in the manual page, the restricted shell reads all the commands from the usual profile. So now if we become 'fred' again, we immediately notice that even at shell startup certain commands are not found as they are not in the directory we prepared. Our PATH is now /usr/local/rbin, and of course we can't "cd", and we can't redirect output into any files. But let's see what commands we do have access to... Oh, look, dd(1). But with dd(1) we can effectively redirect output. So this is an example of accidentally allowing the user to circumvent some of the restrictions of the restricted shell. But we also still have write access to our .profile, and while there's no vi(1), we do have the standard unix editor, ed(1), so we can simply edit this file and remove the last line, so that now... if we log out and log back in... we are still in a restricted shell, but now our PATH is no longer pointing to /usr/local/rbin, which of course means we can again invoke an unrestricted shell and gain access to our executable. So this, then, illustrates that we have to be very careful about what make available in the PATH of the restricted shell, as well as that it can be a bit tricky to get this just right. --- In general, the approach for using a restricted shell is similar to what we started out with. - We create a new directory that will be the new PATH - We then have to be _very_ careful about what commands we want to allow, and we then link them into place. - We need to make sure none of the commands can be used to break out of the shell, to circumvent restrictions, or to even allow execution of another shell. - We then set the PATH... - ...and prevent the user from making changes to their startup files, for example using the file flags we discussed in our previous video. - And... then we hope we didn't miss anything. As we cover the topic of restricting processes, you'll notice that this is a pattern: getting it right is really quite difficult, and requires a lot of attention to detail and a deep understanding of how the system works. But ok, so with a restricted shell, we can restrict a user's ability to execute commands or even maneuver around the file system. But at the same time, you may have a need to allow the user to change directories, to perform I/O on local files, or to more generally interact with parts of the filesystem without making available the entire filesystem. To overcome this problem, it'd be useful to expose a restricted copy or view of the entire filesystem to the process: similar to how you might populate a custom PATH for a restricted shell, you could construct a filesystem containing the necessary files and restrict the user to only operate within the confines of this changed root. --- Enter the chroot(2) system call, added to Unix in 1979. The directory name you pass to the syscall becomes the root directory of the process, meaning that all pathnames will be resolved under this new directory. Now if we resolve _all_ pathnames under this directory, then you need to make sure that all libraries and executables are present in this directory, but once you have set that up, you can restrict a process that needs to run with superuser privileges. That is, even if your service requires root privileges and is compromised, the attacker finds themselves in this chroot, unable to access any resources outside of the chroot. --- Let's see what a chroot look like in practice: Here we have a script that creates a minimal chroot. As mentioned a second ago, you need to have _everything_ in the chroot that you need to run whatever commands you want to allow. So for dynamically linked executables, that includes the run-time link editor and all shared libraries our executables might need -- which should look familiar to you from our Week 11 videos. This script does just that: it determines which libraries are needed and copies them into the chroot. When it's done, we have a chroot in which you can run exactly and only the commands sh(1), ps(1), and id(1). [pause/continue] Ok, so let's create a new chroot and look at the files in there. We see the link loader, the libraries, and the executables. So now we can enter the chroot using this command. We specify an interactive shell as the command to run. And there we are, inside the chroot. Where are we? / - no surprise. whoami Oh, hmm, right, we didn't copy the 'whoami' program into the chroot, so we can't run that command. And we're also missing 'ls'. But here's a little trick: In case you ever break ls(1), the shell's builtin 'echo' command can be used in a pinch instead. We let the shell glob the wildcard and thus get ls(1) functionality to some degree. And we can confirm that we can only run three programs here, id(1), ps(1), and sh(1). Ok, so who are we? id(1) confirms that we're root. Note that the output of the id(1) command gives us numeric user IDs only; the file needed to translate UIDs to usernames -- /etc/passwd -- does not exist inside the chroot. But our filesystem is locked to the directory populated for us, in /var/chroot/apue. But we can't change out of that directory. So we really are restricted to this changed root with little to do besides whatever commands have been copied into the chroot. From moving around the filesystem, you wouldn't even know that you are inside a chroot! But here's one thing that gives away that you are plugged into the matrix: when you run ps(1), you can observe processes that are running on the system but that are not within the chroot. --- Ok, so a chroot(2) allows us to expose a restricted copy or view of the filesystem to a process. This lets you - restrict a process's view of the filesystem hierarchy - restrict which commands can be run by only providing the needed executables - but for this to work, you must provide full environment, shared libraries, config files, etc. Again, this is a bit of work, and you can easily make mistakes here, so be careful to only provide what you really need. - If you wish to make available data from other filesystems without having to copy it and in fact while actually _sharing_ it live, you'd combine your chroot with null mounts and then use certain mount options as we had discussed in our last video to protect that view of the filesystem. There are two important aspects, though, that can conceivably lead to an escape from a chroot: - one, a process, when entering a chroot, may still have open file descriptors that it opened outside of the chroot. That can be useful, but may also lead to a risk of a chroot break out, as it means that now the chrooted process _can_ access a resource outside the chroot. - two, as we saw a minute ago, even inside the chroot you may be able to view information about other processes. That is, you're still clearly sharing process space with the processes outside your chroot. It'd be quite useful to be able to restrict a process further, to not even let it see that other processes exist on the system. For that... --- FreeBSD added the jail(2) system call and jail(8) utility around 2000. A jail restricts the process with respect to the other resources on the system such that from within a jail, it's almost impossible to notice that you are not running on a real system. You don't get to see other processes, system accounts or uids, and of course you get your own chroot as well. In addition, a jail may be bound to a particular IP address, and network functionality is then also restricted to this address only. In addition to the chroot restrictions we just saw, Jails... - enforce a per-jail process view - prohibit changing sysctls or securelevels - prohibit mounting and unmounting filesystems - can be bound to a specific network address - prohibit modifying the network configuration, and - disable raw sockets In this fashion, a jail effectively implements a process sandbox, or virtual environment. You can even create jails for different OS versions of the parent OS, so long as your parent kernel is capable of running or emulating the environment. This is particularly useful if you are creating specific build environments for different OS versions. If you're thinking "Wait, why don't we just use docker or something like that", then I'll remind you that this is the year 2000, and Docker didn't exist for another 13 years. But you're on the right track: jails were indeed one of the first approaches of OS-level virtualization. --- Ok, let's summarize what we've covered in today's video: - First, we talked about restricted shells, which allow you to severely restrict a user, but these restrictions are entirely enforced within the shell, not within the OS. - Then we looked at the chroot(2) system call to create a changed root directory, a much more powerful way of restricting processes and allowing even processes with eUID 0 to not look outside of the environment to which we've confined them. - This approach to containing processes has been around since the 80s, but is no longer part of POSIX. Still, it is a common way of locking even standard system daemons to minimize the attack surface they otherwise expose. - Entering a chroot requires root privileges, so is a mechanism primarily for a root process to lower its own privileges irrevocably, a concept we've seen in the past and will see again in our next video. - But with the ability to carry file descriptors into the chroot, there is the possibility of breaking out of the chroot. On the course website, you will find a code example of an old exploit to break out of a chroot -- this doesn't work any longer on NetBSD, but it's still worth your time to read through it and give it a try, perhaps on another platform to see how it works. - We also noted that from within a chroot we still can see the processes outside, and so, to find a solution to that problem, we - then mentioned FreeBSD jails as one of the first real versions of OS-level virtualization. This solution was followed in 2005 or so by Solaris "containers", which built on top of Jails and ZFS Capabilties and Zones, and which paved the way for true containers. We'll look at additional features in modern Unix systems that allow us to build containers and understand how they are implemented in our next videos. Thanks for watching - cheers!