Set-UID 特权程序实验
章节大纲
-
本实验的学习目的是理解环境变量如何对程序和系统产生影响。环境变量是一组动态的命名值,可以影响计算机上进程的行为。自 1979 年被引入 Unix 系统以来,环境变量被大多数操作系统使用。尽管环境变量会影响程序行为,但许多程序员对其工作原理并不熟悉。因此,如果一个程序使用了环境变量,但程序员对此并不了解,那么该程序可能存在漏洞。在本实验中,我们将学习环境变量的工作原理、它们如何从父进程传入到子进程,以及它们如何影响系统/程序的行为。我们尤其关注环境变量如何影响 Set-UID 程序的行为,这些程序通常是特权程序。本实验涵盖以下主题:
- 环境变量
- Set-UID 程序
- 安全调用外部程序
- 权限泄露
-
本实验在 SEEDUbuntu20.04 VM 中测试可行。你可以在本页面右端选择虚拟机版本为 SEEDUbuntu20.04,点击“创建虚拟机”来获取虚拟机平台的临时用户名与密码,登录虚拟机平台即可获得一台预先构建好的 SEEDUbuntu20.04 VM,该虚拟机以及用户名密码将在开启 24 小时后自动销毁。你也可以在其他 VM、物理机器以及云端 VM 上自行配置环境进行实验,但我们不保证实验能在其他 VM 下成功。实验所需的文件可从下方下载,解压后会得到一个名为 Labsetup 的文件夹,该文件夹内包含了完成本实验所需的所有文件。
-
在此任务中,我们来学习可以用来设置和取消环境变量的命令。我们使用 SEED 账户中的 Bash。用户使用的默认 shell 在 /etc/passwd 文件中已设置(每条记录的最后一个字段)。你可以使用命令 chsh 将其更改为其他 shell 程序(但请勿在本实验中更改)。请完成以下任务:
-
使用 printenv 或 env 命令打印出环境变量。如果您对某些特定的环境变量感兴趣,例如 PWD,可以使用 "printenv PWD" 或 "env | grep PWD"。
-
使用 export 和 unset 设置或取消环境变量。需要注意的是,这两个命令不是独立的程序,而是 Bash 的内部命令(你无法在 Bash 之外找到它们)。
-
-
在此任务中,我们研究子进程如何从其父进程获取环境变量。在 Unix 系统中,fork() 通过复制调用进程来创建新进程。新进程(称为子进程)是调用进程(称为父进程)的副本。然而,有些内容并不会被子进程继承(请通过命令 man fork 查看 fork() 的手册)。在此任务中,我们想了解父进程的环境变量是否会被子进程继承。
-
在本任务中,我们研究如果使用 execve() 执行新程序,环境变量会受到怎样的影响。execve() 函数会调用系统调用来加载并执行新的程序。该函数不会创建新的进程,而是会用新的程序的内容覆盖进程的代码和数据区域,也就是说,execve() 是在调用进程的内部运行新程序。我们关注的是环境变量的变化,它们是否会被保留在新程序中?
-
在本任务中,我们研究如果通过 system() 函数来执行新程序,环境变量会受到什么影响。 system() 函数用于执行一个命令,但不同于直接执行命令的 execve(),system() 实际执行的是 "/bin/sh -c command",即它会先执行 /bin/sh,然后让 shell 来执行命令。
如果查看 system() 函数的实现,可以发现它使用 execl() 来执行 /bin/sh,execl() 会调用 execve() ,同时将环境变量传递给这个系统调用。因此,使用 system() 时,调用进程的环境变量会传递给新程序 /bin/sh。请编译并运行以下程序以验证这一点。
#include <stdio.h> #include <stdlib.h> int main() { system("/usr/bin/env"); return 0 ; }
-
Set-UID 是 Unix 操作系统中一个重要的安全机制。当一个 Set-UID 程序运行时,它会获得文件所有者的权限。例如,如果程序的所有者是 root,那么任何人运行这个程序时,该程序在运行期间都会获得 root 权限。Set-UID 机制让我们实现了许多有用的功能,但由于它会提升用户权限,因此风险较大。尽管 Set-UID 程序的行为由其程序逻辑决定,不是由用户决定,但用户可以通过环境变量影响程序行为。为了理解 Set-UID 程序如何受到环境变量影响,我们首先要弄清楚 Set-UID 程序的进程是否会从用户的进程中继承环境变量。
-
由于 system() 会调用 shell 程序,在 Set-UID 程序中调用 system() 是非常危险的。这是因为 shell 程序的行为可能受到环境变量(如 PATH)的影响。这些环境变量是由用户提供的,而用户是不可信的。通过更改这些变量,恶意用户可以控制 Set-UID 程序的行为。在 Bash 中,你可以通过以下方式更改 PATH 环境变量(此示例将目录 /home/seed 添加到 PATH 环境变量的开头):
$ export PATH=/home/seed:$PATH
下面的 Set-UID 程序应该执行 /bin/ls 命令,然而,程序员只使用了相对路径来执行 ls 命令,而不是绝对路径:
int main() { system("ls"); return 0; }
请编译上述程序,将其所有者改为 root,并将其设置为 Set-UID 程序。你能否让这个 Set-UID 程序运行你自己的恶意代码,而不是 /bin/ls?如果可以,你的恶意代码是否以 root 权限运行? 描述并解释你的观察结果。
注意:
system(cmd) 函数会首先执行 /bin/sh 程序,然后让该 shell 程序运行 cmd 命令。在 Ubuntu 20.04(以及之前的几个版本)中,/bin/sh 实际上是一个指向 /bin/dash 的符号链接。该 shell 程序有一种防范措施,可以防止自己在 setuid 进程中被执行。如果 dash 检测到它在 Set-UID 进程中执行,它会立即将有效用户 ID 更改为进程的真实用户 ID,从而放弃特权。
由于我们的受害程序是一个 Set-UID 程序,/bin/dash 中的对策可以阻止我们的攻击。 为了查看没有这种对策的情况下攻击的效果,我们将 /bin/sh 链接到另一个没有此防范措施的 shell,我们已在 Ubuntu 20.04 虚拟机中安装了一个这样的名为 zsh 的 shell 程序。我们使用以下命令将 /bin/sh 链接到 /bin/zsh:
$ sudo ln -sf /bin/zsh /bin/sh
-
在本任务中,我们研究 Set-UID 程序如何处理一些环境变量。包括 LD_PRELOAD、LD_LIBRARY_PATH 以及其他 LD_* 在内的多个环境变量会影响动态加载器/链接器的行为。 动态加载器/链接器是操作系统的一部分,它负责将共享库加载到内存并进行链接。
在 Linux 系统中,ld.so 或 ld-linux.so 是动态加载器/链接器(每个针对不同类型的二进制文件)。在影响其行为的环境变量中,LD_LIBRARY_PATH 和 LD_PRELOAD 是本实验关注的两个变量。加载器/链接器首先需要找到程序需要的共享库,在查找标准目录之前会首先搜索 LD_LIBRARY_PATH 环境变量里指定的目录。如果 LD_PRELOAD 环境变量存在,它里面指定了用户定义的一组共享库,这些库会在所有其他库之前被加载。本任务仅研究 LD_PRELOAD。
-
虽然 system() 和 execve() 都可以用来运行新程序,但如果在特权程序(例如 Set-UID 程序)中使用 system(),则可能非常危险。我们已经看到 system() 如何受 PATH 环境变量的影响,因为该变量会影响 shell 的行为。而 execve() 没有这个问题,因为它不会调用 shell。调用 shell 还有另一个危险的后果,这次与环境变量无关。让我们看以下场景。
Bob 在一家审计机构工作,他需要调查一家公司是否存在欺诈行为。为了调查,Bob 需要能够读取公司 Unix 系统中的所有文件。但是为了保护系统的完整性,Bob 不应该能够修改任何文件。为此,系统的超级用户 Vince 编写了一个特殊的 set-root-uid 程序(见下文),并授予 Bob 可执行权限。该程序要求 Bob 在命令行输入一个文件名,然后它将运行 /bin/cat 来显示指定的文件。由于程序以 root 权限运行,它可以显示 Bob 指定的任何文件。然而,由于该程序没有写的操作,Vince 确信 Bob 无法使用这个特殊程序修改任何文件。
int main(int argc, char *argv[]) { char *v[3]; char *command; if(argc < 2) { printf("Please type a file name.\n"); return 1; } v[0] = "/bin/cat"; v[1] = argv[1]; v[2] = NULL; command = malloc(strlen(v[0]) + strlen(v[1]) + 2); sprintf(command, "%s %s", v[0], v[1]); // 仅使用以下两种方法之一。 system(command); // execve(v[0], v, NULL); return 0 ; }
-
为了遵循最小权限原则,Set-UID 程序通常会在不再需要权限时永久放弃其 root 权限。此外,有时程序需要将控制权移交给用户,所以在交权之前,Set-UID 程序会放弃 root 权限。我们可以使用 Set-UID() 系统调用来撤销权限。Set-UID() 设置调用进程的有效用户 ID,但如果调用者的有效 UID 为 root,则实际 UID 和保存的 set-user-ID 也会被设置。因此,如果具有有效 UID=0 的 Set-UID 程序调用 Set-UID(N),则进程将成为普通进程,其所有 UID 都将被设置为 N。
在撤销权限时,一个常见的错误是权限泄漏。进程在仍具有权限时可能已经获得了一些特权功能。当权限被降级时,如果程序没有清除这些功能,它们可能仍然可以被非特权进程访问。换句话说,尽管进程的有效用户 ID 已变为非特权用户,但进程仍可能拥有特权。
编译以下程序,将其所有者更改为 root,并使其成为一个 Set-UID 程序。以普通用户身份运行该程序。你能利用该程序中的权限泄漏漏洞吗?你的目标是做到能够以普通用户的身份修改 /etc/zzz 文件。
void main() { int fd; char *v[2]; /* 假设 /etc/zzz 是一个重要的系统文件, * 并且它的所有者是 root,权限是 0644。 * 在运行此程序之前,你应先创建 /etc/zzz 文件。 */ fd = open("/etc/zzz", O_RDWR | O_APPEND); if (fd == -1) { printf("Cannot open /etc/zzz\n"); exit(0); } // 打印文件描述符值 printf("fd is %d\n", fd); // 通过将有效 uid 设置为与实际 uid 相同来永久放弃特权 setuid(getuid()); // 执行 /bin/sh v[0] = "/bin/sh"; v[1] = 0; execve(v[0], v, 0); }