第38章 编写安全的特权程序

是否需要一个set-user-id或set-group-id程序

一个程序可以通过两种方式以特权方式运行:

  • 程序在特权用户ID下启动,很多daemon程序都是以root身份启动并运行
  • 程序设置了set-user-ID或set-group-ID权限位,当此程序被执行时,它会将进程的有效用户ID(组)修改为程序的所有者(组)

尽量避免编写此类程序
假如一个set-user-ID程序需要更新一个它没有写权限的程序,更加安全的方式是创建一个专用组账号(组ID),将文件所属的组修改为那个组,接着编写一个将进程的有效组ID设置为该专用组ID的set-user-ID程序,由于这个专用组ID没有其他权限位,能够极大的限制程序包含bug或被破坏时造成的损失

以最小权限操作

按需拥有权限
  1. uid_t orig_euid;
  2. orig_euid = geteuid();
  3. // 放弃权限:将有效用户ID改为真实用户ID
  4. if (-1 == seteuid(getuid()))
  5. perror("seteuid error");
  6. // 重获权限:将有效用户ID还原为saved set-user-ID
  7. if (-1 == seteuid(orig_euid))
  8. perror("seteuid error");

最安全的做法是在程序启动的时候立刻删除权限,后面需要的时候临时重新获取权限

无需使用时永久的删除权限

通过将进程用户(组)ID重置为真实(组)ID完成

修改进程身份信息的注意事项
  • Linux上,使用setresuid、setresgid修改用户和组身份信息
  • 即使调用者的有效用户ID为0,修改身份信息的系统调用在程序显式的操作其能力时可能表现出意料之外的行为,如禁用CAP_SETUID能力,修改进程用户ID将失败
  • 实践中,不仅要检查修改身份信息的系统调用是否成功,还需验证修改行为是否达到预期
  • 一些身份信息的变更只能由有效用户ID为0的进程完成

    小心执行程序

    在执行另一个程序前永久的删除权限

    如果一个set-user-ID程序执行了另一个程序,那么应该确保进程用户(组)ID被重置为真实用户(组)ID,这样新程序在启动时就不会拥有这些权限

    避免执行一个拥有权限的shell

    运行于用户控制下的特权程序不应该直接或间接的执行shell,shell的复杂性和强大功能几乎不可能消除所有的安全漏洞,如果必须要执行shell,需确保执行之前永久的删除权限,Linux中,执行脚本时会毫无征兆的忽略set-user-ID和set-group-ID权限位

    在exec之前关闭用不到的文件描述符

    可以显式的关闭或设置close-on-exec标记

    避免暴露敏感信息

    当程序读取密码或其他敏感信息时应该执行完所需的处理后立刻从内存删除这些信息,这种安全隐患的原因:

  • 包含这些数据的虚拟内存页面可能被一个特权程序读取

  • 如果产生core文件,可能从该文件读取

应该避免产生core文件,默认Linux中,不允许set-user-ID程序收到信号时产生一个core

确定进程的边界
考虑使用能力

通过只启用进程所需的能力使得程序能够不拥有root权限的情况下运行,这就降低了程序发生安全问题的可能,使用能力和securebits标记可以创建只拥有有限的一组权限但无需属于root的进程,这样的进程无法使用exec重新获取所有能力

考虑使用chroot

建立一个chroot监牢限制程序能够访问的一组目录和文件,还需确保chdir来将进程的当前工作目录改为监牢的一个位置,但chroot监牢不足以限制一个set-user-ID程序
除此之外,虚拟服务器比chroot监牢更加安全和灵活

小心信号和竞争条件

在程序的合适地方捕获、阻塞或忽略信号可以防止可能存在的安全问题,信号处理器设计尽可能简单可以降低竞争条件的风险

执行文件操作和文件IO的缺陷

应该遵循的指南:

  • 需要将进程的umask设置为一个能确保进程永远无法创建公共可写的文件的值
  • 文件所有权由有效用户ID决定,需要使用seteuid或setreuid临时修改进程的身份信息以确保创建的文件不会属于错误的用户
  • 如果set-user-ID-root程序必须创建一个开始由其拥有最终由另一个用户拥有的文件,所创建的文件开始应该不对其他用户开放写权限,可通过open的mode参数或open之后设置进程的mask完成,之后程序使用fchown修改文件的所有权,然后根据需要使用fchmod修改文件的权限
  • 先打开文件,再检查特性,如open之后再fstat
  • 如果一个程序必须确保自己是文件的创建者,open时指定O_EXCL标记
  • 特权进程应该避免创建或依赖/tmp这样的公共可写的目录

    不要完全相信输入和环境

    不应信任环境列表

    set-user-ID和set-group-ID程序不应假设环境变量的值是可靠的,特别是PATH和IFS;PATH确定了shell、system、popen、execlp、execvp的搜索路径,恶意用户可以改变PATH值,更好的做法是指定绝对路径或调用之前的函数前先删除权限,IFS确定了shell解释器用来分割命令行中的单词的分隔符,应将这个变量设置为空字符串,表示shell只会把空白字符当成单词分隔符;某些情况下,最安全的方式是先删除整个环境列表,执行完再还原

    防御性的处理不可信用户的输入

    包括检验数字是否位于接收范围内、字符串的长度等

    避免对进程的运行时环境进行可靠性假设

    set-user-ID程序应该避免假设其初始的运行环境是可靠的,如标准输入、输出或错误可能会被关闭

    小心缓冲区溢出

    永远不要使用gets、使用scanf、sprintf、strcpy、strcat时需要谨慎,最好使用其安全版本如snprintf、strncpy、strncat等,即使使用它们也要小心

    小心拒绝服务攻击

    通过向服务器发送能导致其奔溃的错误数据或使用虚假请求给服务器增加负载使得系统无法提供正常服务,应该采取的措施:

  • 进行负载控制,如超过预先设定的限制就丢弃请求

  • 服务器与客户端的通信设置超时时间
  • 服务器遇到超预期的负载不应该奔溃
  • 设计的数据结构应该能够避免算法复杂度攻击

    检查返回状态和安全的处理失败情况

    特权程序应该总是检查系统调用和库函数调用是否成功以及是否返回了预期的值