/* test_priv.c - 提权模块里那几条"不能搞错"的不变量 * * 这个模块摔过一跤:上一版把认证路径也 setsid + 接管道,于是 sudo 拿不到 * 控制终端,真机上认证 100% 失败(而替身 sudo 无条件成功,给了个假绿)。 * 所以这里把两条硬不变量钉住: * * 1. **提权调用里不能有 -n**。带 -n 就等于"不许提问",需要密码时直接失败, * 而用户要的恰恰是"提权要有身份验证"。 * 2. **没有控制终端时能识别出来**。识别不出来就会起一个等不到输入的进程。 */ #include "priv.h" #include "util.h" #include #include #include #include #include static int failures = 0; static int checks = 0; #define CHECK(cond, ...) \ do { \ checks++; \ if (!(cond)) { \ failures++; \ printf(" ✗ %s:%d: ", __FILE__, __LINE__); \ printf(__VA_ARGS__); \ printf("\n"); \ } \ } while (0) static int argv_has(char *const *a, const char *want) { for (size_t i = 0; a[i]; i++) if (strcmp(a[i], want) == 0) return 1; return 0; } int main(void) { puts("包装成 sudo -u <用户> -- "); /* 显式转换,和 priv.c 里 w[0] = (char *)"sudo" 是同一个理由: * priv_sudo_argv 的签名要 char *const[](execvp 的要求),而这里放进去的 * 字面量没有任何人会改写。把整条链路改成非 const 才是真的丢 const 正确性。 */ char *plain[] = { (char *)"/bin/sh", (char *)"-c", (char *)"rm -rf /tmp/x", NULL }; char **w = priv_sudo_argv("root", plain); CHECK(w != NULL, "应当返回数组"); CHECK(strcmp(w[0], "sudo") == 0, "第一个是 sudo,实际 %s", w[0]); CHECK(strcmp(w[1], "-u") == 0 && strcmp(w[2], "root") == 0, "-u <用户> 紧跟其后"); CHECK(strcmp(w[3], "--") == 0, "必须有 -- :否则命令里的选项会被 sudo 当成自己的"); CHECK(strcmp(w[4], "/bin/sh") == 0 && strcmp(w[5], "-c") == 0 && strcmp(w[6], "rm -rf /tmp/x") == 0, "原 argv 原样接在后面,不被拆开"); CHECK(w[7] == NULL, "数组要 NULL 结尾"); /* 命门外的那条不变量 */ CHECK(!argv_has(w, "-n"), "**不能带 -n**:带 -n 就不许提问,需要密码时直接失败 —— " "而提权的要点就是让 sudo 能问密码"); CHECK(!argv_has(w, "-S"), "也不能带 -S:那要求把密码从 stdin 喂进去,等于让 ligent 经手密码"); free(w); puts("用户名为空时按 root"); w = priv_sudo_argv(NULL, plain); CHECK(strcmp(w[2], "root") == 0, "空用户应落到 root,实际 %s", w[2]); free(w); w = priv_sudo_argv("", plain); CHECK(strcmp(w[2], "root") == 0, "空串也应落到 root"); free(w); puts("sudo 的失败原话要换成能照做的中文"); char err[512]; memset(err, 0, sizeof err); priv_explain(1, "sudo: no tty present and no askpass program specified", err, sizeof err); CHECK(strstr(err, "终端") != NULL, "缺终端要说终端,实际 %s", err); priv_explain(1, "Sorry, try again.", err, sizeof err); CHECK(strstr(err, "认证") != NULL, "认证失败要说认证,实际 %s", err); priv_explain(1, "pluv is not in the sudoers file.", err, sizeof err); CHECK(strstr(err, "sudo") != NULL && strstr(err, "权限") != NULL, "没有 sudo 权限要说清楚,实际 %s", err); /* 认不出来就原样透传:宁可给原文,也不编一句可能不对的解释 */ priv_explain(1, "some brand new message", err, sizeof err); CHECK(strstr(err, "some brand new message") != NULL, "认不出来要透传原文,实际 %s", err); priv_explain(127, "无法执行 sudo:No such file or directory", err, sizeof err); CHECK(strstr(err, "没有 sudo") != NULL, "没有 sudo 时要说清楚,实际 %s", err); puts("没有控制终端时能识别出来"); int fds[2]; if (pipe(fds) != 0) { perror("pipe"); return 2; } pid_t pid = fork(); if (pid < 0) { perror("fork"); return 2; } if (pid == 0) { close(fds[0]); int rc = -1; if (setsid() >= 0) { /* 摘掉控制终端 */ rc = priv_have_tty(); /* 必须是 0 */ } ssize_t wr = write(fds[1], &rc, sizeof rc); (void)wr; close(fds[1]); _exit(0); } close(fds[1]); int got = -1; ssize_t rd = read(fds[0], &got, sizeof got); close(fds[0]); while (waitpid(pid, NULL, 0) < 0) { } CHECK(rd == (ssize_t)sizeof got && got == 0, "setsid() 之后不该认为有终端(识别不出来就会卡住)"); printf("\n%d 项检查,%d 项失败\n", checks, failures); return failures ? 1 : 0; }