--- title: attack xv6 categories: [lab,xv6] --- ## 思路 被这个实验折磨了两天,可能是2024新出的一个实验内容,网上资料少,参考了一篇仅有的博客,吭哧吭哧分析出来了个大概吧...在此记录一下,以便帮助有需要的人。 attack xv6的ans只有几行代码,根据实验描述,大概能猜到是secret程序结束之后,attack程序复用了它的物理内存,然后读取之前写入内存中的密码。难点在于我们如何定位到那段内存。 在开始之前直接先给出ans,可以看到代码是很简单的: ```c #include "kernel/types.h" #include "kernel/fcntl.h" #include "user/user.h" #include "kernel/riscv.h" int main(int argc, char *argv[]) { // your code here. you should write the secret to fd 2 using write // (e.g., write(2, secret, 8) char *end = sbrk(17*PGSIZE); end += 16 * PGSIZE; write(2, end+32, 8); exit(1); } ``` 下面说说实验思路,实验目的是找到被复用的**物理内存**,而内核的物理内存使用栈式链表管理(`kalloc.c`),secret程序通过`kalloc`从栈顶取内存页来使用,程序结束后通过`kfree`将这些内存页放回栈顶。到attack运行的时候同样使用`kalloc`从栈顶取内存页,因此就给了attack复用物理内存的机会。 值得注意的是,secret分配内存时这些页的出栈顺序,不一定与attack分配内存时的出栈顺序相同,比如secret分配页的顺序为1,2,3,归还的顺序为2,3,1,那么此时栈顶到栈底的页分别为1,3,2,当attack来分配的时候,拿到页的顺序就是1,3,2。因此核心在于分析程序运行时物理内存页的分配以及回收顺序,才能知道attack应该到哪块内存中获取密码。 ## fork secret 我们从attacktest中的第一个`fork`开始分析。 ![image-20241218210711222](https://cdn.jsdelivr.net/gh/NOS-AE/assets@main/img/image-20241218210711222.png) `fork`调用了`allocproc`来创建proc,`allocproc`中首次使用了`kalloc`为`p->trapframe`分配一页物理页。 ![image-20241218210834186](https://cdn.jsdelivr.net/gh/NOS-AE/assets@main/img/image-20241218210834186.png) 接着`allocproc`调用`proc_pagetable`创建页表,其中,先调用`uvmcreate`使用`kalloc`为根页表分配一页: ![image-20241218211533731](https://cdn.jsdelivr.net/gh/NOS-AE/assets@main/img/image-20241218211533731.png) 然后为`trampoline`和`p->trapframe`建立映射,这两页在最虚拟内存的最高地址处,处于同一个三级页目录(xv6使用sv39,即三级页表),因此又`kalloc`了两页,分别对应一页二级页表、一页三级页表: ![image-20241218211559339](https://cdn.jsdelivr.net/gh/NOS-AE/assets@main/img/image-20241218211559339.png) 因此`allocproc`一共创建了4页(trapframe、三级页表)。 回到`fork`中,由于此时xv6的fork还没实现copy on write特性,因此需要把父进程用户内存中的内容(用户内存即stack、heap这些低地址内存,不包括trapframe、trampoline)使用`uvmcopy`全部复制到子进程中。此时父进程用户内存占用此时为4页,因此子进程也复制了这4页进来,由于这4页位于虚拟内存中的低地址,其二级三级页表与trapframe/trampoline的都不一样,所以还会创建两页分别用于二级三级页表,一共`kalloc`了4+2=6页(为什么是4页具体原因在后面分析`exec`时会揭晓)。 综上,fork一共分配了4+6=10页。 ## exec secret 然后就是执行`exec`: ![image-20241218212525915](https://cdn.jsdelivr.net/gh/NOS-AE/assets@main/img/image-20241218212525915.png) 在`exec`的实现中,会使用`proc_pagetable`创建新的页表来替换旧页表(这个也好理解,因为`exec`目的就是替换整个程序镜像,相当于从头开始执行一个新的程序,之前程序的相关内容全部丢弃)。根据之前的分析,`proc_pagetable`会分配3页。 接着,`exec`遍历elf文件的program header,将所有LOAD段加载进内存中。具体是通过`uvmalloc`分配物理内存,`loadseg`将段加载进内存。xv6程序的elf文件包含两个LOAD段,data段和text段,可以通过readelf看一下: ![image-20241218213350943](https://cdn.jsdelivr.net/gh/NOS-AE/assets@main/img/image-20241218213350943.png) 这两个段分别加载到虚拟内存的第0页和第1页中。同理,这两页属于低地址的用户内存,需要2页(二级页表、三级页表)+2页来分别存放这两个段。 接着,`exec`为用户栈分配内存: ![image-20241218213731624](https://cdn.jsdelivr.net/gh/NOS-AE/assets@main/img/image-20241218213731624.png) 这里的`USERSTACK`值为1,因为xv6固定用户栈大小为一页,后面的`+1`多出的一页用于page guard,便于栈溢出的处理。另外栈是紧挨着data段和text段之后分配的,他们属于同一个三级页表,不需要额外分配页表,因此栈分配一共分配了2页 `exec`的最后,还调用了`proc_freepagetable`来释放旧页表和旧用户内存: ![image-20241218214350688](https://cdn.jsdelivr.net/gh/NOS-AE/assets@main/img/image-20241218214350688.png)s'd's 其中的两个`uvmunmap`释放pte映射(避免后续`uvmfree`的时候意外释放trampoline和trapframe的物理内存),并不释放物理页,因为trampoline是整个操作系统共享的不需要释放,而trapframe是用户态和内核态转换时的用到的存储区域,十分重要,同样不会释放(关于trapframe和trampoline的详细说明可以查阅book-riscv)。最后的`uvmfree`则是释放旧页表占用的内存(5页)以及用户内存(4页),共9页。 综上,`exec`一共分配了3+4+2=9页,然后又释放了9页。 ## secret 然后就是执行secret程序了,使用了`sbrk`分配了32页内存,然后在第10页写入数据。 至此,经过`fork`、`exec`、`sbrk`之后,secret程序一共占用了10+32=42页 ## wait secret attacktest程序调用`wait(0)`,获取到secret的proc,调用`freeproc`释放其内存: ![image-20241218215540939](https://cdn.jsdelivr.net/gh/NOS-AE/assets@main/img/image-20241218215540939.png) 下面我们重点关注释放内存的顺序,即文章开头思路中说到的`kfree`“入栈”。 ![image-20241218215720587](https://cdn.jsdelivr.net/gh/NOS-AE/assets@main/img/image-20241218215720587.png) 首先是释放一页trapframe。然后进入`proc_freepagetable`中的`uvmfree`: ![image-20241218215822928](https://cdn.jsdelivr.net/gh/NOS-AE/assets@main/img/image-20241218215822928.png) 先调用`uvmunmap`从低到高地址释放用户内存,根据之前内存分配的分析,释放顺序为data段+text段、用户栈+page guard、32页堆内存(每页从低地址页到高地址页依次释放/入栈),共36页 最后释放页表,共5页 此时我们可以知道,`kmem`维护的空闲链表栈,从栈顶开始的页依次为:5页页表、第32页堆内存、第31页堆内存、...、第10页堆内存(密码所在的页)、...、第1页堆内存、page guard..... ## attack attacktest运行attack的方式和运行secret的一样,都是通过`fork`+`exec`,直接拿前面的分析结果,我们知道在开始执行attack程序之前,fork分了配10页,exec分配9页又释放了9页,其中fork分配的10页来自于5页页表、第32 ~ 28页堆内存,exec分配的9页来自第27 ~ 19页堆内存,后面又释放了9页回栈顶(因为我们要的第10页堆内存还在下面,所以我们不用关心这9页分别是什么),此时栈顶开始的页依次为:9页、第18页堆内存、...、第10页堆内存、...,通过用手指头数一下发现,我们只需要分配17页,密码就在最后一页中。 ## 参考