源码调试脏牛运行过程
调试代码
把POC阉割一下好调试
1 | |
mem_write函数的调用链
因为当write往/proc/self/mem写的时候,是调用的mem_write函数

1 | |
调试过程
https://elixir.bootlin.com/linux/v4.7/source/mm/memory.c
主要是先关注__access_remote_vm这函数,主要是关注下面这几行代码
1 | |
这个page是怎么得到的呢,可以往上看到第3737行,它是调用get_user_pages_remote去获取了一个page
1 | |
然后我们跟入进去,它只是__get_user_pages_locked的封装

进入到__get_user_pages_locked,首先write是1,会给flags加上一个FOLL_WRITE,然后调用__get_user_pages函数

第一次调用follow_page_mask(原因是缺页)
进入到__get_user_pages,do前面是对VMA虚拟空间的一些操作,不用看,重点看红框的
首先调用了cond_resched函数,调度别的任务,这样就提供了一个竞态条件
然后调用follow_page_mask函数去寻找一个page,如果没有找到page,就进入下面的if判断,调用faultin_page函数进行缺页异常处理

那我们来看看follow_page_mask是怎么找page的,它的功能主要是从一级目录二级目录等等来寻找页表项的过程,可以通过阉割版的POC来动态调试看看
我们在cond_resched下个断点
1 | |

步进,执行完574行,我们再看看page和mmap地址处的值是多少

可以发现,page的值是0,说明没有找到合适的page,而且mmap处的地址不可访问,说明还没有分配到物理地址
我们来看看page的0是怎么返回的,回到follow_page_mask,上面说过这个函数就是去找页表项的,那么前面的函数我们不看,直接去看最后一级页目录是怎么找页表项PTE的

因为刚开始最后一级页表项肯定是空的,所以直接看87行,直接跳转到no_page,会调用no_page_table,然后返回了NULL


然后退回去,page得到是NULL,进入下面if分支,调用faultin_page进行缺页处理

进入到faultin_page函数,之前flags就设置了FOLL_WRITE的标志,所以会给fault_flags加上FAULT_FLAG_WRITE标志,说明这是因为写操作造成的缺页异常

之后会调用handle_mm_fault

可以在378行下断点看看
1 | |
然后执行完这个函数看看,发现可以访问mmap的地址了,里面的值就是ABCDEFG等等,也就是文件里的内容

现在我们就可以看看handle_mm_fault究竟做了什么

前面不用看,然后再看看__handle_mm_fault,这个函数前面的都跟我们分析没什么关系,主要看最后return返回时调用的函数

handle_pte_fault函数,首先先把*pte给了entry,这里entry是为空的,而且vma是私有映射,最后会调用do_fault函数

然后进入到do_fault看看,因为我们flags有FAULT_FLAG_WRITE没有VM_SHARED,注意if里的取反符号,所以最后调用的是do_cow_fault函数做COW操作,所以之前的mmap能访问到数据,就是因为这里做了COW,做了一个副本

第二次调用follow_page_mask(原因是没有写权限,有FOLL_WRITE标志)
发现第二次调用page还是0,但是我们的mmap是有了实际的物理地址,但为什么page还是为0呢

我们重新进入follow_page_mask,然后进入follow_page_pte查找页表项这个函数,第一次我们pte是空的,所以进入的no_page了,第二次我们pte不为空了,而且flags标志有了FOLL_WRITE,但是pte是不可写的,因为我们的文件是只有root才有写权限,所以最后还是返回的NULL

那么又跑回下面这里了

第二次进入faultin_page缺页处理,然后跟上面一样,一路进到handle_pte_fault里,此时的pte是不为空的,所以调用了3378行代码

do_wp_page函数,这个函数总的来说就是判断有没有做COW,如果有的话就调用wp_page_reuse,使用上一步分配好的副本页

wp_page_reuse函数,这个函数最后会返回VM_FAULT_WRITE

最后这个VM_FAULT_WRITE是返回到了faultin_page的378行

然后到411行有个判断,此时我们的ret是VM_FAULT_WRITE,而且vma不具有写权限,所以flags会把FOLL_WRITE去掉,说明以及做好了COW的副本页,可以往副本页中写了

所以在第二次缺页处理的时候把FOLL_WRITE去掉了
第三次调用follow_page_mask
第三次调用的时候,page已经有地址了,这是一个正常的COW的流程

对比
正常的流程:
- 第一次follow_page_mask(FOLL_WRITE),因为page不在内存中,进行缺页处理
- 第二次follow_page_mask(FOLL_WRITE),因为page没有写权限,并去掉FOLL_WRITE
- 第三次follow_page_mask(无FOLL_WRITE),成功返回page地址
脏牛的流程
- 第一次follow_page_mask(FOLL_WRITE),因为page不在内存中,进行缺页处理
- 第二次follow_page_mask(FOLL_WRITE),因为page没有写权限,并去掉FOLL_WRITE
- 另一个线程释放上一步分配的COW页
- 第三次follow_page_mask(无FOLL_WRITE),因为page不在内存中,进行缺页处理
- 第四次follow_page_mask(无FOLL_WRITE),成功返回page,但没有使用COW机制
我们来看看脏牛的第三次follow_page_mask之后进行的缺页处理,进入到faultin_page的时候,是没有FOLL_WRITE的,那么fault_flags也不会有FAULT_FLAG_WRITE的标志

最后到do_fault函数中来,因为flags没有FAULT_FLAG_WRITE标志,就调用do_read_fault

系统以为是一个只读异常,就会返回文件的物理地址,以为你不会写操作,没有做COW副本页,但是返回了这个地址后,最后是要到__access_remote_vm这个函数的,最后还是往这个文件写入了数据。
