CSAPP Attack Lab
个人感觉非常有意思的一个 Lab,涉及的知识面比较窄,主要关注 缓冲区溢出漏洞 这一个方面,并基于此进行代码攻击,体验一把做黑客的感觉,对应知识点为书中的 3.10 节内容。
这个 Lab 上手便给了我当头一棒,在环境配置上琢磨了好一阵。直接运行 ./ctarget -q ,程序没有让进行输入,而是直接触发了段错误,后来尝试在跑在学校的 Linux 服务器上得以正常运行,原因不明,推测是 WSL 的锅??
phase_1在倒腾好环境之后,终于可以开始着手完成实验了。
phase_1 要求我们在调用 getbuf 读取标准输入后,不返回到 test 函数接着执行 printf,而是转而执行 touch1.
void test()
{
int val;
val = getbuf();
printf("No exploit. Getbuf returned 0x%x\n", val);
}
可以利用书中 3.10.3 节提到的知识,向缓冲区中写入过量的数据,大到足以覆盖掉调用 getbuf 时压入栈中的返回地址,将其修改为我们想要跳转执行的 ...
CSAPP Bomb Lab
本 Lab 可以说是 CSAPP 的几个 Lab 中最为人津津乐道的一个,对应知识点为书中的第 3 章(程序的机器级表示),要求使用 GDB 调试器,对汇编语言进行调试,从而得出正确的“拆弹密码”。共分为 6 个关卡和一个隐藏关卡,每个关卡都分别考察了一种语法结构或数据结构的汇编表示,部分关卡逻辑比较复杂,要求对 x86 汇编有一定的熟悉度。
bomb.cint main(int argc, char *argv[])
{
char *input;
/* Note to self: remember to port this bomb to Windows and put a
* fantastic GUI on it. */
/* When run with no arguments, the bomb reads its input lines
* from standard input. */
if (argc == 1) {
infile = stdin;
}
...
CSAPP Data Lab
CSAPP 的第一个 Lab,对应知识点为书中的第 2 章(信息的表示与处理),要求使用受限制的运算符和表达式实现一些位操作。主要分为两个部分:整数部分和浮点数部分。其中整数部分限制较多,比较偏重技巧性,部分题个人认为很有难度。而浮点数部分则比较基础,主要考察对 IEEE 754 标准的熟悉程度,代码较长,但思路相对简单。
bitXor思路使用德-摩根定律进行推导,推导过程如下:
代码int bitXor(int x, int y) {
// 德-摩根定律
return ~(~(x & ~y) & ~(~x & y));
}
tmin思路最小整数即最高位(负数权重)为 1,其余(正数权重)为 0。
代码int tmin(void) {
return 1 << 31;
}
isTmax思路由于不能使用左移运算符,因此没办法直接构造出 tmax,需要仔细考虑 tmax 的性质:tmax = 0x7fffffff ,而 tmax + 1 = 0x80000000 ,这两个数的二进制位 ...
MIT6.s081 2021 Lab File system
Large files思路xv6 文件系统的 inode 中地址域 addrs[] 由 12 个直接地址和 1 个一级间接地址组成,本实验要求将地址域更改为 11 个直接地址、1 个一级间接地址和 1 个二级间接地址组成,以支持更大文件的存储。
代码的实现有了直接地址和一级间接地址做参考,就很简单了,直接查看代码部分即可。
代码diff --git a/kernel/file.h b/kernel/file.h
index b076d1d..5c4eb3a 100644
--- a/kernel/file.h
+++ b/kernel/file.h
@@ -26,7 +26,7 @@ struct inode {
short minor;
short nlink;
uint size;
- uint addrs[NDIRECT+1];
+ uint addrs[NDIRECT+2];
};
// map major device number to device functions.
diff --git a/kernel/fs.c b ...
MIT6.s081 2021 Lab Multithreading
Uthread: switching between threads思路xv6 已经实现了进程的切换机制,本实验要求参考进程的切换,实现一个用户态线程的切换。
要实现线程切换,必然涉及上下文,即寄存器的保存和恢复,那么需要保存哪些寄存器?实际上,只需要保存被调用者保存寄存器(callee-saved registers),而实现调用者保存寄存器(caller-saved registers)的保存与恢复的代码由编译器自动生成。关于调用者保存与被调用者保存寄存器有哪些可以参照下述 RISC-V 的 calling convention:
另外,根据 user/uthread_switch.S 的注释,thread_switch 最后通过 ret 指令将当前程序计数器的值切换为 ra 寄存器中存储的地址,实现进程的“切换”,因此 struct thread 中还需要保存每个线程对应程序的起始地址(即函数指针)。
在了解需要保存哪些寄存器之后以及如何进行线程切换之后,还有一个细节需要考虑,即栈指针寄存器(sp)的初始化。线程栈的存储位置为 struct thread 中的 stack 数组 ...
MIT6.s081 2021 Lab Copy on-write
Implement copy-on write背景xv6 使用 fork() 系统调用创建子进程时,需要将父进程的地址空间进行 深拷贝 ,即将页表和实际物理空间同时进行拷贝,以实现父进程和子进程地址空间的独立性。但很多时候,如 shell 程序,fork() 通常与 exec() 搭配使用,首先使用 fork() 创建子进程,随后在子进程中使用 exec() 将指定的程序加载到当前地址空间,这样在 fork() 中进行的地址空间拷贝就白白浪费了。
本实现要求实现一个写时复制(copy-on write)的 fork() 系统调用。具体来说,在进行虚拟内存拷贝时,不直接进行物理内存的拷贝,只是将父进程的页表复制给子进程,这样子进程和父进程的每个虚拟页面都指向了同一个物理页面,当子进程需要对某个虚拟页面进行写入时,为了保证父进程和子进程之间的独立性,子进程此时将进行物理内存的分配和拷贝,再进行写入。
实现方案根据提示,可以将上述的写时复制的思路用 异常 的方式来实现。
首先可以利用页表项的 flags 中的 RSW 位来表示页表项是否为 COW 页,以便后续的异常处理。
修改 uvmcop ...
MIT6.s081 2021 Lab Traps
使用gdb调试xv6内核从最近两个 Lab 开始,代码逻辑的复杂度明显上升,对内核进行调试可能是帮助理解操作系统机制的绝佳方法。因此在开始本 Lab 之前,我们先来配置一下针对 xv6 内核的 gdb 调试器。
安装 gdb-multiarch.
利用包管理工具进行安装,我使用的是 Ubuntu 系统,执行以下命令:
sudo apt install gdb-multiarch
在 xv6 项目根目录下可以看到 .gdbinit 文件,其中已经写好了一些 gdb 的初始化选项,使用文本编辑器或 cat 命令查看:
set confirm off
set architecture riscv:rv64
target remote 127.0.0.1:26000
symbol-file kern ...
MIT6.s081 2021 Lab Page tables
Speed up system calls思路题目要求在每个进程初始化时为它的页表插入一个页表项,内核通过这样预先缓存页表项的操作,来加速特定系统调用的执行速度。
由于前不久刚过完一遍《OSTEP》,因此我认为自己对页表机制还算比较熟悉,应对本 Lab 理应比较轻松,但在真正上手的时候,还是觉得有些无所适从,无奈老老实实地把 xv6 手册的第 3 章对照着代码仔细研读了一番,从中提炼出了几个关键的函数:
kernel/kalloc.c:kalloc
void *kalloc(void);
遍历空闲链表,寻找一个可分配的物理页面。若找到,返回该页面的首(物理)地址;否则,返回 0 (空指针)。
kernel/kalloc.c:kfree
void kfree(void *pa);
释放已分配的首地址为 pa 的物理页面,并更新空闲链表。
kernel/proc.c:allocproc
static struct proc *allocproc(void);
遍历进程数组 proc,寻找未被使用的 struct proc。若找到,则初始化其状态,为创建一个新的页表,并返回 ...
MIT6.s081 2021 Lab System calls
xv6系统调用实现不同于 Lab1 利用已实现的系统调用来实现一些用户态下的命令行程序,本 Lab 是要在内核层面实现一些系统调用。这其中难免涉及到一些对内核数据结构的操作,以及处理器体系结构(本系列 Lab 基于 RISCV)相关的内容,那么首先有必要梳理一下 xv6 下系统调用的实现过程。
xv6 系统调用的实现:
以 trace 系统调用为例,用户通过调用 user/user.h 中的函数 trace 进行系统调用。
通过调用 Perl 脚本 user/usys.pl 生成的一系列汇编代码,该汇编代码的作用是设置寄存器的内容并实现用户态到内核态的切换,内核后续针对寄存器中的内容执行相应的系统调用操作。以下是对 user/usys.pl 代码的逐行解析:
#!/usr/bin/perl -w:这是一个Perl脚本的“shebang”行,指定使用/usr/bin/perl解释器执行此脚本,并开启警告(-w)选项。
print "# generated by usys.pl - do not edit\n";:打印注释说明此文件是由usys.pl脚本自动生 ...
MIT6.s081 2021 Lab Utilities
Boot xv6按照示例切换到 util 分支后,看到目录下包含 Makefile 文件,执行 make qemu 即可。
sleep思路借助系统调用 sleep 实现一个命令行程序,关键是要找到封装了系统调用的 C 函数的位置,根据提示:
… user/user.h for the C definition of sleep callable from a user program …
可知该函数的声明位于 user.h 头文件中,声明方式很简单:
int sleep(int);
将其“拷贝”(include)到需要编写的代码 user/sleep.c 中,调用 sleep(<睡眠时间>) 即可。
最后,按照提示,将编写的 sleep 代码添加到 Makefile 的 UPROGS 中,添加后如下所示:
UPROGS=\
$U/_cat\
$U/_echo\
$U/_forktest\
$U/_grep\
$U/_init\
$U/_kill\
$U/_ln\
$U/_ls\
$U/_mkdir ...