1 实验内容

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

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

2 实验步骤及结果

**2.1 **Task 1: Manipulating Environment Variables

使用printenv命令查看PATH环境变量,env |grep PWD命令过滤输出环境变量
image.png
再使用export “Myname=Enboy” 设置环境变量Myname为Enboy,此时查看环境变量,可以找到名为Myname的环境变量如下
image.png

再运行unset Myname进行清除,此时查看环境变量,已经找不到了名为 Myname 的环境变量
image.png
原因分析: export 和 unset 都是shell自身的命令,操作的是shell变量,而使用export命令设置的shell变量会被shell传递到子进程中, 所以shell fork得到的子进程env 能看到export命令设置的环境变量,而unset清除之后就不会再传递过去,所以就看不到了。

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

在进行实验之前,我们首先学习一下fork函数用法:

使用man fork命令,我们发现fork函数创建一个父进程的副本,即子进程,除了一些进程ID,memory lock等不同外,其余均相同,当然也包括父进程的环境变量值,全部复制给子进程.

image.png

即:经过fork()以后,父进程和子进程拥有相同内容的代码段、数据段和用户堆栈,就像父进程把自己克隆了一遍。事实上,父进程只复制了自己的PCB块。而代码段,数据段和用户堆栈内存空间并没有复制一份,而是与子进程共享。只有当子进程在运行中出现写操作时,才会产生中断,并为子进程分配内存空间。

观察函数返回值信息:
image.png

即fork函数运行后,程序会出现两个进程,一个父进程(本身),一个子进程,如果成功父进程返回子进程的ID,子进程返回0,如果失败(可能原因有很多,比如:系统进程数过多,违背操作系统相关策略等),父进程返回-1,没有子进程产生,则产生相应的错误码

step1

运行myprintenv.c

1
2
gcc -o myprintenv myprintenv.c
./myprintenv

结果如下(所占篇幅太大,省略一部分):
image.png
将myprintenv保存到file1

1
$ myprintenv > file1

step2

把子进程打印环境变量的代码注释掉,父进程的注释去掉

1
2
3
4
5
6
7
8
9
10
11
12
void main()
{
pid_t childPid;
switch(childPid = fork()) {
case 0: /* child process */
//printenv();
exit(0);
default: /* parent process */
printenv();
exit(0);
}
}

重新编译运行myprintenv.c

1
2
gcc -o myprintenv myprintenv.c
./myprintenv

将myprintenv这次打印的内容保存到file2

1
$ myprintenv > file2

step3

比较二者的不同,使用的是diff命令比较两个输出文件的不同。可以看到,子进程继承了父进程的全部环境变量
image.png

**2.3 Task 3: Environment Variables and **execve()

1
int execve(const char *filename, char *const argv[],char *const envp[]);

进程通过execve函数,同样可以执行新的程序,但不会创建子进程来运行,此时进程的text,data,bss等内存数据将会被新程序的数据覆盖,进程中存储的环境变量将会丢失,所以需要显式地利用上面函数第三个参数传递给新运行的程序(注意,不是子进程),第一个参数指向要运行的新程序的路径,第二个参数为一个数组,包含新进程的所有参数,一般情况下,第一个参数就是第二个参数数组argv[0]的值。

step1

运行所给程序myenv.c

1
2
gcc -o myenv myenv.c
./myenv

image.png

step2

可以发现没有打印出任何东西,因为最后给execve传的参数是NULL。我们把NULL改为environ,重新编译运行:
image.png
代码中的/usr/bin/env为一个程序路径,功能为打印当前进程的环境变量。execve函数运行了这个程序,新程序是否有环境变量取决于是否传递了环境变量参数。所以,fork与execve在传递环境变量功能方面都可以达到相同效果,但是实现机理不同。

step3

此时可以得出结论,第一次实验中,发现没有打印出环境变量,这是因为在执行execve()函数时,传递了一个NULL的参数,自然不会有什么打印结果。当在execve()函数中,传递了全局环境变量environ时候,可以成功将传递进去的环境变量打印出来。

**2.4 Task 4: Environment Variables and **system()

system函数解析
功能:与execve一样,进行运行一段命令,仅有一个参数,和execve不同的是,system可以运行多个命令
system通过调用/bin/sh -c command 命令来执行command,也就是说,借助了外部程序shell来执行命令,shell程序首先被执行,然后shell将command作为输入并解析它,然后执行输入的任何命令,也可以多条,只需要加上一个符号.

我们发现,system实际上首先运行fork,产生一个子进程,然后使用execl函数进行运行命令/bin/sh,产生一个shell程序,运行command命令,同时,将环境变量显式传递给新程序,同时在父进程中调用wait去等待子进程结束。环境变量经过了三个阶段:

  1. 进程本身拥有
  2. fork时复制给子进程
  3. execl函数运行时,显式赋值给新程序

运行如下代码:

1
2
3
4
5
6
7
#include <stdlib.h>
int main(int argc, char const *argv[])
{
system("/usr/bin/env");
return 0;
}

结果如下:
image.png
可以打印出所有环境变量,说明system函数会将环境变量传递给新程序。

2.5 Task 5: Environment Variable and Set-UID Programs

step1

Set-UID程序:
:::info
当一个程序需要以root权限运行,而此时又不想将root权限赋予用户,此时可以设置为一个Set-UID程序,在程序中运行指定的需要root权限的程序,注意:Set-UID程序是指使文件对任何可以执行此文件的用户执行时,以文件所有者的权限执行。也就是说,如果需要以root权限运行Set-UID程序时,还需要将程序文件所有者设置为root.同时也要保证用户具有运行这个文件的权限.
:::
实验所给示例代码:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
int main()
{
int i = 0;
while (environ[i] != NULL) {
printf("%s\n", environ[i]);
i++;
}
}

运行结果:
image.png

step2

编译该程序,在执行step2中指令之前,先查看一下当前文件的用户以及模式。(我把前面的代码注释了,用的还是的myenv.c)如下:

1
2
$ sudo chown root myenv //root模式
$ sudo chmod 4755 myenv //set-uid的设置

image.png

step3

按照要求对环境变量进行修改:
image.png
可以看到没有LD_LIBRARY_PATH环境变量:
image.png
image.png
对LD_LIBRARY_PATH的环境变量单独过滤。
image.png
可以看到,依然是没有成功显示出来,对此,不是没有成功export的原因,后续查阅资料后得知:

为什么叫子进程:

shell中执行程序,当输入程序名称时,shell会生成一个子进程并在子进程中执行该程序。这个过程通常为先使用fork函数创建一个子进程,再使用execve()函数(或者其变种之一)来完成。

为什么LD_LIBRARY_PATH环境变量不会被包含到子进程的环境变量中:

这是因为动态链接器的保护机制,设置LD_LIBRARY_PATH环境变量其实就是设置动态库的查找路径,而这是不会出现在子进程的环境变量中的,这样一来子进程是看不到的。根据动态链接库的防御机制,在编译后设置了该程序的有效ID为root,而在普通用户shell中运行时,真实用户为seed,因此此处设置的环境变量被忽略,没有起作用。

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

运行代码:

1
2
3
4
5
6
#include<stdlib.h>
int main()
{
system("ls");
return 0;
}

直接执行,效果与ls命令相同
image.png
Ubuntu20.04(以及之前的几个版本)中,/bin/sh实际上是一个指向/bin/dash的符号链接。这个shell程序有一个对策,可以防止自己在Set-UID进程中被执行。基本上,如果dash检测到它是在Set-UID进程中执行的,它会立即将有效用户ID更改为进程的真实用户ID,本质上放弃了特权。由于我们的受害者程序是一个Set-UID程序,因此在/bin/dash中的对策可以防止我们的攻击。为了了解我们的攻击在没有这种对策的情况下如何工作,我们将把/bin/sh链接到另一个没有这种对策的壳体。

1
$ sudo ln -sf /bin/zsh /bin/sh

将task6设置为root用户下的Set_UID程序
image.png
将/bin/sh程序复制到/tmp目录中,重命名为ls,按照示例改了PATH,发现运行task6没有变化。将/tmp放置到PATH开头,首先被搜索
image.png
过滤一下环境变量,可以看到PATH已经被改变了,再运行task6发现:
image.png
虽然程序中命令为ls,但实际上运行了我们自己设定的程序/bin/sh,同时由于程序为Set-UID,有效用户为root,所以task6运行时以root权限运行,得到root权限的shell.

注意:
在实验中,我们将ls 命令执行的PATH进行了修改,所有后续我们ls命令将会不可用,但是,由于export命令设置的环境变量仅适用于此shell,所以我们可以关闭shell,再重新打开,PATH就会恢复默认值了.
image.png

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

step1

将示例所给代码命名为mylib.c,试图运行,但其没有main函数

1
2
3
4
5
6
7
8
#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");
}

根据示例,用以下方式编译

1
2
$ gcc -fPIC -g -c mylib.c 
$ gcc -shared -o libmylib.so.1.0.1 mylib.o -lc

再设置LD_PRELOAD的环境变量

1
$ export LD_PRELOAD=./libmylib.so.1.0.1

最后,编译以下程序myprog,并在与上述动态链接库libmylib.so.1.0.1相同的目录中:

1
2
3
4
5
6
7
/* myprog.c */
#include <unistd.h>
int main()
{
sleep(1);
return 0;
}

运行结果如下:
image.png

step2

• myprog 现在是普通程序,用普通用户运行
image.png
• 设置 myprog 为 root用户, set-uid程序,普通用户运行
image.png
• 设置 myprog 为 root用户, set-uid程序,使用 root 用户设置 LD_PRELOAD 环境变量,使用 root 用户运行myprog。
image.png
• 将 myprog 改成 set-uid user1程序,并在 user2 账号里设置 LD_PRELOAD 环境变量并运行。这里我新创建了账户meow,user1=meow, user2=seed。结果如下
image.png

step3

原因分析:这个关键在于 LD_PRELOAD 环境变量有没有被动态连接器屏蔽,LD_PRELOAD 和 LD_LIBRARY_PATH 类似,具体机制可以查看上面 Task 5 及其分析。

对于第一种情况,effective uid 等于 real uid,均为 seed,LD_PRELOAD 环境变量没有被屏蔽,所以链接的是 libmylib.so.1.0.1,输出一句话

对于第二种情况,effective uid为root,real uid为seed,不相同,LD_PRELOAD 环境变量被屏蔽,从标准路径查找链接库,所以链接的是标准库。所以结果为等待一秒后结束,无输出。

对于第三种情况,effective uid 等于 real uid,均为 root,LD_PRELOAD 环境变量没有被屏蔽,所以链接的是 libmylib.so.1.0.1,输出一句话

对于第四种情况,effective uid为temp,real uid为seed,不相同,LD_PRELOAD 环境变量被屏蔽,从标准路径查找链接库,所以链接的是标准库。所以结果为等待一秒后结束,无输出。

2.8 Task 8: Invoking External Programs Usingsystem()versus execve()

step1

编译catall.c并设置为 root set-uid程序,正常运行没有什么问题
image.png
这里要演示实施一个删除文件攻击, 用 root 账户在 /root 目录下 新建一个文件 tempfile, 然后切换到 seed 账户,可以看到无法访问也无法删除该文件
image.png
普通用户只要运行 ./task8 “./task8.c; rm /root/tempfile” 即可删除刚才的文件,
image.png
从下面结果可以看出,该文件已经成功被删除
image.png

step2

将 step 1 中的调用 system 函数 改成调用 execve。重复上面的流程,可以发现,运行报错并且未删除该文件
image.png
image.png
原因分析:在使用 system函数时,最终是 shell 去执行命令,而且未仔细检查用户输入,使得其执行了两条命令。其中第二条命令为恶意构造的命令,删除了文件。改成使用 execve 之后,这种攻击方式是不会成功的,因为它是通过系统调用的方式去执行,只能执行一个进程,且进程名已指定,不会产生这种漏洞。

2.9 Task 9: Capability Leaking

修改所给代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#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);
}
}

先用 root 权限创建 /etc/zzz 文件。然后编译上面代码,并设置为 root set-uid程序。使用普通账户运行该程序发现等了一秒,无输出。查看 /etc/zzz 可以发现文件内容已经被修改。
image.png
原因分析:这个特权程序打开了一个重要的的系统文件,并且在放弃特权时没有关闭该文件,而后调用Fork,子进程会继承这个文件描述符,造成特权泄露。子进程可以通过泄露的文件描述符向文件写入恶意内容