目录
1.0 前言
2.0 实验任务
2.1任务1:操纵环境变量
2.2任务2:将环境变量从父进程传递给子进程
2.3任务3:环境变量和execve()
2.4 任务4:环境变量和system()
2.5 任务5:环境变量和Set-UID程序
2.6 任务6:PATH环境变量和Set-UID程序
2.7 任务7:LD预加载环境变量和Set-UID程序
2.8 任务8:使用system()而不是execve()调用外部程序
2.9 任务9:内存泄漏
本实验的学习目的是让学生了解环境变量如何影响程式及系统的行为。环境变量是一组动态命名值,可以影响正在运行的进程在计算机上的行为方式。自1979年它们被引入Unix以来,大多数操作系统都使用它们。尽管环境变量影响程序行为,但许多程序员并不清楚它们是如何实现的。因此,如果一个程序使用了环境变量,但是程序员不知道它们被使用了,那么这个程序可能会有漏洞。
在这个实验中,学生将了解环境变量是如何工作的,它们如何从父进程传播到子进程,以及它们如何影响系统/程序的行为。我们特别感兴趣的是环境变量如何影响Set-UID程序的行为,这些程序通常是特权程序。本实验包括以下主题:
阅读资料和观看视频。Set-UID机制、环境变量及其相关安全问题的详细介绍如下:
实验环境。这个实验已经在我们预建的Ubuntu 16.04虚拟机上测试过了,可以从SEED网站下载。
在本任务中,我们将研究子进程如何从父进程获取环境变量。在Unix中,fork()通过复制调用进程来创建一个新进程。新进程(称为子进程)与调用进程(称为父进程)完全相同;然而,有一些东西不能被子进程继承(请通过键入以下命令查看fork()的手册:man fork)。在这个任务中,我们想知道父进程的环境变量是否被子进程继承。
Step1:请编译并运行以下程序,并描述你的观察结果。由于输出包含许多字符串,您应该将输出保存到一个文件中,例如使用a.out >子文件(假设a.out是您的可执行文件名)。
#include
#include
#include
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);
}
}
① 创建文件夹vim:mkdir vim
② 进入vim文件夹 :cd vim
③创建 task2.c 文件并写入上面的代码:vim task2.c
注:代码复制进来的时候格式不太对、最好把注释去掉。ctrlC、ctrlV复制不进去,右键 paste粘贴就行。
相关Linux操作:
编辑不了代码时,按“a”,可编辑。
编辑好退出并保存:先按Esc、再底下输入 “:wq”
④ls 查看创建好的task2.c文件
⑤运行task2.c文件:gcc task2.c
ls :查看生成的a.out文件
⑤把 a.out 文件编译的结果输出至child:a.out >> child
看看child文件的存放的环境变量:vim child
出现绿色的提示,按 o 查看就行。
Step2:现在注释掉子进程案例中的printenv()语句(第①行),取消注释父进程案例中的printenv()语句(第➁行)。再次编译并运行代码,并描述您的观察结果。将输出保存到另一个文件中。
⑥ 我们将代码改成打印父进程,我们把a.out数据打印到parent中
进入task2.c修改部分代码:vim task2.c
保存并退出 : :wq
⑦运行task2.c 文件:gcc task2.c
输出到parent 中:a.out >> parent
查看parent文件的内容:vim parent
Step3:使用diff命令比较这两个文件的差异。请得出你的结论。
由此可知,child 子进程 和parent 父进程 共享环境变量。
在本任务中,我们将研究通过execve()执行新程序时环境变量是如何受到影响的。
execve()函数调用一个系统调用来加载一个新命令并执行它;这个函数永远不会返回。没有创建新进程;相反,调用进程的文本、数据、bss和堆栈被加载的程序的文本覆盖。实际上,execve()在调用进程中运行新程序。我们感兴趣的是环境变量发生了什么;它们会自动被新程序继承吗?
Step1:请编译并运行以下程序,并描述你的观察结果。这个程序只是执行一个名为/usr/bin/env的程序,它打印出当前进程的环境变量。
#include
#include
extern char **environ;
int main()
{
char *argv[2];
argv[0] = "/usr/bin/env";
argv[1] = NULL;
execve("/usr/bin/env", argv, NULL); //①
return 0 ;
}
创建一个文件夹vim : mkdir vim
进入vim文件夹:cd vim
创建task3.c文件,把上面代码复制进去:vim task3.c
保存并退出:按Esc、然后输入 “ :wq”
运行taks3.c文件:gcc task3.c
报错: warning: implicit declaration of function ‘execve’ [-Wimplicit-function-declaration]
execve("/usr/bin/env", argv, environ);
解决:
运行指令:man execve 看看缺少哪个头文件
在taks3.c加上:#include
运行task3.c:gcc task3.c 没有报错
查看文件:ls
把a.out 文件编译结果输出到test1:a.out >> test1
查看test1的内容:vim test1
tese1中没有任何内容
Step2:将①行中execve()的调用更改为以下内容:execve("/usr/bin/env", argv, environ) ; 描述你的观察
打开task3.c:vim task3.c
把NULL改为environ
保存并退出:按Esc,输入“:wq” (默认大家已经熟练此命令,以后不再重复书写此命令)
运行task3.c文件:gcc task3.c
把a.out文件编译结果存放到test2 :a.out >> test2
查看test2文件的内容:vim test2
test2中有存放环境变量
Step3:关于新程序如何获取环境变量,请得出你的结论。
结论:首先,我们在execve第三个参数传递为null,可以看到test1中没有任何内容,也就是不传递环境变量。 我们在execve第三个参数传递为environ。在test2中看到了环境变量的内容。也就是说execve中的第三个参数就是控制环境变量的传递。
在这个任务中,我们研究当通过system()函数执行一个新程序时,环境变量是如何受到影响的。此函数用于执行命令,但与execve()直接执行命令不同的是,system()实际执行的是"/bin/sh -c command",即执行/bin/sh,并请求shell执行该命令。如果你看一下system()函数的实现,你会看到它使用execl()来执行/bin/sh;execl()调用execve(),并将环境变量数组传递给它。因此,使用system(),调用进程的环境变量被传递给新的程序/bin/sh。请编译并运行以下程序来验证这一点。
#include
#include
int main()
{
system("/usr/bin/env");
return 0 ;
}
创建task4.c文件并把代码复制进去:vim task4.c
把a.out 编译文件输入到test3中:a.out >> test3
结论:在这个任务中,我们需要观察system,system的本质是fork出⼀个子进程,然后通过execve执行,我们 可以看到system的子进程是继承自父进程,所以环境变量会传递过去。
Set-UID是Unix操作系统中一种重要的安全机制。当Set-UID程序运行时,它假定拥有所有者的特权。例如,如果程序的所有者是root,那么当任何人运行时
这个程序,这个程序在它的执行过程中获得了根权限。Set-UID允许我们做很多有趣的事情,但是在执行时它会升级用户的特权,这使得它非常危险。虽然Set-UID程序的行为是由它们的程序逻辑决定的,而不是由用户决定的,但是用户确实可以通过环境变量影响这些行为。为了理解Set-UID程序是如何受到影响的,让我们首先弄清楚Set-UID程序的进程是否从用户的进程继承了环境变量。
Step1:编写以下程序,可以打印出当前进程中的所有环境变量。
#include
#include
extern char **environ;
void main()
{
int i = 0;
while (environ[i] != NULL) {
printf("%s\n", environ[i]);
i++;
}
}
创建task5.c文件并复制代码进去:vim task5.c
运行:gcc task5.c
输出结果到test:a.out >> test
查看test内容:vim test
Step2:编译上述程序,将其所有权更改为root,并使其成为Set-UID程序。
// Asssume the program’s name is foo
$ sudo chown root foo
$ sudo chmod 4755 foo
首先把task5.c 的可执行文件命名为foo:gcc task5.c -o foo
再执行指令:
sudo chown root foo
sudo chmod 4755 foo
Step3:在你的shell中(你需要使用一个普通的用户帐户,而不是root帐户),使用export命令设置以下环境变量(它们可能已经存在):
•LD库路径
•任何名称(这是一个由你定义的环境变量,所以选择任何你想要的名称)
这些环境变量是在用户的shell进程中设置的。现在,在shell中运行第2步中的Set-UID程序。在shell中输入程序名后,shell会派生一个子进程,并使用该子进程运行程序。请检查您在shell进程(父进程)中设置的所有环境变量是否都进入set - uid子进程。描述你的观察。如果你感到惊讶,描述一下。
定义一个普通用户,使用export命令设置:export name=“sunyanqian”
查看环境变量:env
查看当前目录:ls
foo为红色:说明foo现在是set-uid程序
foo 从可执行文件 到 set-uid程序的转变只需要执行sudo chown root foo 命令和sudo chmod 4755 foo命令:
验证一下:
生成可执行文件foo:gcc task5.c -o foo
查看foo属性 :ll foo
提升权限:sudo chown root foo、sudo chmod 4755 foo
查看现在的foo属性:ll foo
这个task中,我们要验证Set-UID程序是不是可以从⽤户进程中继承环境变量。我们通过chown改变foo 执⾏⽂件的拥有者为root,然后通过chmod将foo执⾏⽂件设置为set-UID程序。⾸先如果我们不定义额外的变量,直接打印环境变量,可以看到,Set-UID程序可以获得环境变量。定义⼀个变量export name="sunyanqian",我们可以看到Set-UID中出现了这个环境变量。说明Set-UID程序 可以从user进程中通过export获得环境变量。
由于调用的是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权限运行?描述并解释你的观察结果。
说明(仅适用于Ubuntu 16.04虚拟机):system(cmd)功能先执行/bin/sh程序,再通过shell程序运行cmd命令。在Ubuntu 12.04和Ubuntu 16.04虚拟机中,/bin/sh实际上是一个指向/bin/dash shell的符号链接。然而,这两个vm中的dash程序有一个重要的区别。Ubuntu 16.04中的dash shell有一个防止自己在Set-UID进程中执行的对策。基本上,如果dash检测到它是在Set-UID进程中执行的,它就会立即将有效用户ID更改为进程的真实用户ID,本质上就是取消特权。Ubuntu 12.04中的dash程序没有这种行为。
因为我们的受害者程序是一个Set-UID程序,所以/bin/dash中的对策可以阻止我们的攻击。为了了解我们的攻击在没有这种反措施的情况下是如何工作的,我们将/bin/sh链接到另一个没有这种反措施的shell。我们已经在Ubuntu 16.04虚拟机中安装了一个名为zsh的shell程序。我们使用以下命令将/bin/sh链接到zsh(在Ubuntu 12.04中不需要这样做):
$ sudo rm /bin/sh
$ sudo ln -s /bin/zsh /bin/sh
首先创建一个task6.c文件并复制代码:vim task6.c
然后我们创建一个攻击文件task6_fake_ls.c: vim task6_fake_ls.c
使其输出“this is a fake ls”
运行task6.c文件:
使用ls命令,正常输出:
开始攻击:
我们提升权限:sudo chown root a.out sudo chmod 4755 a.out ,然后查看,a.out变红,已经是set-uid程序了。
注:export PATH =/xxx/xxx : $PATH ,通过pwd命令来查看路径:
ls命令已经不是原来的功能、攻击成功。
总结:第⼀次程序可以调⽤ls的system调⽤,我们尝试将ls这个系统调⽤改成我们的攻击程序。第⼆个程序就是我们的攻击程序,在这⾥,我让攻击程序就是打印"this is a fake ls",当我们输入ls命令而系统却打印出来this is a fake ls的时候,就说明我们已经攻击成功。
(ls功能被改变了、影响了做后面的实验,恢复过来只需要重启虚拟机:输入reboot命令即可)
在这个任务中,我们研究Set-UID程序如何处理一些环境变量。几个环境变量,包括LD预加载、LD库路径和其他LD *会影响行为
动态加载器/链接器。动态加载器/链接器是操作系统(OS)的一部分,它在运行时加载(从持久存储到RAM)并链接可执行文件所需的共享库。
Linux下,ld.so或ld-linux。动态加载器/链接器也是如此(每种都适用于不同类型的二进制文件)。
在影响其行为的环境变量中,LD库路径和LD预加载是本实验室关注的两个。在Linux中,LD库路径是一个以冒号分隔的目录集,在标准目录集之前,首先搜索库。LD PRELOAD指定要在所有其他库之前加载的附加的、用户指定的共享库列表。在本课题中,我们将只研究LD预加载。
Step1:首先,我们将看到这些环境变量在运行正常程序时如何影响动态加载器/链接器的行为。请遵循以下步骤:
1. 让我们构建一个动态链接库。创建以下程序,并命名为mylib.c。它基本上覆盖了libc中的sleep()函数:
#include
void sleep (int s)
{
/* If this is invoked by a privileged program,
you can do damages here! */
printf("I am not sleeping!\n");
}
2. 我们可以使用以下命令编译上述程序(在-lc参数中,第二个字符是 l ):
% gcc -fPIC -g -c mylib.c
% gcc -shared -o libmylib.so.1.0.1 mylib.o -lc
3.现在,设置LD预加载环境变量:
% export LD_PRELOAD=./libmylib.so.1.0.1
4. 最后,编译以下程序myprog,并在与上述动态链接库libmylib.so.1.0.1相同的目录下:
/* myprog.c */
int main()
{
sleep(1);
return 0;
}
Step2:完成以上操作后,请在以下条件下运行myprog并观察会发生什么。
•使myprog成为一个常规程序,并以普通用户的身份运行它。
•将myprog设置为Set-UID根程序,并作为普通用户运行它。
•将myprog设置为Set-UID根程序,在根帐户中再次导出LD预加载环境变量并运行它。
•将myprog设置为Set-UID user1程序(即,所有者是user1,这是另一个用户帐户),在另一个用户帐户(非root用户)中再次导出LD PRELOAD环境变量并运行它。
普通用户运行:
待解决
Step3:即使您正在运行相同的程序,您也应该能够在上述场景中观察到不同的行为。你需要找出造成差异的原因。环境变量在这里起了作用。请设计一个实验,找出主要的原因,并解释为什么第二步的行为会不同。(提示:子进程可能不会继承LD *环境变量)。
待解决
尽管system()和execve()都可以用于运行新的程序,但如果在特权程序(如Set-UID程序)中使用system()是非常危险的。我们已经看到了PATH环境
变量影响system()的行为,因为变量影响shell的工作方式。execve()没有这个问题,因为它不调用shell。调用shell有另一个危险的后果,这一次,它与环境变量无关。让我们看看下面的场景。
Bob为一家审计机构工作,他需要调查一家公司是否有欺诈嫌疑。为了进行调查,Bob需要能够读取公司Unix系统中的所有文件;另一方面,为了保护系统的完整性,Bob应该不能修改任何文件。为了实现这个目标,系统的超级用户Vince编写了一个特殊的set-root-uid程序(见下文),然后将可执行权限授予Bob。这个程序要求Bob在命令行输入文件名,然后运行/bin/cat来显示指定的文件。由于程序是作为根程序运行的,所以它可以显示Bob指定的任何文件。然而,由于这个程序没有写操作,Vince非常肯定Bob不能使用这个特殊的程序来修改任何文件。
#include
#include
#include
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 ;
}
Step1:编译上面的程序,使它成为根拥有的Set-UID程序。程序将使用system()调用该命令。如果你是Bob,你会破坏系统的完整性吗?例如,你能删除一个对你不可写的文件吗?
登录root用户、在vim目录下再创建一个room文件、room文件里面再创建一个test.c文件:
从root用户切换到seed用户,并尝试删除test.c文件:删除失败(权限限制)
执行命令:a.out "hello.c ;rm -rf ./room/test.c"
再回到room、发现test.c文件已被删除。(注:;前的部分写什么都可以随意,最主要是要有 ;分号)
Step2:注释掉system(command)语句,取消注释execve()语句;程序将使用execve()调用该命令。编译程序,并使其成为根拥有的Set-UID。你在第一步中的攻击还有效吗?请描述并解释你的观察结果。
使用execve、而不是system,重复上面的攻击:
打开task8.c :注释掉system(command)语句,取消注释execve()语句
报错:查看man execve
少了头文件:
加上就行:
重复我们的攻击:
攻击失败,room文件中的test.c仍然存在
为了遵循最小特权原则,如果不再需要这些特权,Set-UID程序通常会永久地放弃它们的根特权。此外,有时程序需要将其控制权移交给用户;在这种情况下,必须撤销根特权。setuid()系统调用可以用来撤销特权。根据手册,“setuid()设置调用进程的有效用户ID。如果调用者的有效UID是root,那么也会设置真正的UID和保存的set-user- id”。因此,如果具有有效UID 0的set -UID程序调用setuid(n),该进程将成为一个正常进程,其所有的UID都被设置为n。当撤销特权时,常见的错误之一是内存泄漏。这一过程可能在它仍然享有特权的时候获得了一些特权能力;当特权被降级时,如果程序没有清除这些功能,它们仍然可以被非特权进程访问。换句话说,尽管进程的有效用户ID变为非特权的,但进程仍然是特权的,因为它拥有特权功能。
编译以下程序,将其所有者更改为root,并使其成为Set-UID程序。作为普通用户运行程序,并描述您所观察到的情况。/etc/zzz文件会被修改吗?
请解释你的观察结果。
#include
#include
#include
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);
}
}
在这个task中,我们的任务是在利用Set-UID程序,在不具备写入权限的文件中,利用权限泄漏写入文件。
#include
#include
#include
#include
void main()
{
int fd;
fd = open("./room/test.c", O_RDWR | O_APPEND);
if (fd == -1) {
printf("Cannot open ./room/test.c\n");
exit(0);
}
sleep(1);
if (fork()) { /* In the parent process */
close (fd);
exit(0);
} else {
write (fd, "Malicious Data\n", 15);
close (fd);
}
}
在root状态创建test.c文件:touch test.c
在room下的test.c文件要写一些代码:
test.c文件的内容:
在seed状态查看test.c:只读权限readonly
创建task9.c并编译:
此时运行a.out访问不了test.c文件
task9.c的内容:
修改了两个地方:open的路径,我写了room下的test.c文件、还要加上#include
一开始test.c的内容还是原来的内容,然后我们提升权限,并运行a.out、没有提示不能访问(Can not open ./room/test.c)运行结果就是我们把test.c的文件内容改变了。
总结:我们的程序核⼼思想是在fork之后因为⼦进程会拷⻉⽗进程的⼀份fd,当⽗进程为关闭对应的fd的时候,被⽗进程打开的⽂件就可以被⼦进程读写,我们可以看到⼦进程采⽤write写⼊数据,执⾏完程序之 后,我们⽤cat可以看到,test.c中已经被我们写⼊攻击的数据
觉得有用的给我点个赞吧~~
有兴趣可以关注我的公众号,不定期更新技术文章和科软生活贴~~