Lab: system calls
在这个lab当中6.1810 / Fall 2025 它要求你在xv6当中添加一个新的系统调用以此来帮助你理解在操作系统当中系统调用的底层实现逻辑和调用链条 之后该lab当中会告诉你一个故意留下来的系统漏洞要求你利用该漏洞获取之前的进程已经被清理的进程的私有数据通过此lab你可以学到操作系统是如何隔离每个进程的同时也会告诉你在回收进程的资源时如果处理不当会导致原本应该被清理的进程它的私有数据可能会被其他进程窃取从而打破了操作系统的进程隔离机制。1.Using gdb 这一部分涉及gdb的调试所以我们暂时跳过更多GDB的调试技巧可以去网上搜索一下这里就不再阐述了。2.Sandbox a command(中等难度) 在这一小节当中我们需要给xv6操作系统引入一种进程级系统调用限制机制sandbox。具体而言允许用户进程通过一个新的系统调用interpose(mask, path)为当前进程及其子进程设置一组“被禁止的系统调用”使得后续执行中一旦触发这些系统调用就会被内核拒绝。官网当中告诉我们interpose接收两个参数一个是是屏蔽掩码mask另一个是路径path当前用不到。来自官网的提示个人解析版在 Makefile 中向 UPROGS 添加$U/_sandbox以保证编译器会编译该源文件。由于interpose没有任何声明和实现所以要在user/user.h中添加一个interpose原型不要遗漏参数 。在user/usys.pl当中增加一个新的项该文件是用户态系统调用接口的生成脚本它会帮助生成一个汇编文件user/usys.S该文件中指定了每一个系统调用的参数陷入指令和返回指令。因为interpose是一个新的系统调用所以我们要在kernel/syscall.h当中添加一个新的系统调用码用于之后syscall函数的使用。因为要添加一个新的系统调用所以我们要严格按照xv6关于系统调用函数声明的规范进行命名我们可以参考xv6当中已有的函数声明所以我们在kernel/sysproc.c当中实现一个名为sys_interpose(void)的函数它就是最终的调用实现。按照官网的要求我们的屏蔽掩码需要父进程传递给子进程或者说是子进程继承了父进程的屏蔽掩码所以这就代表了这个屏蔽掩码需要被持久存储于进程中于是我们需要在进程的结构体当中添加一个字段用于记录屏蔽掩码同时因为子进程是父进程通过调用fork创造出来的所以一定存在一个函数用于将父进程当中某些状态/属性纹丝不动地赋值给子进程当中对应的字段因此根据官网的提示我们可以在kernel/proc.c当中找到一个名为kfork的函数这里就是父子进行状态/属性继承的地方我们需要在这里修改一下使得其可以将父进程新添加的“屏蔽掩码”字段同样赋值给子进程。因为每个系统调用都是一个函数指针所以在kernel/syscall.c当中有一个数组syscalls里面存放的是每一个系统调用的入口地址我们需要在该数组当中添加一项新的数据同时需要在此文件中添加sys_interpose的声明可以参考已有的xv6代码照葫芦画瓢。因为我们要实现的是系统调用的屏蔽机制所以在xv6当中任何系统调用最终都会通过内核态函数syscall进行调用号的识别和分发调用所以我们可以在此函数当中添加某些判断逻辑通过将当前请求系统调用的进程当中的屏蔽掩码与当前进程请求的系统调用的调用码向比对来得到是否要屏蔽该系统调用。以下是代码相关内容##user/user.h中新增的内容用户态函数声明 int interpose(int,char *path); ##user/usys.pl中新增的内容 entry(interpose); ##kernel/syscall.h中新增的内容系统调用号 #define SYS_interpose 22 //interpose的系统调用码 ##kernel/proc.c/kfork函数体内中新增的内容子进程继承父进程的mask ... ... /*修改点父进程的状态mask传递给子进程 * 父进程的mask已经被修改此时若创建新 * 的子进程则mask也要一并传递。 */ np-mask p-mask; ... ... ##kernel/syscall.c中新增/修改的内容系统调用声明添加新的的项到函数指针数组修改syscall函数 extern uint64 sys_interpose(void); //新添加的系统调用声明 // An array mapping syscall numbers from syscall.h // to the function that handles the system call. static uint64 (*syscalls[])(void) { [SYS_fork] sys_fork, [SYS_exit] sys_exit, [SYS_wait] sys_wait, [SYS_pipe] sys_pipe, [SYS_read] sys_read, [SYS_kill] sys_kill, [SYS_exec] sys_exec, [SYS_fstat] sys_fstat, [SYS_chdir] sys_chdir, [SYS_dup] sys_dup, [SYS_getpid] sys_getpid, [SYS_sbrk] sys_sbrk, [SYS_pause] sys_pause, [SYS_uptime] sys_uptime, [SYS_open] sys_open, [SYS_write] sys_write, [SYS_mknod] sys_mknod, [SYS_unlink] sys_unlink, [SYS_link] sys_link, [SYS_mkdir] sys_mkdir, [SYS_close] sys_close, [SYS_interpose] sys_interpose, //新添加的系统调用 }; //这里是syscall函数修改后的样子 void syscall(void) { int num; struct proc *p myproc(); // 取出在a7中存放的调用号 num p-trapframe-a7; if(num 0 num NELEM(syscalls) syscalls[num]) { //将调用屏蔽掩码和系统调用码进行相与的操作判断当前调用是否被屏蔽/禁止 if(p-mask (1 num) ){ p-trapframe-a0 -1; return; } // Use num to lookup the system call function for num, call it, // and store its return value in p-trapframe-a0 p-trapframe-a0 syscalls[num](); } else { printf(%d %s: unknown sys call %d\n, p-pid, p-name, num); p-trapframe-a0 -1; } } ##kernel/proc.h当中修改和新增的内容屏蔽掩码 / Per-process state struct proc { struct spinlock lock; // p-lock must be held when using these: enum procstate state; // Process state void *chan; // If non-zero, sleeping on chan int killed; // If non-zero, have been killed int xstate; // Exit status to be returned to parents wait int pid; // Process ID int mask; // 新进程的系统调用屏蔽掩码 ... ... ##kernel/syscall.c当中新增的内容sys_interpose的实现 uint64 sys_interpose(void){ //获取参数 int n; argint(0, n); //修改状态 struct proc *p myproc(); p-mask n; return 0; }验收成果按照官网给出的输入进行输入如果输出的和官网结果一致则代表成功。在ubuntu的shell当中xv6目录下输入./grade-lab-syscall sandbox_mask后如果出现以下提示则代表成功 Test sandbox_mask sandbox_mask: OK (1.5s)3.Sandbox with allowed pathnames(简单难度) 这一小节是对上一阶段系统调用屏蔽的扩展上一小节只是简单粗暴地屏蔽了某个系统调用一棒子打死的那种在本小节我们用到了interpose的第二个参数Path这个参数的具体意思是“允许访问的路径”。 假设我们的屏蔽掩码屏蔽了open和exec这个两个系统调用但是这两个系统调用在调用时都需要向其传入一个路径我们假设该路径的名字为pathA当我们调用open和exec时如果向其传入的路径 pathA和之前的Path一致则代表open和exec正常进行不会被屏蔽反之则直接返回不再执行open和exec。个人的一些解析由于用到了interpose的第二次参数因此我们需要在进程结构体当中添加新的字段用于存放允许访问的路径。由于在进程结构体当中添加了新成员因此父子进程继承状态/属性时需要传递刚才添加的新成员。官网说了如果屏蔽码屏蔽的是open和exec则会继续判断PathA和进程结构体当中的特点字段是否一致一致则代表open和exec可以正常执行所以结合前面的例子屏蔽掩码具体屏蔽了谁应该在syscall当中进行判断而进一步地判断需要在sys_open和sys_exec两个调用的具体实现当中。以下是代码相关内容##kernel/proc.h当中修改和新增的内容屏蔽掩码 // Per-process state struct proc { struct spinlock lock; // p-lock must be held when using these: enum procstate state; // Process state void *chan; // If non-zero, sleeping on chan int killed; // If non-zero, have been killed int xstate; // Exit status to be returned to parents wait int pid; // Process ID int mask; // 进程的系统调用屏蔽字 char allowPathName[MAXPATH]; // 被允许的路径名 ... ... ##kernel/proc.c/kfork函数体内中新增的内容子进程继承父进程的mask ... ... /*修改点父进程的状态mask传递给子进程 * 父进程的mask已经被修改此时若创建新 * 的子进程则mask也要一并传递。 * 子进程也要继承父进程的allowPathName。 */ np-mask p-mask; strncpy(np-allowPathName,p-allowPathName,MAXPATH); ... ... ##kernel/syscall.c当中修改的内容遇到open和exec则“放行”在open和exec中再次判断 void syscall(void) { int num; struct proc *p myproc(); // 取出在a7中存放的调用号 num p-trapframe-a7; if(num 0 num NELEM(syscalls) syscalls[num]) { //修改前将调用屏蔽掩码和系统调用码进行相与的操作判断当前调用是否被屏蔽/禁止 //修改后如果屏蔽的是open和exec则在open或者exec当中再次判断 if(p-mask (1 num) ){ if(num SYS_open || num SYS_exec){ p-trapframe-a0 syscalls[num](); return; } p-trapframe-a0 -1; return; } // Use num to lookup the system call function for num, call it, // and store its return value in p-trapframe-a0 p-trapframe-a0 syscalls[num](); } else { printf(%d %s: unknown sys call %d\n, p-pid, p-name, num); p-trapframe-a0 -1; } } ##kernel/sysfile.c/sys_open函数体内新增的内容添加判断逻辑 ... ... // 只有当 open 被 mask 掉时才检查路径 if(p-mask (1 SYS_open)){ if(strncmp(path,p-allowPathName,MAXPATH) ! 0){ return -1; } } ... ... ##kernel/sysfile.c/sys_exec函数体内新增的内容添加判断逻辑 ... ... // 只有当 exec 被 mask 掉时才检查路径 if(p-mask (1 SYS_exec)){ if(strncmp(path, p-allowPathName,MAXPATH) ! 0){ return -1; } } ... ...验收成果按照官网给出的输入进行输入如果输出的和官网结果一致则代表成功。在ubuntu的shell当中xv6目录下输入make grade后如果出现以下提示则代表成功 Test sandbox_mask $ make qemu-gdb sandbox_mask: OK (3.0s) Test sandbox_fork $ make qemu-gdb sandbox_fork: OK (1.1s) Test sandbox_path $ make qemu-gdb sandbox_path: OK (1.2s) Test sandbox_most $ make qemu-gdb sandbox_most: OK (0.7s) Test sandbox_minus $ make qemu-gdb sandbox_minus: OK (1.0s) Test attack $ make qemu-gdb attack: OK (1.1s)4、Attack xv6 (中等难度) 这一小节我们将利用系统漏洞打破进程之间的屏障从而中进程B当中访问到进程A已被回收但未彻底重置该进程使用过的内存当中的私有数据。 xv6 通过虚拟内存和系统调用机制实现了进程之间、用户态与内核态之间的隔离在正常情况下一个用户进程不可能直接访问另一个进程的内存数据。正常情况下进程在被销毁时其使用过的内存空间也要被清理一下例如全部置为0或者其他值但是xv6当中负责回收进程内存的逻辑没有对进程使用过的内存进行清理这就导致新的进程被创建后其私有的内存空间很可能与之前的进程相重叠方便理解先这么说后面会给出具体的解释导致新进程可能访问到旧进程的私有数据。 所以在本次小节xv6会先通过secret程序创建进程并且向该进程的私有内存空间当中存放一些数据最后销毁进程注意存放的数据没有被销毁之后我们通过实现attack这个程序来让一个新的进程尝试从自己的私有内存空间当中寻找secret进程遗留下来的蛛丝马迹找到后输出它。官网的一些提示和本人的解析user/secret.c是secret的源文件。我们在user/attack.c当中实现本小节让我们做的内容。官网说通过sbrk()这个系统调用来请求分配一块内存空间堆区然后在该堆区当中寻找蛛丝马迹。因为secret会向内存当从存放字符串所以我们在遍历堆区时需要判断当前访问的内存当中存放的内容是否符合字符串的特征同时字符串的字符应该是大于等于2个字符连续并且以\0结尾。要为进程分配合适大小的堆区并且检测字符串时存放字符串的容器长度也要设计合理。相关代码##user/attack.c #include kernel/types.h #include kernel/fcntl.h #include user/user.h #include kernel/riscv.h #define DATASIZE (8*4096) //heap的大小为8页共32k一页4kB int main(int argc, char *argv[]) { // Your code here. //分配heap char* buf sbrk(DATASIZE); //堆的大小为8页 char ch[DATASIZE/4]; //字符串大小为8k int j 0;// for(int i 0; i DATASIZE; i){ char c buf[i]; if(c \0 (j 2 j DATASIZE/4)){ //遇到/0,并且j大于2且在合法范围内则代表可能找到了想要的东西截断字符串 ch[j] \0; //打印 printf(%s,ch); printf(\n); //j置为0继续找剩余符合条件的字符串假设还没有扫码到heap的尽头的情况下 j 0; } //符合字符条件并且j的范围合理 if( ((c a c z) || (c A c Z) || (c 0 c 9)) j DATASIZE/4 - 1){ //符合条件则赋值给字符串数组 ch[j] c; } else{ //不连续或者不是字符则将j值为0 j0; } } exit(1); }验收成果按照官网给出的输入进行输入如果输出的和官网结果一致则代表成功。在ubuntu的shell当中xv6目录下输入./grade-lab-syscall attack后如果出现以下提示则代表成功 Test attack attack: OK (1.1s)关于内存方面的解释在 xv6 中物理内存被划分为固定大小的物理页每页 4096 字节。当一个进程创建时内核会为其建立页表用于将进程的虚拟页映射到具体的物理页上。假设进程 A 通过页表映射将数据写入某个物理页例如编号为 P 的物理页。当进程 A 退出时其页表会被销毁并且该物理页会被归还到空闲页链表中但由于本实验中内核没有对该物理页执行清零操作该物理页中的内容仍然保留。随后当进程 B 创建并调用sbrk()分配内存时内核可能会将该物理页重新分配给进程 B并通过新的页表项将其映射到进程 B 的虚拟地址空间中。此时进程 B 只要访问对应的虚拟地址就能够读取到此前进程 A 遗留下来的数据从而造成信息泄露。为什么官方文档中提到“第一次攻击可能失败需要第二次”当 secret 进程退出后其使用过的物理页会被归还到内核的空闲页链表中但这些物理页未被清零。随后 attack 进程通过sbrk()申请新的内存页时内核会从空闲页链表中分配物理页。由于空闲页的分配顺序取决于内核内部状态例如此前的内存分配和释放顺序attack 进程在第一次运行时未必能恰好获得 secret 进程曾使用过的物理页因此可能无法读取到残留数据。当 attack 程序再次运行时物理页分配状态可能发生变化此时更有可能分配到此前包含 secret 的物理页从而成功读取到敏感信息。因此攻击的成功具有一定的概率性。5、写在最后接下来要开始研究6.1810 / Fall 2025了。由于还要复习408数学所以会更新很慢。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2447804.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!