从用户态到内核态逐步解析chroot沙箱机制(上~用户态动作和系统调用)

512 阅读2分钟

关于chroot

chroot在man文档中有两种指代,1是指chroot命令,2是指chroot系统调用,两者指代相近,命令工具是chroot的具体实现,chroot系统调用则是glibc的对其的封装。

命令官方文档的描述为

使用指定root目录执行命令或者交互式shell

由于linux文件系统是以root目录为根,以树状姿态散开,这就决定了我们根下层,无法访问根上层的目录,这样就可以视作一种简单的沙箱机制。当然和docker相比,这种沙箱机制略有些简陋,且chroot出现的比较早,那时候也还没有各种namespace的概念。

我们从简单的使用中观察chroot的一些特征。我们以alpine镜像作为rootfs:

sudo chroot ./alpine /bin/sh

这样之后就会进入alpine,并与之sh进行交互。

echo $$

输出248594,说明进程的确没有隔离。

lsof -p 248594

查看该进程的一些信息,可看到rtd标志值即为alpine镜像所在根目录。

chroot的用户态

chroot命令工具解析

chroot的具体实现有很多,我采用的GUN coreutils中所携带的chroot方案。 核心代码:

if (chroot (newroot) != 0)
    die (EXIT_CANCELED, errno, _("cannot change root directory to %s"),
         quoteaf (newroot));

if (! skip_chdir && chdir ("/"))
    die (EXIT_CANCELED, errno, _("cannot chdir to root directory"));

if (argc == optind + 1)
{
  /* No command.  Run an interactive shell.  */
  char *shell = getenv ("SHELL");
  if (shell == NULL)
    shell = bad_cast ("/bin/sh");
  argv[0] = shell;
  argv[1] = bad_cast ("-i");
  argv[2] = NULL;
}
else
{
  /* The following arguments give the command.  */
  argv += optind + 1;
}
......
/* Execute the given command.  */
  execvp (argv[0], argv);

核心实现逻辑较为简单:

  • chroot(newroot)执行chroot系统调用切换根
  • chdir("/")执行chdir切换到当前目录到"/",进而形成沙箱机制。
  • execvp (argv[0], argv)执行指定命令或默认SHELL

chroot系统调用解析

具体实现里面,chroot(newroot)函数来自<unistd.h>,我本机由glibc实现,glibc对系统调用进行了封装。

glibc对于系统调用的封装比较“隐晦”,我追了良久,最后也是借助一些资料才理清了头绪。

  1. 查看chroot的汇编实现

下载glibc进行编译,make之后,从libc.a中解压出chroot.o,当然从libc.so中也能找到,就是需要首先确定chroot所在地址。不能从本机自带的中去看,本机release版本会摘去很多有用信息。

objdump -S -l chroot.o

chroot.o:     文件格式 elf64-x86-64  
  
  
Disassembly of section .text:  
  
0000000000000000 <chroot>:  
chroot():  
/home/yiran/code/c/glibc-2.36/misc/../sysdeps/unix/syscall-template.S:120  
  
# if SYSCALL_ULONG_ARG_1  
T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS,  
         SYSCALL_ULONG_ARG_1, SYSCALL_ULONG_ARG_2)  
# else  
T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)  
  0:   b8 a1 00 00 00          mov    $0xa1,%eax  
  5:   0f 05                   syscall  
  7:   48 3d 01 f0 ff ff       cmp    $0xfffffffffffff001,%rax  
  d:   73 01                   jae    10 <chroot+0x10>  
/home/yiran/code/c/glibc-2.36/misc/../sysdeps/unix/syscall-template.S:122  
# endif  
       ret  
  f:   c3                      ret  
/home/yiran/code/c/glibc-2.36/misc/../sysdeps/unix/syscall-template.S:123  
T_PSEUDO_END (SYSCALL_SYMBOL)  
 10:   48 8b 0d 00 00 00 00    mov    0x0(%rip),%rcx        # 17 <chroot+0x17>  
 17:   f7 d8                   neg    %eax  
 19:   64 89 01                mov    %eax,%fs:(%rcx)  
 1c:   48 83 c8 ff             or     $0xffffffffffffffff,%rax  
 20:   c3                      ret

这里注意要用-S而不是-d,我之前在这里用-d,舍去了一些宏定义,就一直没想明白,syscall之前怎么没有传参数的动作,其实都在宏里面。

  1. 追PSEUDO

T_PSEUDO-><sysdep.h>(PSEUDO)

# if SYSCALL_ULONG_ARG_1
#  define PSEUDO(name, syscall_name, args, ulong_arg_1, ulong_arg_2) \
  .text;							      \
  ENTRY (name)							      \
    DO_CALL (syscall_name, args, ulong_arg_1, ulong_arg_2);	      \
    cmpq $-4095, %rax;						      \
    jae SYSCALL_ERROR_LABEL
# else
#  define PSEUDO(name, syscall_name, args) \
  .text;							      \
  ENTRY (name)							      \
    DO_CALL (syscall_name, args, 0, 0);				      \
    cmpq $-4095, %rax;						      \
    jae SYSCALL_ERROR_LABEL
# endif

  1. 追ENTRY
# define ENTRY(name)                                            \  
       .align 4                                ASM_LINE_SEP    \  
       .globl C_SYMBOL_NAME(name)              ASM_LINE_SEP    \  
       .type C_SYMBOL_NAME(name),%function     ASM_LINE_SEP    \  
       C_LABEL(name)                           ASM_LINE_SEP    \  
       cfi_startproc                           ASM_LINE_SEP    \  
       CALL_MCOUNT

......
# define C_LABEL(name)      name##
# define cfi_startproc                      .cfi_startproc

从.cfi_startproc关键字可以看出ENTRY是定义了一个名为name的函数来供调用。

  1. 追DO_CALL
# define DO_CALL(syscall_name, args, ulong_arg_1, ulong_arg_2) \
    DOARGS_##args				\
    ZERO_EXTEND_##ulong_arg_1			\
    ZERO_EXTEND_##ulong_arg_2			\
    movl $SYS_ify (syscall_name), %eax;		\
    syscall;

看到syscall基本就能断定已经找对地方了,在syscall之前进行了传参,并传递了系统调用号。至此,用户态这边的行为到此为止,接下来是内核进行工作了。