您的位置:首页 > 其它

sudo源码分析(一)

2015-09-09 15:43 881 查看
首先申明,sudo命令虽然很常用,不过以前使用的时候从来都不带任何参数,后面直接跟着想要得到root权限的命令。知道最近研究了下sudo源码后才知道sudo居然也有这么多参数。当时看源码时也被它处理各种参数以及各种情况的代码所困扰,本文尽量避开这些枯燥的代码,清晰明白地分析sudo是如何提升程序的权限的。

首先看一下main函数主要做了些什么事:

int
main(int argc, char *argv[], char *envp[])
{
/* Reset signal mask and save signal state. */
(void) sigemptyset(&mask);
(void) sigprocmask(SIG_SETMASK, &mask, NULL);
save_signals();
// do something check and prepare
init_signals();

/* Become full root (not just setuid) so user cannot kill us. */
setuid(ROOT_UID);
exitcode = run_command(&command_details);
exit(exitcode);
}

经过我的删减,sudo的main函数已经变得十分简单,然而事实并非如此,在save_signals和init_signals之间还做了很多事,包括插件的加载和policy的开启等等。run_command也只是其中一个分支,但是我们大部分情况都是进入这个分支运行,因此下面我们直接看run_command的实现:

int
run_command(struct command_details *details)
{
struct command_status cstat;
int exitcode = 1;
debug_decl(run_command, SUDO_DEBUG_EXEC)

cstat.type = CMD_INVALID;
cstat.val = 0;

sudo_execute(details, &cstat);

switch (cstat.type) {
case CMD_ERRNO:
/* exec_setup() or execve() returned an error. */
exitcode = 1;
break;
case CMD_WSTATUS:
/* Command ran, exited or was killed. */
if (WIFEXITED(cstat.val))
exitcode = WEXITSTATUS(cstat.val);
else if (WIFSIGNALED(cstat.val))
exitcode = WTERMSIG(cstat.val) | 128;
break;
default:
sudo_warnx(U_("unexpected child termination condition: %d"), cstat.type);
break;
}
debug_return_int(exitcode);
}

run_command的核心代码如上所示,该函数主要调用了sudo_execute函数,sudo_execute则是调用exec_cmnd并最终调用了execve执行了我们的程序:

int
sudo_execute(struct command_details *details, struct command_status *cstat)
{
struct sigforward *sigfwd, *sigfwd_next;
const char *utmp_user = NULL;
struct sudo_event_base *evbase;
struct exec_closure ec;
bool log_io = false;
sigaction_t sa;
pid_t child;
int sv[2];
debug_decl(sudo_execute, SUDO_DEBUG_EXEC)

dispatch_pending_signals(cstat);

/* If running in background mode, fork and exit. */
if (ISSET(details->flags, CD_BACKGROUND)) {
switch (sudo_debug_fork()) {
case -1:
cstat->type = CMD_ERRNO;
cstat->val = errno;
debug_return_int(-1);
case 0:
/* child continues without controlling terminal */
(void)setpgid(0, 0);
break;
default:
/* parent exits (but does not flush buffers) */
sudo_debug_exit_int(__func__, __FILE__, __LINE__,
sudo_debug_subsys, 0);
_exit(0);
}
}

exec_cmnd(details, cstat, -1);
debug_return_int(cstat->type == CMD_ERRNO ? -1 : 0);
}


注意上面的代码,如果用户指定了后台运行的选项(-b,--background),则会先进行fork,子进程调用setpgid将自己设置为一个新进程组的组长,即使父进程是位于前台进程组,子进程也将脱离前台进程组而成为后台进程。

void
exec_cmnd(struct command_details *details, struct command_status *cstat,
int errfd)
{
debug_decl(exec_cmnd, SUDO_DEBUG_EXEC)

restore_signals();
if (exec_setup(details, NULL, -1) == true) {
/* headed for execve() */
sudo_debug_execve(SUDO_DEBUG_INFO, details->command,
details->argv, details->envp);
sudo_execve(details->command, details->argv, details->envp,
ISSET(details->flags, CD_NOEXEC));
cstat->type = CMD_ERRNO;
cstat->val = errno;
sudo_debug_printf(SUDO_DEBUG_ERROR, "unable to exec %s: %s",
details->command, strerror(errno));
}
debug_return;
}


exec_cmnd在调用sudo_execve之前先调用了exec_setup,这个函数设置了很多用户程序的运行环境参数,例如组ID,进程优先级,文件模式创建掩码,工作目录等,这些都是根据sudo命令选项调用对应系统函数而已。最重要的是用户权限的设置:

bool
exec_setup(struct command_details *details, const char *ptyname, int ptyfd)
{
bool rval = false;
debug_decl(exec_setup, SUDO_DEBUG_EXEC);
// 这里还可能执行设置组ID,优先级,文件模式创建掩码,
// 切换工作目录等操作

/*
* Unlimit the number of processes since Linux's setuid() will
* return EAGAIN if RLIMIT_NPROC would be exceeded by the uid switch.
*/
unlimit_nproc();

#ifdef HAVE_SETRESUID
if (setresuid(details->uid, details->euid, details->euid) != 0) {
sudo_warn(U_("unable to change to runas uid (%u, %u)"), details->uid,
details->euid);
goto done;
}
#elif defined(HAVE_SETREUID)
if (setreuid(details->uid, details->euid) != 0) {
sudo_warn(U_("unable to change to runas uid (%u, %u)"),
(unsigned int)details->uid, (unsigned int)details->euid);
goto done;
}
#else
if (seteuid(details->euid) != 0 || setuid(details->euid) != 0) {
sudo_warn(U_("unable to change to runas uid (%u, %u)"), details->uid,
details->euid);
goto done;
}
#endif /* !HAVE_SETRESUID && !HAVE_SETREUID */

/* Restore previous value of RLIMIT_NPROC. */
restore_nproc();

rval = true;

done:
debug_return_bool(rval);
}

POSIX规定的设置用户权限的函数主要有3个:

setresuid:设置实际用户ID、有效用户ID和保存的设置用户ID
setreuid:设置实际用户ID和有效用户ID
seteuid:设置有效用户ID

不同操作系统对这三个函数的支持不一样,因此程序使用了宏来判断系统支持哪些函数。这三个函数在ROOT权限时参数可以是任何有效ID,在普通用户条件下参数有限制,一般都只能取实际用户ID或保存的设置用户ID,具体自行百度或者查阅手册。好在sudo程序一开始就已经调用了setuid(ROOT_UID)使程序获得了完全的ROOT权限,因此这三个函数都能成功修改进程的用户权限。其实,除非我们在使用sudo是指定了-u选项,否则这里修改用户权限是没必要的,因为默认情况下sudo会将权限提升为ROOT,而在此之前程序就已经获得ROOT权限了。

int
sudo_execve(const char *path, char *const argv[], char *const envp[], bool noexec)
{
/* Modify the environment as needed to disable further execve(). */
if (noexec)
envp = disable_execute(envp);

execve(path, argv, envp);
if (errno == ENOEXEC) {
int argc;
char **nargv;

for (argc = 0; argv[argc] != NULL; argc++)
continue;
nargv = reallocarray(NULL, argc + 2, sizeof(char *));
if (nargv != NULL) {
nargv[0] = "sh";
nargv[1] = (char *)path;
memcpy(nargv + 2, argv + 1, argc * sizeof(char *));
execve(_PATH_SUDO_BSHELL, nargv, envp);
free(nargv);
}
}
return -1;
}

这个函数其实就仅仅是调用了系统的execve函数,然而还是有个小细节需要注意的。第一次调用execve可能会失败,因为execve只能执行可执行文件,然而很多情况下sudo后的命令时脚本文件,这时execve调用失败,设置错误码为ENOEXEC。于是只能使用shell程序来运行该脚本文件。其实shell第一次也会直接调用execve,就和之前一样,但是它发现失败且错误码为ENOEXEC时,会读取脚本文件第一行的解释器路径,再调用execve运行解释器来执行脚本。所以这里至少会调用4次execve呢。

代码看的够累的,下面做个总结吧,sudo的执行步骤大概分为下面5步:

修改信号处理函数:保存原来的信号处理函数,设置新的信号处理函数
调用setuid将实际用户设置为ROOT
恢复信号处理函数
设置用户程序指定的权限(默认ROOT),并设置其他运行环境参数
调用execve执行用户程序

其中第二步很关键,因为它将进程的实际用户ID也修改为ROOT,这就保证了sudo执行的进程不会被我们(普通用户)杀死。下面详细介绍下setuid(摘抄自手册):
int setuid(uid_t uid);

这个函数实际上设置的是进程的有效用户ID,对于有效实际用户ID为普通用户的进程来讲,只能将有效用户ID设置实际用户ID或者保存的设置用户ID,若是有效用户ID为ROOT,则能将实际用ID、有效用户ID和保存的设置用户ID都设置为uid。
对于有效用户ID为ROOT的进程来讲,调用这个函数是比较危险的。一方面,如果参数uid为0,则会将实际用户ID修改为ROOT,使得我们无法终止进程(可能还有其他弊端)。另一方面,如果参数为普通用户ID,则会将所有用户ID都设置为uid,这会使进程丧失获得ROOT权限的能力。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: