Using gdb (easy)
略
Sandbox a command(moderate)
1. 任务全景:系统调用是如何被“沙盒化”的?
在 xv6 中,所有的系统调用最终都会流经 kernel/syscall.c 中的 syscall() 函数。我们要做的就是在执行真正的功能函数之前,插一个“安检口”。
执行流程图:
用户程序调用 interpose(mask, "-") -> 触发 ecall -> 进入内核 syscall() -> 执行 sys_interpose 保存掩码 -> 之后该进程再次执行其他系统调用 -> syscall() 检查掩码 -> 若命中则拒绝。
2. 详细实现步骤
第一阶段:打通用户态与内核态的“隧道”
你需要让内核识别 interpose 这个名字和对应的编号。
- 定义编号:在
kernel/syscall.h中添加#define SYS_interpose 22。 - 用户态声明:在
user/user.h中添加int interpose(int, char*);,让 C 编译器知道这个函数的存在。 - 生成存根:在
user/usys.pl末尾添加entry("interpose");。这样make时会自动生成汇编代码,负责将SYS_interpose放入a7寄存器并执行ecall。 - Makefile 配置:在
UPROGS中添加$U/_sandbox\,确保sandbox.c会被编译进磁盘镜像。
第二阶段:在内核中建立“记忆”
内核需要为每个进程单独存储一份“禁止名单”。
- 修改 PCB:在
kernel/proc.h的struct proc结构体中添加int interpose_mask;。 - 实现功能函数:在
kernel/sysproc.c中编写sys_interpose。
- 使用
argint(0, &mask)获取第一个参数。 - 使用
argstr(1, path, sizeof(path))获取第二个参数并进行错误检查。 - 将
mask写入当前进程的p->interpose_mask。
- 内核映射:在
kernel/syscall.c中,使用extern声明sys_interpose,并在syscalls[]数组中建立映射。
第三阶段:权限拦截与继承
这是沙盒真正起作用的地方。
- 拦截逻辑:修改
kernel/syscall.c的syscall()函数。
- 在
num = p->trapframe->a7;之后,检查(p->interpose_mask & (1 << num))。 - 如果为真,说明该调用被禁止。将
p->trapframe->a0(返回值寄存器)设为-1,然后直接return,不再执行后续逻辑。
- 实现继承:修改
kernel/proc.c中的kfork()。
- 在父进程创建子进程
np后,执行np->interpose_mask = p->interpose_mask;。 - 面试点:为什么要拷贝?因为像
cat或ls这样的命令通常是由sandbox程序通过fork出来的子进程运行的,如果不拷贝,沙盒就失效了。
3. Q&A
-
位掩码 (Bitmask) 的优势:
-
为什么用
int就能存下所有禁止项?(因为目前系统调用号只有 20 多个,1 个 bit 代表 1 个调用,非常节省内核内存)。 -
内核如何访问用户态数据:
-
argstr内部是如何工作的?(它会查找进程的页表p->pagetable,将用户空间的虚拟地址翻译成内核可以访问的物理地址,然后拷贝数据)。 -
安全性边界:
-
如果用户程序尝试自己调用
interpose(0, "-")来解除禁令怎么办?(在这个实验中我们允许了,但在真正的沙盒如 Linux Seccomp 中,通常设置为“只增不减”的模式,一旦开启禁止就无法撤销)。
Sandbox with allowed pathnames(easy)
1. 核心区别:从“行为控制”到“资源控制”
-
上一个任务(基础沙箱):
-
逻辑:只要系统调用号在掩码中,就直接拒绝。
-
效果:如果禁用了
open,进程就无法打开任何文件。这通常会导致程序无法运行(例如grep无法读取输入文件)。 -
当前任务(路径名沙箱):
-
逻辑:即使系统调用在掩码中被禁用,内核也会额外检查:该调用是否在尝试访问一个“特许路径”。
-
效果:你可以禁用
open,但同时指定“允许访问README”。此时,进程打开README能成功,但打开其他任何文件都会失败。
2. 开发要点(核心修改逻辑)
要在代码中实现这种区别,需要关注以下四个环节:
A. 状态存储的扩展 (Storage)
- 要点:内核不仅要记住一个
int掩码,还要记住一个路径名字符串。 - 实现:在
kernel/proc.h的struct proc中增加char allowed_path[MAXPATH]。 - 注意:必须使用
MAXPATH来限制大小,防止内核内存被恶意的大字符串撑破。
B. 数据拷贝的安全性 (Data Copying)
- 要点:在
sys_interpose中,你需要使用argstr将用户空间的路径名拷贝到内核的allowed_path中。 - 面试点:为什么要拷贝?因为用户空间的内存是不可信的。如果内核只存一个指针,用户程序可以在检查完路径后偷偷修改内存内容(即 TOCTOU 攻击)。
C. 子进程的完整继承 (Inheritance)
- 要点:在
kernel/proc.c的kfork()中,必须确保safestrcpy(np->allowed_path, p->allowed_path, MAXPATH)。 - 原因:因为
sandbox命令通常是先设置权限,然后exec运行目标程序。如果子进程不继承这个字符串,沙盒在目标程序运行时就失效了。
D. 特殊系统调用的拦截处理 (Special Casing)
- 要点:在
kernel/syscall.c的syscall()函数中,你需要识别SYS_open和SYS_exec。 - 步骤:
- 检查掩码是否命中。
- 如果命中,且是
open或exec,则提取该调用的第一个参数(路径名)。 - 使用
strncmp比较提取出的路径与p->allowed_path。 - 如果匹配,放行(By-pass);如果不匹配,拒绝。