网络攻防-软件漏洞3-环境变量攻击,涉及实验:SEED Labs – Environment Variable and Set-UID Program Lab ,代码见:https://github.com/Seanxz401/seed-labs
理论
C语言访问环境变量:
- 从主函数中访问:
void main(int argc, char* argv[], char* envp[])
- 使用全局变量:
extern char** environ
ps.当对环境变量进行更改时,存储环境变量的位置可能会移动到堆中,因此environ会发生改变而envp[]不会。因为envp是main函数中的局部变量,从缓冲区溢出那一节的知识可以知道,函数内局部变量存储在栈中。
进程获取环境变量:
- fork创建新进程,子进程继承父进程的环境变量
- execve启动新程序(此程序中的旧环境变量会丢失),通过参数传递环境变量
int execve(const char* filename, char* const argv[], char* const envp[])
- filename是命令对应的文件,如
/bin/bash
- argv是命令及对应参数,如
bash -c ...
- filename是命令对应的文件,如
/proc是linux中的一个虚拟文件系统。它包含每个进程的一个目录,使 用进程ID作为目录的名称。每个进程目录都有一个名为environ的虚拟文件,其中包含进程的环境。当在bash shell中调用env程序时,它将在子进程中运行。因此,它打印 出shell子进程的环境变量,而不是它自己的环境变量。
shell命令变量与环境变量:当shell程序启动时,它会将环境变量复制到自己的shell变量中。对 shell变量所做的更改将不会反映出在环境变量上。但是exported后的shell变量可以传到子进程。
setuid
允许用户以程序所有者的权限运行程序
每个进程都有两个用户ID:
- RUID:进程的真正所有者
- EUID:进程的权限,访问控制。
攻击原理:当执行setuid程序时,RUID≠EUID,RUID是当前用户的UID,而EUID为程序所有者的UID,如果程序归root所有,则程序以root权限运行。
设置为setuid程序:
1 | sudo chown root a.out |
通过动态链接器攻击
链接:
- 静态链接:包含源代码和涉及的外部库的代码(编译出的程序较大)
- 动态链接:在运行时,使用环境变量,将外部库的可执行文件加载到内存(.so/.dll)。(可以通过
ldd a.out
来查看a程序依赖什么共享库)
通过动态链接器进行攻击:设置用户变量,控制链接过程的结果
- LD_PRELOAD包含一个共享库的,链接器将首先搜索它
- 如果没找到,将在几个文件夹列表中搜索,包括LD_LIBRARY_PATH指定的文件夹
eg.用户创建一个共享库(链接到用户指定的函数),并将共享库添加到LD_PRELOAD环境变量中。执行其他调用sleep函数程序时,执行的sleep为用户自定义的函数。
1 | # sleep.c中自定义sleep函数,main.c是调用了sleep函数的程序 |
ps.当EUID≠RUID时,动态链接器会忽略LD_PRELOAD和LD_LIBRARY_PATH
通过外部程序攻击
调用外部程序:
- exec函数家族,它们以不同的形式调用execve,直接运行程序
- system,调用execl,execl最终调用execve运行/bin/sh,然后通过shell运行程序。shell程序会继承环境变量
操作PATH变量进行攻击:shell运行命令而没有提供绝对路径时,它将使用PATH变量来找到该命令
eg.正常的程序(vul.c)中存在system("cal");
,通过操作路径变量,使shell将cal命令定位到自定义的cal程序(攻击者可在这个程序中通过system继续继承环境变量,以root权限执行其他命令)
1 | sudo chown root vul.o |
ps.由此可知,当在特权程序中调用外部程序时,应该使用execve,不会继承环境变量。
通过外部库的攻击
程序通常使用来自外部库的函数。如果这些函数使用环境变量,则用户可以设置利用。
通过应用程序代码攻击
程序可以直接使 用环境变量。如 果这些是特权程 序,它可能会导 致不可信任的输 入。
eg.getenv("PWD");
获取环境变量PWD,PWD的值来自于shell程序,用户可以改变
服务方法
允许正常用户执行特权操作的两种方法:
- setuid:普通用户必须运行一个特殊的程序才能临时获得根权限
- 服务方法:普通用户必须请求特权服务才能为他们执行操作。(多了一个服务请求的过程)
实验
Task1
查看指定环境变量PWD
export设置环境变量
unset取消环境变量
Task2
- 编译运行:
gcc myprintenv.c
- 将运行结果保存到文件中:
a.out > child
- 修改myprintenv.c文件,注释子进程中的printenv函数,并取消父进程中的注释,再次编译,将结果保存在另一个文件中:
a.out > parent
- 比较两个文件
diff child parent
,发现是相同的,即子进程完全继承父进程的环境变量。
Task3
myenv.c
1 | execve("/usr/bin/env", argv, NULL);//修改前 |
修改前没有输出,修改后能输出环境变量。原因:
extern char **environ;
获取环境变量表;- execve的第三个参数就是传入环境变量的位置;
Task4
编译运行,能输出环境变量。
system('/usr/bin/env')
调用execl()执行/bin/sh
,execl()调用execve()并将环境变量传给它。
Task5
编译得到foo后,修改权限:
1 | sudo chown root foo |
以普通用户修改环境变量ANY_NAME
,执行foo后可以看到修改后的ANY_NAME
,即父进程中设置的环境变量能进入到setuid(foo)的子进程中。
Task6
- 在原有PATH变量首部添加
/home/seed
路径:export PATH=/home/seed:$PATH
,寻找命令时会优先选择靠前的。 - 关闭保护措施:
sudo ln -sf /bin/zsh /bin/sh
- 新建两个文件并编译。
1 | //ls.c 真的调用ls命令,编译为ls,修改权限类似task5 |
- 执行真的ls程序时,system调用首选了/home/seed下的ls程序,即调用了恶意代码。
Task7
1 | gcc -fPIC -g -c mylib.c |
- 普通程序+普通用户:seed用户已经拥有LD_PRELOAD环境变量
- setuid_root程序+普通用户:sleep 1秒;(进程ID与当前用户ID不一致时,忽略LD_PRELOAD环境变量)
- setuid_root程序+LD_PRELOAD环境(root):(
sudo /bin/bash
进入root) - setuid_user1程序+LD_PRELOAD环境(user2):
- seed用户编译程序后将权限改为sean用户;
- seed用户导入环境变量后执行,睡眠1s。
结论:LD_PRELOAD环境变量让链接器将sleep()函数和用户的代码链接起来,将该环境变量加入新的共享库,程序会调用用户的sleep()函数,输出I am not sleeping! 但是当进程的真实用户ID和有效用户ID不一致时,进程忽略LD_PRELOAD环境变量。因此第二个和第四个的sleep没有被替换。
Task8
system()
创建test.txt文件,修改权限
1
2sudo chown root test.txt # 修改为root用户所有
sudo chmod 600 test.txt # 修改权限为所属用户能读写编译catall.c,此时仍不能读取test.txt,修改权限如task5后可以读取。
执行
./cat "test.txt;rm test.txt"
,会先输出内容再删除该文件。(以分号分割两条命令)
execve()
- 创建test.txt步骤类似;
- 注释掉system函数,取消execve的注释,编译,修改cat权限,仍然没有权限读取test.txt;
- 构建删除语句,execve无法识别为两条命令
可以看出execve()比system()更安全。
Task9
创建文件:
sudo vim /etc/zzz
注释execve函数,添加代码:
1
2
3
4
5
6
7if(fork()){//创建进程
close(fd);
exit(0);
}else{//在子进程中,向已经打开的文件描述符中写入字符
write(fd,"\ntask9 attack\n",15);
close(fd);
}编译,修改权限,运行,发现指定字符串被写入文件
在此实验过程中,通过setuid(getuid());
降低了程序的权限,但是由于文件已经被打开,降级后的进程仍可访问该文件。