第 38 章 编写安全的特权程序 (Writing Secure Privileged Programs)

      +

      核心结论

      • 特权程序的两种形式:以特权 UID (root) 启动的守护进程;或者带 set-user-ID (set-group-ID) 位的程序——exec 后进程 euid 变为文件所有者。最高危的是 set-user-ID-root,因为它直接拿到 root 凭证。

      • 最小特权原则:程序应在需要时持有特权,做完即永久丢弃;最危险的是「长期保留 root 然后做普通工作」。第 9 章的 saved set-user-ID 与第 39 章的 capabilities 都是为此设计的。

      • 特权变更的正确姿势:临时降权用 seteuid(getuid()) + seteuid(orig_euid);永久降权必须改 所有 ID (real/effective/saved),否则 setuid(getuid()) 不会清掉 saved set-user-ID——必须先 seteuid(orig_euid)setuid(getuid()),或用 setresuid()/setreuid()

      • 执行其他程序前要丢特权、清 fd、关闭 fd:exec 新程序前必须 setuid(getuid()) 清掉 saved set-user-ID;不要 exec shell(system()/popen()/execlp()/execvp() 都跑 /bin/sh);关闭所有特权 fd 或设 FD_CLOEXEC

      • 信号与 TOCTOU 竞态:用户可以 SIGTSTP/SIGSTOP 暂停进程,修改环境(权限、符号链接、文件),再 SIGCONT 恢复——检查→使用的窗口被拉长。任何「先 stat 再 open」「先 access 再操作」都是 TOCTOU 漏洞。

      • 输入/资源/文件 I/O 的隐患:umask 防公开可写文件;O_EXCL 防止攻击者预占路径;mkstemp() 防止 /tmp 攻击;不要信任 PATHIFSLD_*、stdin/stdout/stderr;用 snprintf/strncpy/strncat 防止栈溢出。

      本章主旨

      特权程序(set-UID-root、root 守护进程)一旦被攻破,整个系统的安全就被拿下——所以「安全 = 减少攻击面 + 限制爆炸半径」。作者给出 11 条核心准则:能不写 set-UID 就别写;非 root 特权更安全(用专用 group);只用能完成任务的最小特权;执行外部程序前永久丢弃特权、清 fd;及时擦除敏感数据;用 chroot/capabilities/虚拟化隔离;防信号 TOCTOU;防堆溢出;防 DoS;检查返回值;fail safe(终止或拒绝请求)。读者应理解每条准则背后的「为什么」——大多数是数十年 UNIX 漏洞史的真实教训。

      一、核心概念

      本章围绕 6 个核心概念展开:先辨别何时才真需要特权,再深入「最小特权的具体操作 (ID 变更)」「执行子程序前的清场」「TOCTOU 与信号竞态」「输入与环境信任」「栈溢出防御」「拒绝服务与返回值检查」。

      概念 定义 + 重要性 实现提示

      是否真的需要 set-UID?

      写 set-UID 之前先问:能否用普通进程完成?能否把需要特权的部分隔离到一个小型 set-UID helper (如 pt_chown)?能否用专用 group ID + set-GID 替代 set-UID-root?

      §38.1;如果 root 不是必须,就用专用 group——「running with root opens the gates to possible security compromises」。

      最小特权 / 临时与永久降权

      set-UID 程序仅在需要时刻保留特权;用 seteuid() 临时降/升,用 setresuid()/setreuid() 永久降(同时改 real/effective/saved)。降级后做非特权工作,必要时再升级。变更后必须用 geteuid()/getresuid() *验证*生效。

      §38.2;记得非特权进程 setuid(getuid()) 仅改 effective——saved 不动;要永久降必须先 seteuid(orig_euid) 升回,或直接用 setreuid(getuid(), getuid())。umask、非特权用户调用 setuid() 行为各异——用 setresuid() 更稳。

      exec 前的清场 (drop privs + close fd)

      执行其他程序前需 (1) setuid(getuid()) 把 saved 也清掉;(2) 不 exec shell——shell 太强大,几乎无法消除所有漏洞;(3) 关闭所有特权 fd 或设 FD_CLOEXEC,防止新进程拿到对特权文件的引用。

      §38.3;特权 fd (root 读到的 /etc/shadow 等) 一旦泄露,新进程即可旁路所有访问控制。Linux 与多数现代 UNIX 同样忽略脚本的 set-UID 位(脚本会被 exec 成 /bin/sh 子进程,行为太复杂)。

      TOCTOU 与信号竞态

      Time-of-check to time-of-use 漏洞:用户用 SIGSTOP/SIGTSTP 暂停进程,修改权限/符号链接,再 SIGCONT 恢复。「stat() then open()」、「access() then open file」都属于这类——必须用 fd (fstat()) 或 O_NOFOLLOW

      §38.6;用 open() + fstat() 取代 stat() + open()mkstemp() 生成不可预测路径;特权进程持有句柄而非路径名。

      栈溢出防御

      不要用 gets();对 scanf/sprintf/strcpy/strcat 都加长度检查;用 snprintf/strncpy/strncat 并检查返回值/截断;2.6.12+ 内核开启地址空间随机化 (ASLR);x86-64/x86-32+ 支持 NX (no-execute) 页。

      §38.9;strncpy() 性能差、且可能不 null 终止;glibc 没有 strlcpy(),且它只是把 buffer overflow 换成 silent discard。

      DoS + 返回值 + fail safe

      服务器应 throttle (拒绝超载请求)、设 timeout、限 logging、可承受畸形输入;用平衡树或哈希避免 algorithmic-complexity 攻击 (Crosby-Wallach);每个 syscall 都要检查返回值(即使 root 也会遇到 EMFILE/ENOSPC);遇错就 exit()drop request不要尝试修复——修复常意味着做未经验证的假设。

      §38.10-11;特权进程检查 open() 是否意外返回 0/1/2;遇到非常规情况应「fail closed」。

      二、详细笔记

      38.1 是否真的需要 set-user-ID / set-group-ID?

      What:在写 set-UID 程序之前先问:能否用普通进程 + 别的方式完成?若必须,能否「只让程序拥有某个专用 group 的写权限」而非 root 凭证?

      Why:root 凭证几乎万能——一旦程序被漏洞攻陷,整个系统的安全地基被掀开;专用 group 凭证限制了爆炸半径(如更新文件用 gid=backup 而不是 uid=root)。

      How

      1. 能不写就不写:普通进程能否借工具解决?例如,许多 daemon 改用 /run/<name>.sock 暴露给用户进程,抛弃 set-UID。

      2. 将特权隔离到 helper:类似 pt_chown 的做法——daemon 负责主流程,需要特权的操作(如改伪终端所有权)单独 exec 一个原子 helper。

      3. 替代 root 用专用 group:要更新 gid=app 拥有的文件,就创建 app group,让程序 set-GID 到 app 而不是 set-UID 到 root。

      4. 库也可走 helper 模式:把需要特权的代码抽到一个 set-UID helper,主程序通过 fork-exec 调用它。

      When:每次准备给程序设置 u+s 位之前;问三个问题「这是真的必要吗?能换 group 吗?能外置 helper 吗?」

      Example

      // 摘自《The Linux Programming Interface》 第 38 章
      // pt_chown 在第 64 章 — 一个最小化的 set-UID-root helper,用于
      // 把 slave 伪终端的所有权改成调用进程的 UID/GID,避免整个
      // login/SSH 都跑 set-UID-root

      38.2 用最小特权运行:临时降权、永久降权

      Whatseteuid(getuid()) 临时把 euid 降回真实 uid;永久降权必须同时改动 real/effective/saved 三个 ID。

      Why:永久降权堵住了「stack-crashing 攻击让 saved set-UID 复活」的路径;最小特权降低攻击面。

      How

      降权/升权序列(来自 §38.2):

      // 摘自《The Linux Programming Interface》 第 38 章
      uid_t orig_euid;
      orig_euid = geteuid();
      if (seteuid(getuid()) == -1)            /* Drop privileges */
          errExit("seteuid");
      /* Do unprivileged work */
      if (seteuid(orig_euid) == -1)           /* Reacquire privileges */
          errExit("seteuid");
      /* Do privileged work */

      永久降权的陷阱:当进程的 euid == 0 (set-UID-root) 时,要把它永久降到非 0 看起来简单:

      /* Initial UIDs:    real=1000 effective=0 saved=0 */
      orig_euid = geteuid();
      if (seteuid(getuid()) == -1) errExit("seteuid");
      /* UIDs changed to: real=1000 effective=1000 saved=0 -- WRONG! */
      /* 现在 saved 还是 0 -- 并没有真正永久丢掉 root 能力 */
      if (setuid(getuid()) == -1)             /* Only changes effective */
          errExit("setuid");
      /* UIDs unchanged: real=1000 effective=1000 saved=0 */

      正确做法是在 setuid(getuid()) 之前 升回 root:

      /* Correct permanent drop: */
      seteuid(getuid());                       /* real=1000 effective=1000 saved=0 */
      seteuid(orig_euid);                      /* real=1000 effective=0   saved=0 */
      setuid(getuid());                        /* real=1000 effective=1000 saved=1000 */

      或者用 setreuid(getuid(), getuid())/setresuid() 一步完成——Linux 上 setreuid() 的特殊语义(ruid 非 -1 时同时改 saved)让一调用即可。

      When:每个需要长期运行或会被 fork/exec 的 set-UID 程序都应在 init 后立刻降权,并校验最终状态。

      Example

      // 摘自《The Linux Programming Interface》 第 38 章
      /* Verify changes actually took effect */
      uid_t ruid, euid, suid;
      if (getresuid(&ruid, &euid, &suid) == -1)
          errExit("getresuid");
      if (ruid != getuid() || euid != getuid() || suid != getuid())
          fatal("Failed to drop privileges fully");

      Linux 还有规则 (来自 §9.7.4):改多个 ID 时,最后才升 root euid最先才降 root euid——避免 root 半途中介态。

      38.3 执行其他程序前的注意事项

      What:set-UID/set-GID 程序要 exec 其他程序时,必须 (1) 把所有 user/group ID 复位到 real (group) ID;(2) 不 exec shell;(3) 关闭特权 fd 或设 FD_CLOEXEC

      Why:(1) 让新进程「既不拿到特权,也不能再拿回」;(2) shell 太强大($PATH/$IFS/通配/alias 全可被攻击者利用),漏洞面太大;(3) 防止特权文件描述符被无关进程继承。

      How

      执行清场 (来自 §38.3):

      /* Drop privileges permanently before exec: */
      if (setuid(getuid()) == -1)          /* euid nonzero → only changes euid,
                                              but a successful exec() copies
                                              euid to saved set-user-ID */
          errExit("setuid");
      execlp("/bin/some-tool", "some-tool", (char *)NULL);

      不可 exec shell 的理由——system(cmd)cmd 传给 /bin/sh -c,攻击者可以传 ; rm -rf / 或者用 $IFS/$PATH 拼凑陷阱命令。即使 shell 不允许交互,它的内部命令 (eval, alias, function) 也构成可编程的攻击面。

      例外popen()/system() 在程序是特权且无法改写为无特权版本时偶尔必需——必须 永久降权到非特权 UID,才能用。

      关闭 fd:所有通过特权获取的 fd (root 读到的 /etc/shadow、打开的 raw socket 等) 必须在 exec 前 close(),或用 fcntl(fd, F_SETFD, FD_CLOEXEC)

      When:任何 execve() / system() / popen() 之前。

      Example

      // 摘自《The Linux Programming Interface》 第 38 章
      /* Set close-on-exec for privilege-bearing descriptors */
      int fd = open("/etc/shadow", O_RDONLY);
      if (fd == -1) errExit("open");
      fcntl(fd, F_SETFD, FD_CLOEXEC);    /* auto-close on exec() */
      /* Later, if we exec(), fd is gone */

      38.4 不要让敏感信息驻留内存

      What:密码、密钥等敏感数据使用后立即清零(覆盖原 buffer 再 free),并防止 core dump 带走 (setrlimit(RLIMIT_CORE, 0))。

      Why:虚拟内存页可能被换出到 swap,其他特权进程可读;如果进程崩溃生成 core dump,敏感信息落地。攻击者常用这两条路径偷凭证。

      Howmemset(buf, 0, sizeof(buf)) (编译器不优化掉) 或者 explicit_bzero();读密码用 getpass()/读后立刻清零 buffer;信号 handler 别碰敏感数据;用 mlock() 锁 page 也是一种思路。

      When:每次读完密码/密钥/token 之后立刻清。

      Example

      // 摘自《The Linux Programming Interface》 第 8.5 + 第 38 章
      char *password = getpass("Password: ");
      /* ... use password ... */
      for (p = password; *p != '\0'; ) *p++ = '\0';   /* scrub */

      38.5 进程沙箱:capabilities / chroot / 虚拟化

      What:现代 Linux 提供三种「限制一个进程能做什么」的能力:(1) Capabilities(细粒度特权拆分,详细见第 39 章);(2) chroot() 限制可见的目录树;(3) 虚拟服务器 (UML、Xen、KVM) 把进程放到独立内核里。

      Why:即使程序被攻陷,沙箱把爆炸半径卡死在最小集合里。chroot 不防 root (第 18.12),但对非特权进程有效;Caps 对 root 进程也有效。

      How:把 daemon 改成:drop capabilities 至最小集合 + chroot()/var/empty + setuid() 到 nobody + chdir() 到 chroot 内 → 然后做实际工作。虚拟化用 UML/Xen/KVM。

      When:所有长期运行的网络 daemon 应该用此模式启动。

      Example

      // 现代 sandbox 模式
      cap_set_proc(minimal_caps);          /* 来自 libcap */
      chroot("/var/empty");
      chdir("/");
      setuid(65534);                         /* nobody */
      setgroups(0, NULL);
      /* 现在进程能访问的最糟就是 /var/empty 里的内容 */

      38.6 小心信号与竞态 (TOCTOU)

      What:set-UID 进程会被攻击者发送任意信号;特别是 SIGTSTP/SIGSTOP 暂停进程,让攻击者修改外部资源(权限、符号链接、文件内容),再 SIGCONT 继续。

      Why:暂停期间改变了检查的假设(permission/路径/inode),resume 时使用过时假设 → TOCTOU 漏洞。

      How

      1. 用 fd 而非路径:先 open() 拿到 fd,再 fstat(fd)fchown(fd)fchmod(fd)——基于 fd 的操作不经过路径。

      2. O_NOFOLLOW/O_EXCL:防止符号链接替换、O_CREAT | O_EXCL 防止覆盖已有文件。

      3. 对「检查后使用」的窗口要短:原子操作(rename/open(O_EXCL)/O_NOFOLLOW)优先。

      4. signal handler 保持简单:复杂的 handler 自身就可能制造竞态。

      When:所有特权进程都应用 fd 代替路径;用 O_RDONLY | O_NOFOLLOW 打开符号链接。

      Example

      // 摘自《The Linux Programming Interface》 第 38 章
      // BAD: stat + open 都用路径
      struct stat sb;
      stat("/tmp/x", &sb);                  /* 检查——攻击者可能改符号链接 */
      int fd = open("/tmp/x", O_RDWR);      /* 使用——已是不同的文件了 */
      // GOOD: 用 open + fstat 走 fd
      int fd = open("/tmp/x", O_RDWR | O_NOFOLLOW);  /* 防 symlink */
      if (fd == -1) errExit("open");
      struct stat sb;
      fstat(fd, &sb);                       /* 检查走 fd,无 TOCTOU */

      38.7 文件操作与文件 I/O 的陷阱

      What:特权进程创建文件时所有权、权限、路径都有隐患;umask 必须防「公开可写」;不要在 /tmp 这类公开可写目录建文件,除非用 mkstemp() 不可预测名字。

      Why:攻击者可以构造 symlink 让 set-UID 程序覆盖关键文件;或者预占路径让程序读到攻击者的内容。

      How

      1. umaskumask(0077) 或更严——保证创建的文件 owner 之外不可读/写。

      2. O_EXCLopen(path, O_CREAT | O_EXCL | O_WRONLY, 0600) 保证自己就是创建者。

      3. 文件所有权切换:需要换 owner 时,用 fchown() 先改 owner,再调 fchmod(),中间不能让 file 处于「其他用户可写」状态(§38.7 第 3 条)。

      4. mkstemp():在 /tmp 生成不可预测名字——mkstemp(template),template 末尾必须是 XXXXXX

      When:每个 open(O_CREAT, …​) 之前检查 umask + O_EXCL。

      Example

      // 摘自《The Linux Programming Interface》 第 38 章
      #include <unistd.h>
      umask(0077);
      char tpl[] = "/tmp/myapp.XXXXXX";
      int fd = mkstemp(tpl);
      if (fd == -1) errExit("mkstemp");
      unlink(tpl);                /* 用完即删 */

      38.8 不要信任输入或环境

      What:set-UID 程序绝不信任 PATHIFS、命令行参数、环境变量、stdin/stdout/stderr、CGI 输入、网络包等。

      Why:攻击者可以设 PATH=/tmpsystem(cmd) 跑到攻击者的脚本;IFS='/' 让 shell 把命令词切开成多个参数;不存在的 fd 在 open() 时可能被复用。

      How

      1. PATH:用绝对路径;exec 时 execl("/full/path", …​) 而非 execlp/execvp/system/popen

      2. IFS:在 fork+exec shell 之前清空/重置。

      3. 信任度盘点:识别每个输入源(用户、文件、网络、环境)的可信度,并据此校验。

      4. stdin/stdout/stderr:若可能被关闭,open() 可能复用 fd 0/1/2——检查 open() 的返回是否就是 0/1/2。

      When:每个特权程序的入口清空/重置可信环境变量;exec 前 if (pfd[1] != STDOUT_FILENO) 守护。

      Example

      // 摘自《The Linux Programming Interface》 第 38 章
      /* Use absolute pathnames + reset PATH/IFS */
      setenv("PATH", "/usr/sbin:/usr/bin:/sbin:/bin", 1);
      setenv("IFS", " \t\n", 1);
      execl("/usr/sbin/some-tool", "some-tool", arg1, (char *)NULL);

      38.9 缓冲区溢出(栈崩溃)

      What:用 gets() 或无长度限制的 strcpy/strcat/sprintf 让攻击者写入超过 buffer 的内容,覆盖栈上的返回地址——然后程序被劫持执行任意代码 (栈崩溃/stack smashing)。

      Why:缓冲区溢出是 UNIX 系统最常见的漏洞来源 (CERT/Bugtraq 几十年统计榜首);在网络 server 上尤其致命——远程可攻击。

      How

      1. 永不用 gets():用 fgets(buf, sizeof buf, stdin)

      2. 用带长度的版本:`snprintf()、`strncpy()/strncat() + 检查返回值/截断;注意 strncpy() 不保证 null 终止(buf 不够大时)、性能差。

      3. 编译选项:`-D_FORTIFY_SOURCE=2` (glibc)、-fstack-protector (gcc)。

      4. 运行时防护:2.6.12+ 内核地址空间随机化 (ASLR);x86 NX 位阻止栈上执行代码。

      When:每个接受外部输入的函数都该用长度受限版本。

      Example

      // 摘自《The Linux Programming Interface》 第 38 章
      /* BAD: */
      char buf[64];
      gets(buf);                       /* never */
      strcpy(buf, user_input);         /* depends on size */
      
      /* GOOD: */
      char buf[64];
      if (snprintf(buf, sizeof buf, "%s", user_input) >= sizeof buf)
          fatal("input too long");

      38.10 DoS 攻击与处理

      What:网络 server 容易被恶意客户端用畸形数据 / 大量请求 / 算法复杂度攻击 (algorithmic-complexity attack) 拖垮。

      Why:远程 DoS 让合法用户得不到服务。

      How:

      1. throttle: 超过负载阈值拒绝新请求。

      2. timeout: 通信用 SO_RCVTIMEO/alarm 限时。

      3. 限 logging: logging 本身能拖垮系统 (写满磁盘)。

      4. 数据结构防退化: 用平衡树 (红黑树/std::map) 而非裸 BST,避免精心构造的输入把树变成链表。

      5. 不崩溃: 输入验证 + 边界检查。

      When: 每个面向网络的 server 实现。

      38.11 检查返回值与 fail safe

      What:每个 system call / library function 都要检查返回值;错误时不要尝试修复,要终止或拒绝。

      Why:root 也可能遇到 open() 失败 (只读文件系统)、fork() 失败 (per-UID 进程数上限);试图修复往往基于未经验证的假设,会创造新的攻击面。

      How:

      1. 每个 syscall 后:`if (syscall(…​) == -1) errExit("syscall");`。

      2. open() 后:确认没返回 0/1/2。

      3. fail closed:遇错 → exit(EXIT_FAILURE) 或对 server drop request

      When: 写每个调用都做这个检查——不需要思考「这个会失败吗」,直接检查。

      三、关键图表

      非可视化条目(系统调用 / 准则表)
      主题 描述

      准则 1:少写 set-UID (§38.1)

      能不写就别写;用 helper 隔离特权;专用 group 优于 root

      准则 2:最小特权 (§38.2)

      seteuid() 临时升降;setresuid()/setreuid() 永久降;验证返回值

      准则 3:exec 前清场 (§38.3)

      永久降权;不 exec shell;关特权 fd 或 FD_CLOEXEC

      准则 4:清敏感数据 (§38.4)

      使用完立刻清零;RLIMIT_CORE=0 防 core dump

      准则 5:进程沙箱 (§38.5)

      capabilities / chroot / 虚拟化;缩爆炸半径

      准则 6:防信号 TOCTOU (§38.6)

      用 fd 而非 path;O_NOFOLLOW;atomic ops

      准则 7:文件 I/O 陷阱 (§38.7)

      umask(0077) + O_EXCL + mkstemp()

      准则 8:不信任输入/环境 (§38.8)

      PATH/IFS 复位;绝对路径;fd 守护

      准则 9:栈溢出 (§38.9)

      永不用 gets();带长度 API;ASLR + NX

      准则 10:DoS (§38.10)

      throttle、timeout、限 logging;防复杂度攻击

      准则 11:检查返回 + fail closed (§38.11)

      每个 syscall 检查;遇错终止而非修复

      核心 set-user-ID 操作对比
      操作 调用 结果

      临时降权

      seteuid(getuid())

      real 不变,effective=real,saved 不变

      临时升权

      seteuid(orig_euid)

      effective=saved (即原 euid)

      永久降权(end of need)

      seteuid(orig_euid); setuid(getuid());

      三个 ID 都到 getuid()

      永久降权(shorter form)

      setresuid(getuid(), getuid(), getuid())

      一步到位

      永久降权(非 root 特权)

      setreuid(getuid(), getuid())

      real=effective=getuid,Linux 也清 saved

      四、思维导图

      mindmap
        root((第 38 章 安全特权程序))
          是否需要 set-UID
            不写优先
            helper 隔离
            专用 group 代替 root
          最小特权
            seteuid 临时升降
            setresuid 永久降
            getresuid 验证
            saved setuserID 复活风险
          exec 前清场
            setuid getuid 清 saved
            不 exec shell
            FD_CLOEXEC 关特权 fd
          沙箱隔离
            chroot
            capabilities
            虚拟化
          信号 TOCTOU
            fd 替 path
            O_NOFOLLOW
            atomic ops
          I/O 陷阱
            umask O_EXCL
            mkstemp 路径不可预测
            fchown 不留公开可写窗口
          不可信输入
            PATH IFS 复位
            绝对路径
            fd 守护
          栈溢出
            永不用 gets
            snprintf strncpy
            ASLR NX
          DoS 与返回值
            throttle timeout
            检查所有 syscall
            fail closed

      五、重点与易错点

      1. 「能不写 set-UID 就别写」 是第 1 条准则,比其他一切「如何安全写 set-UID」更根本——少 1 个 set-UID 程序就少 1 个攻击面。

      2. 最小特权原则对应到 saved set-UID 机制:saved 是「升级回路」——如果只清 effective 而不清 saved,攻击者可借 /bin/shexecve 复活。

      3. 永久降权常见陷阱setuid(getuid()) 在 effective != 0 时只改 effective,不改 saved——必须先 seteuid(orig_euid) 让 effective 回到 0,或用 setreuid(getuid(), getuid()) / setresuid() 一步到位。

      4. 检查返回值*:不要用 if (syscall()) 草草判断——每个 open/read/write/fork/exec 都查。root 也能 EMFILE/ENOSPC/EACCES

      5. 不 exec shellsystem()/popen()/execlp()/execvp() 都通过 /bin/sh -c 执行,shell 的 $PATH/$IFS/alias/function 太复杂,几乎无法保证安全。要 exec shell 必须先永久降权。

      6. 关特权 fdopen("/etc/shadow") 拿到的 fd 留给新程序就是大漏洞——FD_CLOEXEC 或显式 close()

      7. 不信任 PATH/IFS:清空 IFS(shell 的 word separator),重设 PATH,用绝对路径 exec。

      8. 不信任 stdin/stdout/stderr:可能被关闭后被 open() 复用,造成「以为写到 stdout,实则写到攻击者文件」。例:open() 后检查 if (fd ⇐ 2)

      9. fd 而非 pathstat() + open() 之间就是攻击窗口;改用 open() + fstat() / O_NOFOLLOW 一步到位。

      10. mkstemp vs /tmp:在公开可写目录构造文件必须用 mkstemp() 不可预测名,否则攻击者预先创建符号链接让你覆盖 /etc/passwd

      11. 永远不要 gets():永远带长度限制;snprintf() 检查返回值判断截断;strncpy 不保证 null 终止且性能差。

      12. TOCTOU 不只是 access():所有「先 stat 再 open」「先 readdir 再 open」「先 readlink 再 open」都是——只要「检查」与「使用」经过路径就会出问题。

      13. Umask: 创建文件前 umask(0077);否则 set-UID 程序创建的文件可能同组/其他可读。

      14. fail safe: 遇错 exit() 或 drop request——不要试着修;试着修常意味着对环境做未经验证的假设。

      15. DoS 不要在网络 server 忽视:truncate 输入大小、限 logging、设置 timeout;防 Crosby-Wallach 风格算法复杂度攻击(哈希冲突、二叉树退化)。

      16. 跨章衔接:第 9 章 set-user-ID 与 saved set-user-ID 基础;第 18 章 chroot;第 39 章 capabilities (把特权拆成独立能力);第 27 章 execve;第 21/22 章信号;第 30 章线程;第 36 章资源限制。