网络攻防技术——环境变量与set-uid实验

文章详细介绍了通过一系列实验让学生理解环境变量对程序行为的影响,特别是SET-UID程序的特性,以及如何安全地调用外部程序和处理环境变量泄漏。实验涉及环境变量的设置与传递、Set-UID程序的行为、system()与execve()的区别、能力和权限管理等。
摘要由CSDN通过智能技术生成

一、实验主题

    本实验的学习目标是让学生了解环境变量如何影响程序以及系统行为。环境变量是一组动态命名值,可以影响正在运行的进程将在计算机上运行。大多数操作系统都使用它们,因为它们是1979年引入Unix。尽管环境变量会影响程序行为,但它们是如何实现的这一点很多程序员都不太理解。因此,如果程序使用环境变量程序员不知道它们被使用,程序可能有漏洞。

    在本实验中,学生将了解环境变量是如何工作的,它们是如何从父进程到子进程,以及它们如何影响系统/程序行为。我们特别感兴趣的是如何环境变量影响Set-UID程序的行为,这些程序通常是特权程序。

    本实验室涵盖以下主题:

•环境变量

•SET-UID程序

•安全地调用外部程序

•能力泄漏

•动态加载程序/链接器

二、实验环境

SEED Ubuntu 20.04 VM

三、实验内容

Task 1: Manipulating Environment Variables

实验目的:

    在这个任务中,我们将研究可用于设置和取消设置环境变量的命令。

实验内容:

    1. 使用env或者printenv命令打印环境变量。如果想要查看特定的环境变量,如PWD,可以使用 "printenv PWD" 或者 "env | grep PWD"。

    2. 使用export和unset来设置或者取消环境变量。

    这里export设置了一个环境变量Lab6_Date并赋值为日期,使用printenv查看,成功打印值。

    这里释放了环境变量Lab6_Date,再次使用printenv查看,返回为空,说明变量不存在。Task1完成。

Task 2: Passing Environment Variables from Parent Process to Child Process

实验目的:

    在这个任务中,我们研究一个子进程如何从其父进程中获取其环境变量。在Unix中,fork()通过复制调用进程来创建一个新的进程。新进程(称为子进程),是调用进程(称为父进程)的完全副本;但是,子进程没有继承一些东西(输入 man fork可以查看fork()手册具体内容)。在此任务中,我们想知道父进程的环境变量是否被子进程继承。

实验内容:

    1. 编译并运行myprintenv.c,观察并描述你看到的结果。

    可以看到,程序将全部的环境变量输出出来保存在了Task2_out中。

    2. 现在将子进程case中的printenv()注释掉,然后将父进程case中的printenv()取消注释。再次编译运行,将结果保存在另一个文件中,观察并描述。

    3. 使用diff比较两个输出文件,得出结论。

    用diff比较两个输出文件,可以发现基本是相同,这里不同可能是因为我使用vscode远程连接导致的,正常seed应该没有这个环境变量。

    我在seed虚拟机中重复了一次实验,使用diff没有输出,两文件相同,可以证明fork()产生的子进程继承了父进程的环境变量。Task2完成。

Task 3: Environment Variables and execve()

实验目的:

    在这个任务中,我们将研究通过 execve() 执行新程序时环境变量如何受到影响。execve()调用系统调用以加载新命令并执行它。此函数永不返回,没有创建新进程;相反,调用进程的文本、数据、bss和堆栈被加载程序覆盖。本质上,execve()会在调用进程中运行新程序。我们感兴趣的是环境变量发生了什么,它们会被新程序自动继承吗?

实验内容:

    1. 编译并运行myenv.c,观察并描述你看到的结果。

    该程序会运行程序/usr/bin/env,打印出当前进程的环境变量。

    根据结果,程序运行没有任何输出。

    2. 修改execve()参数NULL为 environ,再次编译运行观察结果。

    可以看到程序运行后打印出了所有的环境变量(没有截完)。

    3. 请得出新程序如何获得环境变量的结论。

    在(1)中我们传入空指针NULL,新程序没有打印任何环境变量。说明execve()在启动一个新程序时,在此场景中所有内存空间将被覆盖,旧的环境变量都会丢失。

    每个程序都有一个环境表,它是一个字符指针数组,其中每个指针包含一个以NULL结尾的C字符串的地址。全局变量environ则包含了该指针数组的地址

    在(2)中我们将environ传递给execve(),新程序得到了旧的环境变量指针数组的地址,就可以执行命令将环境变量打印出来。Task3完成。

Task 4: Environment Variables and system()

实验目的:

    在这个任务中,我们将继续通过system() 执行新程序时环境变量如何受到影响。这个函数用于执行命令,但与execve() 直接执行命令不同的是,system() 实际上执行 "/bin/sh -c command",即它执行 ”/bin/sh”,然后要求bash执行命令。

    如果你查看system() 的源代码,可以发现它使用execl() 来执行 /bin/bash。execl() 会调用execve(),并将环境变量数组传递给它。因此,使用system() 后,当前进程的环境变量会传递给新程序 /bin/bash 。

实验内容:

    编译运行以下程序来证明使用system() 后,当前进程的环境变量会传递给新程序 /bin/bash 。

    成功打印了环境变量,证明成功。Task4完成。

Task 5: Environment Variable and Set-UID Programs

实验目的:

    Set-UID是Unix操作系统中一个重要的安全机制。当一个Set-UID程序运行时,它会获取所有者的权限。例如,如果一个程序的所有者为root,当任何人运行这个代码时,这个程序都会在它运行时获取root的权限。Set-UID允许我们做很多有趣的事情,但由于它升级了使用者的权限,这是相当冒险的。虽然Set-UID程序的行为是由其程序逻辑决定的,而不是由用户决定的,但用户确实可以通过环境变量来影响这些行为

    要了解Set-UID程序是如何受到影响的,让我们首先弄清楚Set-UID程序的进程是否从用户的进程中继承了环境变量

实验内容:

1. 编写代码,该代码能够打印出当前进程的所有环境变量:

    2. 编译上述程序,将其所有权更改为root,并使其变为Set-UID程序

    3. 在普通用户(非root用户)的shell中,使用export命令设置下列环境变量的值(它们可能已经存在):

    这些环境变量是在用户进程中设置的。现在在用户shell中运行(2)中的代码,在shell中输入程序名后,shell将产生子进程,并使用子进程运行该程序。请检查你在shell进程(父进程)中设置的所有环境变量是否都进入了Set-UID子进程,描述你的观察结果。

    通过观察,设置的三个环境变量中,只有LD_LIBRARY_PATH没有进入Set-UID子进程。LD_LIBRARY_PATH是Linux环境变量名,该环境变量主要用于指定查找共享库(动态链接库)时除了默认路径之外的其他路径。在Set-UID子进程中不继承该环境变量可能是为了安全性考虑,防止攻击者利用该机制进行攻击。Task5完成。

Task 6: The PATH Environment Variable and Set-UID Programs

实验目的:

    因为调用了shell程序,在一个Set-UID程序中调用system() 是相当危险的。这是因为shell程序的实际行为能够被环境变量影响,例如PATH;而提供这些环境变量的用户,可能是恶意的。通过修改环境变量,恶意用户就可以控制Set-UID程序的行为。

    在Bash中,您可以通过以下方式更改PATH环境变量(本示例会将目录/home/seed添加到PATH环境变量的开头):

    下面的Set-UID程序应该执行/bin/ls命令;但是,程序员只使用ls命令的相对路径,而不是绝对路径:

    在Ubuntu20.04(以及之前的几个版本)中,/bin/sh实际上是一个指向/bin/dash的符号链接。这个shell程序有一个对策,可以防止自己在Set-UID进程中被执行。基本上,如果dash检测到它是在Set-UID进程中执行的,它会立即将有效的用户ID更改为进程的真实用户ID,本质上是放弃了特权。

    由于我们的受害者程序是一个Set-UID程序,因此在/bin/dash中的对策可以防止我们的攻击。为了了解我们的攻击在没有这种对策的情况下如何工作,我们将把/bin/sh链接到另一个没有这种对策的shell。实验设计者已经在Ubuntu 20.04 VM中安装了一个名为zsh的shell程序。我们使用以下命令将bin/sh链接到bin/zsh:

实验内容:

    请编译上述程序,将其所有者更改为root,并将其设置为Set-UID程序。你能让这个Set-UID程序运行你自己的恶意代码,而不是/bin/ls吗?如果可以,你的恶意代码是否使用root权限运行?描述并解释你的观察结果。

    1. 编写上述代码,编译,将其所有者更改为root,并将其设置为Set-UID程序:

    2. 运行该程序,可以发现其正常输出命令ls的内容:

    3. 编写恶意代码:

    编译并命名为ls:

    4. 为了让该Set-UID程序运行我们的恶意代码,我们可以将我们存放恶意代码的路径添加在PATH的前面,那么shell执行ls时就会优先到我们设定的路径中寻找目标:

    5. 再次运行该程序,观察结果:

    成功执行了我们的恶意代码,Task6完成。

Task 7: The LD PRELOAD Environment Variable and Set-UID Programs

实验目的:

    在这个任务中,我们将研究Set-UID程序如何处理一些环境变量。包括 LD_PRELOAD, LD_LIBRARY PATH,和其他LD_*,都会影响动态加载器/链接器的行为。动态加载器/链接器是操作系统(OS)的一部分,它负责加载(从持久存储器加载到RAM),并在运行时链接可执行文件所需的共享库。

    在Linux中,ld.so或ld-linux.so是动态加载器/链接器(每种都用于不同类型的二进制文件)。在能够影响程序行为的环境变量中,本实验关注LD_LIBRARY PATH 和LD_PRELOAD这两个。在Linux中,LD_LIBRARY_PATH环境变量是一个以冒号分隔的目录集,用于在程序加载运行期间查找动态链接库时指定除了系统默认路径之外的其他路径,注意,LD_LIBRARY_PATH中指定的路径会在系统默认路径之前进行查找。LD_PRELOAD指定了要在所有其他库之前加载的其他用户指定的共享库的列表。在本次Task中,我们只研究LD_PRELOAD。

实验内容:

    1. 首先,我们将看到这些环境变量如何在运行正常程序时影响动态加载器/链接器的行为。

    让我们建立一个动态链接库。创建以下程序,并将其命名为mylib.c。它覆盖了libc中的sleep() 函数:

    使用下述命令编译上述代码:

    然后设置环境变量LD PRELOAD:

    在与动态链接库相同目录下编译程序myprog.c:

    2. 在完成上述操作后,请在以下不同条件下运行myprog,并观察会发生什么:

    (1)将myprog作为一个常规程序,以普通用户身份运行:

    sleep() 的功能被成功覆盖。

    (2)将myprog设置为一个Set-UID root权限的程序,并以普通用户的身份运行它。

    程序运行后等待了一秒后结束运行,运行了正常的sleep() 功能。

    (3)将myprog设置为一个Set-UID root权限的程序,在root帐户中再次使用export设置环境变量LD_PRELOAD再运行。

    sleep()再次被覆盖。

    (4)将myprog设置为一个Set-UID user1权限的程序(这里我用的seed用户),然后再另一个用户user2(不能是root用户,此处我使用wion03)的进程中再次设置环境变量LD_PRELOAD再运行myprog。

    程序停顿一秒后结束,说明sleep() 没有被覆盖。

    3. 设计实验,说明上述不同行为运行相同程序产生不同结果的原因。这里我们借鉴Task2中样例程序的思路,分别输出父子进程中的环境变量LD_PRELOAD,下面是修改后的代码:

    注释掉不同case的提示输出和printenv(),该程序能够分别打印出父子进程中的LD_PRELOAD环境变量(如果存在的话)。编译该代码,生成Task7。

    现在我们模拟上述四个不同的方法,运行程序检验父子进程的环境变量:

    (1)将Task7作为一个常规程序,以普通用户身份运行:

    通过两次检验父进程和子进程,可以观察到父进程的LD_PRELOAD成功传递给子进程,故该方法成功覆盖了sleep()。

    (2)将Task7设置为一个Set-UID root权限的程序,并以普通用户的身份运行它。

    可以看到子进程中没有LD_PRELOAD环境变量,故运行myprog时sleep()正常运行。

    (3)将Task7设置为一个Set-UID root权限的程序,在root帐户中再次使用export设置环境变量LD_PRELOAD再运行。

    可以观察到父进程的LD_PRELOAD成功传递给子进程,故该方法成功覆盖了sleep()。

    (4)将Task7设置为一个Set-UID user1权限的程序(这里我用的seed用户),然后再另一个用户user2(不能是root用户,此处我使用wion03)的进程中再次设置环境变量LD_PRELOAD再运行Task7。

    这里注意用户2编译之后用户1再设置权限,不然不能成功。结果显示子进程没有继承父进程的LD_PRELOAD环境变量,故sleep()正常运行。

    导致这些现象的原因,是因为动态链接库的一种保护机制:只有当文件执行者的ID与文件拥有者的ID一致时,才会将父进程的LD_PRELOAD继承给子进程,否则不会。

    用该思路分析上述四个方法:

    (1)拥有者为普通用户,执行者也为普通用户,故继承成功;

    (2)拥有者被修改为root,但执行者是普通用户,故继承失败;

    (3)拥有者被修改为root,执行者也为root,故继承成功;

    (4)拥有者为user1,执行者为user2,故继承失败。

Task7完成。

 Task 8: Invoking External Programs Using system() versus execve()

实验目的:

    虽然system()和execve()都可以用来运行新程序,但如果在特权程序(如Set-UID程序)中使用system(),就会非常危险。我们已经看到了环境变量PATH如何影响system()的行为,因为这个变量会影响shell的工作方式。Execve()则没有这个问题,因为它不调用shell。调用shell还有另一个危险的后果,这一次,它与环境变量无关。让我们看看下面的场景。

    Bob在一家审计机构工作,他需要调查一家涉嫌欺诈的公司。出于调查的目的,Bob需要能够读取公司Unix系统中的所有文件;另一方面,为了保护系统的完整性,Bob不应该能够修改任何文件。为了实现这一目标,系统的超级用户Vince编写了一个特殊的set-root-uid程序(见下文),然后将可执行权限授予Bob。这个程序要求Bob在命令行中输入一个文件名,然后它将运行/bin/cat来显示指定的文件。因为这个程序是作为root运行的,所以它可以显示Bob指定的任何文件。然而,由于该程序没有写操作,Vince非常确定Bob不能使用这个特殊程序修改任何文件。

    下面是程序catall.c:

实验内容:

    1. 编译上面的程序,把它变成一个root拥有的Set-UID程序。程序将使用system()调用该命令。如果你是Bob,你能够破坏系统的完整性吗?例如,你可以删除一个不可写的文件吗?

    在root下创建一个文件Task8.txt,并设置为其他用户只可读:

    编译程序catall.c,将其变成一个root拥有的Set-UID程序:

    登录另一个用户模拟Bob,查看Task8.txt的权限:

    只可读,没有写的权限,我们尝试运行catall来读取Task8.txt:

    这里我们尝试删除Task8.txt,显示没有权限:

    如何通过catall来删除Task8.txt?我们可以看到catall.c通过调用system()来执行命令,而Linux系统允许多条命令连续执行,用分隔符 “;” 进行连接,注意用双引号将整条命令包括起来,否则rm与文件名中间的空格会截断字符串导致rm在普通用户权限下运行:

    成功删除了文件。

    2. 注释掉system(command)语句,取消注释execve()语句,程序将使用execve()调用该命令。编译这个程序,把它变成一个根用户拥有的Set-UID。(1)中的攻击仍然有效吗?请描述并解释你的观察结果。

    再次编译运行,结果显示不存在该文件或路径。

    导致这种结果的原因是system()和execve()的运行原理不同,system()是通过调用/bin/sh来执行一条命令,不会检查命令本身的内容。而execve()则是直接执行这条命令,故命令的格式会对其产生影响。

    这里正常运行命令就可以执行:

    Task8完成。

Task 9: Capability Leaking

实验目的:

    为遵循最小特权原则,Set-UID程序通常会在不再需要root权限时永久放弃其root权限。此外,有时程序需要将控制权移交给用户;在这种情况下,必须撤销root权限。setuid()系统调用可用于撤销权限。根据手册,“setuid()设置调用进程的有效用户ID。如果调用者的有效UID是root,则还将设置真正的UID和保存的set-user-ID”。因此,如果一个有效UID为0的set -UID程序调用了setuid(n),该进程将变成一个普通进程,其所有UID都设置为n。

    在撤销特权时,一个常见的错误是权限泄漏。进程可能在仍然享有特权时获得了一些特权能力;当特权降级时,如果程序不清除这些能力,则非特权进程仍然可以访问它们。换句话说,尽管进程的有效用户ID变为非特权用户ID,但进程仍然是特权用户,因为它拥有特权能力。

    编译以下程序,将其所有者更改为root,并将其设置为Set-UID程序。以普通用户的身份运行该程序。您可以利用这个程序中的能力泄漏漏洞吗?其目标是以普通用户的身份写入/etc/zzz文件。

实验内容:

    1. 在root权限下创建/etc/zzz,并设置权限为0644

    2. 编译该程序,将其所有者更改为root,并将其设置为Set-UID程序

    3. 尝试运行cap_leak,可以看到程序输出的文件描述符为3,表示正常打开了文件。之后程序运行execve()启动了一个shell程序。

    4. 在普通用户下查看/etc/zzz的权限,并尝试写入:

    提示没有权限。

    5. 现在我们启动cap_leak进入shell:

    利用文件描述符向/etc/zzz中写入内容:

    成功向该文件写入了内容,原因是该程序启动shell时使用的是root权限,在shell中运行命令时依然保留了权限。Task9完成。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值