什么是 chroot

chroot 最早是作为系统调用引入 1979 年的 Unix V7 系统,目的是为了将当前进程及其子进程的 root 目录重定向到某个指定目录。1982 年,chroot 功能被加入到 BSD 中,后经 20 多年,FreeBSD 团队引入虚拟化技术的概念,在原本的 chroot 机制上,开发了新的 jail 机制。

简单来说:一个正在运行的进程经过 chroot 操作后,其根目录将被显式映射为某个指定目录,它将不能够对该指定目录之外的文件进行访问动作。这是一种非常简单的资源隔离化操作,类似于现在 Linux 的 Mount Namespace 功能。当年 Docker 刚开源的时候,有个人就利用 Linux 下 chroot 命令,用 100 多行的 Bash 代码实现了一个模拟版的 Docker

chroot 的使用

在现今的 Linux 上,chroot 既是一个 CLI 工具(chroot(8)),又是一个系统调用(chroot(2))。

使用 chroot(8) 命令

我们可以利用 Linux 下的 chroot(8) 命令来创建出一种类似于进入某个隔离容器内部的效果。

chroot(8) 的用法很简单,格式如下所示:

1
chroot [OPTION] NEWROOT [COMMAND [ARGS]...]

COMMAND 指的是切换 root 目录后需要执行的命令,如果没有指定,默认是 ${SHELL} -i,大部分情况是 /bin/bash。执行 chroot(8) 需要使用 root 权限。

简单地,我们可以这样使用:

1
$ sudo chroot /path/to/new/root /bin/bash

下面就让我们来建造我们的监狱(jail)(备注:基于 Ubuntu 16.04)。

  1. 创建对应的新的根目录

    1
    2
    3
    
     $ export J=$HOME/jail
     $ mkdir -p $J
     $ mkdir -p $J/{bin,lib/x86_64-linux-gnu,lib64,etc,var}
  2. 将几个必要的命令工具 copy 到 bin/

    1
    
     $ sudo cp -vf /bin/{bash,ls} $J/bin
  3. 将步骤 2 中可执行命令中的依赖动态库 copy 到 jail/

    1
    2
    3
    4
    5
    6
    
     # 也许可以整合成一个单独的 shell 命令
     $ list=`ldd /bin/ls | egrep -o '/lib.*\.[0-9]'`
     $ for i in $list; do sudo cp -vf $i $J/$i; done
          
     $ list=`ldd /bin/bash | egrep -o '/lib.*\.[0-9]'`
     $ for i in $list; do sudo cp $i -vf $J/$i; done
  4. 执行 chroot 命令

    1
    
     $ sudo chroot $J /bin/bash

此时可见进入了一个 bash shell 的界面:

1
2
3
4
5
6
7
8
   bash-4.3# ls
   bin  etc  lib  lib64  var
   bash-4.3# cd /
   bash-4.3# ls
   bin  etc  lib  lib64  var
   bash-4.3# cd ..
   bash-4.3# ls
   bin  etc  lib  lib64  var

无论我们如何改变目录,其根目录都被隔离在 $J 中,执行 exit 命令可退出这一环境;

使用 chroot(2) 系统调用

chroot(2) 的原型是:

1
2
3
#include <unistd.h>
  
int chroot(const char *path);

chroot() 将调用进程及其子进程的根目录指定为 path。执行该调用需要使用 root 权限。

如以下代码所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <error.h>
#include <unistd.h>
#include <stdlib.h>
  
char *const path[] = "/home/test/jail"; # 如上文实验所述目录
char *const argv[] = {"/bin/bash", NULL};
  
int
main(void) {
    if (chroot(path) != 0) {
        perror("chroot error");
        exit(1);
    }
    chdir("/");                // 忽略返回值
    execvp("/bin/bash", argv); // 忽略返回值
    return 0;
}

编译和运行代码:

1
2
3
4
5
6
7
8
9
$ gcc test_chroot.c -o test_chroot

# 非 root 用户执行命令
$ ./test_chroot 
chroot error: Operation not permitted

# 使用 sudo 命令,从 shell 提示符的改变可看出 chroot 调用生效
$ sudo ./test_chroot
bash-4.3#

如何获知某个进程是否处于 chroot 监禁

可通过查看进程的 /proc/<pid>/root 来查看对应进程是否处于 chroot 监禁中,如上文,其 chroot 下 bash 的执行进程为 15768,则有:

1
2
$ sudo ls -ld /proc/15768/root
lrwxrwxrwx 1 root root 0 Apr 17 22:47 /proc/15768/root -> /home/test/jail

可见其根目录已经被修改为 /home/test/jail

通过读取 Linux 专有的 /proc/<pid>/root 符号链接的内容,我们可以获取任何进程的根目录

chroot 的应用

ftp 程序就是应用 chroot(2) 的典型实例之一。

当用户匿名登陆 ftp 时,ftp 程序将使用 chroot() 为新进程设置根目录:一个专门预留给匿名登陆用户的目录。这样,chroot() 作为一种安全措施,将用户受困于文件系统中新根目录下的子树中,从而无法访问文件系统的其他路径。

chroot 的安全问题

chroot 机制从一开始就并非安全,存在很多安全漏洞,因此产生了不少「越狱」(jailbreak)手段(这部分内容可参考书籍 《Linux/UNIX 系统编程手册》 上册中第 18 章的相关内容)。比如:

  • 特权级程序可以在随后对 chroot() 的进一步调用中利用种种手段越狱成功

    • 如果特权级(CAP_MKNOD)程序能够利用 mknod() 来创建一个内存设备文件(类似于 /dev/mem),并通过该设备访问 RAM 的内存,那就可借此越狱成功;
  • 即便是无特权程序,也可以有以下几种越狱手段

    • 调用 chroot() 并未改变进程的当前工作目录。因此,通过应在调用 chroot() 之前或者之后再调用一次 chdir() 函数(例如 chroot() 调用之后再执行 chdir("/"))。如果不这么做,那么进程就能够使用相对路径取访问监狱之外的文件和目录;

    • 如果进程一直持有监禁区之外的某一目录的打开文件描述符,那么通过有限次 chdir() 就可以越狱成功,如下代码片段所示:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      
      int main(void) {
          int x;
          int fd_before_chroot;
          mkdir("jail", 0755);
          fd_before_chroot = open(".", O_RDONLY);
          chroot("jail");
          fchdir("fd_before_chroot");   // 利用 fchdir() 调用将工作目录切换
                                        // 到对应的 fd 且该 fd 于监禁前获得
          for (x = 0; x < 1024; x++) {  // 经过有限次的 chdir() 调用
              chdir("..");              // 理论上可到达监禁区外的根目录 
          }
          chroot(".");                  // 将工作目录切换为监禁区外的根目录
          system("/bin/bash");          // 执行监禁区外的根目录下的 shell
      }
    • 利用 UNIX 域套接字来接受另外一个进程指向监禁区外目录的文件描述符

综上,要确保使用 chroot() 相对安全,应做到:

  • 限制监禁区内程序的权限;
  • 执行 chroot() 调用后再调用 chdir() 切换工作目录;
  • 禁止监禁区中的程序持有监禁区外的文件描述符;

参考文档