软件安全实验——lab1(环境变量和SetUIDLD_PRELOAD动态库加载劫持)
Posted 大灬白
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了软件安全实验——lab1(环境变量和SetUIDLD_PRELOAD动态库加载劫持)相关的知识,希望对你有一定的参考价值。
目录标题
1 概述
本实验室的学习目标是让学生了解环境变量如何影响程序和系统行为。环境变量是一组动态命名值,它们会影响计算机上运行进程的行为方式。自1979年引入Unix以来,大多数操作系统部使用它们。尽管环境变量会影响程序的行为,但是它们是如何实现的呢?因此,如果一个程序使用了环境变最,但是程序员不知道它们被使用了,那么程序可能存在漏洞。在这个实验室中,学生们将了解环境变是如何工作的,它们如何从父过程中传递给子过程的,以及它们如何影响系统/程序的bahivors
。我们对环境变量如何影响 Set-UID(通常是特权程序) 的行为特别感兴趣。
2 实验室任务
2.1任务 1 :操作环境变量
在这个任务中,我们研究可以用来设置和取消设置环境变的命令。我们在种子帐户使用Bash 。用户使用默认 shell
设置在/etc/passwd文件(每个条目的最后一个字段)中。您可以使用chsh 命令将其更改为另一个
Shell程序(请不要在实验室中这样做):请做以下工作:
· 使用printenv 或 env 命令输出环境变:如果您对某些特定的环境变量感兴趣,比如 PWD ,您可以使用“ printenv PWD ”或“ env|grep PWD " :
· 使用export和unset设置或unset环境变量。需要注意的是,这两个命令并不是独立的程序;它们是 Bash 的两个内部命令(您将无法在 Bash 之外找到它们)。
export直接设置一个新的环境变量,然后显示
unset删除一个环境变量,然后显示
2.2任务2:从父类继承环境变量
在这个任务中,我们研究环境变量是如何被子进程从它们的父进程继承的。在Unix中,fork()通过复制调用进程来创建一个新进程。新进程,称为子进程,是调用进程的精确副本,称为父进程;但是,有一些东西不是由子节点继承的(请参阅fork()手册,输入以下命令:man fork)。在这个任务中,我们想知道父进程的环境变量是否被子进程继承。
步骤1:
请编译并运行以下程序,并描述您的观察结果。因为输出包含很多字符串,所以应该将输出保存到文件中,例如使用a.out > child(假设a.out是您的可执行文件名)。
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);
}
}
源代码:
运行结果:
子进程是父进程的副本,它将获得父进程的环境变量、数据空间、堆、栈等资源的副本。但是,子进程持有的是上述存储空间的“副本”,这意味着父子进程间不共享这些存储空间,它们之间共享的存储空间只有代码段。
步骤2:
现在,在子进程用例中注释printenv()语句,并在父进程用例中取消注释printenv()语句。编译并运行代码,并描述您的观察结果。将输出保存到另一个文件中。
源代码:
运行结果:
子进程不继承父进程进程号,他们的进程号不同;他们有不同的父进程号;子进程有自己的文件描述符和目录流的拷贝,子进程不继承父进程的进程,正文(text), 数据和其它锁定内存等。
步骤3:
使用diff命令比较这两个文件的差异。请得出你的结论。
如果两个二进制文件相同,diff就什么也不显示。现在只简单报告一下这两个文件是不相同的,所以我认为子进程继承了父进程所有的环境变量。
2.3任务3:环境变量和execve()
在这个任务中,我们研究当通过execve()执行新程序时,环境变量是如何受到影响的。execve()函数调用一个系统调用来加载一个新命令并执行它;这个函数永远不会重新出现转弯。没有创建新的进程;相反,调用进程的文本、数据、bss和堆栈被加载的程序覆盖。实际上,execve()在调用过程中运行新程序。我们感兴趣的是环境变量的变化;它们会自动被新程序继承吗?
步骤1:
请编译并运行以下程序,并描述您的观察结果。这个程序简单地执行一个名为/usr/bin/env的程序,它打印出当前进程的环境变量。
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
int main()
{
char *argv[2];
argv[0] = "/usr/bin/env";
argv[1] = NULL;
execve("/usr/bin/env", argv, NULL);
return 0 ;
}
源代码:
运行结果:
传递给执行文件的新环境变量数组为NULL,所以没有新的环境变量。
步骤2:
现在,将execve()调用更改为以下内容,并描述您的观察结果。
execve("/usr/bin/env", argv, environ);
源代码:
运行结果:
步骤3:
关于新程序如何获得其环境变量,请作出您的结论。
函数定义 int execve(const char *filename, char *const argv[ ], char *const envp[ ]);
返回值:函数执行成功时没有返回值,执行失败时的返回值为-1.
函数说明:execve()用来执行参数filename字符串所代表的文件路径,第二个参数是利用数组指针来传递给执行文件,并且需要以空指针(NULL)结束,最后一个参数则为传递给执行文件的新环境变量数组。
新程序通过第三个参数获得环境变量数组。且调用execve()之后,会转到新的进程,自己会被“杀死”,也就是execve()函数之后代码都不会被执行。
2.4任务4:环境变量与系统()
在这个任务中,我们研究当通过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 ;
}
源代码:
运行结果:
实际上system()函数执行了三步操作:
1、 fork一个子进程;
2、 在子进程中调用exec函数去执行command;
3、 在父进程中调用wait去等待子进程结束。
若fork失败,system()函数返回-1。如果exec执行成功,也即command顺利执行完毕,则返回command通过exit或return返回的值。(注意,command顺利执行不代表执行成功,例如command:“rm debuglog.txt”,不管文件存不存在该command都顺利执行了)如果exec执行失败,也即command没有顺利执行,比如信号被中断,或者command命令根本不存在,system()函数返回127,如果command为NULL,则system()函数返回值非0,一般为1。
2.5任务5:环境变量和Set-UID程序
Set-UID是Unix操作系统中的一种重要的安全机制。当Set-UID程序运行时,它会接受所有者的特权。例如,如果程序的所有者是root,那么当任何人运行这个程序时,程序就会在执行期间获得root的特权。Set-UID允许我们做许多有趣的事情,但它在执行时提升了用户的特权,使其相当危险。虽然Set-UID程序的行为是由程序逻辑决定的,而不是由用户决定的,但用户确实可以通过环境变量影响行为。为了理解Set-UID程序是如何受到影响的,让我们首先确定Set-UID程序的进程是否从用户的进程继承了环境变量。
步骤1:
我们将编写一个程序,可以打印出当前进程中的所有环境变量。
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
void main()
{
int i = 0;
while (environ[i] != NULL) {
printf("%s\\n", environ[i]);
i++;
}
}
步骤2:
编译上述程序,将其所有权更改为root,并将其设置为Set-UID程序。
步骤3:
在Bash shell中(您需要在一个普通用户帐户中,而不是根帐户中),使用export命令设置以下环境变量(它们可能已经存在):
1、PATH(可执行程序的查找路径)
2、LD_LIBRARY_PATH(动态库的查找路径)
3、ANY_NAME(这是由您定义的环境变量,因此选择您想要的任何名称)。
这些环境变量是在用户的shell进程中设置的。现在,在shell中从步骤2运行Set-UID程序。在shell中键入程序的名称之后,shell将派生子进程,并使用子进程运行程序。请检查您在shell进程(父进程)中设置的所有环境变量是否进入set-uid子进程。描述你的观察。如果你有惊喜,描述一下。
答:我在shell进程(父进程)中设置的所有环境变量PATH、LD_LIBRARY_PATH、HUANG进入了set-uid子进程。如图所示:
2.6任务6:路径环境变量和Set-UID程序
由于调用了shell程序,在Set-UID程序中 调用system()非常危险。这是因为shell程序的实际行为可能受到环境变量(如IPATH)的影响;这些环境变量由用户提供,可能是恶意的。通过更改这些变量,恶意用户可以控制Set-UID程序的行为。在Bash中,您可以用以下方式更改PATH环境变量(本示例将目录/home/seed添加到PATH环境变量的开头):
$ export PATH=/home/seed:$PATH
下面的Set-UID程序应该执行/bin/Is命令;然而,程序员只使用Is命令的相对路径,而不是绝对路径:
int main()
{
system("ls");
return 0;
}
请编译上述程序,并将其所有者更改为root,使其成为Set-UID程序。
您能让这个Set-UID程序运行您的代码而不是/bin/ls吗?如果可以,您的代码是否使用根特权运行?描述并解释你的观察结果。
答:运行task6,发现该程序执行的shell是/bin/ls命令。由于system()函数是调用了shell环境变量,而之前运行task5,发现SHELL=/bin/bash。于是将自己的可执行文件夹所在的目录加在了SHELL环境变量的开头:从而使这个Set-UID程序运行我的代码而不是/bin/ls。又将task6.c中的system()函数的参数改为task5,即想让该程序执行task5程序。
运行./task6。在终端中输出了所有的环境变量。
2.7任务7:LD_PRELOAD环境变量和Set-UID程序
在本任务中,我们将研究Set-UID程序如何处理一些环境变量。几个环境变量,包括LD_PRELOAD、LD_LIBRARY_PATH和其他LD_*,影响行为动态加载器/链接器。动态加载器/链接器是操作系统(OS)的一部分(用于从持久性存储到RAM),并在运行时链接可执行文件所需的共享库。
在Linux中,ld.so或ld-linux。动态加载器/链接器也是如此(每种加载器对应不同类型的二进制文件)。在影响它们行为的环境变量中LD_PRELOAD和LD_LIBRARY_PATH是我们在实验室里关注的两个。在Linux中,LD_LIBRARY_PATH是一个执行搜索的目录,它应该在标准目录之前首先搜索。LD_PRELOAD指在所有其他库之前加载的用户指定的其他共享库列表。在这项任务中,我们只会研究LD_PRELOAD。
一般情况下,动态库加载的加载顺序为LD_PRELOAD>LD_LIBRARY_PATH>/etc/ld.so.cache>/lib>/usr/lib。
步骤1:
首先,我们将看到这些环境变量如何影响动态加载器/链接器的行为。当运行一个正常的程序。请遵循以下步骤:
1.让我们构建一个动态链接库。创建下面的程序,命名为mylib.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.我们可以使用下面的命令来编译上面的程序(在-lc参数中是第二个角色的):
% gcc -fPIC -g -c mylib.c
% gcc -shared -o libmylib.so.1.0.1 mylib.o -lc
3.现在,设置LD PRELOAD环境变量:
% export LD_PRELOAD=./libmylib.so.1.0.1
- 最后,编译下面的程序myprog,并将其放在与上述动态程序相同的目录中链接库libmylib.so.1.0.1:
/* myprog.c */
int main()
{
sleep(1);
return 0;
}
步骤2:
完成以上操作后,请在以下条件下运行myprog,并观察会发生什么。
•1.使myprog成为一个常规程序,并以普通用户的身份运行它。
此时myprog依赖的共享库:
•2.将myprog设置为Set-UID根程序,并以普通用户的身份运行。
•3.使myprog成为Set-UID根程序,再导出LD_PRELOAD环境变量,用root帐户并运行它。
结果沉睡1秒,证明调用的是系统的sleep函数
说明通过设置执行文件的setgid/setuid标志,在有SUID权限的执行文件,通过root帐户运行它,系统会忽略LD_PRELOAD环境变量。
•4.将myprog设置为Set-UID user1程序(即,所有者是user1,这是另一个用户帐户),在不同用户的帐户(非root用户)中再次导出LD_PRELOAD环境变量运行它
通过test用户去运行所属seed的myprog程序,导出LD_PRELOAD环境变量之后运行myprog
export LD_PRELOAD=./libmylib.so.1.0.1
发现调用的是系统的sleep函数。
说明用户的LD_PRELOAD环境变量对其他用户的程序不生效。
步骤3:
您应该能够在上述场景中观察到不同的行为,尽管您正在运行相同的程序。你需要找出造成这种差异的原因。环境变量在这里发挥作用。请设计一个实验来找出主要原因,并解释为什么会有这些行为在步骤2中是不同的。(提示:子进程可能不会继承LD_*环境变量)
在步骤二中,我们分别在seed用户、root用户、test用户导出LD_PRELOAD环境变量,然后运行所属是seed的myprog程序。结果只有在seed用户时,导出LD_PRELOAD环境变量指定的依赖库libmylib.so.1.0.1,在被插入到程序的共享库中之后会对程序起作用,
Linux调用子进程,和父进程同时调用sleep函数,表现不同,证明是环境变量的影响:
1.在前面task 2,我们已经知道了fork()函数获得了父进程的环境变量、数据空间、堆、栈等资源的副本,所以fork()子进程会获得父进程的LD_PRELOAD环境变量,验证的forktest.c源代码如下所示:
#include <unistd.h>
#include <stdio.h>
int main()
{
printf("main sleep:\\n");
sleep(1);
if(0 == fork()){
printf("fork sleep:\\n");
sleep(1);
}
return 0;
}
运行结果:
Fork()函数复制一个子进程获得了父进程的的LD_PRELOAD环境变量,调用的是重写的sleep函数。
2.execve()函数没有产生新的进程,只是开辟了新的进程空间来执行新程序,它的环境变量需要在调用时手动传递,不传递则没有环境变量,验证的源代码如下所示:
execvetest.c:
#include <unistd.h>
#include <stdio.h>
extern char** environ;
int main()
{
printf("main sleep:\\n");
sleep(1);
char *argv[2]={NULL};
printf("execve(NULL) sleep:\\n");
execve("myprog", argv, NULL);
return 0;
}
execvetest2.c:
#include <unistd.h>
#include <stdio.h>
extern char** environ;
int main()
{
printf("main sleep:\\n");
sleep(1);
char *argv[2]={NULL};
printf("execve(NULL) sleep:\\n");
execve("myprog", argv, environ);
return 0;
}
运行结果:
execvetest.c调用的是execve(“myprog”, argv, NULL)没有传递环境变量,所以myprog程序调用的sleep函数是系统的sleep函数;execvetest2.c调用的是execve(“myprog”, argv, environ)传递了当前程序的环境变量,所以myprog程序调用的sleep函数被劫持是重写的sleep函数。
2.8任务8:使用system()和execve调用外部程序
虽然system()和lexecve()都可以用来运行新程序,但是如果在特权程序(如Set-UID程序)中使用,那么system()就非常危险。我们已经看到PATH环境变量如何影响system()的行为,因为变量影响shell的工作方式。execve()没有问题,因为它不调用shell.调用selli还有另一一个危险的后果,这一次,它与环境变量无关。让我们看看下面的场景。
鲍勃在一家审计机构工作,他需要调查一家公司涉嫌欺诈。为了调查目的,Bob需要能够读取公司Unix系统中的所有文件;另一方面,为了保护系统的完整性,Bob不应该修改任何文件。为了实现这个目标,系统的超级用户Vince编写了-一个特殊的set-root-uid程序(见下文),然后将可执行权限授予Bob。这个程序需要Bob在命令行中键入文件名,然后运行/bin/cat来显示指定的文件。由于程序以根目录运行,所以它可以显示Bob指定的任何文件。然而,由于程序没有写操作,Vince非常确定Bob不能使用这个特殊程序修改任何文件。
#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 ;
}
步骤1:
编译上述程序,使root成为其所有者,并将其更改为Set-UID程序。
程序将使用system()调用该命令。如果你是Bob,你能打破系统的完整性吗?例如,您可以删除一个不能写入的文件吗?
该程序将会使用system()来调用命令。若将上述代码中的
v[0] = "/bin/cat"改为v[0] = “rm”,即删除命令。并新建一个test.c文件,将其权限改为000,执行以下命令:./task8 ./test.c
步骤2:
注释掉系统(命令)语句,取消注释execve()语句;程序将使用execve()调用该命令。编译程序,并使其设置为uid(属于root用户)。你在第一步中的攻击还有效吗?请描述并解释你的观察结果。
答:我在第一步中的攻击无效,并输出了代码中的:please type a file name.
实验过程如图所示:
2.9任务9:性能泄漏
为了遵循最小特权的原则,如果不再需要这些特权,Set-UID程序通常会永久地放弃它们的根特权。此外,有时程序需要将其控制权移交给用户;在这种情况下,必须撤销根特权。setuid()系统调用可用于撤消特权。根据手册,“setuid()设置调用进程的有效用户ID。如果调用者的有效UID是根,那么真正的UID和保存的集用户id也会被设置”。因此,如果具有有效UID0的set-UID程序调用setuid(n),则该进程将成为-一个正常进程,其所有UID都被没置为n。
撤消特权时,常见的错误之一-是功能泄漏。这个过程可能在它仍然享有特权的时候获得了一-些特权能力;当特权被降级时,如果程序不清理这些功能,它们仍然可以被非特权进程访问。换句话说,尽管流程的有效用户ID不再具有特权,但流程仍然具有特权,因为它具有特权功能。编译以下程序,将其所有者更改为root,并将其设置为Set-UID程序。以普通用户的身份运行程序,并描述您所观察到的情况。文件/etcIzzz会被修改吗?请解释你的观察。
#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);
}
}
结果输出了:Cannot open /etc/zzz
所以文件/etc/zzz不会被修改。
因为你编译了这个程序,并将其所有者更改为root,并将其设置为Set-UID程序。之后你以普通用户的身份运行程序,程序运行到open(“/etc/zzz”,O_RDWR | O_APPEND);因为打开权限不够,返回了-1给fd,之后输出了Cannot open /etc/zzz。
Setuid(getuid())返回了当时真正的uid,而在父进程中fork()返回了新创建子进程的进程ID,并使用close(fd)函数,主要包括两部分:释放文件描述符fd;关闭文件。
3提交
您需要提交一份详细的实验室报告来描述您所做的工作以及您所观察到的内容,包括屏幕截图和代码片段。您还需要对有趣或令人惊讶的观察结果提供解释。鼓励您进行进一步的调查,超出实验室要求。你可以通过额外的努力(由你的导师决定)获得加分。
以上是关于软件安全实验——lab1(环境变量和SetUIDLD_PRELOAD动态库加载劫持)的主要内容,如果未能解决你的问题,请参考以下文章