
笔记和练习博客总目录见开始读TLPI。特权程序可以访问普通用户无法使用的功能和资源文件、设备等。程序可以通过两种方式获得特权运行程序在特权用户ID下启动。许多守护进程和网络服务器通常以root身份运行就属于这一类。程序的 set-user-ID 或 set-group-ID 权限位被设置。当一个 set-user-IDset-group-ID程序被执行时它会将进程的有效用户组ID 改为与程序文件的所有者组相同。我们在第9.3节首次介绍了 set-user-ID 和 set-group-ID 程序。在本章中我们有时会使用“set-user-ID-root”一词以区分赋予进程超级用户权限的 set-user-ID 程序和赋予进程其他有效身份的程序。如果一个特权程序有漏洞或者可能被恶意用户利用那么系统或应用程序的安全就可能受到威胁。从安全的角度来看我们应该编写程序以尽量减少被攻破的可能性以及一旦被攻破可能造成的损失。本章的内容就是围绕这些主题提供一套安全编程的推荐做法并介绍在编写特权程序时应该避免的各种陷阱。38.1 Is a Set-User-lD or Set-Group-lD Program Required?关于 set-user-ID 和 set-group-ID 程序的最佳建议之一就是尽可能避免编写它们。如果有不需要赋予程序特权的替代方法完成某个任务我们通常应该采用该替代方法因为这样可以消除安全漏洞的可能性。有时候我们可以将需要特权的功能隔离到一个独立的程序中该程序只执行单一任务并在需要时在子进程中执行这个程序。这个技巧对库特别有用。例如第 64.2.2 节中描述的 pt_chown 程序就是这样的一个例子。即使在需要设置用户IDset-user-ID或组IDset-group-ID的情况下也并不总是必须让设置用户ID的程序赋予进程 root 权限。如果给进程其他权限就足够了那么应该优先选择这种方式因为以 root 权限运行可能会带来安全风险。想象一个需要让用户更新他们没有写权限的文件的 set-user-ID 程序。更安全的做法是为这个程序创建一个专用的组账户组ID将文件的组所有权改为这个组并让该组可以写文件然后写一个 set-group-ID 程序将进程的有效组ID设置为这个专用组ID。因为这个专用组ID本身没有其他权限如果程序有漏洞或被利用这样做能大大限制可能造成的损坏。38.2 Operate with Least Privilege一个设置用户ID或设置组ID的程序通常只在执行某些操作时需要特权。当程序尤其是以超级用户特权运行的程序执行其他工作时它应该禁用这些特权。当特权不再需要时它们应该被永久放弃。换句话说程序应该始终以完成当前任务所需的最小特权来运行。保存的设置用户ID功能就是为此设计的第9.4节。Hold privileges only while they are required在一个设置用户ID的程序中我们可以使用下面的 seteuid() 调用序列来暂时放弃然后重新获得权限uid_torig_euid;orig_euidgeteuid();if(seteuid(getuid())-1)/* Drop privileges */errExit(seteuid);/* Do unprivileged work */if(seteuid(orig_euid)-1)/* Reacquire privileges */errExit(seteuid);/* Do privileged work */第一次调用会使调用进程的有效用户ID与其真实ID相同。第二次调用则会将有效用户ID恢复为保存在已保存的设置用户ID中的值。对于设置组ID的程序已保存的设置组ID会保存程序最初的有效组ID而 setegid() 用于放弃并重新获取权限。我们在第9章中描述了 seteuid()、setegid() 以及其他类似的系统调用并在表9-1第181页中进行了总结。最安全的做法是在程序启动时立即放弃权限然后在程序后续需要的时候暂时重新获取权限。如果在某个时刻再也不需要这些权限那么程序应该不可逆地放弃它们同时确保保存的设置用户ID也被更改。这样可以避免程序被诱导重新获取权限比如通过第38.9节描述的栈崩溃技术。Drop privileges permanently when they will never again be required如果一个 set-user-ID 或 set-group-ID 程序完成了所有需要权限的任务那么它应该永久放弃这些权限以消除程序因为漏洞或其他意外行为被攻破时可能产生的任何安全风险。永久放弃权限是通过将所有进程的用户组ID 重置为与实际组ID 相同的值来实现的。对于一个当前有效用户 ID 为 0 的 set-user-ID-root 程序我们可以使用以下代码重置所有用户 IDif(setuid(getuid())-1)errExit(setuid);不过上面的代码不会重置保存的 set-user-ID如果调用进程的有效用户 ID 当前非零的话当从有效用户 ID 非零的程序中调用时setuid() 只会改变有效用户 ID见第 9.7.1 节。换句话说在一个 set-user-ID-root 程序中下面的顺序不会永久降低用户 ID 0/* Initial UIDs: real1000 effective0 saved0 *//* 1. Usual call to temporarily drop privilege */orig_euidgeteuid();if(seteuid(getuid()-1)errExit(seteuid);/* UIDs changed to: real1000 effective1000 saved0 *//* 2. Looks like the right way to permanently drop privilege (WRONG!) */if(setuid(getuid()-1)errExit(setuid);/* UIDs unchanged: real1000 effective1000 saved0 */相反我们必须在永久放弃权限之前先重新获得权限通过在上面步骤1和步骤2之间插入以下调用if(seteuid(orig_euid)-1)errExit(seteuid);另一方面如果我们有一个由非 root 用户拥有的 set-user-ID 程序那么由于 setuid() 不足以改变 set-user-ID 标识我们必须使用 setreuid() 或 setresuid() 来永久放弃特权标识。例如我们可以使用 setreuid() 来实现所需的结果如下所示if(setreuid(getuid(),getuid())-1)errExit(setreuid);这段代码依赖于 Linux 实现的 setreuid() 的一个特性如果第一个ruid参数不是 -1那么保存的设置用户 ID 也会被设置为与新的有效用户 ID 相同的值。SUSv3 并没有指定这个特性但许多其他实现的行为和 Linux 一样。同样在一个 set-group-ID 程序中如果想永久放弃一个特权组 ID就必须使用 setregid() 或 setresgid() 系统调用因为当程序的有效用户 ID 非零时setgid() 只会改变调用进程的有效组 ID。General points on changing process credentials在前面的页面中我们描述了临时和永久降低权限的技术。现在我们来补充一些关于使用这些技术的一般性建议一些用于更改进程凭证的系统调用的语义在不同系统中有所不同。此外这些系统调用的一些语义也会根据调用者是否具有特权有效用户ID为0而有所不同。具体细节请参见第9章特别是第9.7.4节。由于这些差异[Tsafrir 等, 2008] 建议应用程序应使用系统特定的非标准系统调用来更改进程凭证因为在许多情况下这些非标准系统调用提供的语义比标准系统调用更简单且一致。在 Linux 上这意味着使用setresuid() 和setresgid() 来更改用户和组的凭证。虽然这些系统调用并非所有系统都有但使用它们出错的可能性较小。([Tsafrir 等, 2008] 提出一个函数库通过他们认为在每个平台上最好的接口来进行凭证更改。)在 Linux 上即使调用者的有效用户 ID 是 0如果程序显式修改了自己的能力改变凭证的系统调用也可能不会按预期工作。例如如果 CAP_SETUID 能力被禁用了那么尝试改变进程用户 ID 的操作会失败或者更糟的是只会悄悄地改变部分请求的用户 ID。由于前面两点中列出的各种可能性强烈建议例如参见 [Tsafrir 等人, 2008]不仅要检查凭证更改系统调用是否成功还要验证更改是否按预期发生。例如如果我们暂时降低或重新获取特权用户 ID 使用 seteuid()那么我们应该在该调用之后跟一个 geteuid() 调用以确认实际有效用户 ID 是我们预期的。同样如果我们永久降低特权用户 ID则应验证真实用户 ID、有效用户 ID 和保存的用户 ID 是否都已成功更改为非特权用户 ID。不幸的是虽然有标准系统调用可以获取真实和有效 ID但没有标准系统调用可以获取保存的用户 ID。Linux 和一些其他系统提供 getresuid() 和 getresgid() 来实现这个目的在其他一些系统上我们可能需要使用解析/proc文件信息等技巧。有些凭证的更改只有有效用户ID为0的进程才能进行。因此在更改多个ID——附加组ID、组ID和用户ID时我们应该在丢弃特权ID时最后放弃特权有效用户ID。相反在提升特权ID时我们应该首先提升特权有效用户ID。38.3 Be Careful When Executing a Program当一个特权程序执行另一个程序时无论是直接通过 exec()还是间接通过 system()、popen() 或类似的库函数都需要小心。Drop privileges permanently before execing another program如果一个 set-user-ID或 set-group-ID程序去执行另一个程序那么它应该确保所有的进程用户组ID 都被重置为与实际用户组ID 相同的值这样新程序就不会带着特权启动也无法重新获取特权。实现这个的方法之一是在执行 exec() 之前重置所有 ID可以使用第 38.2 节中描述的技术。同样的效果也可以通过在 exec() 前调用 setuid(getuid()) 来实现。即使这个 setuid() 调用仅改变有效用户 ID在有效用户 ID 非零的进程中特权还是会被取消因为如第 9.4 节所述一次成功的 exec() 会将有效用户 ID 复制到保存的 set-user-ID。如果 exec() 失败那么保存的 set-user-ID 将保持不变。如果程序之后需要执行其他有特权的操作这可能会很有用因为 exec() 失败了。类似的方法即 setgid(getgid())也可以用于设置组ID的程序因为一次成功的 exec() 同样会将有效组 ID 复制到保存的设置组 ID。例如假设我们有一个由用户 ID 为 200 的用户拥有的 set-user-ID 程序。当这个程序被一个 ID 为 1000 的用户执行时生成的进程的用户 ID 将如下所示real1000effective200saved200如果这个程序随后执行调用 setuid(getuid())那么进程的用户 ID 会被更改为以下内容real1000effective1000saved200当进程执行一个非特权程序时进程的有效用户ID会被复制到保存的设置用户ID中从而形成以下一组进程用户IDreal1000effective1000saved1000Avoid executing a shell (or other interpreter) with privileges在用户控制下运行的特权程序绝不应该执行 shell无论是直接执行还是通过 system()、popen()、execlp()、execvp() 或其他类似的库函数间接执行。shell以及其他不受限制的解释器比如 awk的复杂性和强大功能意味着几乎不可能消除所有安全漏洞即使被执行的 shell 不允许交互式访问。由此带来的风险是用户可能在进程的有效用户 ID 下执行任意 shell 命令。如果必须执行 shell务必确保提前永久放弃特权。在第 27.6 节关于 system() 的讨论中提到了一种执行 shell 时可能出现的安全漏洞示例。一些 UNIX 实现会在解释器脚本上应用 set-user-ID 和 set-group-ID 权限位时生效见第 27.3 节这样当脚本运行时执行脚本的进程会获得其他有权限的用户的身份。由于前面提到的安全风险Linux 和其他一些 UNIX 实现会在执行脚本时默默忽略 set-user-ID 和 set-group-ID 权限位。即使在允许使用 set-user-ID 和 set-group-ID 脚本的实现中也应该避免使用它们。Close all unnecessary file descriptors before an exec()在第27.4节中我们提到默认情况下文件描述符在 exec() 调用后仍然保持打开状态。一个有权限的程序可能会打开普通进程无法访问的文件。由此产生的打开的文件描述符代表一个特权资源。在 exec() 之前应该关闭这个文件描述符这样被 exec 的程序就无法访问相关文件。我们可以通过显式关闭文件描述符或者设置它的 exec 关闭标志见第27.4节来做到这一点。38.4 Avoid Exposing Sensitive Information当一个程序读取密码或其他敏感信息时它应该完成所需的处理然后立即从内存中清除这些信息。我们在第8.5节中展示了一个例子。把这些信息留在内存中是一个安全风险原因如下包含数据的虚拟内存页可能会被换出除非使用 mlock() 或类似方法锁定在内存中然后可能会被有权限的程序从交换区读取。如果进程收到一个导致它生成核心转储文件的信号那么可以读取该文件来获取信息。 8.5的例子在Listing 8-2中其实就是将密码对应的内存区域全设为’\0’。延续上一个观点作为一个一般原则一个安全的程序应该防止生成核心转储文件这样核心转储文件就不能被用来查看敏感信息。程序可以通过使用 setrlimit() 将 RLIMIT_CORE 资源限制设置为 0 来确保不创建核心转储文件见第 36.3 节。默认情况下即使程序已经放弃了所有权限Linux 也不允许具有 set-user-ID 的程序在接收到信号时生成核心转储见第 22.1 节。然而其他 UNIX 实现可能没有提供这个安全功能。78938.5 Confine the Process在本节中我们将考虑一些方法看看如何限制程序的运行范围以减少程序被入侵时造成的损害。Consider using capabilitiesLinux 的能力机制将传统的全有或全无的 UNIX 权限方案拆分成了称为“能力”的独立单元。一个进程可以单独启用或禁用某个能力。通过仅启用它所需的能力程序运行时的权限会比以完全 root 权限运行时少。这可以减少程序被攻破时可能造成的损害。此外通过使用能力和 securebits 标志我们可以创建一个拥有有限能力但不是 root 用户拥有的进程也就是说它的所有用户 ID 都非零。这样的进程不能再通过 exec() 来恢复完整的能力集合。我们将在第 39 章中介绍能力和 securebits 标志。Consider using a chroot jail在某些情况下一个有用的安全技术是建立一个 chroot 监狱以限制程序可以访问的目录和文件集合。同时确保调用 chdir() 将进程的当前工作目录更改到监狱内的某个位置。但是请注意chroot 监狱不足以限制 set-user-ID-root 程序的行为参见第 18.12 节。使用 chroot 监狱的一个替代方案是虚拟服务器它是在虚拟内核之上实现的服务器。因为每个虚拟内核都与可能在同一硬件上运行的其他虚拟内核隔离开来所以虚拟服务器比 chroot 监狱更安全、更灵活。许多现代操作系统也提供了它们自己的虚拟服务器实现。在 Linux 上最早的虚拟化实现是用户模式 LinuxUML它是 Linux 2.6 内核的标准部分。关于 UML 的更多信息可以在 http://user-mode-linux.sourceforge.net/ 找到。更新的虚拟内核项目包括 Xen (http://www.cl.cam.ac.uk/Research/SRG/netos/xen/) 和 KVM (http://kvm.qumranet.com/)。38.6 Beware of Signals and Race Conditions用户可以向其启动的设置用户 IDset-user-ID程序发送任意信号。这类信号可在任意时刻、以任意频率送达。我们需要考虑若信号在程序执行过程中的任意时间点递达可能引发的竞态条件。必要时应当捕获、阻塞或忽略信号防范潜在安全问题。此外信号处理函数的设计应尽可能简洁降低无意间引入竞态条件的风险。该问题对于能够暂停进程的信号例如 SIGTSTP 和 SIGSTOP尤为突出。存在风险的场景如下set-user-ID 程序获取自身运行时环境的相关信息。用户设法暂停运行该程序的进程并修改运行时环境的细节。这类修改包括更改文件权限、修改符号链接的指向或是删除程序所依赖的文件。用户通过 SIGCONT 信号恢复该进程。此时程序会基于已经失效的运行时环境假设继续执行而这些错误假设可能造成安全漏洞。此处描述的场景实际上只是检查时间到使用时间TOCTOU竞态条件的一种特例。特权进程不应基于先前的校验结果执行操作因为该校验结果可能已经失效参见 15.4.4 节中关于access()系统调用的实例讨论。即便用户无法向进程发送信号本准则依然适用。暂停进程的能力只是让用户得以拉长检查时刻与使用时刻之间的时间窗口。虽然单次尝试很难恰好卡在检查与使用的间隙中将进程暂停但恶意用户可以反复执行该设置用户 IDset-user-ID程序并借助另一程序或 Shell 脚本持续向该 SUID 程序发送停止信号、修改运行时环境。这种做法会大幅提升攻破该 set-user-ID 程序的概率。38.7 Pitfalls When Performing File Operations and File I/O如果特权进程需要创建文件则必须妥善处理该文件的属主与权限确保不存在任何一个时间点 —— 无论多么短暂 —— 让该文件容易遭受恶意篡改。需遵循以下准则应当设置进程的文件创建掩码umask见 15.4.6 节保证进程不会创建其他用户可写的文件因为这类文件有可能被恶意用户修改。文件的属主取自创建进程的有效用户 ID。为确保新建文件不会归属到错误用户可能需要审慎调用seteuid()或setreuid()临时变更进程身份凭证。文件的组属主取自进程的有效组 ID参见 15.3.1 节该原则同样适用于设置组 IDset-group-ID程序可调用对应的组 ID 相关函数来规避此类问题。严格来说在 Linux 上新文件的所有者由进程的文件系统用户 ID决定该 ID 通常与进程有效用户 ID 取值相同详见 9.5 节。如果一个root 权限的 set-user-ID 程序需要创建文件文件创建初期必须归该程序所有但最终要归属其他用户。创建该文件时应当保证初始状态下其他用户不可写。可以给open()传入合适的 mode 参数或是在调用open()之前设置进程的 umask。之后程序可以通过fchown()修改文件属主如有必要再用fchmod()修改权限。核心要点set-user-ID 程序绝对不能创建由程序属主拥有、哪怕只是一瞬间允许其他用户写入的文件。文件属性校验应当基于已打开的文件描述符完成例如先open()再执行fstat()而不是先根据路径名检查属性、再打开文件例如先stat()再open()。后一种方式会引入 TOCTOU检查时间 / 使用时间竞态漏洞。如果程序必须保证自己是文件的创建者调用open()时应当使用O_EXCL标志。特权程序应尽量避免在/tmp这类全局可写目录中创建文件也不要依赖这类目录下的文件。因为攻击者可以利用该目录以特权程序预期的文件名提前创建非法文件从而攻击程序。如果程序不得不在全局可写目录创建文件则至少要使用mkstemp()见 5.12 节这类函数生成不可预测的文件名。38.8 Don’t Trust Inputs or the Environment设置用户 IDset-user-ID与设置组 IDset-group-ID程序不应假定环境变量的值是可信的。其中两个尤其需要关注的变量是PATH和IFS。PATH决定了 Shell以及system()、popen()还有execlp()和execvp()函数查找可执行程序的目录。恶意用户可以篡改PATH的值诱导使用上述函数的 set-user-ID 程序以特权执行任意程序。如果必须使用这些函数应当将PATH设置为可信目录列表但更佳做法是执行程序时直接指定绝对路径。不过前文已经提到最稳妥的方式是在拉起 Shell 或调用上述函数前先放弃特权。IFS用于指定 Shell 认定的命令行单词分隔符。应当将该变量置为空字符串这样 Shell 只会把空白字符视作单词分隔符。部分 Shell 在启动时会自动按此方式设置IFS。27.6 节介绍过旧版 Bourne Shell 中一处和 IFS 相关的安全漏洞。 bash下默认的IFS值为空格、Tab和换行$echo-n$IFS|od-bc0000000 040 011 012\t\n 0000003某些场景下清空全部环境变量表见 6.7 节再按需恢复少数已知安全取值的环境变量可能是最安全的做法在执行其他程序、或是调用会受环境变量影响的库函数时尤其推荐这种处理方式。Handle untrusted user inputs defensively特权程序在依据来自不可信来源的输入执行操作前必须仔细校验所有这类输入。此类校验工作可包括验证数值处于合法范围、字符串长度合规且仅包含允许的字符。需要按此方式校验的输入来源包括用户创建的文件、命令行参数、交互式输入、CGI 输入、电子邮件消息、环境变量以及不可信用户可访问的进程间通信通道命名管道、共享内存等和网络数据包。Avoid unreliable assumptions about the process’s run-time environmentset-user-ID 程序不应对自身初始运行时环境做出不可靠的假设。例如标准输入、标准输出或标准错误有可能已经被关闭这些文件描述符可能在执行该 SUID 程序的父进程中就已经关闭。在这种情况下打开文件时可能意外复用描述符 1举例于是程序自以为正在向标准输出写入数据实际却写入了它刚刚打开的文件。还有许多其他情况需要考虑。例如进程可能耗尽各类资源限制可创建进程数上限、CPU 时间资源限制、文件大小资源限制等。其后果是各类系统调用会失败或是触发各类信号。恶意用户可能刻意制造资源耗尽的场景以此尝试攻破程序。38.9 Beware of Buffer Overruns警惕缓冲区溢出输入数据或待复制字符串超出已分配的缓冲区空间。永远不要使用gets()使用scanf()、sprintf()、strcpy()、strcat()这类函数时务必谨慎例如通过 if 判断做防护阻止缓冲区溢出。缓冲区溢出可被用于栈崩溃也叫栈破坏 / 栈粉碎stack smashing攻击。恶意用户利用缓冲区溢出向栈帧中植入精心构造的字节码迫使特权程序执行任意代码。网上多处资料讲解栈崩溃的细节另可参阅文献 [Erickson, 2008] 与 [Anley, 2007]。缓冲区溢出大概是计算机系统里最常见的安全漏洞根源CERThttp://www.cert.org/与 Bugtraqhttp://www.securityfocus.com/频繁发布安全公告就是佐证。缓冲区溢出在网络服务程序中尤为危险因为它会让系统暴露于来自网络任意位置的远程攻击。为提升栈粉碎攻击的难度 —— 尤其是让针对网络服务程序的远程攻击耗费更多时间 —— 自 Linux 2.6.12 内核起系统实现了地址空间布局随机化ASLR。该技术会在虚拟内存顶部 8MB 范围内随机改变栈的位置。此外如果软资源限制RLIMIT_STACK不是无限制且 Linux 特有文件/proc/sys/vm/legacy_va_layout的值为 0内存映射的位置也会被随机化。较新的 x86-32 架构提供硬件支持可将页表标记为NXno execute不可执行。该特性用于禁止在栈上执行程序代码从而增加栈粉碎攻击的难度。上面提到的许多函数都存在安全替代版本例如snprintf()、strncpy()和strncat()调用者可以通过它们指定最多可复制的字符数量。这些函数会考虑传入的最大长度避免溢出目标缓冲区。总体而言应当优先选用这类替代函数但使用时仍必须小心。尤其需要注意以下几点对于其中大多数函数若达到指定的长度上限源字符串会被截断后存入目标缓冲区。截断后的字符串在程序语义上可能失去原有含义因此调用方必须检查是否发生截断例如利用snprintf()的返回值一旦发生截断就要执行相应处理。使用strncpy()会带来一定性能开销。在调用strncpy(s1, s2, n)时如果s2指向的字符串长度小于n字节函数会向s1填充空字节保证总共写入n个字节。如果传给strncpy()的最大长度不足以容纳末尾的字符串终止空字符那么目标字符串将不会自动添加空终止符。部分 UNIX 实现提供strlcpy()函数给定长度参数n时它最多向目标缓冲区复制n−1字节并且总会在缓冲区末尾追加空终止符。但该函数并未在 SUSv3 标准中定义glibc 库也没有实现它。此外如果调用者没有仔细检查字符串长度该函数仅仅是把一类问题缓冲区溢出替换成了另一类问题静默丢弃数据。38.10 Beware of Denial-of-Service Attacks随着互联网服务的增多远程拒绝服务攻击的可乘之机也随之增加。这类攻击试图让合法客户端无法访问服务手段要么是向服务器发送畸形数据使其崩溃要么通过大量虚假请求压垮服务器。本地拒绝服务攻击同样有可能发生。最广为人知的例子就是用户运行简易fork 炸弹程序反复调用fork()创建进程耗尽系统全部进程槽位。不过本地拒绝服务攻击的来源更容易定位一般可通过合理的物理安全与账号口令安全措施加以防范。服务器应当执行负载限流当负载超过预设阈值时丢弃请求。该做法的代价是会丢弃部分合法请求但能够防止服务器及其宿主机过载。使用资源限制与磁盘配额也有助于抑制过高负载。磁盘配额更多信息参见http://sourceforge.net/projects/linuxquota/服务器与客户端通信时应当设置超时这样即便客户端可能是故意不回复服务器也不会无限阻塞等待该客户端。发生过载时服务器应当记录恰当日志通知系统管理员该问题。但日志本身也需要限流避免日志输出造成系统过载。服务器程序需要保证遭遇突发负载时不会崩溃。例如必须严格执行边界检查防止大量请求造成数据结构溢出。设计数据结构时需要规避算法复杂度攻击。举例二叉树在常规负载下保持平衡性能尚可但攻击者可以构造特定输入序列生成不平衡树最坏情况下退化为链表从而严重拖累系统性能。文献 [Crosby Wallach, 2003] 详细阐述了这类攻击原理并给出可用于防范的数据结构设计方案。38.11 Check Return Statuses and Fail Safely特权程序必须始终检查系统调用与库函数是否执行成功以及返回值是否符合预期。当然这一点适用于所有程序但对特权程序尤为关键。即便是以 root 身份运行的程序各类系统调用也有可能失败。例如达到系统全局进程数量上限时fork()会失败在只读文件系统上以写模式调用open()会失败目标目录不存在时chdir()会失败。即便系统调用执行成功有时也需要校验其返回结果。例如在需要关注该问题的场景下特权程序应当校验成功的open()调用返回的文件描述符不能是 0、1、2 这三个标准文件描述符之一。最后如果特权程序遇到非预期状况恰当的处理方式通常是直接终止程序若是服务端程序则丢弃该客户端请求。尝试自行修复异常问题往往需要做出一些假设而这些假设未必在所有场景下都成立可能引入安全漏洞。在这类场景中更安全的做法是终止程序或者由服务器记录日志并丢弃客户端请求。38.12 Summary特权程序能够访问普通用户无法使用的系统资源。如果这类程序被攻破系统安全就会遭到破坏。本章给出了编写特权程序的一系列准则。这些准则有双重目标降低特权程序被攻破的可能性并且一旦特权程序遭到入侵尽可能减小造成的破坏。Further information[Viega McGraw, 2002] 涵盖与安全软件设计和实现相关的广泛主题。UNIX 系统安全的通用资料以及专门讲解安全编程技术的章节可以在 [Garfinkel et al., 2003] 中找到。[Bishop, 2005] 对计算机安全做了较为详尽的阐述同一作者在 [Bishop, 2003] 一书中的论述则更为深入。[Peikari Chuvakin, 2004] 讲解计算机安全重点介绍各类攻击系统的手段。[Erickson, 2008] 和 [Anley, 2007] 均全面介绍各类安全漏洞利用技术内容足够详实可帮助审慎的程序员规避这类漏洞。[Chen et al., 2002] 是一篇论文描述并分析 UNIX 的设置用户 IDset-user-ID模型。[Tsafrir et al., 2008] 修订并扩充了 [Chen et al., 2002] 中的多处论述。[Drepper, 2009] 提供大量 Linux 平台安全防御式编程的实用建议。互联网上也有若干关于安全程序编写的资料来源包括如下马特・毕晓普Matt Bishop撰写了大量安全相关论文可在线查阅http://nob.cs.ucdavis.edu/~bishop/secprog。其中最值得一读的是《如何编写 Setuid 程序》最初发表于;login:12 (1)1986 年 1‑2 月刊。尽管该论文年代有些久远但包含大量实用技巧。戴维・惠勒David Wheeler所著《Linux 与 Unix 安全编程 HOWTO》在线地址http://www.dwheeler.com/secure‑programs/。一份实用的 set‑user‑ID 程序编写检查清单在线地址http://www.homeport.org/~adam/setuid.7.html。