接上文,继续分析主函数。
drop_privs()
首先看到如下结构体:
struct passwd *passwdbuf;
passwd是UNIX系统下pwd.h
的默认定义
struct passwd {
char*pw_name; /*user name */
char*pw_passwd; /*user password */
uid_t pw_uid; /*user id */
gid_t pw_gid; /*group id */
char*pw_gecos; /*real name */
char*pw_dir; /*home directory */
char*pw_shell; /*shell program */
};
getuid函数获取当前的进程号。getpwuid函数通过用户的uid查找用户的passwd数据。
if (getuid() == 0) { //当前进程拥有root权限时
struct passwd *passwdbuf;
passwdbuf = getpwuid(server_uid); //server_uid由哪里传递过来?
if (passwdbuf == NULL) {
DIE("getpwuid");
}
initgroups()用来从组文件(/etc/group)中读取一项组数据, 若该组数据的成员中有参数user时,便将参数group组识别码加入到此数据中。如果pw_gid中有pw_name,则把group组识别码加入到asswdbuf中。
if (initgroups(passwdbuf->pw_name, passwdbuf->pw_gid) == -1) {
DIE("initgroups");
}
setgid(server_gid)
和setuid(server_uid)
用来设定权限[1]。
if (setgid(server_gid) == -1) {
DIE("setgid");
}
if (setuid(server_uid) == -1) {
DIE("setuid");
}
这里一旦setgid,则进程会由root权限变为普通用户权限。
可以看出这个函数的作用就是drop privileges——降权的,如果以root身份运行程序,现在尝试放弃root身份,转为boa.conf配置文件中的身份。
setuid()函数的正确使用方法
开始时,某个程序需要root权限完成一些工作,但后续的工作不需要root权限。可以将该可执行程序文件设置set_uid位,并使得该文件的属主为root。这样,普通用户执行这个程序时,进程就具有了root权限,当不再需要root权限时,调用setuid(getuid())恢复进程的实际用户ID和有效用户ID为执行该程序的普通用户的ID 。对于一些提供网络服务的程序,这样做是非常有必要的,否则就可能被攻击者利用,使攻击者控制整个系统。
回忆一下su命令的作用,可以发现su是一个 set_uid程序。执行一个设置了set_uid位的程序时,内核将进程的有效用户ID设置为文件属主的ID(root的ID)。而内核检查一个进程是否具有访问某权限时,是使用进程的有效用户ID来进行检查的。su程序的文件属主是root,普通用户运行su命令时,su进程的权限是root权限。
对于设置了set_uid位的可执行程序也要注意,尤其是对那些属主是root的更要注意。因为Linux系统中root用户拥有最高权力。黑客们往往喜欢寻找设置了set_uid位的可执行程序的漏洞。这样的程序如果存在缓冲区溢出漏洞,并且该程序是一个网络程序,那么黑客就可以从远程的地方轻松地利用该漏洞获得运行该漏洞程序的主机的root权限。即使这样的成不是网络程序,那么也可以使本机上的恶意普通用户提升为root权限。
APUE section4.4 Set-User-ID and Set-Group-ID一节专门论述了uid,GID的用法。
分析到这里似乎可以去了解一下手机root的原理了,Android获取ROOT权限原理解析 。甚至可以阅读一下 su命令的源代码。
守护进程
if (max_connections < 1) {
struct rlimit rl;
c = getrlimit(RLIMIT_NOFILE, &rl);
if (c < 0) {
perror("getrlimit");
exit(1);
}
max_connections = rl.rlim_cur;
}
getrlimit获取当前进程的资源限制,RLIMIT_NOFILE表示每个进程能打开的最大文件数。rlimit结构体包含两个变量,rlim_cur表示当前限制,rlim_max表示最大限制,硬限制。
/* background ourself */
if (do_fork) {
switch(fork()) {
case -1:
/* error */
perror("fork");
exit(1);
break;
case 0:
/* child, success */
break;
default:
/* parent, success */
exit(0);
break;
}
如果do_folk标志为1,则表示以守护进程方式运行。调用folk()后,父进程退出,子进程继续。到此为止,除了setsid,创建守护进程的其它规则都已经完成[4][5]。
select_loop
select()机制中提供一fd_set的数据结构,实际上是一long类型的数组,每一个数组元素都能与一打开的文件句柄(不管是socket句柄,还是其他文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成。select具体作用见[6]。
void select_loop(int server_s)
{
FD_ZERO(&block_read_fdset);//将set清零使得集合中不含任何fd
FD_ZERO(&block_write_fdset);
req_timeout.tv_sec = (ka_timeout ? ka_timeout : REQUEST_TIMEOUT);
req_timeout.tv_usec = 0l; /* reset timeout */
max_fd = -1;
while (1) {
if (sighup_flag)
sighup_run();
if (sigchld_flag)
sigchld_run();
if (sigalrm_flag)
sigalrm_run();
if (sigterm_flag) {
if (sigterm_flag == 1)
sigterm_stage1_run(server_s);
if (sigterm_flag == 2 && !request_ready && !request_block) {
sigterm_stage2_run();
}
}
max_fd = -1;
if (request_block)
fdset_update();
process_requests(server_s);
if (!sigterm_flag && total_connections < (max_connections - 10)) {
BOA_FD_SET(server_s, &block_read_fdset);
}
req_timeout.tv_sec = (request_ready ? 0 :
(ka_timeout ? ka_timeout : REQUEST_TIMEOUT));
req_timeout.tv_usec = 0l; /* reset timeout */
if (select(max_fd + 1, &block_read_fdset,
&block_write_fdset, NULL,
(request_ready || request_block ? &req_timeout : NULL))
== -1) {
if (errno == EINTR)
continue;
else if (errno != EBADF) {
DIE("select");
}
}
time(¤t_time);
if (FD_ISSET(server_s, &block_read_fdset))
pending_requests = 1;
}
}
req_timeout用作select的时间限制,#define REQUEST_TIMEOUT 60
,然后进入while循环。首先检测这几个flag:sighup_flag,sigchld_flag,sigalrm_flag,sigterm_flag。
信号SIGPIPE,SIGUSR1,SIGUSR2被忽略,SIGSEGV,SIGBUS,SIGTERM,SIGHUP,SIGINT,SIGCHLD,SIGALRM有自己的信号处理函数。
对于段错误SIGSEGV,记录一下出错时间写到日志里,然后就abort了。毕竟无法恢复。
对于SIGBUS,在另外两处视情况可能要好好的处理SIGBUS。默认情况下像SIGSEGV一样,也是记录一下,abort掉。SIGBUS这个信号,在一些体系结构上,访问未对齐的地址会产生。
对于SIGINT,收到这个信号时,记录一下,正常退出。这个信号可以由ctrl+c发送给foreground process产生。
剩下的四个信号SIGCHLD,SIGALRM,SIGHUP,SIGTERM在while中处理,signals.c中的处理函数只是把sighup_flag,sigchld_flag,sigalrm_flag,sigterm_flag四个标志位置位。
对于SIGHUP
的处理:定义在signals.c中的sighup函数将sighup_flag置位,表示信号发生。这时调用sighup_run()。
void sighup_run(void)
{
sighup_flag = 0;
time(¤t_time);
log_error_time();
fputs("caught SIGHUP, restarting\n", stderr);
FD_ZERO(&block_read_fdset);
FD_ZERO(&block_write_fdset);
/* clear_common_env(); NEVER DO THIS */
dump_mime();
dump_passwd();
dump_alias();
free_requests();
log_error_time();
fputs("re-reading configuration files\n", stderr);
read_config_files();
log_error_time();
fputs("successful restart\n", stderr);
}
函数用来重新读取config_file。先清空fdset,清空read_config_file里动态分配的内存,清空request_free链表,然后调用read_config_file。
对于SIGCHLD
的处理是典型的子进程处理方式,如下:
void sigchld_run(void)
{
int status;
pid_t pid;
sigchld_flag = 0;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0)
if (verbose_cgi_logs) {
time(¤t_time);
log_error_time();
fprintf(stderr, "reaping child %d: status %d\n",
(int) pid, status);
}
return;
}
它的应用场合是:一个并发服务器, 每一个客户端连接服务器就fork一个子进程。当同时有n多个客户端断开连接时, 服务器端同时有n多个子进程终止, 这时候内核同时向父进程发送n个sigchld信号,在while中一并进行处理[8],收集僵尸进程留下的信息,同时使这个进程彻底消失。
对于SIGALRM
,只用来将mime_hashtable和passwd_hashtable里的数据写到日志文件里。
对于SIGTERM
,有两种处理方式。
sigterm_stage1_run,记录一下时间,清空block_read_set,关掉server_s,意味着不再接受新的连接。然后设置sigterm_flag = 2; 下一次由sigterm_stage2_run来处理。
sigterm_stage2_run,完成正常结束的第二阶段:clear_common_env();dump_mime();dump_passwd();dump_alias();free_requests(); 然后exit(0)。SIGTERM通过两个函数使程序适当的中断。
Reference
[1].http://blog.csdn.net/flagonxia/article/details/4041714
[2].http://blog.sina.com.cn/s/blog_701371290100yetd.html
[3].http://www.fookwood.com/archives/569
[4].APUE.Page 342
[5].http://tuzhii.com/2014/06/30/daemon/
[6].http://genime.blog.163.com/blog/static/1671577532012418341877/
[7].http://blog.csdn.net/tricky1997/article/details/6941129(*)
[8].http://blog.csdn.net/guzhouke19910920/article/details/7645034