SEEDLab Environment Variable and Set-UID Program Lab 实验报告

task1 Manipulating Environment Variables

运行结果:

task2 Passing Environment Variables from Parent Process to Child Process

fork函数解析

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

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

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

#include <unistd.h> //fork函数
#include <stdio.h> //printf函数
#include<stdlib.h> //exit函数
int main(int argc, char const *argv[])
{
	pid_t PID;
	/* only one process at this time*/
	PID=fork();
	switch(PID)
	{
		case 0: //child process
			printf("I am the child process, my process ID is %d\n",getpid());
			exit(0);
		case -1:
			printf("Attention,fork error!\n");
			exit(0);
		default: // parent process
			printf("I am the parent process, my process ID is %d\n",getpid());
			exit(0);
	}
	return 0;
}

产生如下结果:

即程序产生一个子进程,即最终运行两次.

实验内容:

测试结果:

子进程,父进程的环境变量相同.

实验tips:

extern char **environ;

代码中,environ为一个全局二级指针变量,名称不可变,在实验中,也可以使用main函数中:

int main(int argc, char const *argv[],char*envp[])

的envp参数来获取环境变量,用法一致,但是,当修改环境变量后,由于环境变量发生了改变,environ全局变量会发生更新,而局部变量envp不会更新,需要使用putenv(),setenv()等函数改变,所以推荐使用全局变量.

task3 Environment Variables and execve()

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

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

而fork函数为:

同时,函数返回信息:

运行实验手册代码:

#include<unistd.h>
#include <stdlib.h>
extern char**environ;
int main()
{
	char *argv[2];
	argv[0]="/usr/bin/env";
	argv[1] = NULL;
	execve(argv[0], argv, environ); //此处进行改变第三个参数
	return 0;
}

得到如下运行结果:

代码中的/usr/bin/env为一个程序路径,功能为打印当前进程的环境变量.execve函数运行了这个程序,新程序是否有环境变量取决于是否传递了环境变量参数.
所以,fork与execve在传递环境变量功能方面都可以达到相同效果,但是实现机理不同.

task4 Environment Variables and system()

system函数解析

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

通过man system,得到信息:

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

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

实验过程

运行如下代码:

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

得到如下结果:
在这里插入图片描述
可以打印出所有环境变量,说明system函数会将环境变量传递给新程序.

task5 Environment Variable and Set-UID Programs

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

设定为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++;
	}
}

运行结果:
在这里插入图片描述

发现设置LD_LIBRARY_PATH环境变量(动态库的查找路径)不会出现在子进程的环境变量中,而其他两个均可以被包含.

这里为什么叫子进程呢?

这是因为,shell中执行程序,当输入程序名称时,shell会生成一个子进程并在子进程中执行该程序.
这个过程通常为先使用fork函数创建一个子进程,再使用execve()函数(或者其变种之一)来完成.
当在新进程中执行新程序时,shell程序会为新程序设置环境变量,比如使用execve函数,使用第三个参数进行传递…,而父进程会调用wait系统调用,一直等待子进程结束,关闭子进程,回到原始点,即父进程shell中.
同理,在shell中的外部命令如ls,cat等命令,也是类似的过程,会根据环境变量或者命令中指定路径寻找程序,如/bin/ls,来执行.
而对如cd,pwd等这类的命令为内部命令,shell可以直接调用,效率更高.

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

在解释问题之前,我们首先了解一下LD_LIBRARY_PATH和相关环境变量LD_PRELOAD的作用:
现代编译器基本默认为动态链接,而动态链接就需要使用支持动态链接的程序库,程序在编译阶段不进行链接,在运行阶段,可执行文件被装载到内存后,再进行链接.
在链接阶段,Linux动态编译器会在一些默认目录中寻找程序所使用的库,这里有两个攻击点:可以被用户进行修改的LD_PRELOAD环境变量和LD_LIBRARY_PATH环境变量
LD_PRELOAD:

在UNIX的动态链接库的世界中,LD_PRELOAD是这样一个环境变量,它包含共享库的一个列表,动态链接器会首先在这个列表中搜索库函数的实现,所以被称为preload,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。
一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码)
而另一方面,我们也可以以向别人的程序注入恶意程序,从而达到那不可告人的罪恶的目的。

LD_LIBRARY_PATH:

如果在上面路径中没有找到,动态链接器会在其他几个列表中指定的目录中继续寻找,其中就包括LD_LIBRARY_PATH环境变量指定的目录列表.

由于这两个变量是可以被用户修改的,攻击者理论上可以通过设置此变量,来影响程序的函数调用选择方式,如果是一个Set-UID程序,则会导致更大的安全问题,攻击者可能会编写root权限下运行的不安全函数,通过环境变量在链接阶段冒充真实函数来运行.
那么动态链接器会没有防御机制吗?
当然不会
动态链接器程序有一个防御机制,当进程的真实用户ID与有效用户ID不一样时,或者真实组ID与有效组ID不一致时,进程将会忽略LD_PRELOAD,LD_LIBRARY_PATH环境变量.
在本次实验中,我们将编译后的task5程序设置为root为所有者的Set-UID程序,此时有效用户ID为0(root),而真实用户为1000(seed),所以环境变量会被忽略,而PATH和自己设定的Myname环境变量不会被动态链接器使用,无安全威胁,所以并不会被进程忽略.

task6 The PATH Environment Variable and Set-UID Programs

运行代码:

#include<stdlib.h>
int main()
{
	system("ls");
	return 0;
}

测试结果:
在这里插入图片描述
能够设置这个Set-UID程序运行你自己的代码而不是/bin/ls吗?如果你能的话,你的代码具有root权限吗?描述并解释你的观察。
/bin/ls为一个符号链接,实际上指向其他解释器,我的系统为16.04,所以实验前需要确保/bin/目录下的sh 符号链接到zsh,而不是bash.
bash的保护机制

此解释器有一个保护机制,当它发现自己是在一个Set-UID进程运行时,会立即将有效用户ID变成实际用户ID,主动放弃特权

随后进行如下实验:

再次运行task6,得到如下运行结果:

我们发现,虽然程序中命令为ls,但实际上运行了我们自己设定的程序/bin/sh,同时由于程序为Set-UID,有效用户为root,所以task6运行时以root权限运行,得到root权限的shell.
注意:
在实验中,我们将ls 命令执行的PATH进行了修改,所有后续我们ls命令将会不可用,但是,由于export命令设置的环境变量仅适用于此shell,所以我们可以关闭shell,再重新打开,PATH就会恢复默认值了.

task7 The LD_PRELOAD Environment Variable and Set-UID Programs

不同运行环境,分别得到如下结果:

场景一

Make myprog a regular program, and run it as a normal user.

在这里插入图片描述
正常执行,pause 1秒,退出程序

场景二

Make myprog a Set-UID root program, and run it as a normal user
在这里插入图片描述正常执行,pause 1秒,退出程序

场景三

Make myprog a Set-UID root program, export the PLD_PRELOAD environment variable again in the root account and run it.

  • root权限运行:在这里插入图片描述正常执行,pause 1秒,退出程序
  • 非root权限运行:
    在这里插入图片描述
    此时,程序以seed身份运行,结果发现,程序并不会正常运行,而是显示了我们自己设定的sleep函数.
    结果分析
    针对Set-UID程序,动态链接器部署了防御机制,在task5中说过,这里不再重复了…
    当非Set-UID下运行时,由于PLD_PRELOAD路径上设置了我们自己设定的共享库,并且函数名称为sleep,所有首先调用,即出现此攻击成功现象.

场景四

Make myprog a Set-UID user1 program (i.e., the owner is user1, which is another user account),export the LD_PRELOAD environment variable again in a different user’s account (not-root user) and run it.
将程序所有者设置为tony1,运行,得到如下结果:
会出现sleep函数重载现象.
将程序设置为Set-UID后,得到如下结果:
不会出现函数重载现象

实验总结:

可见,如果程序为设置了Set-UID的程序,运行时不会出现函数重载,即运行myprog程序的子进程不继承LD_PRELOAD变量,否则,无论运行用户是不是文件所有者,只要有权限运行,都会产生函数重载现象.当然这样设计也会有设计的初衷:我们可以以此功能来使用自己的或是更好的函数(无需别人的源码)但也造成了被恶意重载的安全威胁.

LD_PRELOAD变量的保护策略

  1. 通过静态链接。使用gcc的-static参数可以把libc.so.6静态链入执行程序中。但这也就意味着你的程序不再支持动态链接。
  2. 通过设置执行文件的setgid /setuid标志。在有Set-UID权限的执行文件,系统会忽略LD_PRELOAD环境变量。也就是说,如果你有以root方式运行的程序,最好设置上SUID权限.( 如:chmod 4755 daemon)

task 8: 使用system()和execve()调用外部程序

可以使用此程序进行破坏系统完整性吗?或者删除某一个文件?

可以,进行如下实验过程:

我们将一个root为所有者,所有用户权限仅仅为r的文件,通过task8这个Set-UID程序进行了删除,说明程序以root权限执行了rm命令.
程序还可以获得具有root权限的shell:

注意
我们输入多个命令时,需要在第二个参数位置输入双引号(""),否则,变成了运行两条指令,一条为

./task8 secret

来运行task8程序,由于;表示另一条指令,即:

/bin/sh

与task8程序无关,为shell中一条命令了,所以,此时也不会得到root权限.

注释system函数,换成execve函数后:

得到如下运行结果:
在这里插入图片描述
并不会得到root权限shell.

结果分析

在前面写到过,实际上system()函数执行了三步操作:
1 fork一个子进程;
2 在子进程中调用exec函数去执行command;
3 在父进程中调用wait去等待子进程结束。

在第二步中,引入了外部程序shell,而在shell中,是可以执行任何指令的,所以,我们可以执行多条指令,在设置setuid程序后,可以获取root权限.
而execve函数为执行一个系统调用函数

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

第二个参数中如果包含额外的指令,他们仍然会被视为一个参数,并非一个指令.所以才会出现:

/bin/cat: ‘secret;/bin/sh’: No such file or directory

的错误,secret;/bin/sh被视为了一个字符串参数.
system()函数违背了最小权限原则,调用了shell,而shell可以执行任意命令.以及输入验证原则,过分信任了用户的输入.所以,system()函数需要谨慎使用,推荐使用execve()函数,进行了运行程序与程序参数的分类,更安全.

task9: 权限泄露

setuid()函数作用:

在这里插入图片描述

  • 进程有root权限:

    当有效用户id为root,而真实用户ID和保留用户ID为普通用户,即Set-UID进程,时,会设置有效用户ID,真实用户ID,保留用户ID为参数uid,包括设置为0,即可以通过在Set-UID程序中使用:setuid(geteuid());代码,使进程的真实用户ID,保留用户ID均为0.

  • 进程不具有 root 权限

若进程不具有 root 权限,那么普通用户使用 setuid() 时参数 uid 只能是自己的,没有权限设置别的数值,否则返回失败.

所以说,setuid()函数执行是不可逆过程,等同于放弃root权限,不可以再重新通过setuid()函数获得.
但是 su 命令却能使一个普通用户变成特权用户。这并不矛盾,因为 su 是一个Set-UID程序。执行一个设置了 “Set-UID” 位的程序时,内核将进程的有效用户 ID 设置为文件所有者的 ID(root 的 ID)。而内核检查一个进程是否具有访问某权限时,是使用进程的有效用户 ID 来进行检查的。su 程序的文件所有者是 root,所以普通用户运行 su 命令时,su 进程的权限是 root 权限。

使用场景:

这也表明setuid()函数作用场景有限,开始时,某个程序需要 root 权限完成一些工作,但后续的工作不需要 root权限。可以将该可执行程序文件设置为Set-UID程序,并使得该文件所有者为 root。这样,普通用户执行这个程序时,进程就具有了 root权限,当不再需要 root 权限时,调用

setuid(getuid()) //getuid()函数返回真实用户ID

恢复进程的有效用户 ID 为执行该程序的真实用户的 ID 。对于一些提供网络服务的程序,这样做是非常有必要的,否则就可能被攻击者利用,使攻击者控制整个系统。

实验过程

创建secret文件,进行如下操作:
在这里插入图片描述
对代码进行路径修改,最终代码如下(由于出现多个warning,这是由于头文件不全,所以添加了几个头文件):

#include <stdio.h>
#include <stdlib.h>
#include<unistd.h>
#include <fcntl.h>
#include <sys/types.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("/home/seed/lab_setuid/secret", O_RDWR | O_APPEND);
	if (fd == -1) 
	{
		printf("cannot open /home/seed/lab_setuid/secret\n");
		exit(0);
	}
	// printf("real id:%d\n effective id:%d\n",getuid(),geteuid() );
	 /* 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 */
	// setuid(geteuid());
	// printf("real id:%d\n effective id:%d\n",getuid(),geteuid() );
	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);
	}
}

设置为Set-UID的root程序后,运行,查看secret,我们发现,恶意数据被写入文件:
在这里插入图片描述
这是因为,在子进程中,文件描述符fd在进入close()函数前仍然有效,此时虽然进行了setuid(getuid());进行进程的特权解除,随后的非特权进程仍然可以进行修改文件.所以应该在降低特权前运行close(fd),销毁文件描述符.

实验总结

实验任务比较多,知识点比较琐碎,如何将琐碎知识点整合是一个挑战,在写实验报告过程中,我也在尽量地试图去捋清楚各个task之间相关函数的功能上的互补关系,比如fork与execve函数之间的传递环境变量功能上的相似性以及实现机理的差异性,system函数与execve函数在执行命令功能上的相似性与实现机理的差异性,同时总结了一些安全策略,比如关于LD_PRELOAD的保护策略等
这个实验为SEEDlab的第一个实验,在教材上用了两个章节来讲解,所以知识点很多,由于本学期学过相关知识,只是没有专门去做这个实验,所以做的轻松一些,但也学到了不少新知识,对一些攻击方式理解更深,纸上得来终觉浅,还是要多做实验,期末复习时,看的课本,觉得有些地方晦涩,做完实验感觉有些通透了.

参考博客:
超级大佬的博客,关于LD_PRELOAD

  • 3
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值