使用 PAM 保护 Linux 上的应用程序
PAM(可插拔身份验证模块)的基本概念,开发启用 PAM 的应用程序以及编写 PAM 配置文件。
作者:Savio Fernandes 和 KLM Reddy
身份验证是一种机制,用于验证实体是否是其声称的身份。在 Linux 系统上,应用程序(例如 su、passwd 或 login)用于在用户获得访问系统资源权限之前对其进行身份验证。
在几乎所有 Linux 发行版上,用户信息都存储在 /etc/passwd 中。这是一个文本文件,其中包含用户的登录名、加密密码、唯一的数字用户 ID(称为 uid)、数字组 ID(称为 gid)、可选的注释字段(通常包含用户的真实姓名、电话号码等项目)、主目录和首选 shell。/etc/passwd 中的典型条目如下所示
aztec:K52xi345vMO:900:900:Aztec software,Bangalore:/home/aztec:/bin/bash
但实际上,如果您查看您的 /etc/passwd,您很可能看到类似这样的内容
aztec:x:900:900:Aztec software,Bangalore:/home/aztec:/bin/bash加密密码去哪儿了?
/etc/passwd 文件可供所有用户读取,这使得任何用户都可以获取系统上所有人的加密密码。尽管密码已加密,但密码破解程序已广泛可用。为了应对日益增长的安全威胁,开发了影子密码。
当系统启用影子密码时,/etc/passwd 中的密码字段将被“x”替换,用户的真实加密密码存储在 /etc/shadow 中。由于 /etc/shadow 只能由 root 用户读取,因此恶意用户无法破解其他用户的密码。/etc/shadow 中的每个条目都包含用户的登录名、其加密密码以及许多与密码过期相关的字段。一个典型的条目如下所示
aztec:/3GJajkg1o4125:11009:0:99999:7:::
在 Linux 的早期,当应用程序需要验证用户身份时,它只需从 /etc/passwd 和 /etc/shadow 中读取必要的信息。如果需要更改用户密码,它只需编辑 /etc/passwd 和 /etc/shadow。
这种方法虽然简单,但有点笨拙,并且给系统管理员和应用程序开发人员带来了许多问题。每个需要用户身份验证的应用程序都必须知道如何在处理多种不同的身份验证方案时获取正确的信息。因此,授权应用程序软件的开发与身份验证方案紧密相关。此外,随着新的身份验证方案的出现,旧的方案变得过时。换句话说,如果系统管理员想要更改身份验证方案,则必须重新编译整个应用程序。
为了克服这些缺点,我们需要提出一种灵活的架构,将授权软件的开发与安全和适当的身份验证方案的开发分离开来。Linux 可插拔身份验证模块 (PAM) 就是这样一种架构,它成功地消除了身份验证方案和应用程序之间的紧密耦合。
从应用程序程序员的角度来看,PAM 负责此身份验证任务并验证用户的身份。从系统管理员的角度来看,可以自由地指定 Linux 系统上任何启用 PAM 的应用程序使用哪种身份验证方案。
图 1 显示了 PAM 生态系统的四个主要组成部分。第一个组成部分是 PAM 库,它为开发启用 PAM 的应用程序和模块提供了必要的接口和功能。
第二个是启用 PAM 的应用程序,它是一种提供某些服务的应用程序。它也可能需要在授予服务之前对用户进行身份验证。为了执行身份验证步骤,应用程序与 Linux-PAM 库接口,并调用它需要的任何身份验证服务。应用程序不知道已配置的身份验证方法的任何细节。应用程序需要提供一个“对话”函数,该函数允许加载的身份验证模块直接与应用程序进行通信,反之亦然。
可插拔身份验证模块是第三个组成部分,它是一个二进制文件,为某些(任意)身份验证方法提供支持。加载后,模块可以通过应用程序提供的“对话”函数直接与应用程序进行通信。从用户处请求(或提供给用户)的文本信息可以通过使用应用程序提供的“对话”函数进行交换。
最后一个组成部分是 PAM 配置文件。它是一个文本文件,系统管理员可以在其中指定特定应用程序使用哪种身份验证方案。在 Linux 系统上,此配置信息可以存储在 /etc/pam.d 文件夹下的文件中,也可以作为 /etc/conf 配置文件中的一行。当应用程序初始化 PAM 库时,将读取 PAM 配置文件。然后,PAM 库加载配置为支持特定模块指定的身份验证方案的相应身份验证模块。
为了启用 PAM 应用程序,我们需要在 PAM 库中调用适当的身份验证例程。我们还需要提供一个“对话函数”,模块可以使用该函数直接与应用程序通信。
PAM API 的身份验证例程包含以下三个主要函数
pam_start():应用程序必须调用的第一个 PAM 函数。它初始化 PAM 库,读取 PAM 配置文件,并按照配置文件中提到的顺序加载所需的身份验证模块。它返回 PAM 库的句柄,应用程序可以使用该句柄进行所有后续与库的交互。
pam_end():应用程序应在 PAM 库中调用的最后一个函数。返回后,PAM 库的句柄将不再有效,并且与其关联的所有内存都将失效。
pam_authenticate():此函数充当已加载模块的身份验证机制的接口。当应用程序需要验证请求服务的用户的身份时,将调用此函数。
除了身份验证例程之外,PAM API 还提供了以下函数,应用程序可以调用这些函数
pam_acct_mgmt():检查当前用户的帐户是否有效。
pam_open_session():开始新会话。
pam_close_session():关闭当前会话。
pam_setcred():管理用户凭据。
pam_chauthtok():更改用户的身份验证令牌。
pam_set_item():为 PAM 会话写入状态信息。
pam_get_item():检索 PAM 会话的状态信息。
pam_strerror():返回错误字符串。
这些 PAM API 例程通过 security/pam_appl.h 接口提供给应用程序。
对话函数促进了加载的模块和应用程序之间的直接通信。它通常为模块提供一种提示用户输入用户名、密码等的方法。对话函数 conv_func 的签名如下
int conv_func (int,const struct pam_message **, struct pam_response **,void *);
加载的身份验证模块通过 pam_message 结构提示应用程序输入一些信息。应用程序通过 pam_response 结构将请求的信息发送到模块。
但是,模块如何获取指向对话函数的指针?答案是对话结构:struct pam_conv。对话结构需要由应用程序使用指向对话函数的指针进行初始化。初始化后,对话结构作为参数传递给调用 pam_start() 期间的 PAM 库。使用此指针,模块然后可以开始与对话函数进行通信。
现在,让我们开发一个返回当前时间的应用程序。此应用程序是需要在提供服务之前验证用户身份的应用程序。
首先,包含必要的头文件。头文件 security/pam_appl.h 是 PAM API 的接口。然后,初始化对话结构
static struct pam_conv conv = { my_conv, //function pointer to the //conversation function NULL };
然后编写 main() 方法。为此,首先加载 PAM 库。我们知道应用程序需要调用 PAM 库中的方法,以便委派所需的身份验证任务。但是应用程序如何获取 PAM 库 libpam 的句柄?调用 pam_start() 会使用需要身份验证服务的应用程序的服务名称、要验证身份的个人的用户名以及指向 pam_conv 结构的指针来初始化 libpam。此函数返回 libpam 的句柄 *pamh,该句柄为后续对 PAM 库的调用提供连续性
pam_handle_t *pamh = NULL; int retval = 0; retval = pam_start("check_user",NULL,&conv,&pamh); if(retval != PAM_SUCCESS) exit(0);如果我们不想将用户名传递给 pam_start(),我们可以传递 NULL。然后,加载的身份验证模块将在稍后的时间点使用对话函数提示用户输入用户名。
编写 main() 方法的第二步是验证用户身份。现在到了关键时刻,我们要决定用户是否是其声称的身份。我们如何发现这一点?函数 pam_authenticate() 充当已加载模块的身份验证机制的接口。它通过与适当的身份验证模块交互来验证用户提供的用户名和密码。成功时返回 PAM_SUCCESS,如果没有匹配项,则返回指示失败性质的错误值
retval = pam_authenticate(pamh,0); if(retval == PAM_SUCCESS) printf("%s\n","Authenticated."); else printf("%s\n","Authentication Failed.");
您可能会注意到我们传递了句柄 pamh,它是我们从之前调用 pam_start() 获得的。
此过程的第三步是提供对所需服务的访问权限。现在用户已通过身份验证,他将被授予对请求的服务的访问权限。例如,我们的服务显示当前时间
return current_time();
最后,卸载 PAM 库。在用户完成使用应用程序后,需要卸载 PAM 库。此外,需要使与句柄 pamh 关联的内存失效。我们通过调用 pam_end() 来实现这一点
int pam_ status = 0; if(pam_end(pamh,pam_status) != PAM_SUCCESS) { pamh = NULL; exit(1); }该值由 pam_end() 的第二个参数获取。pam_status 用作模块特定的回调函数 cleanup() 的参数。通过这种方式,模块可以在取消链接之前执行任何适合模块的最后一分钟任务。函数成功返回后,将释放与句柄 pamh 关联的所有内存。
基本对话函数的实现如清单 1 所示。
对对话函数的调用的参数涉及模块和应用程序交换的信息。也就是说,num_msg 保存指针数组 msg 的长度。成功返回后,指针 *resp 指向 pam_response 结构的数组,其中包含应用程序提供的文本。
消息传递结构(从模块到应用程序)由 security/pam_appl.h 定义为
struct pam_message { int msg_style; const char *msg; };
拥有消息数组的目的是可以从模块的单个调用中将许多事物传递给应用程序。msg_style 的有效选择是
PAM_PROMPT_ECHO_OFF:获取字符串,但不回显任何文本(例如,密码)。
PAM_PROMPT_ECHO_ON:获取字符串,同时回显文本(例如,用户名)。
PAM_ERROR_MSG:显示错误。
PAM_TEXT_INFO:显示一些文本。
响应传递结构(从应用程序到模块)通过包含 security/pam_appl.h 定义为
struct pam_response { char *resp; int resp_retcode; };
目前,没有 resp_retcode 值的定义;正常值为 0。
使用以下命令编译应用程序
gcc -o azapp azapp.c -lpam -L/usr/azlibs
/usr/azlibs 文件夹应该是通常包含 Linux-PAM 库模块的文件夹,即 libpam.so。此库文件包含 pam_appl.h 中声明的函数的定义。
当面临开发模块的任务时,我们首先需要明确我们要实现的模块类型。
模块可以分为四种独立的管理类型:身份验证、帐户、会话和密码。为了正确定义,模块必须在至少这四种管理组中的一个组内定义所有函数。
使用函数 pam_sm_authenticate() 来实现身份验证模块,该模块执行实际的身份验证。然后使用 pam_sm_setcred()。通常,身份验证模块可以访问比用户的身份验证令牌更多的有关用户的信息。第二个函数用于使此类信息可供应用程序使用。它只应在用户通过身份验证之后但在会话建立之前调用。
对于帐户管理模型实现,pam_sm_acct_mgmt() 是执行确定用户此时是否被允许获得访问权限的任务的函数。用户需要在此步骤之前通过身份验证模块进行验证。
会话管理模块通过调用 pam_sm_open_session() 来启动会话。
当需要终止会话时,将调用 pam_sm_close_session() 函数。会话应该可以由一个应用程序打开,而由另一个应用程序关闭。这要么要求模块仅使用从 pam_get_item() 获取的信息,要么与会话相关的信息以某种方式存储在操作系统中(例如,在文件中)。
最后,pam_sm_chauthtok() 实现了密码管理模块,并且是用于(重新)设置用户的身份验证令牌(更改用户密码)的函数。Linux-PAM 库连续两次调用此函数。身份验证令牌仅在第二次调用中更改,在验证它与先前输入的令牌匹配之后。
除了这些模块函数外,PAM API 还提供了以下函数,模块可以调用这些函数
pam_set_item():为 PAM 会话写入状态信息。
pam_get_item():检索 PAM 会话的状态信息。
pam_strerror():返回错误字符串。
模块开发所需的 PAM API 函数通过 security/pam_modules.h 接口提供给模块。
现在,让我们开发一个执行身份验证管理的模块。为此,我们需要实现身份验证管理组中的函数。首先,包含必要的头文件。头文件 security/pam_modules.h 是 Linux-PAM 库的接口。
接下来,验证用户身份;清单 2 显示了 pam_sm_authenticate() 的基本实现。此函数的目的是提示应用程序输入用户名和密码,然后根据密码加密方案验证用户身份。
清单 2. pam_sm_authenticate() 的基本实现
获取用户名是通过调用 pam_get_user() 实现的,如果应用程序在调用 start_pam() 期间尚未提供密码。
一旦我们获得用户名,我们需要通过调用 _read_password() 提示用户输入其身份验证令牌(在本例中为密码)。此方法通过与应用程序提供的对话函数交互来读取用户的密码。
在 _read_password() 中,我们首先在 pam_message struct 数组中设置适当的数据,以便能够与对话函数进行交互
struct pam_message msg[3], *pmsg[3]; struct pam_response *resp; int i, replies; /* prepare to converse by setting appropriate */ /* data in the pam_message struct array */ pmsg[i] = &msg[i]; msg[i].msg_style = PAM_PROMPT_ECHO_OFF; msg[i++].msg = prompt1; replies = 1;
现在调用对话函数,期望从对话函数获得 i 个响应
retval = converse(pamh, ctrl, i, pmsg, &resp);converse() 函数基本上是模块到应用程序提供的对话函数的前端。
最后,调用 _verify_password()。_verify_password() 方法本质上是根据适当的加密方案验证用户的凭据。
通常,身份验证模块可以访问比用户的身份验证令牌中包含的信息更多的有关用户的信息。pam_sm_setcred 函数用于使此类信息可供应用程序使用。pam_sm_setcred 的基本实现如清单 3 所示。在此函数的示例实现中,我们只是向应用程序提供调用 pam_sm_authenticate() 的返回代码。
converse() 函数充当模块-应用程序对话的前端。converse() 的示例实现如清单 4 所示。
指向对话函数的指针是使用 pam_get_item(pamh,PAM_CONV,&item) 获取的。使用该指针,模块现在可以开始直接与应用程序通信。
模块可以静态链接到 libpam。实际上,对于基本 PAM 发行版附带的所有模块都应该是这样。要进行静态链接,模块需要以不与其他模块冲突的方式导出有关其包含的函数的信息。
构建静态模块所需的额外代码应使用 #ifdef PAM_STATIC 和 #endif 分隔。静态代码应定义单个结构 struct pam_module。这称为 _pam_modname_modstruct,其中 modname 是文件系统中使用的模块的名称,减去前导目录名(通常为 /usr/lib/security/)和后缀(通常为 .so)。
#ifdef PAM_STATIC struct pam_module _pam_unix_auth_modstruct = { "pam_unix_auth", pam_sm_authenticate, pam_sm_setcred, NULL, NULL, NULL, NULL, }; #endif
现在我们的模块已准备好编译为静态或动态模块。使用以下命令编译模块
gcc -fPIC -c pam_module-name.c ld -x --shared -o pam_module-name.so pam_module-name.o
由 Linux-PAM 控制的系统安全方面的本地配置包含在两个位置之一,即单个系统文件 (/etc/pam.conf) 或 /etc/pam.d/ 目录。
/etc/pam.conf 文件的通用配置行具有以下形式:服务名称 模块类型 控制标志 模块路径 参数。
我们还可以在 /etc/pam.d 文件夹中的单独文件中为应用程序指定 PAM 配置,在这种情况下,配置文件具有以下形式:模块类型 控制标志 模块路径 参数。服务名称 成为配置文件的名称。通常,服务名称是给定应用程序的常规名称,例如 azServer。
存在四种模块类型:auth、account、session 和 password。
auth:确定用户是否是其声称的身份,通常通过密码完成,但也可能通过更复杂的方式(例如生物识别技术)确定。
account:确定是否允许用户访问服务,以及用户的密码是否已过期等等。
password:为用户提供一种更改其身份验证令牌的机制。同样,这通常是用户的密码。
session:在用户通过身份验证之前和/或之后应完成的事情。这可能包括挂载/卸载用户主目录、记录登录/注销以及限制/取消限制用户可用的服务等。
此外,还有四个控制标志:required、requisite、sufficient 和 optional。
Required:表示模块的成功是模块类型设施成功所必需的。在执行完所有剩余模块(相同模块类型)之前,用户不会注意到此模块的失败。
Requisite:与 required 相同,不同之处在于,如果模块失败,它会直接将结果返回给应用程序。
Sufficient:如果此模块已成功,并且所有先前的 required 模块都已成功,则不再调用后续的 required 模块。
Optional:将模块标记为对于用户应用程序的服务成功或失败并非至关重要。仅当先前或后续堆叠模块没有任何明确的成功或失败时,才考虑其值。
动态加载对象文件(可插拔模块本身)的路径名是模块路径。如果模块路径的第一个字符是 /,则假定它是完整路径。如果不是这种情况,则给定的模块路径将附加到默认模块路径 /usr/lib/security。
参数是传递给模块的令牌列表,就像典型 Linux shell 命令的参数一样。通常,有效参数是可选的,并且特定于任何给定模块。
最后,要编写配置文件,请编辑 /etc/pam.conf 文件以添加以下代码行
check_user auth required /lib/security/pam_unix.so
这表示对于服务名称 check_user 和 auth 模块类型是必需的。要加载以支持此身份验证方法的模块是 pam_unix.so,它位于目录 /lib/security/ 中。

