环境变量 (Environment Variables) 是在操作系统中存储配置信息和系统状态的动态值。它们为操作系统和应用程序提供了一种在运行时传递信息的方式。每个进程都可以有自己的环境变量,允许它们在不同的上下文中运行。
例:
PATH: PATH
是一个非常常见的环境变量,它包含了系统用于查找可执行文件的路径列表。当您在命令行中输入一个命令时,系统将在 PATH
中列出的路径中查找该命令的可执行文件。
1 | PATH=/usr/local/bin:/usr/bin:/bin |
set-uid (Set User ID) 是一种在 Unix 和类 Unix 操作系统中用于设置进程特权级别的机制。通过设置 set-uid 位,一个可执行文件将在执行时获得文件所有者的权限,而不是执行者的权限。这意味着普通用户可以以具有文件所有者权限的身份执行该程序,通常这是超出用户普通权限的操作。
Overview
The learning objective of this lab is for students to understand how environment variables affect program
and system behaviors. Environment variables are a set of dynamic named values that can affect the way
running processes will behave on a computer. They are used by most operating systems, since they were
introduced to Unix in 1979. Although environment variables affect program behaviors, how they achieve
that is not well understood by many programmers. As a result, if a program uses environment variables, but
the programmer does not know that they are used, the program may have vulnerabilities.In this lab, students will understand how environment variables work, how they are propagated from
parent process to child, and how they affect system/program behaviors. We are particularly interested in how
environment variables affect the behavior of Set-UID programs, which are usually privileged programs.
This lab covers the following topics:
• Environment variables
• Set-UID programs
• Securely invoke external programs
• Capability leaking
• Dynamic loader/linker
实验资料网址:https://seedsecuritylabs.org/Labs_20.04/Software/Environment_Variable_and_SetUID/
实验要求见实验说明PDF。
简单操作环境变量。
输入printenv或者env命令查看环境变量:
使用export和unset命令设置环境变量。
验证环境变量会不会传递到子进程。
实验步骤:
第一步:先编译Labsetup中的myprintenv.c文件。
这个程序在子进程和父进程中验证各自的环境变量是否相同,但父进程只是创建后就退出,没有打印环境变量,而子进程打印了环境变量后也退出。
第二步:将子进程输出的环境变量保存至child.txt文件。
第三步:修改代码,重新编译c文件,使这一次打印父进程的环境变量。
第四步:将父进程输出的环境变量保存至parent.txt文件。
第五步:比较child.txt和parent.txt。
diff和cmp都没有输出,表明这两个文件内容是一样的。
即在这段代码中,子进程和父进程的环境变量是一样的。当子进程被创建时,它会继承父进程的环境变量。这意味着子进程将具有与父进程相同的环境变量,包括相同的变量名称和值。因为 fork() 函数会复制父进程的环境,包括环境变量。
关于environ:
extern char **environ; 是一个在C程序中用于访问环境变量的声明。这个声明用于引用一个全局变量,这个全局变量通常包含了当前进程的环境变量的信息。
environ 是一个指向指针数组的指针,每个指针指向一个以 key=value 格式表示的环境变量字符串。
通过访问 environ 这个全局变量,你可以获取当前进程的环境变量列表。
在Unix-like操作系统中,环境变量是一种在操作系统级别用于存储配置信息和参数的机制,它们通常以字符串的键值对形式存在。通过 environ 变量,你可以访问这些环境变量并读取它们的值。
通常,可以通过循环遍历 environ 数组,以获取和操作环境变量的值。
当使用execve()时,会产生system call,然后通过此system call来加载命令并执行。当调用 execve() 时,它会将当前进程替换为新程序,因此它本身不会返回到原始程序的源代码。这是 execve() 的关键行为,它用于执行新程序,并不会返回到调用它的地方。但是本质上还是在调用execve()的程序之中运行的命令,本次task探讨通过execve()执行命令的过程中的环境变量。
实验步骤:
第一步:编译初始的myenv.c文件并运行。
初始的myenv.c内容:
编译运行:
分析:
将 envp 参数设置为 NULL,execve() 在执行新程序时会清空环境变量,这导致了没有输出。
第二步:将execve()函数中的第三个参数envp设为全局变量environ。
重新编译并执行:
分析:
传入了 environ 变量作为 envp 参数,因此 /usr/bin/env 将继承当前进程的环境变量。在这种情况下,environ 包含当前进程的环境变量,所以 /usr/bin/env 将使用当前进程的环境变量。
第三步:对新程序的环境变量的总结
execve 函数在执行命令时的环境变量取决于传递给它的 envp 参数。envp 参数是一个指向字符串指针数组的指针,其中包含了新程序执行时的环境变量。这个参数允许你自定义新程序的环境变量。
具体来说,分析 execve 函数执行命令时的环境变量涉及以下几个方面:
继承当前进程的环境变量:如果你将 envp 参数设置为 environ,execve 将继承当前进程的环境变量,即当前进程的环境变量将成为新程序的环境变量。这意味着新程序将具有与当前进程相同的环境变量。
设置自定义环境变量:你可以通过设置 envp 数组来自定义新程序的环境变量。在 envp 中,每个元素都是一个以 key=value 格式表示的环境变量字符串。例如,envp[0] = “jay1an=755”; 将设置一个名为 jay1an 的环境变量,其值为 755。
清除环境变量:如果你不传递 envp 参数(即将其设置为 NULL),新程序将不具有自定义环境变量,而只会使用系统默认的环境变量。
总之,execve 函数允许你对新程序的环境变量进行精确控制。你可以选择继继承当前进程的环境变量,添加、修改或删除特定的环境变量,或者在不传递 envp 参数的情况下,让新程序使用默认的环境变量。这提供了灵活性,使你能够满足特定应用场景的需求。
本次task探究通过system()函数执行命令的环境变量。
实验步骤:
编译以下代码并运行:
运行a.out发现打印出了当前程序的环境变量,并且在system()函数结束后执行了下面的printf代码,这与execve()不同。
……
分析:
system 函数是一个高级接口,它允许执行一个外部命令,并等待该命令完成。
在system函数的内部,使用了fork创建子进程,然后在子进程中调用execl函数,execl进而调用execve函数,同时把环境变量传递给execve,因为子进程是fork出来的,所以环境变量与原进程是相同的。并且父进程也会继续执行,所以printf语句会被执行。
system 函数用于执行外部命令,并等待其执行完成,环境变量与原程序相同,而 execve 函数用于在当前进程内启动新程序,并控制新程序的环境,且不会返回到原始程序的源代码。
一个 Set-UID程序是一个在执行时具有文件所有者的用户权限的程序。这意味着,无论哪个用户运行该程序,它都以程序文件所有者的权限运行。这通常用于允许普通用户执行需要更高特权级别的操作的程序。
虽然程序的运行轨迹是由程序代码本身的逻辑决定的,但是执行它的用户仍然可以通过改变环境变量的方式来影响程序的运行。
本次task的目的就是探究Set-UID程序的环境变量。
实验步骤:
第一步:编写一个可以print当前程序的环境变量的c程序
第二步:编译c文件,将拥有者设为root,使其变成一个Set-UID程序
第三步:在普通用户的shell中,使用export命令设置以下的环境变量:
最后编译运行task5这个Set-UID程序,shell会fork一个子进程,由子进程来运行这个程序。该程序会打印出子进程的环境变量。
但是使用grep命令查找刚才修改过的三个环境变量,发现LD_LIBTATY_PATH没有被继承,但是其余两个都成功被继承了:
分析:
当运行一个 Set-UID 程序时,环境变量 LD_LIBRARY_PATH 通常不会被继承。这是因为 Set-UID 程序是以文件所有者的权限来执行的,而不是调用者的权限,为了增加安全性,系统通常会限制一些环境变量的继承,以避免潜在的滥用和安全漏洞。
特别是,LD_LIBRARY_PATH 是一个环境变量,用于指定动态链接器查找共享库的路径。如果它被继承,那么恶意用户可能会设置一个恶意的 LD_LIBRARY_PATH,以引导程序加载恶意共享库,从而引发安全漏洞。
因此,在安全性考虑下,系统通常会限制 Set-UID 程序对 LD_LIBRARY_PATH 的继承。这意味着 Set-UID 程序将使用默认的共享库路径,而不是根据 LD_LIBRARY_PATH 进行动态库加载。
在Set-UID程序中调用system()函数是非常危险的,这样的话,shell程序的实际行为会受到环境变量的影响。因为环境变量是由运行用户提供的,恶意的用户可以通过修改环境变量来控制程序的运行行为。
本次task的目标就是通过修改环境变量来实现对Set-UID程序的攻击。
实验步骤:
第一步:编写c程序,使用system函数执行ls功能,编译并使其成为Set-UID程序。
编译并修改为Set-UID程序:
运行效果:
第二步:修改环境变量,改变程序行为。
使用export PATH=’.’:$PATH将当前目录放在PATH的最前面。
这样system(“ls”)函数就会先从当前目录寻找。
如果我们在当前目录下创建一个ls可执行文件,那么系统就会执行我们所创建的ls文件,这样就可以控制程序的行为了。
创建ls.c并编译为名为ls的可执行文件:
由于直接修改了PATH,我们在当前shell运行ls命令,也会定位到当前目录的ls可执行文件,并不会打印当前的目录信息。
那么此时,我们执行Set-UID程序task6,发现它的程序行为也发生了改变。
分析:
因为system()函数运行的命令的环境变量与原程序相同,所以改变原程序的PATH可以影响system()函数的运行环境,进而改变程序的行为。
当运行 ls 命令时,系统会在 PATH 中指定的目录中查找 ls 命令的可执行文件。如果 ls 可执行文件存在于 PATH 中的某个目录中,系统就会执行该文件。在本次task中,PATH中的第一个值是当前目录,那么系统就会先在当前目录寻找ls,所以系统会执行当前目录下的ls,而不会执行/usr/bin/ls。
获得ROOT权限:
先执行命令:
修改 task6.c的代码,使用system()函数打开一个shell,由于task6是一个Set-UID程序,由task6打开的shell权限必然也是root的。
task6.c内容:
编译运行,验证是否为ROOT:
运行task6发现打开了一个新的shell,并且是root权限,表明我们成功通过Set-UID程序打开了ROOT shell。
LD PRELOAD
Environment Variable and Set-UID
Programs本次task仍然继续探究Set-UID程序是如何处理环境变量的,主要探究有关动态链接的LD_PRELOAD。
实验步骤:
第一步:探究LD_PRELOAD如何影响动态链接器。
创建以下的程序,并命名为mylib.c,它覆盖了sleep的功能。
编译该程序,生成动态链接库
设置LD_PRELOAD的值
最后,编译并运行一个带有sleep()函数的C文件
运行:
发现运行的sleep函数是我们刚自定义的sleep函数。
第二步:在不同的条件下运行mysleep程序
① mysleep作为普通程序,在普通用户权限下运行。
② mysleep作为Set-UID程序,在普通用户权限下运行
发现其并没有运行我们自定义的sleep函数,即LD_PRELOAD没有被继承到Set-UID程序的运行环境中。
③mysleep作为Set-UID程序,在管理员权限下重新export LD_PRELOAD并运行mysleep
在ROOT用户下,mysleep运行的是自定义的sleep函数,即LD_PRELOAD起作用了。
④ mysleep作为Set-UID程序,但是owner为user1,现使用user2(非ROOT)重新export LD_PRELOAD并运行mysleep
创建新用户jay1an:
将mysleep的owner改为jay1an,并重设为Set-UID程序:
以seed的身份执行mysleep程序:
显然,并没有执行我们自定义的sleep函数,即LD_PRELOAD未起作用。
第三步:思考第二步中为什么运行同一个程序,但是却出现了不同的结果。
未成功调用我们自定义的sleep函数的原因就是LD_PRELOAD并没有继承到子程序中。
system()
versus execve()
虽然system()和execve()都可以用来运行新程序,但是如果在特权程序中使用system()是相当危险的,例如Set-UID程序。我们已经看到 PATH 环境变量如何影响 system() 的行为,因为PATH会影响 shell 的工作方式。 execve() 就没有问题,因为它不调用 shell。调用 shell 还有另一个危险的后果,这次,它与环境变量无关。
实验步骤:
第一步:编译catall.c文件
如果catall 是普通程序,那么无法通过其查看有root权限的文件。
第二步:设置为Set-UID程序
这样就可以访问需要ROOT权限的文件了。
第三步:尝试仅使用catall修改系统文件,跨权限修改或者删除一些文件
先创建一个仅能root用户读写的文件zzrootfile作为测试:
但是我们阅读代码会发现,主要问题在于这段代码使用 sprintf 将命令构建为一个字符串,而没有对参数进行任何验证或过滤。
我们可以提供提供恶意的文件名,比如使用分号或者管道符号,使得程序可以执行我们指定的命令。例如获得ROOT shell。
使用分号将/bin/zsh命令隔开,system也会处理/bin/zsh命令,唤起一个shell,又因为catall程序是一个Set-UID程序,所以可以唤起一个ROOT shell,这样即可随意对系统文件进行修改。
第四步:修改catall.c,注释掉system函数,使用execve函数执行命令,并尝试第三步中的行为还能否成功实施攻击。
重新将其设为Set-UID程序:
结果表明这种方法已经不能奏效。
为什么不奏效:
system(command) 使用一个字符串变量 command 来构建整个命令行,这意味着可以在 command 中编写完整的命令,包括命令和参数。这使得 system 更容易使用,但也更容易受到命令注入攻击的威胁。
execve(v[0], v, NULL) 使用一个参数数组 v 来指定可执行文件和其参数。这种方式更加安全,因为参数是在数组中明确指定的,而不是从一个字符串中解析。这有助于防止命令注入攻击。
简而言之,execve比system更安全。
Set-UID 程序允许普通用户以高特权级别执行程序,但根据”最小权限原则”,它们通常在不再需要高特权时放弃这些权限。这是为了确保以最小权限运行程序,以降低潜在的安全风险。
setuid() 系统调用用于撤销进程的高特权状态。当 Set-UID 程序以高特权执行时,通过调用 setuid(n),可以将其特权降级为普通用户,设置其RUID、EUID 和SUID 为 n。
Real User ID(真实用户标识符,RUID):RUID 是进程实际所属的用户的标识符。这是进程启动时分配给它的用户标识符。
Effective User ID(有效用户标识符,EUID):EUID 是决定进程当前权限级别的用户标识符。当进程需要执行某些操作时,操作系统会检查 EUID,以确定进程是否具有足够的权限来执行这些操作。EUID 可能会在进程运行期间改变,通常是通过调用系统调用来改变权限级别。
Saved Set-User-ID(保存的设置用户标识符,SUID):SUID 是进程启动时分配给它的用户标识符的一个备份,用于在必要时将 EUID 恢复到原始的 RUID。
本次task介绍了”capability leaking” 的漏洞。这种漏洞发生在权限降级时,可能导致程序仍然保留了一些特权能力。尽管EUID 已降级为非特权用户,但由于特权能力未被清理,程序仍然具有特权。
实验步骤:
第一步:创建/etc/zzz文件,并将权限设置为0644
第二步:编译cap_leak.c文件,owner设为root并将其改为Set-UID程序
第三步:执行cap_leak程序,理解特权泄露漏洞
根据代码分析,我们成功用root权限打开了/etc/zzz文件,但又立即收回了特权,降级为普通用户,所以后来打开的shell是普通用户权限下的shell。
由于程序在打开 /etc/zzz 文件后并没有关闭文件描述符 fd,因此在新的 /bin/sh shell 中,可以继续使用 fd 来修改 /etc/zzz 文件,因为文件描述符是可以在同一进程中传递的。
在这个情况下,fd 在新的 /bin/sh shell 中仍然保持打开状态,所以尽管程序已经禁用了特权。但仍然可以使用 /bin/sh shell 来执行针对fd=3的文件操作,即随意写入数据到/etc/zzz文件,而不需要特权。
这个漏洞强调了正确的权限管理的重要性,尤其是在使用 Set-UID 程序时,开发者必须小心确保特权和文件描述符等资源受到适当的限制和控制。