SEED实验——Environment Variable and Set-UID Program实验描述与实验任务

时间:2022-05-02 21:46:30

第一部分:实验描述

该实验的学习任务是理解环境变量是如何影响程序和系统行为的。环境变量是一组动态命名的变量


第二部分:实验任务

2.1 任务一:操作环境变量

在这个任务中,我们研究可以用来设置和取消设置环境变量的命令。我们在seed实验环境中使用Bash。用户使用的默认shell在/etc/passwd文件(每个条目的最后一个字段)中设置。您可以使用命令chsh 将其更改为另一个shell程序(请不要在该实验中实现)。执行以下任务:

  • 使用printenv或env命令打印出环境变量。也可以单独打印出某个感兴趣的环境变量的值。例如:PWD,可以用如下命令:”printenv PWD “或者“env | grep PWD”单独打印出来。
  • 使用export或者unset命令设置或去掉环境变量。需要注意的是,这两个命令是Bash内部分命令。

2.2  任务二:继承环境变量

在这个任务中,我们学习环境变量是如何通过子进程完成继承机制。在Unix操作系统中,fork()通过负值调用进程建立一个新的进程。新的进程称为子进程,与其父进程完全相同。但是,子进程没有继承父进程的某些特性。(man fork)。在该任务中,我们会了解原坏境变量是否被子进程继承。

  • 步骤一:编译并运行程序:
    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    extern char **environ;
    
    void printenv()
    {
      int i = 0;
      while (environ[i] != NULL) {
         printf("%s\n", environ[i]);
         i++;
      }
    }
    
    void main()
    {
      pid_t childPid;
    
      switch(childPid = fork()) {
        case 0:  /* child process */
            printenv();
        exit(0);
        default:  /* parent process */
            //printenv();
        exit(0);
      }
    }
    运行的结果(保存在child文件中):
  • 步骤二:在代码的switch语句中,子进程的情形下,注释掉printenv语句;父进程保持不变。编译并运行程序,将结果保存在child2文件中。
  • 步骤三:比较child和child2文件,写出结论。

2.3 任务三:环境变量和execve()

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

  • 步骤一:编译并运行以下程序。描述观察到的实验结果。该程序简单地调用了/usr/bin/env,该系统调用能够打印出当前进程的环境变量。
  • 步骤二:现在,改变execve()函数的参数,描述你观察到的结果。
  • 步骤三:请得出关于新程序如何获取其环境变量的结论。

2.4 任务四:环境变量和system()

在该任务中,我们研究当通过system()执行新程序时华环境变量如何受到影响。system()函数被用于执行命令,但是不像execve()那样直接执行一个命令。system()函数实际上执行的是如下命令:“/bin/sh -c command”,例如执行“/bin/sh”,即请求一个shell。

如果你看system()函数的实现,你会看到它使用execl()执行/bin/sh;excel()调用execve(),传递给它的环境变量数组。因此,使用system(),调用进程的环境变量被传递被新的程序/bin/sh。请编译并运行以下程序来验证这一点。

#include <stdio.h>
#include <stdlib.h>

int main()
{
  system("/usr/bin/env");

  return 0 ;
}

2.5 任务五:环境变量和Set-UID程序

Set-UID是一种重要的安全机制。当一个Set-UID程序运行时,它会获得程序所有者的特权。例如,如果程序所有者是root用户,那么任何人运行该程序时,该程序都会获得root用户的特权。Set-UID允许我们做许多有趣的事情,但是当它运行时,会升级用户的权限,会带来很大的风险。尽管Set-UID程序的行为是由代码逻辑决定的,而不是用户,用户能够通过环境变量来影响行为。为了理解Set-UID程序是如何被影响的,让我们首先指出哪些环境变量是否由用户进程的Set-UID程序的进程继承。

  • 步骤一:在当前步骤中写一个能够输出所有环境变量的程序
#include <stdio.h>
#include <stdlib.h>

extern char **environ;

void main()
{
  int i = 0;
  while (environ[i] != NULL) {
     printf("%s\n", environ[i]);
     i++;
  }
}
  • 步骤二:编译以上程序,将其权限改为roo权限,使其成为一个Set-UID程序。
  • 步骤三:使用一般用户登录终端,使用export命令设置如下环境变量:PATH LD_LIBRARY_PATH ANY_NAME

这些环境变量被设置在终端进程中。运行该Set-UID程序。在终端输入程序的名字后,终端会建立一个子进程,并用该子进程去运行程序。请检查上一步骤中设置的环境变量是否在子进程的shell中。描述观察到的结果。

2.6 任务六:PATH环境变量和Set-UID程序

由于调用了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吗?描述和解释你的观察。

任务七:LD_PRELOAD环境变量和Set-UID程序

在这个任务中,我们研究Set-UID程序如何处理环境变量。一些影响动态链接器的环境变量,包括LD_PRELOAD、LD_LIBRARY_PATH和其他LD_*。一个动态装载器/链接器是操作系统的一部分,用于下载并链接可执行文件运行过程中需要的公共库。

ld.so或者ld-linux.so是动态加载器/链接器。在那些影响他们行为的环境变量中,LD_LIBRARY_PATH和LD_PRELOAD是该实验涉及到的两个环境变量。在linux中,LD_LIBRARY_PATH是一个冒号分隔的目录集,首先需要在标准的目录集之间搜索库。LD_PRELOAD指定要在所有其他库之前加载的其他用户指定的共享库列表。在这个任务中,我们只研究LD_PRELOAD。

  • 步骤一:首先,我们通过一个正常的程序理解环境变量如何影响动态加载器/链接器的行为。请按照以下步骤执行:

  1.我们新建一个动态链接库。命名下面的代码为mulib.c,该程序基本上覆盖了libc中的sleep函数:

#include <stdio.h>
  void sleep (int s)
  {
    /* If this is invoked by a privileged program, 
       you can do damages here!  */
    printf("I am not sleeping!\n");
  }

  2.用下列命令编译mylib.c:

% gcc -fPIC -g -c mylib.c
% -shared -o libmylib.so.1.0.1 mylib.o -lc

  3.设置LD_PRELOAD环境变量:

% export LD_PRELOAD=./libmylib.so.1.0.1

  4.编译myprog程序,在链接库libmylib.so.1.0.1的相同目录下:

 /* myprog.c */
  int main()
  {
    sleep(1);
    return 0;
  }
  • 步骤二:在以下情况中运行myprog程序,观察发生了什么。

    1.以普通用户的身份运行myprog程序。

    2.以普通用户运行拥有root权限的myprog程序。

    3使myprog成为一个Set-UID user1程序,在user2用户(非root用户)中再次设置PD_PRELOAD环境变量,运行myprog程序。

  • 步骤三:观察以上三次程序的执行结果,理解导致他们不同的原因。环境变量起了作用。设计实验证明主要因素,并解释第二步中行为的不同。

任务八:使用system()和execve()调用外部程序

虽然system()和execve()都可以用于运行新程序,但是如果在特权程序(如Set-UID程序)中使用,则system()非常危险。我们已经看到PATH环境变量是如何影响system()的行为,因为该变量会影响shell的工作原理。execve()没有问题,因为它没有调用shell。调用shell会导致非常危险的后果,而这与环境变量无关。来看下面这种情况:鲍勃为一家审计机构工作,他需要调查一家公司是否有涉嫌欺诈。为了调查目的,鲍勃需要能够读取该公司Unix系统中的所有文件;另一方面,为了保护系统的完整性,鲍勃不能修改任何文件。为了实现这一目标,系统超级用户文斯写了一个特殊的设置root-UID程序(见下文),然后给出了对鲍勃的可执行权限。该程序要求鲍勃在命令行中键入文件名,然后运行/bin/cat显示了指定的文件。由于程序在root权限下运行,它可以显示鲍勃指定的任何文件。然而,由于程序没有写操作的权限,所以文斯非常确定鲍勃不能使用这个特殊的程序来修改任何文件。

#include <string.h>
#include <stdio.h>
#include <stdlib.h>

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]);

  // Use only one of the followings.
  system(command);
  // execve(v[0], v, NULL);

  return 0 ;
}
  • 步骤一:编译上面的程序,赋予其root用户权限,并将其变为SET-UID程序。该程序将会使用system()来调用命令。如果你是鲍勃,你能否打破系统的完整性吗?例如,你可以删除不可写文件吗?
  • 步骤二:注释掉system(command)语句,并取消注释execve()语句;程序将使用execve()来调用命令。编译程序,并使其成为Set-UID程序。那么在步骤一中的攻击是否仍然有效?

任务九:权能泄露

遵循最低权限原则,如果不再需要这种特权,Set-UID程序通常会永久放弃其root权限。此外,有时程序需要将其控制权交给用户,在这种情况下,root权限必须被撤销。setuid()系统调用可以用来撤销权限。根据手册,setuid()设置调用进程的有效用户ID。如果调用程序的有效UID是root,真实的UID和保存的set-user-id也被设置“。因此,如果一个有有效UID的set-uid程序没有调用setuid(n),则该进程将成为正常进程,其所有的UID都设置为n。当撤销权限的时候,最常见的错误就是权能泄露。这个进程在获得一些特权的时候可能已经获得了一些特权。当特权降级时,如果程序没有清理这个功能,则它们仍然可以由非特权进程访问。换句话说,虽然进程的有效用户ID变为非特权,但是该进程仍具有特权,因为它具有特权能力。

编译以下程序,将其所有者更改为root,并将其设置为Set-UID程序。以普通用户身份运行程序,并描述您所观察到的内容。文件/etc/zzz是否被修改?

请解释你的观察过程。

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>

void main()
{ int fd;

  /* Assume that /etc/zzz is an important system file,
   * and it is owned by root with permission 0644.
   * Before running this program, you should creat
   * the file /etc/zzz first. */
  fd = open("/etc/zzz", O_RDWR | O_APPEND);
  if (fd == -1) {
     printf("Cannot open /etc/zzz\n");
     exit(0);
  }

  /* Simulate the tasks conducted by the program */
  sleep(1);

  /* After the task, the root privileges are no longer needed,
     it's time to relinquish the root privileges permanently. */
  setuid(getuid());  /* getuid() returns the real uid */

  if (fork()) { /* In the parent process */
    close (fd);
    exit(0);
  } else { /* in the child process */
    /* Now, assume that the child process is compromised, malicious
       attackers have injected the following statements
       into this process */

    write (fd, "Malicious Data\n", 15);
    close (fd);
  }
}