基础知识
-
mmap(void* start, size_t length, int prot,int flags,int fd, off_t offset)
一个比较常用的函数,将磁盘上的文件映射到虚拟内存中,POC中参数prot为PROT_READ参数,参数flags为MAP_PRIVATE,请参考linux库函数mmap()原理及用法详解_linux mmap函数
-
madvice(caddr_t addr, size_t len, int advice)
告诉内核内存addr~addr+len在接下来的使用状况,以便内核进行一些进一步的内存管理操作。
advice为MADV_DONTNEED时,此系统调用相当于通知内核addr~addr+len部分的内存不再使用,内核将释放掉这一块内存以节省空间,相应的页表项也会被置空。
-
/proc/self/mem
这个文件是一个指向当前进程的虚拟内存文件的文件,当前进程可以通过对这个文件进行读写以直接读写虚拟内存空间,并无视内存映射时的权限设置。也就是说我们可以利用写/proc/self/mem来改写不具有写权限的虚拟内存。可以这么做的原因是/proc/self/mem是一个文件,只要进程对该文件具有写权限,那就可以随便写这个文件了,只不过对这个文件进行读写的时候需要一遍访问内存地址所需要寻页的流程。因为这个文件指向的是虚拟内存。
准备工作
-
在ubuntu中编写dirtycow.c
-
vim dirtycow.c
-
-
在ubuntu中编译irtycow.c,使用如下命令:
-
gcc -pthread -static -m32 -g dirtycow.c -o dirtycow
-
-
将生成的可执行文件dirtycow放入实验⽤用的qemu虚拟机
-
将rootfs⽬目录下的⽂文件_install.tgz解压到/home/nudt⽬目录下:
sudo tar xvzf /home/nudt/aos/lab/rootfs/_install.tgz -C ~
-
将可执行文件dirtycow拷入目录 ~/_install/tmp/
sudo cp dirtycow ~/_install/tmp/
-
重新打包⽣生成qemu虚拟机的根文件系统。可在rootfs目录下执⾏行行如下命令:
./mkrootfs.sh
生成的根⽂文件系统放在映像⽂文件rooTs.img.gz中。
-
-
设置在dirtycow.c的main处加断点,
-
在内核源码目录(“aos/lab/cur”)下,创建指向dirtycow.c和dirtycow所在目录的符号链接dirtycow,使用如下命令
ln -s <dirtycow.c所在目录> dirtycow
比如当前我把dirtycow.c和dirtycow放在~/aos/ 目录下
ln -s ~/aos/ dirtycow
-
查看可执行文件dirtycow的“.text”节(SecQon)的起始虚拟地址(VMA)。在可执行文件dirtycow所在目录(~/aos/ )运行:
objdump -dlx dirtycow| less
然后查看“.text”节的起始虚拟地址
-
调试运行qemu虚拟机,在gdb中导入应用程序符号信息:
dir dirtycow add-symbol-file dirtycow/dirtycow 0x80482d0 注意: add-symbol-file dirtycow/dirtycow <可执行文件dirtycow的“.text”节的起始虚拟地址>
-
设置断点:
b dirtycow.c:main
-
调试过程分析
target remote localhost:1234
set logging file /mnt/hgfs/f/out.log
set logging on
add-symbol-file dirtycow/dirtycow 0x80482d0
b dirtycow.c:main
设置好断点之后,运行虚拟机qemu和调试器gdb,等待qemu中的linux开机后,输入如下命令
cd tmp
echo this is not a test > foo
ls -l foo
chmod 0404 foo
ls -l foo
cat foo
setuidgid 1000 sh
id
ls -l foo
./dirtycow foo youaremodified
在最后一句命令输入后,gdb调试器将停在断点main函数的入口处
接着我们设置如下断点:
b dirtycow.c:83
b __access_remote_vm
当代码执行到 c+=write(f,str,strlen(str))
,读写/proc/self/mem 这个文件,会通过系统调用,在内核中的实现是在 fs/proc/base.c 文件中
static const struct file_operations proc_mem_operations = {
.llseek = mem_lseek,
.read = mem_read,
.write = mem_write,
.open = mem_open,
.release = mem_release,
};
mem_write()函数主要是调用 __access_remote_vm来实现访问用户进程的进程地址空间
此刻通过continue命令
到达__access_remote_vm
,可以看到要写入的虚拟地址为3086991360(0xB7FF C000),通过手动计算可得出该虚拟地址的各项偏移为:
页目录项的偏移:0xB7C
页表项的偏移:0xFF0
页内偏移:0x0
通过p $lx_current().mm->pgd
查看页目录地址,最终可以得到该虚拟地址对应的页表项为空,即用户空间那段内存(dirtycow 程序中map 指针指向的内存)其实还没有和实际物理页面建立映射关系,
接着__access_remote_vm
调用get_user_pages
,将参数write=1 和force=1 以及 page 指针传递给get_user_pages
#define FOLL_WRITE 0x01 /* check pte is writable */
#define FOLL_TOUCH 0x02 /* mark page accessed */
#define FOLL_GET 0x04 /* do get_page on page */
#define FOLL_FORCE 0x10 /* get_user_pages read/write w/o permission */
long get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
unsigned long start, unsigned long nr_pages, int write,
int force, struct page **pages, struct vm_area_struct **vmas)
{
return __get_user_pages_locked(tsk, mm, start, nr_pages, write, force,
pages, vmas, NULL, false, FOLL_TOUCH);
}
get_user_pages
调用并传递给__get_user_pages_locked
,在__get_user_pages_locked
中flags
被转换成 FOLL_WRITE | FOLL_FORCE |FOLL_TOUCH| FOLL_GET 标志位(因此下图中__get_user_pages
的参数flag
的值为23,不信可以手动算一下)
__get_user_pages_locked
调用__get_user_pages
,并将flags
传给__get_user_pages
中的gup_flags
long __get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages,
struct vm_area_struct **vmas, int *nonblocking)
{
long i = 0;
unsigned int page_mask;
struct vm_area_struct *vma = NULL;
if (!nr_pages)
return 0;
VM_BUG_ON(!!pages != !!(gup_flags & FOLL_GET));
/*
* If FOLL_FORCE is set then do not force a full fault as the hinting
* fault information is unrelated to the reference behaviour of a task
* using the address space
*/
if (!(gup_flags & FOLL_FORCE))
gup_flags |= FOLL_NUMA;
do {
struct page *page;
unsigned int foll_flags = gup_flags;
unsigned int page_increm;
/* first iteration or cross vma bound */
if (!vma || start >= vma->vm_end) {
vma = find_extend_vma(mm, start);
if (!vma && in_gate_area(mm, start)) {
int ret;
ret = get_gate_page(mm, start & PAGE_MASK,
gup_flags, &vma,
pages ? &pages[i] : NULL);
if (ret)
return i ? : ret;
page_mask = 0;
goto next_page;
}
if (!vma || check_vma_flags(vma, gup_flags))
return i ? : -EFAULT;
if (is_vm_hugetlb_page(vma)) {
i = follow_hugetlb_page(mm, vma, pages, vmas,
&start, &nr_pages, i,
gup_flags);
continue;
}
}
retry:
/*
* If we have a pending SIGKILL, don't keep faulting pages and
* potentially allocating memory.
*/
if (unlikely(fatal_signal_pending(current)))
return i ? i : -ERESTARTSYS;
cond_resched();
page = follow_page_mask(vma, start, foll_flags, &page_mask);
if (!page) {
int ret;
ret = faultin_page(tsk, vma, start, &foll_flags,
nonblocking);
switch (ret) {
case 0:
goto retry;
case -EFAULT:
case -ENOMEM:
case -EHWPOISON:
return i ? i : ret;
case -EBUSY:
return i;
case -ENOENT:
goto next_page;
}
BUG();
} else if (PTR_ERR(page) == -EEXIST) {
/*
* Proper page table entry exists, but no corresponding
* struct page.
*/
goto next_page;
} else if (IS_ERR(page)) {
return i ? i : PTR_ERR(page);
}
if (pages) {
pages[i] = page;
flush_anon_page(vma, page, start);
flush_dcache_page(page);
page_mask = 0;
}
next_page:
if (vmas) {
vmas[i] = vma;
page_mask = 0;
}
page_increm = 1 + (~(start >> PAGE_SHIFT) & page_mask);
if (page_increm > nr_pages)
page_increm = nr_pages;
i += page_increm;
start += page_increm * PAGE_SIZE;
nr_pages -= page_increm;
} while (nr_pages);
return i;
}
第一次follow_page_mask
__get_user_pages
会调follow_page_mask
来寻找虚拟地址所对应的page,这里按照我们之前手动的计算结果,该虚拟地址对应的页表项为空,用户空间那段内存(dirtycow 程序中map 指针指向的内存)其实还没有和实际物理页面建立映射关系,所以 follow_page_mask函数是不可能返回正确的 page 数据结构的,这里我们进入follow_page_mask
验证一下
follow_page_mask
会依次获取pgd、pmd、pud
等表项,然后调用follow_page_pte
跟随最后的页表项,然后根据页表项获取虚拟地址对应的page,并最终返回给follow_page_mask
。我们继续跟踪follow_page_pte
follow_page_pte
跟随获取页表项pte,发现页表项为空(与我们之前手动得到的结果一致),进入no_page_table
函数
no_page_table
函数返回NULL至follow_page_pte
,follow_page_pte
返回NULL至follow_page_mask
,follow_page_mask
返回NULL至__get_user_pages
__get_user_pages
获取到的page的值为NULL,命中下面的if判断语句,调用faultin_page
函数进行处理,注意传入faultin_page
的是参数foll_flags的引用(地址)foll_flags的值为FOLL_WRITE | FOLL_FORCE | FOLL_GET(数值为23),我们继续跟踪faultin_page
faultin_page
根据传入的faultin_page
的值设置fault_flags
的值
通过上图可以看到,在faultin_page
中只命中了如下判断代码
if (*flags & FOLL_WRITE)
fault_flags |= FAULT_FLAG_WRITE;
因此参数fault_flags
被设为FAULT_FLAG_WRITE(0x01)
,接着在faultin_page
调用handle_mm_fault
,并将fault_flags=1
传给handle_mm_fault
的参数flags
handle_mm_fault
调用__handle_mm_fault
,__handle_mm_fault
调用__handle_pte_fault
__handle_pte_fault
根据页表项pte的值(0,即页表项为空)调用do_fault
do_fault
根据传入的参数flags
进行判断
flags
的值为1,即FAULT_FLAG_WRITE,因此调用do_cow_fault
do_cow_fault
会分配一个新的页面 new_page,并且调用__do_fault
函数,__do_fault
通过文件系统相关的 API 把 page 读到 fault_page 中
这里可以看到__do_fault
调用的文件系统相关的 API就是filemap_fault
,filemap_fault
从cache中或者从磁盘中读取文件内容相应的page放入vmf中并返回至__do_fault
__do_fault
把vmf.page(刚通过filemap_fault
从cache中或者从磁盘中所读取的page)向上通过引用传递返回至do_cow_fault
do_cow_fault
通过__do_fault
获取到fault_page
后继续运行,然后通过下面函数把文件内容拷贝到新页面 new_page 里。注意这里 fault_page 是 page cache,new_page 是匿名页面了。
copy_user_highpage(new_page, fault_page, address, vma);
继续跟踪
do_set_pte
函数会使用新页面new_page和虚拟地址重新建立映射关系,我们这里可以看到do_set_pte
执行之前pte的值为0,do_set_pte
执行之后pte的值为12987173(十六进制为 0x07bdb065),并且根据32位页表项的格式可知,此刻页表项是有效的,脏的(dirty) 的并且是只读的,使用x/s 0xC7bdb000查看内存内容可以看到字符串内容“this is not a test\n”
接着do_cow_fault
继续执行,最后把 fault_page 释放了。do_cow_fault
执行完之后一路返回至faultin_page
faultin_page
返回0至__get_user_pages
第二次follow_page_mask
__get_user_pages
继续运行,根据faultin_page
返回的ret(值为0),在switch语句中转至retry标签继续运行,在运行至follow_page_mask时再次发生意外。
注意这时候传递给该函数的参数 foll_flags
依然没有变化,即 FOLL_WRITE | FOLL_FORCE |FOLL_GET。该 pte entry 的属性是:PRESENT 位被置位,Dirty 位被置位,只读位 RDONLY 也被置位了。
因此在 follow_page_pte 函数中,当判断到传递进来的 flags 标志是可写的,但是实际 pte entry 只是可读属性,那么这里就不会返回正确的 page 结构了,见 follow_page_pte 函数中的(flags & FOLL_WRITE) && !pte_write(pte)
语句。
struct page *follow_page_mask(struct vm_area_struct *vma,
unsigned long address, unsigned int flags,
unsigned int *page_mask)
{
...
return follow_page_pte(vma, address, pmd, flags);
}
static struct page *follow_page_pte(struct vm_area_struct *vma,
unsigned long address, pmd_t *pmd, unsigned int flags)
{
...
if ((flags & FOLL_WRITE) && !pte_write(pte)) {
pte_unmap_unlock(ptep, ptl);
return NULL;
}
...
}
从 follow_page_pte返回 NULL至follow_page_mask,follow_page_mask也返回 NULL,这时候再次进入faultin_page
faultin_page
函数根据flags(由__get_user_pages
的参数foll_flags
传入)的值(即 FOLL_WRITE | FOLL_FORCE |FOLL_GET)设置fault_flags=FAULT_FLAG_WRITE(即1),传入并调用handle_mm_fault
函数
handle_mm_fault
将flag
=FAULT_FLAG_WRITE(即1)传入并调用__handle_mm_fault
__handle_mm_fault
获取页表项pte之后,调用并将flag
=FAULT_FLAG_WRITE传入handle_pte_fault
注意:通过p pte
命令我们可以看到,页表项pte是存在且有效的,结合下面handle_pte_fault
函数的源码
/mm/memory.c
static int __handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, unsigned int flags)
{
...
return handle_pte_fault(mm, vma, address, pte, pmd, flags);
}
int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, unsigned int flags)
{
...
ret = __handle_mm_fault(mm, vma, address, flags);
if (flags & FAULT_FLAG_USER) {
...
}
return ret;
}
static int handle_pte_fault(struct mm_struct *mm,
struct vm_area_struct *vma, unsigned long address,
pte_t *pte, pmd_t *pmd, unsigned int flags)
{
pte_t entry;
spinlock_t *ptl;
entry = *pte;
barrier();
if (!pte_present(entry)) {
...
}
...
if (flags & FAULT_FLAG_WRITE) {
if (!pte_write(entry))
return do_wp_page(mm, vma, address,
pte, pmd, ptl, entry);
...
}
...
}
handle_pte_fault
函数中的判断语句if (!pte_present(entry))
不会命中,而判断语句if (flags & FAULT_FLAG_WRITE)
会命中,跳转到do_wp_page
函数
static int do_wp_page(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
spinlock_t *ptl, pte_t orig_pte)
__releases(ptl)
{
struct page *old_page;
old_page = vm_normal_page(vma, address, orig_pte);
if (PageAnon(old_page) && !PageKsm(old_page)) {
...
if (reuse_swap_page(old_page)) {
/*
* The page is all ours. Move it to our anon_vma so
* the rmap code will not search our parent or siblings.
* Protected against the rmap code by the page lock.
*/
page_move_anon_rmap(old_page, vma, address);
unlock_page(old_page);
return wp_page_reuse(mm, vma, address, page_table, ptl,
orig_pte, old_page, 0, 0);
}
...
}
...
}
static inline int wp_page_reuse(...)
{
...
return VM_FAULT_WRITE;
}
在do_wp_page
函数中,找到页表项orig_pte
所映射的页old_page
(即上文提到的new_page
),此页是匿名页面并且是可以重用的页面(reuse),因此调用wp_page_reuse
函数,wp_page_reuse
函数最终会返回VM_FAULT_WRITE
至do_wp_page
,注意这里返回的值是 VM_FAULT_WRITE
,这个是漏洞的关键所在。
VM_FAULT_WRITE
经由wp_page_reuse
、do_wp_page
、handle_pte_fault
、__handle_mm_fault
、handle_mm_fault
一路返回至faultin_page
的变量ret
中
static int faultin_page(struct task_struct *tsk, struct vm_area_struct *vma,
unsigned long address, unsigned int *flags, int *nonblocking)
{
struct mm_struct *mm = vma->vm_mm;
unsigned int fault_flags = 0;
int ret;
...
ret = handle_mm_fault(mm, vma, address, fault_flags);
...
if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
*flags &= ~FOLL_WRITE;
return 0;
}
faultin_page
函数继续执行,根据ret == VM_FAULT_WRITE
且 VMA
为只读(VM_WRITE)的情况,清除了flag
的FOLL_WRITE 标记位,这里是该漏洞的核心之处
最后faultin_page
函数return 0
至__get_user_pages
函数的变量ret
,__get_user_pages
根据switch(ret)
第二次跳转至标签retry
处接着继续执行
因为刚刚foll_flags ( 即上文中的 flag )
中的 FOLL_WRITE 被拿掉了,所以这时是以只读的方式去调用follow_page_mask
了,正如武侠小说里写的一样,两大绝顶高手交锋正酣大战三百回合不分胜负,说时迟那时快,就在调用 follow_page_mask
之前另外一个线程madviseThread
像小李飞刀一样精准,注意retry
标签处有一个cond_resched
函数给小李飞刀一次出飞刀的机会,madvise(dontneed)
系统调用在内核里的zap_page_range
函数会去解除该页的映射关系
为了调试方便,我们使用gdb调试器中的set *(unsigned int *)0xc61c0ff0 = 0
命令将页表项清零,来等效代替madvise(dontneed)
第三次follow_page_mask
这样在调用follow_page_mask
时,又会像第一次调用 follow_page_mask
那样因为页表项为空而返回NULL至变量page
,我们进入 follow_page_mask
来验证一下
根据上图,因为刚刚foll_flags ( 即上文中的 flag )
中的 FOLL_WRITE 被拿掉了,因此传入follow_page_mask
函数的参数flag = 22
#define FOLL_WRITE 0x01 /* check pte is writable */
#define FOLL_TOUCH 0x02 /* mark page accessed */
#define FOLL_GET 0x04 /* do get_page on page */
#define FOLL_FORCE 0x10 /* get_user_pages read/write w/o permission */
follow_page_mask
函数一路执行,最后把flag=22
等参数传入并调用follow_page_pte
函数
follow_page_pte
函数因为取到的页表项pte
为 0,因此跳转至标签no_page:
处(由于编译器优化,gdb调试未能显示出来)并接着调用no_page_table
函数
no_page_table
未命中条件判断最终返回NULL至follow_page_pte
,follow_page_pte
返回NULL至follow_page_mask
,follow_page_mask
返回NULL至__get_user_pages
的变量page,
__get_user_pages
函数接着命中条件语句 if (!page)
,调用faultin_page
函数,传foll_flags = 22
至faultin_page
函数的参数flags
faultin_page
函数根据flags
将变量fault_flags
的值设置为0,一路经过handle_mm_fault
、__handle_mm_fault
、handle_pte_fault
最终传入do_fault
函数的形参flags
do_fault
函数根据flags
调用do_read_fault
函数
//如果map_pages函数不为空并且fault_around_bytes有效,
//map_pages是预读的操作函数,fault_around_bytes控制预读长度,一般64k
if (vma->vm_ops->map_pages && fault_around_bytes >> PAGE_SHIFT > 1) {
//调用do_fault_around预读几个页的文件内容读取到vmf->page,为了减少页错误异常的次数
do_fault_around(vma, address, pte, pgoff, flags);
if (!pte_same(*pte, orig_pte))
goto unlock_out;
...
}
原文链接:https://blog.csdn.net/sinat_22338935/article/details/118001063
do_read_fault
函数调用do_fault_around
函数预读取虚拟地址address
周围的几页,之后判断新页表项与旧页表项是否不同(旧页表项为0,因此肯定不同),最终执行到标签unlock_out
处,最终return ret
在do_fault_around
函数执行完之后,页表项有效,且页表项所映射的页为文件的page cache,我们手动打印了新页表项的值,并根据物理地址获取到了page cache的内容,即字符串“this is not a test\n”。
之后do_read_fault
函数将ret = 0
(ret
初始化为0,且此过程中未变过)经由do_fault
、handle_pte_fault
、__handle_mm_fault
、handle_mm_fault
一路返回至faultin_page
函数的ret
变量
faultin_page
函数最终return 0
至__get_user_pages
函数的变量ret
第四次follow_page_mask
__get_user_pages
根据条件语句switch (ret)
和ret
的值为0,跳转至标签retry
处,继续执行,第四次调用follow_page_mask
函数
follow_page_mask
函数调用follow_page_pte
函数
这次follow_page_pte
函数执行的很顺利,能够取到有效的页表项pte
,因此也能获取到正确的page
我们手动输出也能确定这一点。
follow_page_pte
将取到的有效page
通过follow_page_mask
传至__get_user_pages
__get_user_pages
终于能够跳出if (!page)
、switch (ret)
和标签retry
所形成的循环,最终返回至get_user_pages
get_user_pages
又返回至__access_remote_vm
__access_remote_vm
最终调用copy_to_user_page
函数,即memcpy
函数将物理地址为0xc69a1000的数据(即"youaremodified)写入物理地址0xc61b3000
memcpy
与__access_remote_vm
函数执行完之后再次使用continue命令则程序运行结束
在虚拟机中查看文件内容,成功被修改。
漏洞利用思路
基本思路就是在一个进程中创建两个线程,一个线程向只读的映射内存通过write系统调用写入数据,这时候发生写时复制,另一个线程通过madvice来丢弃映射的私有副本,两个线程相互竞争从而向只读文件写入数据。
主线程
- 普通用户身份以只读模式打开指定的只读文件
- 使用MAP_PRIVATE参数映射内存
- 找到目标文件映射的内存地址
- 创建两个线程
procselfmem线程
- 向文件映射的内存区域写数据
- 此时内核采用COW机制
madvise线程
- 使用MADV_DONTNEED参数调用madvise来释放文件映射内存区
- 干扰procselfmem线程的COW过程,产生竞争条件
- 当竞争条件发生时就能成功将数据写入文件
原理总结
正常流程:
- 第一次调用
follow_page_mask
函数,查到页表项pte
为空,因此需要在faultin_page进行缺页处理:分配一个新的页表(匿名页),把file对应的 cache 页复制到新页中,建立该新页与页表项的映射,设置页表项为有效的,脏的(dirty) 的并且是只读的 - 第二次调用
follow_page_mask
函数,由于当前操作为写操作,即flags
为 FOLL_WRITE | FOLL_FORCE |FOLL_GET,但是页表项为只读的,所以该页不具有写权限,需要在faultin_page进行处理,因该页为匿名页,所以去掉flags
的FOLL_WRITE
标签 - 第三次调用
follow_page_mask
函数,此时flags
已经无 FOLL_WRITE 标签,并且页表项有效,因此能够成功顺利返回有效的 page,成功!此时的page
不是file
的cache
页,是对cache
页复制的匿名页,所以对page的任何修改不会造成文件的更改。
POC流程:
- 第一次调用
follow_page_mask
函数,查到页表项pte
为空,因此需要在faultin_page进行缺页处理:分配一个新的页表(匿名页),把file对应的cache
页复制到新页中,建立该新页与页表项的映射,设置页表项为有效的,脏的(dirty) 的并且是只读的 - 第二次调用
follow_page_mask
函数,由于当前操作为写操作,即flags
为 FOLL_WRITE | FOLL_FORCE |FOLL_GET,但是页表项为只读的,所以该页不具有写权限,需要在faultin_page进行处理,因该页为匿名页,所以去掉flags
的FOLL_WRITE
标签 - 第2次调用
follow_page_mask
完成后,第3次调用follow_page_mask
函数前,另一个线程通过madvise函数释放上一步分配的COW页(在我们调试中,通过手动清空页表项模拟 madvise 函数) - 第三次调用
follow_page_mask
函数,因为另一个线程释放了分配的页,所以页表项为空,进入faultin_page
进行缺页处理,因为此时flags
已经无 FOLL_WRITE 标签,所以读取file
的内容,并建立file
对应的cache
页与页表项的映射 - 第四次调用
follow_page_mask
函数,此时flags
已经无 FOLL_WRITE 标签,并且页表项有效,能够成功顺利返回有效的 page,成功!此时的page
为file
的cache
页,所以对page的任何修改都会造成文件的更改。
参考致谢:
gup.c - mm/gup.c - Linux source code v4.4.6 - Bootlin
经典内核漏洞复现之 dirtycow | blingbling's blog (blingblingxuanxuan.github.io)
DirtyCow学习与调试记录-安全客 - 安全资讯平台 (anquanke.com)
回眸——CVE-2016-5195 脏牛漏洞浅析-安全客 - 安全资讯平台 (anquanke.com)
linux內核提权漏洞CVE-2016-5195-二进制漏洞-看雪-安全社区|安全招聘|kanxue.com
linux提权之dirtycow_linux脏牛-CSDN博客
Dirty COW漏洞原理与简单利用_dirtycow-CSDN博客
经典内核漏洞复现之 dirtycow | blingbling's blog (blingblingxuanxuan.github.io)
条件竞争学习 之 DirtyCow分析 | Clang裁缝店 (xuanxuanblingbling.github.io)
标签:fault,pte,page,pages,flags,内核,cnblog,follow,DirtyCOW From: https://www.cnblogs.com/xing1223/p/18366200