本文要点
chroot把进程看到的根目录/换成另一个目录:进程从此「看不见」主机文件系统,只能看到沙盒里放进去的东西——实测进入沙盒后ls /只剩自己放进去的bin,ls /root直接报No such file or directory- 零依赖搭沙盒:一个静态编译的
busybox(实测 2.1MB 单文件,本机系统自带;没有的话apt-get install busybox-static即可)复制进目录就是完整用户态 - 沙盒默认没有
/proc、/etc、任何主机文件,ps会报can't open '/proc';需要时可用unshare -m挂载私有proc - chroot 只隔离文件系统视图,不隔离网络:实测在沙盒里起
busybox httpd,宿主curl 127.0.0.1:8080正常返回内容 - chroot 不是安全边界:root 用户可以在进入沙盒前保存真实根目录的文件描述符,之后借
fchdir逃出——本文实测三行 Python 完成逃逸 - 现代容器 = 命名空间(
unshare)+ 换根目录 + cgroup 资源限制:实测unshare -u改主机名只影响命名空间内
Linux chroot 沙盒隔离:从根目录切换到轻量环境构建
假设你想在一台干净服务器上跑某个来路不明的程序,又不想让它碰到 /home、/etc 里的任何东西;或者你想测试一个软件在「空系统」里能不能跑——不需要装虚拟机,也不需要 Docker 镜像,Linux 自带一个最朴素的隔离原语:chroot。
chroot 的作用只有一句话:把当前进程(及其子进程)看到的根目录 /,换成你指定的目录。换完以后,进程访问 /etc/passwd、/root、/home,实际上访问的都是新根目录里对应路径——主机上那些目录它一概看不见。本文在一台 Ubuntu 22.04 云服务器(KVM,内核 5.15.0-30-generic)上用静态 busybox 从零构建一个轻量沙盒,实测文件隔离、网络行为,最后做一个「逃逸」实验——顺便说清楚 chroot 为什么不是安全边界。全部命令真实执行,截图即真实输出。
原料:一个静态 busybox 就够
沙盒里要有可执行文件,最简单的是放一个静态编译的 busybox——单文件集成 sh、ls、cp、httpd 等几百个常用命令,不依赖任何动态库,复制进沙盒即可运行。先确认本机这个二进制:
ls -lh /usr/bin/busybox && busybox 2>&1 | head -1-rwxr-xr-x 1 root root 2.1M Feb 4 2022 /usr/bin/busybox
BusyBox v1.30.1 (Ubuntu 1:1.30.1-7ubuntu3) multi-call binary.静态与否决定它能不能脱离宿主运行环境:ldd /usr/bin/busybox 输出「not a dynamic executable」即为静态。如果你的发行版没有 busybox,一条命令补上(约 1MB;本文演示机系统自带 busybox,此步未执行):
apt-get install -y busybox-static构建沙盒并进入
沙盒目录就放一个 bin/busybox,完事。下面这条命令为了演示可重复,链首先用 rm -rf 重置沙盒目录,然后 mkdir + cp 构建,最后 chroot 进入并环顾四周:
rm -rf /opt/jail && mkdir -p /opt/jail/bin && cp /usr/bin/busybox /opt/jail/bin/ && chroot /opt/jail /bin/busybox sh -c 'echo "== in jail =="; pwd; ls -la /; ls /root 2>&1'
输出值得逐行看:
pwd显示/——进程的根目录已经被换成/opt/jail,所以它在沙盒里看到的一切都以这里为根;ls -la /只有.、..和bin——主机上几十个目录一概不存在,因为沙盒的根目录里只放了我们拷贝进去的 busybox;ls /root报No such file or directory——主机的/root、/etc、/home、/proc在沙盒视角里全都「不存在」。
这就是 chroot 的全部魔法:不是把目录藏起来,而是换了一个根,进程从根上就够不着外面的东西。代价是沙盒里缺的东西全得自己放——比如想看进程列表,得先有 /proc,直接跑 ps 会报错:
chroot /opt/jail /bin/busybox psPID USER COMMAND
ps: can't open '/proc': No such file or directory在沙盒里跑服务:文件隔离,网络不隔离
chroot 只管文件系统视图,管不着网络。沙盒里的进程照样能监听端口、收发网络包。这一点用 busybox 自带的迷你 Web 服务器 httpd 实测最直观:先在沙盒里放一个首页文件,再从沙盒里启动 httpd,然后在宿主上用 curl 访问:
mkdir -p /opt/jail/www; printf 'hello from chroot jail\n' > /opt/jail/www/index.html; chroot /opt/jail /bin/busybox httpd -f -p 127.0.0.1:8080 -h /www >/dev/null 2>&1 & sleep 1; curl -s http://127.0.0.1:8080/; echo; kill $!; echo httpd-stopped
说明几点:
-h /www是沙盒内的相对路径——httpd 在 chroot 之后启动,它的工作目录已经在沙盒根里,/www指向沙盒里的index.html;- httpd 绑定
127.0.0.1:8080,宿主curl一次成功,证明进程虽然被关了「文件系统禁闭」,网络栈仍是宿主同一个; - 这也给出一个实用场景:把不信任的静态站点、旧版程序丢进沙盒跑,文件访问面被锁死,网络功能照常可用(沙盒服务若只服务本机,记得绑
127.0.0.1而不是0.0.0.0)。
逃逸实验:chroot 不是安全边界
chroot 这么好用,能不能拿它当「沙箱」关犯人?答案是否定的,原因很本质:chroot 只改变了路径解析的起点,进程手里的文件描述符(fd)不受影响。root(准确说持有 CAP_SYS_CHROOT)的进程可以在进入沙盒前先打开真实根目录,事后用 fd 把当前目录「拽」回去——经典逃逸手法,man 手册里白纸黑字写着(chroot(2) NOTES)。
把逃逸脚本写到沙盒外(放沙盒目录里会被 rm -rf 一起清掉,放 /opt/escape.py):
cat > /opt/escape.py <<'PY'
import os
fd = os.open("/", os.O_RDONLY) # 进入沙盒前, 先抓住真实根目录的句柄
os.chroot("/opt/jail") # 切换根目录, 进入沙盒
os.fchdir(fd) # 用句柄把当前目录拉回真实根目录
os.chdir("..")
print("真实根目录内容(逃出后):", sorted(os.listdir("."))[:6])
print("沙盒视图内 /etc/hostname 存在:", os.path.exists("/etc/hostname"))
print("真实视图内 etc/hostname 存在:", os.path.exists("etc/hostname"))
PY执行它:
python3 /opt/escape.py
三行输出把逃逸过程讲得明明白白:
- 逃出后
os.listdir(".")看到的又是真实根目录(bin boot dev etc home lib……); - 用绝对路径
/etc/hostname访问——还是被沙盒拦着(False),因为绝对路径仍从沙盒根开始解析; - 用相对路径
etc/hostname(从被拽回真实根目录的 cwd 出发)——真实文件系统触手可及(True)。
注意这个脚本是 root 跑的:非 root 用户连 chroot 本身都调不动(缺 CAP_SYS_CHROOT)。但反过来也说明:凡是有 root 的地方,chroot 都拦不住——历史上真实世界用 chroot 关押「叛逃」进程被越狱的案例一抓一大把。所以 chroot 的定位是环境隔离(测试、救援、依赖打包),不是安全隔离(防恶意进程)。要安全,得靠下一节的能力组合。
从 chroot 到容器:命名空间补齐隔离
容器技术本质上就是补 chroot 缺的那几块:换根目录 + 一堆命名空间 + cgroup 资源限制。命名空间由 unshare/clone 创建,每一种命名空间隔离一类全局资源。实测最直观的 UTS 命名空间——隔离主机名:
hostname && unshare -u sh -c 'hostname jail-demo && echo "in-ns: $(hostname)"' && echo "out-ns: $(hostname)"MFY001832765701
in-ns: jail-demo
out-ns: MFY001832765701沙盒里把主机名改成 jail-demo,命名空间一退出,宿主主机名纹丝不动。类似地,挂载命名空间(unshare -m)可以给沙盒单独挂一个干净的 /proc——这正是上节 ps 报错的标准解法:
unshare -m sh -c 'mkdir -p /opt/jail/proc && mount -t proc proc /opt/jail/proc && echo MNT-PROC-OK && umount /opt/jail/proc'MNT-PROC-OK加上 cgroup 限 CPU/内存(本站 cgroup 资源控制 一文有完整实测),「文件隔离 + 资源隔离 + 系统视图隔离」齐了,这就是 Docker 容器 在底层干的事(网络命名空间的玩法另见本站 网络命名空间)。所以想深入容器原理,chroot + unshare 是最好的起点——它们全是内核原语,不依赖任何镜像层。
清理
演示产物只有两个目录/文件,确认后删掉即可:
rm -rf /opt/jail /opt/escape.py收个尾:chroot 的价值在于它把「隔离」这个概念拆到了最小可理解单元——一个系统调用,一个目录,进程的世界就变了。日常里它最常见的两个用途:一是救援(系统起不来时 chroot 进挂载好的磁盘修引导、改配置),二是给软件一个干净的家(构建环境、测试环境、老软件运行环境)。记住它的边界——能隔离文件视图,隔离不了网络,更拦不住 root——你就不会在错误的地方依赖它了。
评论 (0)
暂无评论,快来抢沙发吧!