In the past few weeks I have been asked a number of times about the concept of using chroot as a security feature. The basic idea is that you can run a process inside of a chroot where it will not have access to various system resources; however, chroot is not a security feature. Let’s find out why.
What is a chroot?
The chroot() system call is almost as old as UNIX itself. When you make this system call, you basically change the “root” of your process, where root in this context is the root of the file system. If you have a typical file system layout and you call chroot(“/tmp”), your / (the root of the file system) is now really /tmp/. This comes with a number of potential issues as your process now cannot see any binaries or libraries. It would only see what is in /tmp/ as being the new file system root. To make a chroot useful, you need to hardlink various binaries and libraries into the respective directories, ensure you mount /proc/, and have access to various parts of /dev/.
While the above is not an ideal example, a chroot can be a great feature for testing a distribution or building packages in a clean environment. For example, the mock build tool uses chroots to maintain various different distribution versions for package building. You can even use mock to open a shell in such a chroot, essentially giving you a shell in a different distribution which you can use for testing.
chroot and the root user
The chroot() system call is only available to the root user. A non-root user cannot execute a chroot() call. This is a good thing because if you are able to call chroot(), you can break out of it. This is not a bug in chroot(), it is just how it works. Even if you wanted to consider this a bug, you have to remember that root generally has access to the system and all resources. Nothing would prevent root from just modifying the process memory directly to change the chroot() location back to ‘/’. The important thing to consider here is that, by design, chroot() will not stop root. This point will be important later on in this article.
chroot and non-root users
We know that only root can execute the chroot() system call. If we lock a regular user in a chroot, it is more secure, right? This argument almost works, until you look at the bigger picture. When you take the whole system into consideration, you do not gain any real security from your chroot().
Putting a regular user in a chroot() will prevent them from having access to the rest of the system. This means using a chroot is not less secure, but it is not more secure either. If you have proper permissions configured on your system, you are no safer inside a chroot than relying on system permissions to keep a user in check. Of course you can make the argument that everyone makes mistakes, so running inside a chroot is safer than running outside of one where something is going to be misconfigured. This argument is possibly true, but note that setting up a chroot can be far more complex than configuring a system. Configuration mistakes could lead to the chroot environment being less secure than non-chroot environments.
The takeaway here is not that a chroot is useless, it is that using one does not mean you are suddenly secure. It could make an attacker’s job a little harder but it probably will not stop a smart one.
Attacking the chroot
To recap what we know up to this point: root can break out of a chroot(), this is not a bug, and regular users cannot break out of a chroot(), nor can they execute the chroot() system call to enter a chroot. This tells us that things expecting to use a chroot will need to be setuid (set user ID) root, or run by root and then drop privileges.
A daemon may be running in a chroot, but it may also have a flaw that allows an attacker to execute commands with the privileges of the user running the daemon (an arbitrary command execution attack). If the daemon has dropped privileges and the commands do not execute with root privileges, it may not be possible to break out of the chroot, but the attacker can still use system resources, such as for sending spam, gaining local network access, joining the system to a botnet, and so on.
If an attacker wants to execute a certain application, but that application has not been hard linked into the chroot, it can still be possible for the attacker to execute it: the daemon running in the chroot may have a flaw that allows the attacker to control the execution flow of the daemon. This allows the attacker to execute arbitrary code (for example, a replication of the application they initially wanted to run, but that is not in the chroot).
In the above two examples, proper systems permissions had the same effect as a chroot environment (the attacker could execute arbitrary commands or code in both cases).
Another possible attack is if the attacker can gain root access. These days, most privilege escalation flaws come from kernel bugs. These are exploitable from inside the chroot. setuid executables are generally good at dropping privileges. For a typical attack against a setuid executable to be successful, the executable must be linked into the chroot environment.
It is not hard to consider the chroot() system call a security feature. In theory, it sounds great, but if you really take the time to understand what is going on, it is not really a security feature, it is closer to what we would call a hardening feature. It might slow down an attacker, but in most situations it is not going to stop them. We are dealing with a situation where calling this a security feature is likely more damaging than not because it creates a false sense of security. It is human nature to let our guard down if we believe we are safe. Using chroot is no safer than not using a chroot. You would be far better off investing your resources into a custom SELinux policy and ensuring your system is properly hardened. Good security has no shortcuts.