三、Set-UID 特权程序

原文:Set-UID Programs and Vulnerabilities

译者:飞龙

这个讲义的主要目标就是来讨论特权程序,为什么需要他们,他们如何工作,以及它们有什么安全问题。特权程序是可以给予用户额外权限的程序,这些权限超出了用户已有的权限。例如,Web 服务器是特权程序,因为他允许远程用户访问服务端的资源;Set-UID 程序也是个特权程序,因为他允许用户在程序执行期间获得 ROOT 权限。

这篇讲义中,我们会专注于 Set-UID 的机制,并且在我们的案例学习中使用它。但是,我们在这里讨论的许多安全原则也可以用在其他特权程序上。

1 Set-UID 机制如何工作

  • 动机

    • 你想让其他人能够搜索文件中的一些单词,但是不想让他们能够读取文件。如何实现它呢?
    • 用户的密码储存在/etc/shadow中,普通用户不可读写。但是,passwd程序允许用户修改它们自己的密码。也就是,当用户执行passwd,它们可以突然修改/etc/shadow,并且用户只能修改/etc/shadow中的一项,但不是其他人的条目。如何实现?
  • Set-UID 程序

    • 有效 UID 和真实 UID 的概念
    • 对于非 Set-UID 程序,二者相同
    • 对于 Set-UID 程序,前者是程序所有者的 UID,后者是程序用户的 UID。
  • 有效 UID 和真实 UID

    • 登录时期,真实 UID 和有效 UID,以及登录过程保存的 UID 都设为负责进程创建的用户的登录 UID。对于真实、有效和保存的组 ID 也一样。它们设为负责进程创建的用户的组 ID。
    • 当进程调用exec系列函数之一,来执行文件(程序)时,进程相关的用户和组标识符会发生改变。如果执行的文件是个 Set-UID 文件,进程的有效和保存的 UID 会设为所执行文件的所有者。如果所执行文件是个 Set-GID 文件,有效 UID、保存的 UID 以及有效 GID、保存的 GID 不会改变。
    • 访问控制基于有效 UID 和 GID。
  • 为什么passwdchshsu程序需要为 Set-UID 程序?

  • Windows NT/2000 中有没有 Set-UID 程序?如果没有,相同问题在 Windows 中如何解决?

    • Windows 没有 Set-UID 这个概念。不同的机制用于实现权限功能。开发者会将特权程序编写为服务,用户使用本地过程调用将命令行参数发给服务。
    • 服务可以自动或手动启动。
    • 每个服务都有安全描述符,制定了哪个用户允许启动、停止和配置该服务。
    • 服务通常在本地系统账户下运行。
  • 如何打开 Set-UID 位:

    % chmod 4755 file ---> -rwsr-xr-x 
    
  • Set-UID 在 Minix 中如何实现?

    /* This is the per-process information */ 
    EXTERN struct fproc {
        uid_t fp_realuid; /* real user id */ 
        uid_t fp_effuid; /* effective user id */ 
        gid_t fp_realgid; /* real group id */ 
        gid_t fp_effgid; /* effective group id */ 
        ...
    }
    
  • Set-UID 机制的恶意使用:

    • 攻击者可以使用你的账户十秒。它能够植入后门,以便之后再回到你的账户吗?

      % cp /bin/sh /tmp 
      % chmod 4777 /tmp/sh
      

      由此,攻击者创建了 Set-UID Shell 程序,你是这个程序的所有者。因此,当攻击者之后再运行 Shell 时,它使用你的权限运行。

  • 问题:

    • 普通用户是否可以调试 Set-Root-UID 程序?
    • 普通用户是否可以使用chown来将文件所有者修改为任何用户?

2 Set-UID 程序的漏洞

2.1 隐藏的输入:环境变量

特权程序必须对所有输入进行安全检查。输入检查实际上是访问控制的一部分,特权程序必须这么做,来确保程序的安全。很多安全问题都是输入检查的错误造成的。

如果输入在程序中显式存在,程序员可能记得执行输入检查;如果输入隐式存在,输入检查可能会忘记,因为程序员可能不知道这个输入的存在。环境变量就是这类输入。

每个 Unix 进程都在特定环境下运行。环境由环境变量表组成,每个变量都有赋值。一些程序内部使用这些环境变量,Shell 程序就是这些程序的例子。换句话说,一些环境变量的值可以 Shell 程序的行为。

由于环境变量由用户控制,如果程序依赖这些变量,用户可以间接影响这类程序的行为,通过修改一些环境变量的值。因此,理解特权程序是否依赖任何环境变量的值就十分重要。一种程序可能被环境变量影响的方式,就是在程序中显式使用环境变量的值。在 C 语言中,程序可以使用getenv来访问环境变量的值。但是,也有许多例子,程序隐式使用环境变量。这就是我们在许多 Set-UID 程序中看到的漏洞。我们会在这一节中展示几个例子。

  • PATH环境变量

    • 在 Shell 中执行命令式,Shell 会使用PATH环境变量搜索所有命令,它包含一个目录列表。Shell 程序通过目录列表(和他们在PATH环境变量的相同顺序)来搜索。第一个匹配命令名称的程序会被执行。

    • 下面会发生什么?要注意system (const char *cmd)库函数首先调用/bin/sh程序,之后让 Shell 程序执行cmd

      system ("mail");
      
  • 攻击者可以将PATH修改成这个,并使当前目录下的mail被执行。

    PATH=".:$PATH"; export PATH
    

    拿超人来比喻的话,如果超人的指令是“左转”(坏人在左边而好人在右边,你可以假设他要攻击坏人)。如果攻击者准确知道左转指令什么时候以及在哪里执行,他就可以做出与上面类似的攻击。因为“左边”是个相对的方向,并不是绝对方向。如果攻击者事先在你想要左转的地方,放置一个旋转设备,并将你旋转 180 度,只要你踏上了它,“左转”就变成了转到好人那里。如果你遵循了指令,你最后就会攻击好人。

  • IFS环境变量

    • IFS变量决定了哪些字符被解释为空白字符。它代表了内部字段分隔符。假设我们设置它来包含斜杠字符:

      IFS="/ \t\n"; export IFS 
      PATH=".:$PATH"; export PATH
      
    • 现在从 Bourne Shell 中调用任何使用绝对PATH的程序(李儒system)。它现在解释为下面的东西,它会在当前用户目录下,尝试执行命令行调用bin

      system("/bin/mail root"); ---> system(" bin mail root");
      
    • IFS 的 bug 现在已经在 Shell 中禁止了;所引用的新的 Shell 进程不会继承 IFS 变量。

    • 假设在超人的故事中。超人知道使用“左转”指令的风险,所以它将其改为“转到北边”,它现在是个绝对方向。这仍然存在漏洞,因为“北”由磁场决定,不幸的是,磁场可以通过攻击者放置的磁铁来影响。

  • LD_LIBRARY_PATH环境变量

    • Linux 中,除非编译时期通过-static显式指定,所有 Linux 程序需要在运行时期链接到动态链接库。动态链接器或加载器ld.so/ld-linux.so加载程序所需的共享库,准备要运行的程序,之后运行它。你可以使用下面的命令来观察程序需要什么共享库。

      % ldd /bin/ls
      
    • LD_LIBRARY_PATH是一个环境变量,被动态链接器或加载器(ld.so/ld-linux.so)使用。它含有一个目录列表,让链接器或者加载器在搜索共享库时寻找。可以列出多个目录,以冒号(:)分隔。对于任何可执行文件,这个列表放在现存的编译器加载路径,以及任何系统默认加载路径的前面。

    • 基本上每个 Unix 程序都依赖于libc.so,并且每个 Windows 程序都一拉李雨 DLL。如果这些苦可以替换为恶意的副本,恶意代码就可以在共享库函数被调用时执行。

    • 由于LD_LIBRARY_PATH可以由用户充值,攻击者可以修改这个变量,并强制库加载器在攻击者的目录中搜索库,从而加载攻击者的恶意库。

      % setenv LD_LIBRARY_PATH .:$LD_LIBRARY_PATH
      
    • 为了使 Set-UID 程序更加安全,不受LD_LIBRARY_PATH环境变量的影响,运行时的链接器或加载器(ld.so)会忽略环境变量,如果程序是个 Set-UID 程序。

    • 防护应用也可以静态链接到可信库来避免这个问题。

    • 在 Windows 主机上,通常在加载 DLL 的时候,在搜索系统目录之前,会搜索当前目录中的 DLL。如果你点击 Word 文档来启动 Office,会在包含该文档的目录下搜索 DLL。

  • LD PRELOAD环境变量

    • 许多 Unix 系统允许你“预加载”共享库,通过设置环境变量LD PRELOAD。这些用户指定的库会在所有其它库之前加载。这可以用于选择性重载其他库中的函数。例如,如果你已经构建了一个库,你可以使用下列命令预加载它:

      % export LD_PRELOAD=./libmylib.so.1.0.1
      

      如果libmylib.so.1.0.1包含函数sleep,它是个标准的libc函数,当程序执行并调用sleep时,libmylib.so.1.0.1中的函数会被调用。

  • 这里是一个程序,重载了libc中的sleep

    #include <stdio.h> 
    void sleep (int s) { 
        printf("I am not sleeping!\n"); 
    }
    

    我们可以使用下列命令编译程序(假设上面的程序名为name.c):

    % gcc -fPIC -g -c a.c 
    % gcc -shared -o libmylib.so.1.0.1 a.o -lc
    

    现在,我们运行下列程序:

    int main() { 
        sleep(1); 
        return 0; 
    }
    

    如果环境变量LD PRELOAD设为libmylib.so.1.0.1,标准libc中的sleep没有使用,反之我们共享库中的sleep函数会调用,并且打印"I am not sleeping!"

  • 为了确保 Set-UID 程序安全,不受LD PRELOAD环境变量的控制,运行时链接器或加载器(ld.so)会忽略这个环境变量,如果程序是 Set-Root-UID 程序,除非真实 UID 也为 0。

2.2 调用其它程序

当特权程序调用其它程序时,必须注意是否调用了非预期的程序。我们知道,环境变量是个我们需要注意的地方,也有一些我们需要注意的其它地方。

  • 如果 Set-UID 程序执行下面的事情,会发生什么?

    // The contents of User_Input are provided by users. 
    sprintf(command, "/bin/mail %s", User_Input); 
    system(command);
    
  • User_Input可能包含 Shell 的特殊字符(例如| & < >)。要记住,system调用实际上先调用 Shell,之后让 Shell 程序执行/bin/mail。如果我们不注意,攻击者就可以执行其它程序,通过让User_Input是下面的字符串:

    xyz@example.com ; rm -f /* ; /bin/sh
    

2.3 其它知名的漏洞模式

除了上面的输入校验漏洞,也有一些其他的知名漏洞模式。我们会在单独的章节中讨论它们。这里是这些模式的总结:

  • 缓冲区溢出
  • 竞态条件
  • 格式化字符串

2.4 杂项漏洞

有许多其他漏洞,并不易于归纳进上面讨论的任何分类。一些可能被归纳为更广泛的“呼入椒盐漏洞”,但是由于他们的独特特性,我们在这里单独讨论它们。我们不能枚举所有漏洞。我们只能给出一些示例,来展示程序员在程序逻辑中的不同错误,并且展示这些错误如何变为漏洞。

  • lpr漏洞:它在/tmp目录下生成临时文件。文件名称应该是随机的,但是,由于伪随机数生成的错误,文件名称每一千次就会发生重复。这个程序是 Set-UID 程序。将可预测的文件名称链接到/etc/passord会导致lpr覆盖/etc/passord
  • chsh漏洞:chsh让用户输入 Shell 程序的名称s,并在/etc/passwd中保存输入。chsh并不会做清晰的检查。程序假设用户的输入只有一行,不幸的是,这个假设可以为假:用户可以键入联行输入,其中第二行是类似xyz::0:0::的东西美丽如,用户可以插入一个新的超级用户账户(UID:0),不带密码。
  • sendmail漏洞
    • sendmail:(1)入境的邮件会添加在/var/mail/wedu。(2)如果/var/mail/wedu的所有者不是 Wedu,sendmail会使用chown将所有者修改为 Wedu。
    • 你能利用它来读取 Wedu 的邮件吗?
    • 你能利用它来给 Wedu 造成更大的损失吗?

3 提升 Set-UID 程序的安全性

  • exec函数
    • exec函数系列通过将当前进程映像包装为新的,来运行紫禁城。有许多exec函数的版本,工作方式不同。它们可以归类为:
      • 使用/不适用 Shell 来启动新程序。
      • 通过 Shell 处理命令行参数(Shell 可以引入比我们预期的更多功能。要注意 Shell 是个强大程序)。
    • 启动子进程涉及到依赖问题以及属性继承,我们之前看到它可能存在问题。函数execlpexecvp使用 Shell 来启动程序。它们使程序的执行依赖于当前用户安装的 Shell。例如,依赖于PATH和其它环境变量的值。函数execv跟家安全,因为它不向代码引入任何这种依赖。
    • system(CMD)调用向 Shell 传递字符串来执行子进程(即作为单独派生的进程)。它是个用于执行 EXEC 函数的方便的前端。
    • popen的标准实现也与之相似。这个函数打开到新锦成管道,以便执行命令,并读取任何输出作为文件流。这个函数也启动 Shell 来解释命令字符串。
  • 如何安全地调用程序?
    • 避免任何 Shell 的调用。不使用system,而是execveexecve不调用 Shell,system调用。
    • 避免execlp (file, ...)execvp(file,...),它们的语义类似于 Shell。它们使用文件内存作为 Shell 的标准输入,如果文件不是有效的可执行目标文件。
    • 小心可能使用 Shell 实现的函数。
      • Perl 的open函数可以执行命令,通常通过 Shell 来这么做。
  • 提升system的安全性
    • 要记住system首先调用/bin/sh。在 Ubuntu 中,它使用参数sh, -c和用户提供的字符串来调用execv /bin/sh

    • 在一些 Ubuntu 的早起版本中(例如 9.11),/bin/sh(实际上是 Bash)忽略 Set-UID 位选项。因此,在 Set-UID 中调用system(CMD)时,CMD 不会使用 Root 权限执行,除非 CMD 本身也是个 Set-UID 程序。下面 代码在 Bash 中丢弃了 Set-UID 位。

      if (running_setuid && privileged_mode == 0) 
          disable_priv_mode ();
      
      ... 
      void disable_priv_mode () { 
          setuid (current_user.uid);
          setgid (current_user.gid); 
          current_user.euid = current_user.uid; 
          current_user.egid = current_user.gid;
      }
      
    • 但是,上面的保护看似破坏了一些需要使用system的 Set-UID 程序。因此,从某个版本起,由于添加了其它条件(对于 11.04 和 12.04),保护被移除了。

      if (running_setuid && privileged_mode == 0 && act_like_sh ==0) 
          disable_priv_mode ();
      

      如果 Bash 通过/bin/sh符号链接调用,act_like_sh设为 1,因此权限没有禁用。但是,如果你直接将 Bash 变成 Set-UID 程序并尝试运行,保护仍然会有效,并且权限会丢弃。

4 最小权限原则

最小权限原则(最早由 Saltzer 和 Schroeder 提出):

每个程序和系统用户都应该具有完成任务所必需的最小权限集合。

限制代码运行所需的安全权限,有一个非常重要的原因,就是降低你的代码在被恶意用户利用时,造成的损失。如果你的代码仅仅使用最小权限来执行,恶意用户就难以使用它造成损失。如果你需要用户使用管理员权限来执行代码,任何代码中的安全缺陷,都会通过利用该缺陷的恶意用户,潜在造成更大的损失。

编写特权程序时的问题:

  • 程序需要该权限吗?
    • 如果程序不需要任何特殊权限来运行,它不应该是个特权程序。
  • 程序需要所有权限吗?
    • 我们只给予程序完成任务所需的最小权限集合。
    • 许多操作系统不向我们提供多种选择;我们可以选择包含所有 Root 权限的集合,或者不包含任何权限的集合。多数 Unix 系统就是这样,你要么是 Root 要么不是,没有中间值。
    • 多数现代 Unix 系统(和 Windows)引入了更多选择。这些系统将 Root 权限划分为多种字权限。使用这种自粒度,我们就可以更好应用最小权限原则。
  • 程序现在需要权限吗?
    • 程序通常偶尔不需要特定权限,它们在这个时候就变得不必要了。我们应该暂时禁用它们来满足最小权限原则。这么做的好处就是,放置程序犯下意外的错误,使之不能对需要禁用权限的事情造成损失。下面的图像展示了这个要点。
    • 稍后,禁用的权限可能就必要了,我们之后可以开启它。
    • 要记住,开启或禁用权限可以在特定场景下降低损失,当攻击者不能像漏洞程序注入代码的时候。如果攻击者可以向漏洞程序注入代码,注入的代码自己就能够开启权限。
  • 程序在未来需要权限吗?
    • 如果权限不再需要了,它就是不必要的,应该永久溢出,所以最小权限集合应基于未来的需求来调整。

Unix 为我们提供了什么机制,来实现最小权限原则?

  • 实用的系统调用:setuid()seteuid()setgid(),和setegid()
  • seteuid(uid):它为调用进程设置有效 UID。
    • 如果调用进程的有效 UID 是超级用户,uid参数可以是任何东西。这通常由超级用户用来暂时让渡/获取权限。但是,进程的超级用户权限并没有丢失,进程可以拿回来。
    • 如果调用进程的有效 UID 不是超级用户,UID 参数只能是有效 UID,真实 UID,以及保存的 UID。这通常由特权程序使用来恢复他的权限(原始的特权有效 UID 保存在保存的 UID 中)。
  • setuid(uid):它设置了当前进程的有效 UID。如果调用者的有效 UID 是 Root,也会设置真实和保存的 UID。
    • 如果调用进程的有效 UID 是超级用户,真实、有效和保存的 UID 全部会设为uid参数。之后,程序就不能够拿回 Root 权限(假设 UID 不是 Root)。这用于永久让渡高权限的访问权。
    • 想要暂时放弃 Root 权限的 Set-Root-UID 程序,假设身份是非 Root 用户,之后不能使用setuid来拿回权限。你可以使用seteuid调用来完成它。
    • 如果调用进程的有效 UID 不是超级用户,但是 UID 是调用进程的真实 UID 或者保存的 UID,那么有效 UID 会设置为uid。这类似于seteuid
  • 示例(在 Fedora 中):进程使用有效 UID = 0 来运行,真实 UID= 500,在调用它们之后,有效和真实 UID 是什么?
    • setuid(500); setuid(0);:答案:500/500(第一个调用生成 500/500,第二个调用失败)。
    • seteuid(500); setuid(0);:答案:0/500(第一个调用生成 500/500,第二个调用生成 0/500)。
    • seteuid(600); setuid(500);:答案:500/500(第一个调用生成 600/500,第二个调用生成 500/500)。
    • seteuid(600); setuid(500); setuid(0);:答案:0/500(第一个调用生成 600/500,第二个调用生成 500/500,第三个调用生成 0/500)。

书籍推荐