分享

PAM 的应用开发和内部实现源码分析

 天蝎泪 2005-11-01

国防科技大学计算机学院在读硕士
2004 年 8 月

系统测试人员负责制定测试计划并依照测试计划进行测试。这些测试包括功能性的测试(黑盒测试)和非功能性的测试(白盒测试)。测试人员需要良好的测试工具来辅助完成测试任务,自动化的测试工具将大幅度提高测试人员的工作效率和质量。

本文主要通过对Linux PAM源代码进行分析,阐述了PAM的内部实现机制和怎样在应用程序中应用PAM进行认证,以及怎样开发PAM服务模块。

1 引言
身份认证是操作系统安全的重要机制之一,系统通过认证机制核查用户的身份证明,并作为用户进入系统的判定条件,是防止恶意用户进入系统的第一道门槛。近年来认证理论和技术得到了迅速发展,产生了各种认证机制,如口令机制,RSA, DCE, kerberos认证体制,S/Key和基于智能卡的身份认证等。然而,当系统中引入新的认证机制时,一些系统入口登录服务如login, rlogin和telnet等应用程序就必须改写以适应新的认证机制。为了解决这个问题,1995年Sun公司的Vipin Samar和 Charlie Lai提出了PAM(Pluggable Authentication Modules),并将其应用在Solaris系统上。PAM框架将应用程序与具体的认证机制分离,使得系统改变认证机制时,不再需要修改采用认证机制的应用程序,而只要由管理员配置应用程序的认证服务模块,极大地提高了认证机制的通用性与灵活性。

现在大多数操作系统都采用PAM实现身份认证,有Linux系统的Linux-PAM和FreeBSD5.x采用的OpenPAM(FreeBSD 4.x采用的是Linux-PAM)等。它们的实现原理一样,只有实现细节不同而已。下面从PAM的应用开发开始介绍。

2 PAM的应用开发

2.1 PAM框架概览
PAM即可插拔认证模块。它提供了对所有服务进行认证的中央机制,适用于login,远程登录(telnet,rlogin,fsh,ftp,点对点协议(PPP)),su等应用程序中。系统管理员通过PAM配置文件来制定不同应用程序的不同认证策略;应用程序开发者通过在服务程序中使用PAM API(pam_xxxx( ))来实现对认证方法的调用;而PAM服务模块的开发者则利用PAM SPI来编写模块(主要是引出一些函数pam_sm_xxxx( )供PAM接口库调用),将不同的认证机制加入到系统中;PAM接口库(libpam)则读取配置文件,将应用程序和相应的PAM服务模块联系起来。PAM框架结构如图所示。

图 PAM框架结构图
图 PAM框架结构图

其中,pamh是一个pam_handle类型的结构,它是一个非常重要的处理句柄,是PAM与应用程序通信的唯一数据结构,也是调用PAM接口库API的唯一句柄。pam_handle数据结构将在下面的源代码分析一节的介绍。

另外,如上图所示的服务模块分auth(认证管理)、account(账号管理)、session(会话管理)、passwd(口令管理)四种类型,各个类型模块的作用以及配置文件的四个组成部分模块类型、控制标志、模块路径、模块参数等在很多讲PAM的配置管理的文章里都有介绍,这里就不再赘述了。

2.2 在应用程序中使用PAM认证
每个使用PAM认证的应用程序都以pam_start开始,pam_end结束。PAM还提供了pam_get_item和pam_set_item共享有关认证会话的某些公共信息,例如用户名,服务名,密码和会话函数。应用程序在调用了pam_start ()后也能够用这些APIs来改变状态信息。实际做认证工作的API函数有六个(以下将这六个函数简称为认证API):

  • 认证管理--包括pam_authenticate ()函数认证用户,pam_setcred ()设置,刷新,或销毁用户证书。
  • 账号管理--包括pam_acc_mgmt ()检查认证的用户是否可以访问他们的账户,该函数可以实现口令有效期,访问时间限制等。
  • 会话管理--包括pam_open_session ()和pam_close_session ()函数用来管理会话和记账。例如,系统可以存储会话的全部时间。
  • 口令管理--包括pam_chauthok ()函数用来改变密码。

下面看一个简单的login模拟程序:



 /* 使用PAM所必需的两个头文件*/
#include <security/pam_appl.h>
#include <security/pam_misc.h>

void main(int argc, char *argv[], char **renvp)
{
    /* 初始化,并提供一个回调函数 */
    if ((pam_start("login", user_name, &pam_conv, &pamh)) != PAM_SUCCESS)
        exit(1);

    /* 设置一些关于认证用户信息的参数 */
    pam_set_item(pamh, PAM_TTY, ttyn);
    pam_set_item(pamh, PAM_RHOST, remote_host);

    while (!authenticated && retry < MAX_RETRIES)
    {
        status = pam_authenticate(pamh, 0);/* 认证,检查用户输入的密码是否正确 */
}

/* 认证失败则应用程序退出*/
    if (status != PAM_SUCCESS)
    {
      	……
        exit(1);
}

    /*  通过了密码认证之后再调用账号管理API,检查用户账号是否已经过期 */
    if ((status = pam_acct_mgmt(pamh, 0)) != PAM_SUCCESS)
    {
        if (status == PAM_AUTHTOK_EXPIRED)
        {
            status = pam_chauthtok(pamh, 0);  /* 过期则要求用户更改密码 */
            if (status != PAM_SUCCESS)
                exit(1);
        }
    }
    /* 通过帐户管理检查之后则打开会话 */
    if (status = pam_open_session(pamh, 0) != PAM_SUCCESS)
        exit(status);
	……
    /* 建立认证服务的用户证书*/
    status = pam_setcred(pamh, PAM_ESTABLISH_CRED);
    if (status != PAM_SUCCESS)
       exit(status);
   	……
pam_end(pamh, PAM_SUCCESS);  /* PAM事务的结束 */
……
 }
 

从上面程序中,我们可以了解到使用PAM认证的一般流程,同时也可以看出PAM API使得使用认证的应用程序不仅不用关心底层使用的服务模块,而且编写起来简洁明了得多。

有关开发使用PAM的应用程序更加详细完整的阐述请参考The Linux-PAM Application Developers‘ Guide。

2.3 怎样开发PAM服务模块
首先在编写的服务模块的源程序里要包含下列头文件:



#include <security/pam_modules.h>

PAM的服务模块是一个一个的动态链接库文件(也可以是静态库),PAM接口库通过dlopen来装载这些库。假设源程序名为pam_module-name.c,则需要用下列命令将其编译成动态链接库:



gcc -fPIC -c pam_module-name.c
ld -x --shared -o pam_module-name.so pam_module-name.o

选项-fPIC是指位置无关代码(Position Independent Code),这类代码支持大偏移。使用--shared选项将目标代码放进共享目标库中。

四种类型的模块各自要实现的函数如下表所示:

模块类型 要实现的函数 函数功能
认证管理 PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) 认证用户
PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) 设置用户证书
账号管理 PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv) 账号管理
会话管理 PAM_EXTERN int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, const char **argv) 打开会话
PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, const char **argv) 关闭会话
口令管理 PAM_EXTERN int pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, const char **argv) 设置口令

当然同一个服务模块可以同时属于多种类型,只要这些类型模块要实现的函数都实现了就可以,比如PAM自带的经典口令认证机制模块pam_unix.so 就可以支持四种模块类型。

下面来看一个最简单的pam_deny模块的源程序pam_deny.c:



1.	#define PAM_SM_AUTH
2.	#define PAM_SM_ACCOUNT
3.	#define PAM_SM_SESSION
4.	#define PAM_SM_PASSWORD
5.	#include "../../libpam/include/security/pam_modules.h"
6.	/* --- 认证管理函数的实现--- */
7.	PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh,int flags,int argc
8.	,const char **argv)
9.	{
10.	    return PAM_AUTH_ERR;
11.	}
12.	PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh,int flags,int argc
13.	,const char **argv)
14.	{
15.	    return PAM_CRED_UNAVAIL;
16.	}
17.	/* --- 账号管理函数的实现 --- */
18.	PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t *pamh,int flags,int argc
19.	,const char **argv)
20.	{
21.	    return PAM_ACCT_EXPIRED;
22.	}
23.	/* --- 口令管理函数的实现 --- */
24.	PAM_EXTERN int pam_sm_chauthtok(pam_handle_t *pamh,int flags,int argc
25.	,const char **argv)
26.	{
27.	    return PAM_AUTHTOK_ERR;
28.	}
29.	/* --- 会话管理函数的实现 --- */
30.	PAM_EXTERN int pam_sm_open_session(pam_handle_t *pamh,int flags,int argc
31.	,const char **argv)
32.	{
33.	    return PAM_SYSTEM_ERR;
34.	}
35.	PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh,int flags,int argc
36.	,const char **argv)
37.	{
38.	    return PAM_SYSTEM_ERR;
39.	}
40.	/* 模块定义结束 */
41.	/* 静态模块数据 */
42.	#ifdef PAM_STATIC
43.	struct pam_module _pam_deny_modstruct = {
44.	   "pam_deny",
45.	    pam_sm_authenticate,
46.	    pam_sm_setcred,
47.	    pam_sm_acct_mgmt,
48.	    pam_sm_open_session,
49.	    pam_sm_close_session,
50.	    pam_sm_chauthtok
51.	};
52.	#endif

很容易看出,pam_deny模块支持四种模块类型。前4行包含静态模块的一些原型申明,第37-39行实现了四种模块类型的函数,因为这只是一个简单的拒绝服务的模块,所以这些函数只是简单地返回认证错或系统错等PAM错误。最后几行定义了该程序被编译成静态模块所需的一个模块数据结构。

因此,PAM SPI使得服务模块的开发也相当简单和专一,因为服务模块不再需要考虑和应用程序的交互,只要将自己采用的算法实现好就可以了。

模块源程序可用的flags参数值和返回值的定义这里不作全面介绍,有兴趣者请参考The Linux-PAM Module Writers‘ Guide。

3 PAM接口库源代码分析
上面我们介绍了怎样使用PAM和怎样开发PAM服务模块,要想对PAM的内部机制有个透彻的理解,还需要进一步分析PAM接口库的代码。下面基于Linux 的pam-0.75-40.src.rpm包所得的源代码进行分析。

3.1 PAM接口库主要数据结构
先看一下PAM接口库用到的一些主要数据结构。pam_handle和其他几个主要的数据结构(见../libpam/pam_private.h)及其之间的关系如下图所示。


其中pam_handle包含认证的用户的token、用户名、应用程序名、终端名等信息,以及一个service结构(handlers);前面几节提到的pamh句柄就是一个pam_handle结构。service结构包含服务模块的相关信息,各个域的含义是:

1. module--该结构包含装载的模块的名字、类型(静态或动态模块)、链接句柄(装载模块时的句柄)。

2. modules_allocated--分配的模块数。

3. modules_used--已使用的模块数。

4. handlers_loaded--是否对操作(handlers结构)进行了初始化,handlers结构和初始化handlers见下面的介绍。

5. conf--由应用程序相对应的配置文件指定的服务模块的handlers。

6. other--为缺省配置文件指定的服务模块的handlers。

handlers结构包含六个handler结构链表的指针,六个指针分别对应六种不同的认证API;libpam通过这些指针找到对应模块的SPI服务函数。如下表所示:

handler指针 API函数 SPI函数
authenticate pam_authenticate( ) pam_sm_authenticate( )
setcred pam_setcred( ) pam_sm_setcred( )
acct_mgmt pam_acct_mgmt( ) pam_sm_acct_mgmt( )
open_session pam_open_session( ) pam_sm_open_session( )
close_session pam_close_session( ) pam_sm_close_session( )
chauthtok pam_chauthtok( ) pam_sm_chauthtok( )

handler数据结构是最直接保存服务模块的SPI服务函数的地址及参数的结构,其包含的主要的域的含义如下:

1. (*func)--该函数指针指向handlers所装载的服务模块的服务函数。

2. argc、**argv--分别为*func所指向的函数的参数个数和参数列表。

3. *next--指向堆栈模块中的下一个服务模块的服务函数。由此指针形成所有堆栈模块的服务函数链。

3.2 PAM接口库重要内部函数分析
PAM接口库中有一系列_pam开头的内部函数,那些APIs主要是调用这些内部函数来完成其功能的。

int _pam_add_handler(pam_handle_t *pamh, int must_fail, int other, int type, int *actions, const char *mod_path, int argc, char **argv, int argvlen)

该函数负责加载服务模块的SPI函数。这个函数代码很长有300多行,这里就不列举了。其主要步骤如下:

1. 根据mod_path提供的模块路径装载服务模块(dl_open)。

2. 由type确定的类型来决定要装入的SPI函数名并找到该函数(dlsym)的地址。type类型对应配置文件中的服务模块的四种类型标记,type的值及其对应的要装入的SPI函数名见下表。

模块类型 type SPI函数
认证管理模块 PAM_T_AUTH pam_sm_authenticate
pam_sm_setcred
会话管理模块 PAM_T_SESS pam_sm_open_session
pam_sm_close_session
账号管理模块 PAM_T_ACCT pam_sm_acct_mgmt
口令管理模块 PAM_T_PASS pam_sm_chauthtok

3. 新分配一个handler结构,将dlsym找到的函数的地址赋给该handler结构的func域,并填充其他的结构信息。

4. 将新分配的handler结构插入到handlers的对应handler结构链表中。

_pam_parse_conf_file函数负责读并分析PAM配置文件,将相关的信息填充到pamh句柄中,并调用_pam_add_handlers加载服务模块的服务函数。

_pam_init_handlers函数主要做handler的初始化工作,先判断handler是否已初始化,若没有则调用_pam_parse_conf_file分析配置文件加载服务模块的服务函数。

_pam_dispatch_aux(pam_handle_t *pamh, int flags, struct handler *h,……)

该函数负责遍历执行模块堆栈中的每一个服务模块对应的SPI函数,即执行h指向的handler结构链表中的每一个func指向的函数,并返回模块堆栈的结果值。

int _pam_dispatch(pam_handle_t *pamh, int flags, int choice)

该函数首先通过调用_pam_init_handlers将模块调度请求转换为指向实际要运行的模块堆栈函数链表的指针,并将该指针传递给_pam_dispatch_aux函数来遍历模块堆栈执行服务函数。该函数是实现六个认证API函数的主要部分和公共调用的函数,通过choice选项来区分是哪个认证API函数。

下面分析我们最关心的部分,也即那些认证API是怎样找到和调度配置文件中配置的服务模块的?

3.3 PAM认证API的实现
为了更加清楚地说明PAM接口库是怎样来调度使用模块的,下面给出了pam_authenticate函数执行的流程图:


其他的认证API函数(pam_open_session等)执行过程前面五个步骤同上图,只是在最后一步时传递给_pam_dispatch_aux的指针参数不同,传递3.1节表中每个API函数相对应的那个handler型指针,然后执行相对应的SPI服务函数链。

4 小结
无论从PAM的应用开发还是它的实现原理来看,这个框架及其思想都是非常完美的,所以几乎各种版本的 UNIX 系统都提供对 PAM 的支持。本文通过对Linux-PAM 进行了深入仔细的分析,阐述了它的内部实现机制,并且讲述了怎样在应用程序中使用PAM和怎样开发PAM服务模块,希望能对大家做认证相关的开发工作有所帮助。

参考资料

  1. [Vipin Samar, Charlie Lai, 1995] Making Login Services Independent of Authentication Technologies. Sun Technical report.
  2. [Andrew G. Morgan, 2001] The Linux-PAM Module Writers‘ Guide. Linux-PAM Documentation.
  3. [Andrew G. Morgan, 2001] The Linux-PAM Module Writers‘ Guide. Linux-PAM Documentation.

关于作者
程卫芳,国防科技大学计算机学院在读硕士,研究方向为安全操作系统。 Email: desertbow@sina.com

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多