章节大纲

      • 让我们假设这台机器非常慢,在 access() 和 fopen() 之间存在 10 秒的时间窗口。为了模拟这种情况,我们可以在两者之间添加一个sleep(10)。程序如下:
        if (!access(fn, W_OK)) {
             sleep(10);
             fp = fopen(fn, "a+");
             ...
         
        加上这行以后,重新编译后的 vulp 程序将暂停 10 秒。你的任务是在这 10 秒内做一些事情,以便当程序在 10 秒后恢复运行时可以帮您添加一个具有root权限的帐户。请演示如何实现这一点。
         
        我们是没法修改文件名 /tmp/XYZ 的,因为它被硬编码在程序里,但是可以通过符号链接来改变其含义。例如,可以将 /tmp/XYZ 重定向到 /dev/null 文件上。当我们向 /tmp/XYZ 写入数据时,实际的内容会写入 /dev/null。以下是一个示例("f" 选项表示如果链接已存在,则先删除旧的链接):
         
        $ ln -sf /dev/null /tmp/XYZ
        $ ls -ld /tmp/XYZ
        lrwxrwxrwx 1 seed seed 9 Dec 25 22:20 /tmp/XYZ -> /dev/null
      • 在前面的任务中,我们实际上是"作弊"了,因为我们要求程序减慢运行速度以便我们发动攻击。这显然不是一个真实的攻击。在这个任务中,我们将执行真正的攻击。在此之前,请确保从  vulp 程序中删除了 sleep() 语句。
         
        竞态条件攻击中的典型策略是在目标程序运行时并行运行攻击程序,希望关键步骤能够在那个时间窗口内完成。当然,这样的概率是很低的,主要是因为那个时间窗口比较短。但我们可以反复进行攻击,直到成功为止。
         
        在模拟攻击过程中,我们使用 "ln -s" 命令来创建或改变符号链接。现在我们需要在程序中做到这一点。可以使用 C 语言中的 symlink() 来创建符号链接。由于 Linux 系统不允许在一个链接已存在的情况下创建新的链接,因此需要先删除旧的链接。以下是一个如何先删除链接再使 /tmp/XYZ 指向 /etc/passwd 的 C 代码片段,请编写你的攻击程序。
         
        unlink("/tmp/XYZ");
        symlink("/etc/passwd","/tmp/XYZ");

        因为我们需要多次运行存在漏洞的程序,所以我们将编写一个程序来做。为了避免手动为 vulp 程序输入内容,可以使用输入重定向。具体做法是将我们的输入保存在一个文件中,并通过 "vulp < inputFile" 来让 vulp 从该文件获取输入(也可以使用管道)。
         
        攻击成功需要一段时间,因此我们需要一种自动检测攻击是否成功的办法。一个简单的办法是监控文件的时间戳。以下是一个 shell 脚本,它运行 "ls -l" 命令,该命令输出有关文件的信息,包括最后修改时间。通过比较此命令的输出与先前产生的输出,我们可以判断文件是否已被修改。
         
        下面的程序循环运行存在漏洞的程序(vulp),它的输入是 echo 通过管道提供的。你需要决定实际输入的内容。如果攻击成功,即密码被修改了,则脚本将停止。你需要有些耐心,攻击成功通常会发生在 5 分钟内。
         
        #!/bin/bash
        
        CHECK_FILE="ls -l /etc/passwd"
        old=$($CHECK_FILE)
        new=$($CHECK_FILE)
        while [ "$old" == "$new" ]     ⬅ 检查 /etc/passwd 是否被修改
        do
           echo "your input" | ./vulp  ⬅ 运行存在漏洞的程序
           new=$($CHECK_FILE)
        done
        echo "STOP... The passwd file has been changed"
         
        当你的脚本终止时,登录到 test 用户,验证是否具有 root 权限。然后在攻击程序的终端窗口中按 Ctrl-C 停止攻击程序。
        如果10分钟后,您的攻击仍未成功,则可以停止攻击,并检查 /tmp/XYZ 文件的所有权。如果此文件的所有者成为 root 用户,请手动删除此文件,然后重试攻击,直到攻击成功。请在实验报告中记录这一观察结果。在任务 2.C 中,我们将解释原因并提供一种改进的攻击方法。
      • 在任务 2.B 中,如果你已正确完成所有操作,但仍无法成功攻击,请检查 /tmp/XYZ 的所有权。 您会发现 /tmp/XYZ 的所有者已成为 root(通常应该是 seed)。 如果发生这种情况,你的攻击将永远不会成功,因为你的攻击程序以 seed 的权限运行,无法再删除或 unlink() 它。 这是因为 /tmp 文件夹有一个“粘性”位,这意味着只有文件的所有者才能删除该文件,即使该文件夹是全局可写的。


        ​在任务 2.B 中,我们让你使用 root 的权限删除 /tmp/XYZ,然后再次尝试你的攻击。而不希望的情况随机发生,因此通过重复攻击(在 root 的“帮助”下),你最终将在任务 2.B 中取得成功。 显然,从 root 获取帮助并不是真正的攻击。 我们想摆脱它,并在没有 root 帮助的情况下做到这一点。

        这种情况发生的主要原因是我们的攻击程序有问题,同样也有一个竞态条件问题,正是我们试图在受害者程序中利用的问题。 (这很有讽刺性!)


        ​攻击程序在删除 /tmp/XYZ(即 unlink())之后,在将名称链接到另一个文件(即 symlink())之前立即执行 access 函数。删除现有符号链接并创建新符号链接的操作不是原子性的(它涉及两个单独的系统调用)。因此,如果函数执行发生在中间,并且目标 Set-UID 程序有机会运行其 fopen(fn, "a+") 语句,它将创建一个以 root 为所有者的新文件。 之后,你的攻击程序将无法再更改 /tmp/XYZ。

        基本上,使用 unlink() 和 symlink() 方法,我们的攻击程序中存在竞态条件。因此,当我们试图利用目标程序中的竞态条件时,目标程序可能会意外地“利用”我们攻击程序中的竞争条件,从而击败我们的攻击。

        为了解决这个问题,我们需要使 unlink() 和 symlink() 原子化。 幸运的是,有一个系统调用可以让我们实现这一点。 更准确地说,它允许我们原子地交换两个符号链接。 下面的程序首先创建两个符号链接 /tmp/XYZ 和 /tmp/ABC,然后使用 renameat2 的系统调用来原子地切换它们。 这允许我们在不引入任何竞争条件的情况下更改 /tmp/XYZ 指向的内容。

        #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");
            renameat2(0, "/tmp/XYZ", 0, "/tmp/ABC", flags);
            return 0; 
        }

        请使用此新策略修改你的攻击程序,然后再次尝试你的攻击。 如果一切都正确完成,你的攻击应该能够成功。