1. 了解环境变量如何工作,如何对环境变量进行操作
2. 父进程创建子进程时,环境变量如何继承
3. 环境变量如何影响系统和程序的行为
4. fork、exceve、system三者的功能,并观察不同现象
5. Set-UID程序的行为
6 环境变量与Set-UID程序中存在的安全问题(如权限泄露)
7. 动态链接器的保护机制
1. 使用printenv或者env命令输出环境变量。
seed@VM:~$ env
2. 输出特定的环境变量(以PWD为例)
(1)printenv PWD方法
(2)env | grep PWD方法
3. 使用unset命令删除环境变量(以PWD为例)
当使用unset命令删除PWD环境变量后,我们使用printenv方法输出PWD环境变量,发现为空
4. 使用export命令设置环境变量(以PWD为例)
当使用export命令设置PWD后,我们输出PWD环境变量
1. 实验目的及原理
目的:研究子进程如何从父进程中获取其环境变量
原理:Unix中,fork()函数会通过系统调用创建一个与父进程几乎完全相同的子进程,fork()函数会有两个返回值,在父进程中返回子进程的ID;在子进程中返回0,若出现错误则返回一个负值。
使用man命令查看fork()函数的功能
seed@VM:~$ man fork
2. 示例代码的解释
Switch()语句中的条件,使用fork()函数创建了一个子进程,但是这里fork()函数会返回两个值:0和子进程的ID号,所以在下面通过注释掉一个printenv(),另一个一定会执行;printenv()函数,用来输出此时的环境变量。
3. Step 1:编译并运行示例代码,输出子进程的环境变量
(1)使用vim编辑器编写程序
seed@VM:~$ vi task2.c
#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:
//printenv(); /* parent process */
exit(0);
}
}
(2)编译运行程序,并将结果保存在child.txt文件中
编译C文件为可执行文件:
seed@VM:~$ gcc task2.c -o task2
将结果保存为一个文档
seed@VM:~$ ./task2 > child.txt
输出结果:
可见已经成功的将子进程的环境变量输出并保存。
4. Step 2:输出父进程的环境变量
(1)利用vi命令进行编辑,将子进程的printenv()注释掉,父进程printenv()取消注释。
#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:
printenv(); /* parent process */
exit(0);
}
}
(2)重新编译源文件并运行,将结果保存到parent.txt文件中
seed@VM:~$ gcc task2.c -o task2
seed@VM:~$ ./task2 > parent.txt
成功地将父进程的环境变量输出并保存。
5. Step 3:使用diff命令查看父/子进程环境变量的区别
使用diff进行父/子进程环境变量的对比:
seed@VM:~$ diff child.txt parent.txt
运行完后,没有输出结果,说明两次输出的环境变量完全相同,使用fork()函数创建的子进程的环境变量继承了父进程全部的环境变量;子进程与父进程共享环境变量。
注: 若有输出,是因为上面编译task2.c文件时,-o 后面的可执行文件名不同。
1. 实验目的及原理
目的:通过execve执行一个新的程序时,环境变量如何变化?
原理:execve()函数通过系统调用执行新的程序,但不会创建子进程,原进程的文本、数据、bss以及堆栈被新的进程覆盖,原进程的环境变量会丢失。
示例代码:
#include
#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 ;
}
2. Step 1:编译并运行示例代码
示例程序会执行/usr/bin/enc程序,并打印当前进程的环境变量。
发现编译过程报错,原因是缺少execve()函数所在的头文件unistd.h
加入头文件并重新编译运行,发现输出为空:
3. Step 2:修改函数参数,并编译运行
将示例代码中的NULL改为environ,编译运行,此时输出了当前进程的环境变量:
execve("/usr/bin/env", argv,environ);
4. Step 3:结论
Step2与Step1进行对比,只是将exevce()函数的第三个参数由NULL改为environ,便输出了当前进程的环境变量,说明原进程将自己的环境变量通过environ变量传入exceve()函数的第三个参数,也就是第三个参数控制环境变量的传递,进而新的进程获取环境变量。
1. 实验目的及原理
目的:通过system()执行新程序时,探究环境变量的变化
原理: system()使用fork创建子进程,子进程继承父进程的环境变量,子进程通过execl()来启动/bin/sh,并调用execve()函数,将环境变量数组传递给新程序;所以调用进程的环境变量可以被传递给新程序/bin/sh
示例代码:
#include
#include
int main()
{
system("/usr/bin/env");
return 0 ;
}
2. 编译运行示例代码,验证上述原理
输出了当前进程的环境变量,整体流程为:system通过fork创建的子进程会继承父进程的环境变量,然后子进程执行execl将环境变量赋给新的程序。
1. 实验目的与原理
目的:环境变量是否由Set-UID程序的进程从用户的进程中继承
原理:Set-UID程序是指使文件对任何可以执行此文件的用户执行时以文件所有者的权限执行。Set-UID程序的行为由程序逻辑决定,但用户可以通过环境变量来影响Set-UID程序的行为。
示例代码:
#include
#include
extern char **environ;
void main()
{
int i = 0;
while (environ[i] != NULL)
{
printf("%s\n", environ[i]);
i++;
}
}
2. Step 1:编译运行示例代码,打印当前进程的所有环境变量
3. Step 2:更改所有权为Root账户并使其成为一个Set-UID程序
在这里可执行文件名为task5:
Chown命令将所有权更改为Root账户
Chmod命令使其成为一个Set-UID程序
我们通过task5和task4的属性对比,说明我们操作成功
4. Step 3:在普通用户Shell中设置环境变量并运行Set-UID程序
(1)设置PATH
(2)设置LD_LIBRARY_PATH
(3)设置ANY_NAME(以name为例)
(4)运行Set-UID程序,输出当前进程的环境变量
Shell会创建一个子进程,子进程执行Set-UID程序,我们发现在shell进程(父进程)中设置的PATH、name环境变量都已经进入了子进程的环境变量,但是我们发现在子进程的环境变量中找不到LD_LIBRARY_PATH环境变量。
解释:
LD_LIBRARY_PATH用于指定查找共享库(动态链接库)时除了默认路径以外的其他路径;LD_LIBRARY_PATH可以被修改,从而加载攻击者的恶意库;为了使Set-UID程序更加安全,不受LD_LIBRARY_PATH
环境变量的影响,运行时的链接器或加载器(ld.so
)会忽略LD*环境变量;或许可以理解为链接器所拥有的一个保护机制:当执行程序的进程ID与拥有者的进程ID不一致时,链接器就会忽略掉LD_LIBRARY_PATH环境变量,所以在子进程的环境变量中找不到此环境变量。
参考:
[翻译] 雪城大学信息安全讲义 三、Set-UID 特权程序-外文翻译-看雪论坛-安全社区|安全招聘|bbs.pediy.com
1. 实验目的与原理
目的:在Set-UID进程中调用system函数执行ls命令,需要通过修改环境变量执行我们自己设定的程序。
原理:system会调用shell,所以在Set-UID中执行system会有一定的安全问题,因为shell的执行可能会受到环境变量的影响,用户可以修改环境变量,恶意控制Set-UID程序的行为。
示例代码:
#include
int main()
{
system("ls");
return 0;
}
2. 使用命令将/bin/sh链接到zsh
seed@VM:~$ sudo rm /bin/sh
seed@VM:~$ sudo ln -s /bin/zsh /bin/sh
Ubuntu16.01上的dash shell有一个保护机制,防止自身在Set-UID进程中执行,如果dash shell检测到自己在Set-UID进程中执行,会立即将有效的用户ID更改为进程的实际用户ID,因为我们要在Set-UID进程中执行system函数会调用shell,上述的保护机制会防止我们的攻击,所以需要更改shell。
3. 修改环境变量PATH,编译示例代码
编译发现warning,添加头文件 stdlib.h重新编译
4. 修改所有者为Root,并将其设置为Set-UID程序
5. 运行上述编译好的task6
发现执行的是ls命令:
6. 修改环境变量,使得system调用的shell执行自己设定的程序
System函数中执行的是ls命令,我们的目的是想要system函数执行的ls命令为我们自己设定的程序,所以我们需要将我们自己设定的程序命名为ls,并且将我们自己设定的程序的路径放入PATH环境变量的开头,因为shell程序执行命令时,如果不提供命令的具体路径,shell程序会按照顺序来查找PATH的每一个目录下是否有同名的可执行文件。
"""命名为ls"""
#include
int main()
{
printf("Hello world!");
}
设定的程序是输出Hello world!,我们将其编译并命名为ls,使用pwd查看编译好的ls文件的路径,并将其添加到PATH路径的开头,然后再执行task6:
然后执行./task6文件,也就是我们的Set-UID程序。发现成功输出Hello world!,task6中system执行的是ls命令,但实际上执行了我们自己设定的程序,而且是root权限运行,因为我们是通过Set-UID程序来运行的,Set-UID的所有者为Root用户。
7. 恢复环境变量:采用重启虚拟机的方法
1. 目的与原理
目的: 设置LD_PRELOAD环境变量,观察一个程序在不同场景中的不同行为表现
原理:许多UNIX系统允许“预加载”共享库,可以通过设置LD_PRELOAD环境变量来指定一些库,这些库会在其他库之前加载,
但是动态链接器会有一些保护机制,子进程继承环境变量可能会受阻
2. Step 1:构建一个动态链接库
(1)创建mylib.c文件
#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)编译上述程序
seed@VM:~$ gcc -fPIC -g -c mylib.c
seed@VM:~$ gcc -shared -o libmylib.so.1.0.1 mylib.o -lc
(3)设置LD_PRELOAD环境变量
seed@VM:~$ export LD_PRELOAD=./libmylib.so.1.0.1
(4)编译myprog.c文件,并放在与libmylib.so.1.0.1相同目录下
/* myprog.c */
int main()
{
sleep(1);
return 0;
}
3. Step 2:在以下四个不同场景中运行myprog可执行文件
(1)普通用户下执行myprog
会执行我们设定的库中的sleep函数,输出字符串,而不会执行原来的sleep函数让它sleep 1秒。
(2)将myprog设置为Set-UID根程序,在普通用户下执行
正常执行程序,sleep 1秒,然后退出程序。
(3)将myprog设置为Set-UID根程序,并在root下设置LD_PRELOAD环境变量并运行。
首先我们需要登入root账户,并设置环境变量
① 以root用户运行
会执行我们设定的库中的sleep函数,而不会执行原来的sleep函数让它sleep 1秒。
② 以普通用户运行
退出root账户,执行myprog程序。
发现正常执行程序,sleep 1秒,然后退出程序。
(4)将myprog设置为Set-UID user1程序,并在seed用户下设置环境变量。
①在root用户下,建立新的用户user1
②设置myprog和环境变量
可见myprog已经设置为Set-UID user1程序,且在seed用户下设置了环境变量。
③在seed用户下运行Set-UID程序
发现正常执行程序,sleep 1秒,然后退出程序。
4. Step 3:设计实验并解释上述现象
(1)猜想:myprog进程会从用户进程中继承环境变量,但是由于动态链接器的一些保护机制,不会继承LD_PRELOAD环境变量。
(2)设计实验:分别在上述四种场景中,将用户进程和子进程的环境变量输出为文件,并进行对比,找出不同(重点关注LD_PRELOAD),在整个实验过程中,seed用户下始终设置LD_PRELOAD环境变量。
(3)实验代码:
命名为test.c,编译为test可执行文件
seed@VM:~$ vi test.c
seed@VM:~$ gcc test.c -o test
#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:
//printenv(); /* parent process */
exit(0);
}
}
(4)实验过程
① 场景一:普通用户(seed)下执行test程序,seed用户下修改环境变量
这里可见test程序与用户进程的环境变量一致,然后分别查看二者的环境变量中是否有LD_PRELOAD变量。
用户进程的环境变量:
test中子进程的环境变量:
结论:场景一下,子进程继承了用户进程的LD_PRELOAD环境变量。
② 场景二:test为Set-UID 根程序,在seed用户下执行
将test设置为Set-UID根程序,并在seed用户下执行,此时输出子进程的环境变量;将test设置为普通程序,在seed用户下执行,此时输出父进程的环境变量。通过diff命令发现二者在LD*环境变量上有差异,当我们打开父进程的环境变量可以发现LD*环境变量,而在子进程的环境变量中找不到。下图为父进程中找到的:
结论:场景二中,子进程并没有继承父进程的LD*环境变量。
③ 场景三:test为Set-UID 根程序,并在Root用户下设置了环境变量,分别在root用户和seed用户运行。
Root用户下运行:
发现此时二者的环境变量一致,分别查看二者的环境变量:
父进程和子进程均为:
Seed用户下运行:
此时,二者环境不一致,分别查看二者的环境变量,当我们打开父进程的环境变量可以发现LD*环境变量,而在子进程的环境变量中找不到。下图为父进程中找到的:
结论:场景三中,若在root用户下运行,子进程会继承父进程的LD_PRELOAD环境变量;在seed用户下运行,子进程不会继承父进程的LD_PRELOAD环境变量。
④ 场景四:test为Set-UID user1程序,在seed用户下执行
此时,二者环境不一致,分别查看二者的环境变量,当我们打开父进程的环境变量可以发现LD*环境变量,而在子进程的环境变量中找不到。下图为父进程中找到的:
结论:场景四中子进程并没有继承父进程的LD_PRELOAD环境变量
②主要原因:动态链接器的保护机制。
当运行进程的真实用户ID与程序的拥有者的用户ID不一致时,进程会忽略掉父进程的LD_PRELOAD环境变量;若ID一致,则子进程会继承此时运行进程的真实用户下的LD_PRELOAD环境变量,并加入共享库。
③解释现象
场景(1):普通用户(seed)下执行myprog程序,此时myprog程序的拥有者是seed,而且在seed用户下设置了环境变量,所以真实用户ID与拥有者用户ID一致,子进程会继承seed用户下的LD*环境变量,并加入共享库,执行设置的sleep函数。
场景(2):myprog为Set-UID 根程序,在seed用户下执行,ID不一致,所以动态链接器会忽略LD_PRELOAD环境变量,子进程不能继承seed用户下的LD*环境变量,所以正常执行sleep函数。
场景(3):myprog为Set-UID 根程序,并在Root用户下设置了环境变量,所以在root用户下运行myprog,ID一致,所以子进程会继承root用户下的LD_PRELOAD环境变量,并加入共享库,执行设置的sleep函数;而在seed用户下运行,ID则会不匹配,环境变量被忽略,从而正常执行sleep函数。
场景(4):myprog为Set-UID user1程序,在seed用户下执行也会遇到ID不一致,所以忽略环境变量,正常执行sleep函数。
1. 实验目的与原理:
目的:system()与execve()在Set-UID根程序下执行,观察其对系统文件的危害行为
原理:system()与execve()在执行时的区别
示例代码:
#include
#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 ;
}
观察代码,当我们在命令行执行此程序时,会输出command命令,command命令由v[0] + v[1]组成也就是进行了字符串的拼接,而v[1]是通过我们在命令行的输入来决定的,所以我们可以控制我们的输入来达到对命令的控制。
Argc 统计发送给main函数的参数个数,Argv为字符串数组。V[1]是输入的第一个字符串。
2. 实验说明
Bob拥有的权限:能够读取所有文件,但是不应该能够修改任何文件
system() 实际调用/bin/sh,然后在 shell 环境中运行该命令:
seed@VM:~$ sudo rm /bin/sh
seed@VM:~$ sudo ln -s /bin/zsh /bin/sh
3. Step 1:使用system()调用外部程序,删除根用户拥有的文件
(1)编译并设置为Set-UID根程序
(2)在root用户下创建一个home1目录,在该目录下创建一个名为为hello.c的文件,并在seed下删除
一开始没有创建home1目录,直接创建了hello.c文件:
这里我们发现在seed用户下竟然可以删除写保护的文件,经过查资料,发现还需要看上一级目录的权限,所以先在root下创建一个home1目录,在此目录下创建hello.c文件,然后再seed下删除hello.c,此时发现权限不够:
(3)执行task8程序,删除hello.c文件(不可写)
根据对代码的说明,我们在命令行输入时需要在task8后面加上参数,然后传递给argv,此时只需让argv[1]包含我们想要执行的删除命令即可,因为command为v[0]+v[1],也就是/bin/cat v[1],因为argv为字符串数组,所以参数应该输入字符串,想要同时在命令行执行两个命令,可以用”;”来进行分割,所以我们在seed用户下执行以下命令:
seed@VM:~$ ./task8 "home1/hello.c;rm home1/hello.c"
那么此时执行的命令为:
/bin/cat home1/hello.c;rm home1/hello.c
第一条命令显示hello.c文件内容,第二条命令删除hello.c文件
结果:显示文件内容,并且成功删除不可写文件。
4. Step 2:使用execve()调用外部程序
(1)修改task8.c文件,重新编译,设置为Set-UID根程序
将system注释掉,execve反注释掉。
编译发现少了头文件unistd.h
加上头文件重新编译并设置为Set-UID根程序
(2)在root用户下创建一个home1目录,在该目录下创建一个名为为hello.c的文件,并在seed下删除。(同Step1中的(2))
(3)执行task8程序,删除hello.c文件(不可写)
重复上述的输入,结果如下:
5. 解释
使用system()可以成功删除不可写文件,是因为system会创建一个子进程,然后子进程会调用一个新的shell程序,而且因为task8是一个Set-UID根程序,所以在执行时会以root权限执行删除文件的命令,可以成功删除。
使用execve()不可以成功删除不可写文件,因为execve会执行一个新程序,而不会调用新的shell程序,所以将我们输入的参数仅仅当成一个字符串,不会执行命令,所以不能删除不可写文件。
1.实验目的与原理
目的:利用权限泄露,对一个没有写权限的root文件写入字符串
原理:Set-UID程序会在不需要root权限时释放root权限,但是有时候仅仅是将程序的拥有者变为非root用户,进程仍然拥有root权限下的一些功能,执行一些root权限下才能进行的操作
示例代码:
#include
#include
#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("/home/seed/etc/zzz", O_RDWR | O_APPEND);
if (fd == -1)
{
printf("Cannot open /home/seed/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
{
write (fd, "Malicious Data\n", 15);
close (fd);
}
}
代码解释:
根据注释,我们首先需要创建一个权限为0644且为根用户所有的文件。然后模拟使用root权限;任务完成后,不再需要根权限,通过setuid(getuid())放弃根权限,恢复进程的拥有者为真实执行该进程的用户ID;然后通过fork创建一个子进程,父进程关闭打开的文件,子进程向创建的新文件中写入字符串。
2. root下创建一个etc文件夹,文件夹内创建zzz文件,并设置其权限为0644
zzz文件内容:
3. 编译示例代码,并设置为Set-UID根程序,在seed下运行
修改示例代码:将文件设置为我们创建的zzz文件:
编译出现多个warning,添加头文件:
然后重新编译并设置为Set-UID根程序:
在seed用户下运行:
我们发现字符串成功写入zzz文件,而zzz文件是root下的只读文件。
4. 现象解释
运行Set-UID程序时,进程暂时获得root权限,打开zzz文件时,获得了root权限下的读写文件、向文件中添加内容的权限,当使用setuid()释放root权限时,没有释放进程已经获得的特权功能—读写文件、向文件中添加内容;导致仅仅是将程序的拥有者降为非root用户,然后进程还拥有root权限下的读写文件、向文件中添加内容的功能,所以造成了权限泄露的问题,当执行fork创建子进程后,子进程从父进程继承环境变量,复制一份fd,所以子进程也拥有父进程一样的权限,所以即使父进程关闭,子进程也可以向zzz文件中添加内容。