119 lines
5.3 KiB
C
119 lines
5.3 KiB
C
/* test_priv.c - 提权模块里那几条"不能搞错"的不变量
|
||
*
|
||
* 这个模块摔过一跤:上一版把认证路径也 setsid + 接管道,于是 sudo 拿不到
|
||
* 控制终端,真机上认证 100% 失败(而替身 sudo 无条件成功,给了个假绿)。
|
||
* 所以这里把两条硬不变量钉住:
|
||
*
|
||
* 1. **提权调用里不能有 -n**。带 -n 就等于"不许提问",需要密码时直接失败,
|
||
* 而用户要的恰恰是"提权要有身份验证"。
|
||
* 2. **没有控制终端时能识别出来**。识别不出来就会起一个等不到输入的进程。
|
||
*/
|
||
#include "priv.h"
|
||
#include "util.h"
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <sys/wait.h>
|
||
#include <unistd.h>
|
||
|
||
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 <用户> -- <argv>");
|
||
/* 显式转换,和 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;
|
||
}
|