向程序添加 shadow 支持实际上相当简单。唯一的问题是程序必须以 root 用户(或 SUID root 用户)身份运行,程序才能访问 /etc/shadow
文件。
这带来了一个很大的问题:在创建 SUID 程序时,必须遵循非常谨慎的编程实践。例如,如果程序存在 shell 逃逸,则当程序为 SUID root 时,绝不能以 root 用户身份发生这种情况。
为了向程序添加 shadow 支持以便它可以检查密码,但其他方面不需要以 root 用户身份运行,以 SUID shadow 用户身份运行程序会安全得多。xlock
程序就是这样的一个例子。
在下面给出的示例中,pppd-1.2.1d
已经以 SUID root 用户身份运行,因此添加 shadow 支持不应使程序更加容易受到攻击。
头文件应位于 /usr/include/shadow
中。还应该有一个 /usr/include/shadow.h
,但它将是指向 /usr/include/shadow/shadow.h
的符号链接。
要向程序添加 shadow 支持,您需要包含头文件
#include <shadow/shadow.h> #include <shadow/pwauth.h>
使用编译器指令有条件地编译 shadow 代码可能是一个好主意(我在下面的示例中这样做了)。
当您安装 Shadow Suite 时,libshadow.a
文件已创建并安装在 /usr/lib
中。
当将 shadow 支持编译到程序中时,需要告知链接器将 libshadow.a
库包含到链接中。
这是通过以下方式完成的
gcc program.c -o program -lshadow
但是,正如我们将在下面的示例中看到的那样,大多数大型程序都使用 Makefile
,并且通常有一个名为 LIBS=...
的变量,我们将对其进行修改。
libshadow.a
库使用一个名为 spwd
的结构来获取从 /etc/shadow
文件中检索的信息。 这是来自 /usr/include/shadow/shadow.h
头文件的 spwd
结构的定义
struct spwd { char *sp_namp; /* login name */ char *sp_pwdp; /* encrypted password */ sptime sp_lstchg; /* date of last change */ sptime sp_min; /* minimum number of days between changes */ sptime sp_max; /* maximum number of days between changes */ sptime sp_warn; /* number of days of warning before password expires */ sptime sp_inact; /* number of days after password expires until the account becomes unusable. */ sptime sp_expire; /* days since 1/1/70 until account expires */ unsigned long sp_flag; /* reserved for future use */ };
Shadow Suite 可以将除编码后的密码之外的其他内容放入 sp_pwdp
字段中。密码字段可能包含
username:Npge08pfz4wuk;@/sbin/extra:9479:0:10000::::
这意味着除了密码之外,还应该调用程序 /sbin/extra
以进行进一步的身份验证。被调用的程序将获得用户名和一个指示其被调用的原因的开关。有关更多信息,请参见文件 /usr/include/shadow/pwauth.h
和 pwauth.c
的源代码。
这意味着我们应该使用函数 pwauth
来执行实际的身份验证,因为它也将处理辅助身份验证。下面的示例就是这样做的。
Shadow Suite 的作者指出,由于大多数现有程序都没有这样做,因此它可能会在 Shadow Suite 的未来版本中被删除或更改。
shadow.h
文件还包含 libshadow.a
库中包含的函数的函数原型
extern void setspent __P ((void)); extern void endspent __P ((void)); extern struct spwd *sgetspent __P ((__const char *__string)); extern struct spwd *fgetspent __P ((FILE *__fp)); extern struct spwd *getspent __P ((void)); extern struct spwd *getspnam __P ((__const char *__name)); extern int putspent __P ((__const struct spwd *__sp, FILE *__fp));
我们将在示例中使用的函数是:getspnam
,它将为我们检索给定名称的 spwd
结构。
这是一个向需要 shadow 支持但默认情况下没有它的程序添加 shadow 支持的示例。
此示例使用点对点协议服务器 (pppd-1.2.1d),它有一种模式,在该模式下,它使用来自 /etc/passwd
文件而不是 PAP 或 CHAP 文件的用户名和密码执行 PAP 身份验证。您不需要将此代码添加到 pppd-2.2.0
,因为它已经在那里了。
pppd 的此功能可能用得不多,但是如果您安装了 Shadow Suite,它将不再起作用,因为密码不再存储在 /etc/passwd
中。
pppd-1.2.1d
下用于验证用户的代码位于 /usr/src/pppd-1.2.1d/pppd/auth.c
文件中。
以下代码需要添加到文件中所有其他 #include
指令的顶部。我们用条件指令包围了 #includes
(即,仅在为 shadow 支持编译时才包含)。
#ifdef HAS_SHADOW #include <shadow.h> #include <shadow/pwauth.h> #endif
接下来要做的是修改实际代码。我们仍在对 auth.c
文件进行更改。
修改前的 auth.c
函数
/* * login - Check the user name and password against the system * password database, and login the user if OK. * * returns: * UPAP_AUTHNAK: Login failed. * UPAP_AUTHACK: Login succeeded. * In either case, msg points to an appropriate message. */ static int login(user, passwd, msg, msglen) char *user; char *passwd; char **msg; int *msglen; { struct passwd *pw; char *epasswd; char *tty; if ((pw = getpwnam(user)) == NULL) { return (UPAP_AUTHNAK); } /* * XXX If no passwd, let them login without one. */ if (pw->pw_passwd == '\0') { return (UPAP_AUTHACK); } epasswd = crypt(passwd, pw->pw_passwd); if (strcmp(epasswd, pw->pw_passwd)) { return (UPAP_AUTHNAK); } syslog(LOG_INFO, "user %s logged in", user); /* * Write a wtmp entry for this user. */ tty = strrchr(devname, '/'); if (tty == NULL) tty = devname; else tty++; logwtmp(tty, user, ""); /* Add wtmp login entry */ logged_in = TRUE; return (UPAP_AUTHACK); }
用户的密码被放入 pw->pw_passwd
中,因此我们真正需要做的就是添加函数 getspnam
。这将把密码放入 spwd->sp_pwdp
中。
我们将添加函数 pwauth
来执行实际的身份验证。如果 shadow 文件为此进行了设置,这将自动执行辅助身份验证。
修改后的 auth.c
函数以支持 shadow
/* * login - Check the user name and password against the system * password database, and login the user if OK. * * This function has been modified to support the Linux Shadow Password * Suite if USE_SHADOW is defined. * * returns: * UPAP_AUTHNAK: Login failed. * UPAP_AUTHACK: Login succeeded. * In either case, msg points to an appropriate message. */ static int login(user, passwd, msg, msglen) char *user; char *passwd; char **msg; int *msglen; { struct passwd *pw; char *epasswd; char *tty; #ifdef USE_SHADOW struct spwd *spwd; struct spwd *getspnam(); #endif if ((pw = getpwnam(user)) == NULL) { return (UPAP_AUTHNAK); } #ifdef USE_SHADOW spwd = getspnam(user); if (spwd) pw->pw_passwd = spwd->sp-pwdp; #endif /* * XXX If no passwd, let NOT them login without one. */ if (pw->pw_passwd == '\0') { return (UPAP_AUTHNAK); } #ifdef HAS_SHADOW if ((pw->pw_passwd && pw->pw_passwd[0] == '@' && pw_auth (pw->pw_passwd+1, pw->pw_name, PW_LOGIN, NULL)) || !valid (passwd, pw)) { return (UPAP_AUTHNAK); } #else epasswd = crypt(passwd, pw->pw_passwd); if (strcmp(epasswd, pw->pw_passwd)) { return (UPAP_AUTHNAK); } #endif syslog(LOG_INFO, "user %s logged in", user); /* * Write a wtmp entry for this user. */ tty = strrchr(devname, '/'); if (tty == NULL) tty = devname; else tty++; logwtmp(tty, user, ""); /* Add wtmp login entry */ logged_in = TRUE; return (UPAP_AUTHACK); }
仔细检查会发现我们也做了另一个更改。原始版本允许访问(如果 /etc/passwd
文件中没有密码,则返回 UPAP_AUTHACK
。这不好,因为此登录功能的一个常见用途是使用一个帐户来允许访问 PPP 进程,然后使用 /etc/passwd
文件中的用户名和 /etc/shadow
文件中的密码来检查 PAP 提供的用户名和密码。
因此,如果我们已将原始版本设置为以用户(即 ppp
)的 shell 身份运行,那么任何人都可以通过将其 PAP 设置为用户 ppp
和空密码来获得 ppp 连接。
我们还通过在密码字段为空时返回 UPAP_AUTHNAK
而不是 UPAP_AUTHACK
来修复了此问题。
有趣的是,pppd-2.2.0
也存在同样的问题。
接下来,我们需要修改 Makefile,以便发生两件事:必须定义 USE_SHADOW
,并且需要将 libshadow.a
添加到链接过程中。
编辑 Makefile,并添加
LIBS = -lshadow
然后我们找到行
COMPILE_FLAGS = -I.. -D_linux_=1 -DGIDSET_TYPE=gid_t
并将其更改为
COMPILE_FLAGS = -I.. -D_linux_=1 -DGIDSET_TYPE=gid_t -DUSE_SHADOW
现在 make 和 install。