sudo源码分析(一)
2015-09-09 15:43
881 查看
首先申明,sudo命令虽然很常用,不过以前使用的时候从来都不带任何参数,后面直接跟着想要得到root权限的命令。知道最近研究了下sudo源码后才知道sudo居然也有这么多参数。当时看源码时也被它处理各种参数以及各种情况的代码所困扰,本文尽量避开这些枯燥的代码,清晰明白地分析sudo是如何提升程序的权限的。
首先看一下main函数主要做了些什么事:
经过我的删减,sudo的main函数已经变得十分简单,然而事实并非如此,在save_signals和init_signals之间还做了很多事,包括插件的加载和policy的开启等等。run_command也只是其中一个分支,但是我们大部分情况都是进入这个分支运行,因此下面我们直接看run_command的实现:
run_command的核心代码如上所示,该函数主要调用了sudo_execute函数,sudo_execute则是调用exec_cmnd并最终调用了execve执行了我们的程序:
注意上面的代码,如果用户指定了后台运行的选项(-b,--background),则会先进行fork,子进程调用setpgid将自己设置为一个新进程组的组长,即使父进程是位于前台进程组,子进程也将脱离前台进程组而成为后台进程。
exec_cmnd在调用sudo_execve之前先调用了exec_setup,这个函数设置了很多用户程序的运行环境参数,例如组ID,进程优先级,文件模式创建掩码,工作目录等,这些都是根据sudo命令选项调用对应系统函数而已。最重要的是用户权限的设置:
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权限了。
这个函数其实就仅仅是调用了系统的execve函数,然而还是有个小细节需要注意的。第一次调用execve可能会失败,因为execve只能执行可执行文件,然而很多情况下sudo后的命令时脚本文件,这时execve调用失败,设置错误码为ENOEXEC。于是只能使用shell程序来运行该脚本文件。其实shell第一次也会直接调用execve,就和之前一样,但是它发现失败且错误码为ENOEXEC时,会读取脚本文件第一行的解释器路径,再调用execve运行解释器来执行脚本。所以这里至少会调用4次execve呢。
代码看的够累的,下面做个总结吧,sudo的执行步骤大概分为下面5步:
修改信号处理函数:保存原来的信号处理函数,设置新的信号处理函数
调用setuid将实际用户设置为ROOT
恢复信号处理函数
设置用户程序指定的权限(默认ROOT),并设置其他运行环境参数
调用execve执行用户程序
其中第二步很关键,因为它将进程的实际用户ID也修改为ROOT,这就保证了sudo执行的进程不会被我们(普通用户)杀死。下面详细介绍下setuid(摘抄自手册):
这个函数实际上设置的是进程的有效用户ID,对于有效实际用户ID为普通用户的进程来讲,只能将有效用户ID设置实际用户ID或者保存的设置用户ID,若是有效用户ID为ROOT,则能将实际用ID、有效用户ID和保存的设置用户ID都设置为uid。
对于有效用户ID为ROOT的进程来讲,调用这个函数是比较危险的。一方面,如果参数uid为0,则会将实际用户ID修改为ROOT,使得我们无法终止进程(可能还有其他弊端)。另一方面,如果参数为普通用户ID,则会将所有用户ID都设置为uid,这会使进程丧失获得ROOT权限的能力。
首先看一下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权限的能力。
相关文章推荐
- 启动scala时出现“error while loading AnnotatedElement”
- git
- HBase物理模型
- hdu2647 拓扑序
- FOJ 1075
- Cube and EarthDistance
- centos6.5内网搭建DNS服务器
- android 数据存取——SharedPreferences
- UILabel显示多行文本,字体设置
- 厦门烂尾楼大起底 “维权”悠着点
- java1.5之线程池
- 1099. Build A Binary Search Tree (30)
- The DOM in JavaScript
- 0909 操作系统
- UnicodeDecodeError: 'ascii' codec can't decode byte 0xe6 in position 9: ordinal not in range(128)
- 深入理解计算机系统——第12章:多线程中共享变量
- UVa120 - Stacks of Flapjacks
- Spring 注解@Transactional readOnly=true
- 初识ASP.NET Mvc5+EF7的奇妙之旅
- String str = new String("abc")和String str = "abc"区别