Linux沙箱之chroot与rbash

本篇介绍Linux与安全相关的三个特性,当正确使用他们时,将可能实现安全沙箱功能,特此记录~

chroot

这里涉及到进程的两个目录cwdroot(为与root用户区分,后写作rootDir),前者表示当前的工作目录,后者表示此进程的根目录,正常来讲,在哪个目录执行的程序,cwd就是该目录,而rootDir总是文件系统的那个根目录,而C库提供两种函数分别对其进行更改:

int chdir(const char *path);     //依据目录名改cwd
int fchdir(int fd);              //依据文件描述符改cwd
int chroot(const char *path);    //依据目录名改rootDir

其中对cwd的更改要常见的多也要容易理解的多,而对rootDir的更改应用就比较特殊了,当它指定一个目录作为新的rootDir,由于此目录为根,它是最顶级目录,那么在文件系统中此目录的父目录等在此进程中将会不可见,例如下图,当新的rootDir/var/ftp/则此时执行ls /结果为bin etc pub

此特性可以做很多事,此处关注安全沙箱,它能通过将用户可使用的文件限定在一定范围来实现安全性,这种根目录限制也叫做chroot jail,注意它能做到一定程度上的安全但它并不是一个安全特性,意味着它是容易被突破的(叫越狱?)。使用man -a chroot可以看到chroot的两个定义,一个是可执行程序一个是库函数,他们都能做如名字所述操作-change root directory!另外,这两个工具都只有拥有CAP_SYS_CHROOT权能的用户才能使用它。

chroot命令

此命令能执行指定程序,并以指定的目录作为进程新的根目录:

NAME
chroot - run command or interactive shell with special root directory

SYNOPSIS
chroot [OPTION] NEWROOT [COMMAND [ARG]…]
chroot OPTION

一个例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
test tree                #test文件夹的目录结构
.
|-- bash
|-- busybox
`-- root
`-- heihei.txt

1 directory, 3 files
test ./bash #直接运行bash
I have no name!@VM-49-106-ubuntu:/home/ubuntu/test# ./busybox ls / #在bash下执行ls / 列出根目录下的文件,这里使用busybox是控制变量,在chroot时新root dir可能没有ls命令
bin dev initrd.img lib32 lost+found opt run sys var
boot etc initrd.img.old lib64 media proc sbin tmp vmlinuz
data home lib libx32 mnt root srv usr vmlinuz.old
I have no name!@VM-49-106-ubuntu:/home/ubuntu/test# exit
exit
test chroot . ./bash #以chroot方式运行bash
bash-5.0# ./busybox ls / #在bash下执行ls / 列出根目录下的文件
bash busybox root

以上busybox和bash都是静态编译的,否则必须将依赖的动态链接库也复制到此目录。静态编译bash命令:

1
2
3
wget http://ftp.gnu.org/gnu/bash/bash-5.0-beta.tar.gz
./configure --enable-static-link --without-bash-malloc
make

特别注意--userspec=USER:GROUP选项是极其重要的,一定不要直接一直以root身份运行程序。
同样的,构建’沙盒环境’只需要把允许的命令及其依赖复制到新建的rootDir即可,当然很多工具其实提供选项与chroot配合,使用这些选项将会更加安全与方便

chroot函数

此函数实现同样的功能,文档描述:

NAME
chroot - change root directory
SYNOPSIS
#include <unistd.h>
int chroot(const char *path);

1
2
3
4
5
6
7
8
9
10
11
12
#include <unistd.h>

int main(int argc, char *argv[])
{
chroot(".");
chdir("/");

char *arrays[]={"ash",NULL};
execvp("ash", arrays);

return 0;
}

另外,使用jailkit可以轻松的创建安全的jail环境,安装方法:

1
2
3
4
5
6
wget https://olivier.sessink.nl/jailkit/jailkit-2.20.tar.bz2
tar xf jailkit-2.20.tar.bz2
cd jailkit-2.20
./configure
make
make install

它的各个工具的用法可以看man手册。

逃逸

越狱依据一个很滑稽的原理:当进程中存在文件在当前root目录树外,即在jail外,即表明越狱成功,此时的root就是原来文件系统的root了。

root用户

它一定能逃逸!相对于seccomp无论什么用户都能限制syscallchroot这个系统调用唯一做的事就是更改了进程的rootDir,所以root用户依然能做任何事,现在需要做的事就是构建一种情形:有一个文件在jail外,于是根据Chw00t: Breaking unices’chroot solutions有如下方法:

  1. 在jail rootDir下调用chroot("test")能将jail缩小到子目录test下,内核不会改变CWD,也就是说CWD还在原来的根下,它不属于test及其子目录,此时就已经越狱成功了,CWD为逃犯,让其不停的..向上移动,再调用chroot(.)即可将rootDir恢复为文件系统真实的根。
  2. 打开jail的rootDir得到文件描述符,cd到jail下的子目录再调用chroot("."),使用fchdir改变CWD,此时CWD已经越狱,让其不停的..向上移动,再调用chroot(.)即可将rootDir恢复为文件系统真实的根。
  3. 和2类似,这种方式不手动打开文件而是搜索自己的文件描述符表查看是否已经有被打开并且在在jail外的文件描述符,接着过程同上了。
  4. 创建子进程,子进程打开jail rootDir并且绑定socket等待连接欸,父进程在jail子目录使用chroot再次缩小jail大小,并使用socket连接子进程,接收子进程的fd,此fd在jail外,越狱成功。
  5. 利用proc,将其挂载到jail子目录下,就可以访问到其他进程(包括未在jail里的进程)数据,这些数据含有文件目录信息,也就是存在不在jail里的文件描述符,这可以通过遍历的方式找到,然后切换CWD到那个地方,越狱成功。
  6. 再次挂载所有块设备,将可能挂载到root
  7. chroot只改变rootDir,使用ps命令还是可以看到其他进程的,此时使用ptrace附着其他未被困的进程,向其中注入shellcode,再连接shellcode创建的shell就可以得到rootDir不受限的shell。(非root用户只能attach到子进程,但是子进程会继承父进程的CWD和rootDir故非root无法用此越狱)
  8. 创建子进程,子进程在jail子目录下使用chroot,并且子进程在这个新的jail下再次创建子目录并cd进入它,父进程把这个新目录移出子进程的jail外,逃逸成功。

以上操作可以直接使用大佬编写的程序完成,编译如下:

1
2
wget https://raw.githubusercontent.com/earthquake/chw00t/master/chw00t.c
gcc chw00t.c -static -o chw00t

非root用户

想办法获取root权限:

  1. 本地提权漏洞
  2. 配置错误:如SUID/SGID,/etc/passwd可写等

问题

嗯,现在还没搞懂其中的原理,应该要看内核代码看文件名->inode这个映射的实现细节才能找到原因吧,这就要留在以后了。

rbash

restricted shell,当以-r参数或rbash启动shell时将会以受限的方式启动shell,在这个受限的shell里:

  1. 在执行命令时命令不能带/
  2. 不能改变当前工作目录
  3. 不能更改PATHSHELL变量
  4. 不能使用重定向输出
  5. etc..
1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@VM-49-106-ubuntu:/home/ubuntu/test# ./lal                      # 1
rbash: ./lal: restricted: cannot specify '/' in command names
root@VM-49-106-ubuntu:/home/ubuntu/test# cd # 2
rbash: cd: restricted
root@VM-49-106-ubuntu:/home/ubuntu/test# echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games
root@VM-49-106-ubuntu:/home/ubuntu/test# PATH=$PATA:/root # 3
rbash: PATH: readonly variable
root@VM-49-106-ubuntu:/home/ubuntu/test# echo $SHELL
/usr/bin/zsh
root@VM-49-106-ubuntu:/home/ubuntu/test# SHELL=bash
rbash: SHELL: readonly variable
root@VM-49-106-ubuntu:/home/ubuntu/test# echo "BetaMao" > ga # 4
rbash: ga: restricted: cannot redirect output

可以看到它本身并不能实现沙箱,因为它只限制可执行的命令名格式,完全不限制文件操作,这并不是一个安全特性,是很容易绕过的,例如配置错误时:

  1. 可使用vim,vi等内部可执行命令的程序时
    1
    2
    3
    4
    5
    6
    7
    root@VM-49-106-ubuntu:/home/ubuntu/test# cd                         #受限
    rbash: cd: restricted
    root@VM-49-106-ubuntu:/home/ubuntu/test# vi #打开vi
    :set shell=/usr/bin/zsh #定义
    shell #执行
    test cd #新生成的shell不受限
    ➜ ~
  2. 可使用mv,cp类文件操作命令:
    1
    2
    3
    4
    5
    6
    root@VM-49-106-ubuntu:/home/ubuntu/test# echo $PATH                 #此时的环境变量
    /home/ubuntu/test
    root@VM-49-106-ubuntu:/home/ubuntu/test# cp /bin/bash /home/ubuntu/test #将外部的程序cp到PATH目录
    root@VM-49-106-ubuntu:/home/ubuntu/test# bash #执行
    root@VM-49-106-ubuntu:/home/ubuntu/test# cd #新生成的shell不受限
    root@VM-49-106-ubuntu:~#
    所以,若要单独使用此特性,须采用白名单方式并严格限制可使用的程序!

    UID

    回顾一下passwd这条命令,所有用户都能够使用它更改自己的密码,而密码等信息被存放在shadowpasswd文件下:
    1
    2
    3
    4
    5
    6
    ➜  ~ ls -l /etc/shadow
    ---------- 1 root shadow 830 Oct 3 2017 /etc/shadow #都无
    ➜ ~ ls -l /etc/passwd
    -rw-r--r-- 1 root root 1223 May 13 2018 /etc/passwd #只有root用户可写
    ➜ ~ ls -la /usr/bin/passwd
    -rwsr-xr-x 1 root root 47032 May 17 2017 /usr/bin/passwd #任何用户可执行改密操作,注意属主的执行位不是x而是s
    理论上既然无权限,passwd命令就无法写这两个文件,但事实却是可以写,究其原因,是SUID的存在(可见Linux系统管理#SUID权限),那么它的具体实现呢?其实Linux的原始的访问控制就只有ugo及其的rwx,所以SUID的实现还是从usergroup下手的(明显的,SUID是从user下手,对应的还有SGID,全都一样,下面只以前者说明)。原来,为了解决类似passwd这类命令权限问题,Linux下为进程设置了有三个UID!
  3. Real UID(RUID):进程创建者的UID,正常情况下它一直不会变化,永远表示进程创建者,但root用户是可以更改它的。
  4. Saved UID(SUID):即上面提到的,属主可以为自己的可执行程序设置SUID位,设置后任何人执行程序,程序启动时都将获得程序属主的权限。这实际是SUID和EUID都变为程序属主的UID。
  5. Effective UID(EUID):正如其名为权限检查时实际生效的UID,意味着在判断用户权限时并不检查RUID及SUID,只看EUID。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
#include<unistd.h>

int main(){
int ruid,euid,suid;
if(getresuid(&ruid,&euid,&suid)==0){
printf("RUID:%d\tEUID:%d\tSUID:%d\n",ruid,euid,suid);
}else{

}
return 0;
}

结果如下:

1
2
3
4
5
6
7
8
9
test sudo -u ubuntu ./tuid                     # ubuntu用户500
RUID:500 EUID:500 SUID:500
test sudo -u root ./tuid # root用户0
RUID:0 EUID:0 SUID:0
test chmod u+s tuid # 为程序设置SUID位
test ls -l tuid
-rwsr-xr-x 1 root root 8614 Jan 31 16:27 tuid # 设置完成,即其他用户执行此程序将得到属主root的权限
test sudo -u ubuntu ./tuid # 看到运行时RUID不变,EUID和SUID都变成了0
RUID:500 EUID:0 SUID:0

现在来做权限切换,从设计的角度来看:

  1. RUID==root:三个UID可以任意赋值
  2. RUID!=root:①RUID无法改变 ②EUID可以变为SUID或RUID ③SUID可以变为RUID或EUID

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
#include<unistd.h>

int main(){
int ruid,euid,suid;
getresuid(&ruid,&euid,&suid);
printf("org : RUID:%d\tEUID:%d\tSUID:%d\n",ruid,euid,suid);

setresuid(-1,-1,2); //这样把suid先改掉
setresuid(2,2,2);

getresuid(&ruid,&euid,&suid);
printf("new : RUID:%d\tEUID:%d\tSUID:%d\n",ruid,euid,suid);

return 0;
}

结果:

1
2
3
4
5
6
test sudo -u ubuntu ./tuid
org : RUID:500 EUID:500 SUID:500
new : RUID:500 EUID:500 SUID:500
test sudo -u root ./tuid
org : RUID:0 EUID:0 SUID:0
new : RUID:2 EUID:2 SUID:2
1
2
3
4
5
6
7
8
9
10
11
12
test chmod u+s ./tuid                        #原属主为root,设置SUID
test sudo -u ubuntu ./tuid #可见只要euid为root可随意变
org : RUID:500 EUID:0 SUID:0
new : RUID:2 EUID:2 SUID:2
test chown ubuntu:ubuntu tuid #把属主改为ubuntu后设置SUID
test sudo -u ubuntu chmod u+s tuid
test sudo -u ubuntu ./tuid
org : RUID:500 EUID:500 SUID:500
new : RUID:500 EUID:500 SUID:500
test sudo -u root ./tuid #全都更改失败了
org : RUID:0 EUID:500 SUID:500
new : RUID:0 EUID:500 SUID:500

一个典型的例子就是当程序执行完高权限后使用setresuid进行降权操作并未完全抹除高权限,如下:

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
#include<stdio.h>
#include<unistd.h>

int main(){
int ruid,euid,suid;
getresuid(&ruid,&euid,&suid);
printf("org : RUID:%d\tEUID:%d\tSUID:%d\n",ruid,euid,suid); //原始UID

printf("seteuid(500)\n"); //典型的,只修改EUID等并未全部修改uid
seteuid(500);
getresuid(&ruid,&euid,&suid);
printf("new : RUID:%d\tEUID:%d\tSUID:%d\n",ruid,euid,suid); //修改成功

printf("setresuid(-1,2,-1)\n"); //此时似乎已经降权成功,不能像root一样任意修改三个UID了
setresuid(-1,2,-1);
getresuid(&ruid,&euid,&suid);
printf("new : RUID:%d\tEUID:%d\tSUID:%d\n",ruid,euid,suid);

printf("setresuid(-1,0,-1)\n"); //但是可以先改回root
setresuid(-1,0,-1);
setresuid(233,233,233); //现在又拿回了root权限,可以任意改了
getresuid(&ruid,&euid,&suid);
printf("new : RUID:%d\tEUID:%d\tSUID:%d\n",ruid,euid,suid);

return 0;
}

结果如下:

1
2
3
4
5
6
7
8
test ./tuid
org : RUID:0 EUID:0 SUID:0
seteuid(500)
new : RUID:0 EUID:500 SUID:0
setresuid(-1,2,-1)
new : RUID:0 EUID:500 SUID:0
setresuid(-1,0,-1)
new : RUID:233 EUID:233 SUID:233

参考

https://github.com/earthquake/chw00t/blob/master/Presentations/Balazs_Bucsay_Hacktivity2015_chw00t.pdf
https://www.ibm.com/developerworks/cn/linux/l-cn-chroot/
https://blog.csdn.net/seekkevin/article/details/50041577
https://en.wikipedia.org/wiki/Restricted_shell#Weaknesses_of_a_restricted_shell
http://www.kernel.org/doc/man-pages/ man {setresuid,getresuid}
https://people.eecs.berkeley.edu/~daw/papers/setuid-usenix02.pdf