@lm911
2018-07-11T08:57:02.000000Z
字数 10942
阅读 4059
PAM
学着写PAM 模块之前了解下PAM的大致情况是不错的选择(也是必须的),所以这里也只好引用一下关于PAM简介的内容了
PAM(Pluggable Authentication Modules)可插拔认证模块,最初是有Sun公司提出的一种认证机制.该机制简化了认证的定制,通过提供
一些动态链接库和一套统一的API,将系统服务和认证方式分离,使得系统管理员可以方便的配置不同服务程序的认证方式,也便于我们向系统中添加新的
认证方式.
可以把PAM分成四部分:
1-PAM API 2-PAM 库 3-PAM SPI 4-配置文件
1 PAM-API是PAM提供的一系列接口,在你的应用程序中可以通过调用这些接口实现你的应用程序利用PAM进行身份认证、账户管理等
2 PAM库是PAM API的实现(当然还有一些其他的操作比如实现PAM API与SPI的映射,就是在用户进行了不同的PAM配置后实现PAM 接口与PAMModule实现的“回调函数”的映射, 如pam_authenticate() 映射至PAM Module中的pam_sm_authenticate()..,这是我的不科学理解啊希望别误导了读者)
3 PAM SPI是PAM体系规定的PAM模块需要实现的一组接口,模块实现了这些接口PAM库就可以将PAM API映射过来(在正确配置的前提下)
4 配置文件 通过配置文件来设置各个服务的认证机制,配置文件在 /etc/pam.d/目录下
有了上面的只是我们可以画出PAM的整体结构了
------------------- ----------------- -------------------
Application App App . . .
------------------- ------------------ ------------------
\ | /
\ | /
\ | /
---------------------------------------
PAM API
-------------------------------------
|
|
|/
------------------------------------
PAM 库 ---------------------------> | configuration file |
-----------------------------------
/|\
/ | \
/ | \
/ | \
------------ -------------- ------------ ---------------
pam_md1.so pam_md2.so pam_md3.so . . . . pam_mdn.so
------------ -------------- ------------- ---------------
上面为PAM的整体结构,四个层次是比较明显的
application层是集成了PAM认证的应用程序,比如 login su sudo gdm ...这些程序都位于这个层次,这些程序都使用PAM机制进行认证,可以查看/etc/pam.d/目录下的配置文件,支持PAM的application都有一个以这个应用名字命名的配置文件。这些应用通过PAM API实现自身与PAM 的集成,下面就转贴一下PAM API
int pam_start(const char *service_name, const char *user, const struct pam_conv *pam_conversation, pam_handle_t **pamh)
使用这个函数来初始化PAM库,service_name 服务名(应该是用来确定使用哪个配置文件的吧, 如service_name为"login",将使用/etc/pam.d/login 这个配置文件--这是我的理解如果不对请纠正)
user 鉴权用户(需要验证身份和权限的用户)
pam_conversation 指向pam_conv结构的一个指针,这个结构很重要的内容如下
struct pam_conv{int (*conv)(int num_msg, const struct pam_message **msg,struct pam_response **resp, void *appdata_ptr);void *appdate_ptr;}
conv--为一个函数指针,指向一个对话函数,对话函数可以实现应用程序与PAM Module(自己定制的认证模块)之间的信息交互,具体是怎样实现对话的将在后面说明.
appdata_ptr 指向特殊的应用程序数据,这个不常用(具体作用我也不知道,写这些的时候我是摸着石头过河的,因此会有很多的不确定和错误的地方,但我想供参考应该是有意义的),对了,这个值呢我们可以看到很多时候直接赋值给了conv函数的最后一个参数..
pam_conv中还有两个结构分别是
struct pam_message{int msg_style;const char *msg;}struct pam_response{char *resp;int resp_retcode;}
现在说下conv的参数
num_msg 这个整数说明struct pam_message *msg[]这个指针数组含有几个元素,就好像 main函数的第一个参数argc 和argv的关系
msg这个参数呢是PAM Module提供的,msg中的每一个指针指向的内容都是PAM Module对应用程序的一条请求,请看下面关于pam_message的解释
说下pam_message吧,这个结构呢是 PAM Module发给应用程序的,里面有一些PAM Module对应用程序的一些 提问? 回答?..等等信息
其中msg_style可以指定这些信息的类型?性质?.. msg_style可以使这样的值:
PAM_PROMPT_ECHO_OFF 这个标志呢表明PAM Module想获得一些东西,比如密码、比如用户的一些其它信息,至于想获得什么信息可以在msg指向的缓冲区中设置提示信息,比如 msg[] = "Password:",那么PAM Module告诉应用程序我想让用户提供登录账户对应的密码, 比如 msg[] = "One-Time-Password:",那么PAM Module告诉应用程序我想让用户提供一个一次性密码. . .,因为是密码所以我们希望其内容不要以明文显示在屏幕上(这个就是在tty上通过login登录时,输入密码不显示这种状况),PAM_PROMPT_ECHO_OFF这个标志呢可以做到这一点,即通知应用程序给用户显示一些提示信息(msg的内容),通知应用程序为PAM Module搜集用户的信息(等待用户输入内容,应用程序搜集到内容后会想方设法--你马上将知道是什么方法--的将内容反馈给PAM Module)
PAM_PROMPT_ECHO_ON 这个标志的作用呢和PAM_PROMPT_ECHO_OFF基本一样,区别在于:用户的输入将回显在屏幕上
PAM_PROMPT_ECHO_ON PAM_PROMPT_OFF这两个标志在我们定制的PAM Module中会用到的,我们需要使用这两个标志获得一些用户的输入信息
PAM_ERROR_MSG 这个标志呢通知应用程序 "你需要向用户显示一条错误提示信息",其中错误提示信息的内容为msg指向的内容
PAM_TEXT_INFO 这个标志呢通知应用程序 "你需要向用户显示一条信息(错误?警示?向导提示?随便了)",内容也是存放在msg指向的缓冲区中
resp是一个pam_response结构指针的数组,是对msg对应请求的一个回答吧
大概说下pam_response
PAM Module通过conv对话函数向应用程序发出需要用户提供一些信息(上面的密码之类的)后,在对话函数返回时会将用户提供的信息通过这个结构返回,怎么样大概意思知道了吧...resp_retcode这东西据说一般可以不用考虑的
至此呢 conv这个对话函数就说完了,可能有些乱?没关系,接着走下去就会明朗了.(在这里需要说明一点,conv这个函数是有应用程序提供的,也就是说login gdm su sudo其实都是实现了这个函数的,PAM Module中会通过一些方法得到login/su/sudo/gdm..实现的这个函数的地址,从而可以在PAM Module中调用这个函数...)
现在要说下pam_start的最后一个参数(刚才解释的东西太多了)...
pam_handle_t *pamh,这个参数是一个 pam_handle_t类型的指针,pam_handle_t这个东西呢还是蛮复杂的,我不想转贴了,有兴趣的自己看吧,但是这个呢其实是一个上下文,是pam_start()函数的出参,以后所有的PAM API SPI都要使用这个参数的,里面其实就是一些配置信息...
和pam_start()对应的是pam_end(pam_handle_t *pamh)函数,这个函数主要是释放资源,在你完成认证之类的操作后可以调用这个函数实现资源的释放.
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <security/pam_appl.h>intconverse(int n, const struct pam_message **msg,struct pam_response **resp, void *data){struct pam_response *aresp;char buf[PAM_MAX_RESP_SIZE];int i;data = data;if (n <= 0 || n > PAM_MAX_NUM_MSG)return (PAM_CONV_ERR);if ((aresp = calloc(n, sizeof *aresp)) == NULL)return (PAM_BUF_ERR);for (i = 0; i < n; ++i) {aresp[i].resp_retcode = 0;aresp[i].resp = NULL;switch (msg[i]->msg_style) {case PAM_PROMPT_ECHO_OFF:aresp[i].resp = strdup(getpass(msg[i]->msg));if (aresp[i].resp == NULL)goto fail;break;case PAM_PROMPT_ECHO_ON:fputs(msg[i]->msg, stderr);if (fgets(buf, sizeof buf, stdin) == NULL)goto fail;aresp[i].resp = strdup(buf);if (aresp[i].resp == NULL)goto fail;break;case PAM_ERROR_MSG:fputs(msg[i]->msg, stderr);if (strlen(msg[i]->msg) > 0 &&msg[i]->msg[strlen(msg[i]->msg) - 1] != '\n')fputc('\n', stderr);break;case PAM_TEXT_INFO:fputs(msg[i]->msg, stdout);if (strlen(msg[i]->msg) > 0 &&msg[i]->msg[strlen(msg[i]->msg) - 1] != '\n')fputc('\n', stdout);break;default:goto fail;}}*resp = aresp;return (PAM_SUCCESS);fail:for (i = 0; i < n; ++i) {if (aresp[i].resp != NULL) {memset(aresp[i].resp, 0, strlen(aresp[i].resp));free(aresp[i].resp);}}memset(aresp, 0, n * sizeof *aresp);*resp = NULL;return (PAM_CONV_ERR);}
注释
这段C代码中首先是包含了一个叫做pam_appl.h的头文件,这个文件在 /usr/include/security/目录下,正如它的名字,这个头文件中声明的是PAM API函数,这些API是供Application使用的.
接下来代码实现了一个叫做对话函数的函数converse,这个函数原型是 int converse(int n, const struct pam_message **msg, struct pam_response **resp, void **data);这个函数的参数意义上一篇已经说过了,
可能还记得pam_start(.., const struct pam_conv *pam_conversation,..)这个函数吧,pam_conv结构的第一个成员就是一个对话函数指针,不错,应用程序里如果想使用PAM进行认证之类的操作就需要自己实现一个类似本示例程序中的对话函数,并通过pam_start()的pam_conversation这个参数把应用程序实现的对话函数告诉PAM库.
下面看下对话函数的内容吧.
示例的对话函数首先检查了第一个参数n是否合法,n表示了msg中请求的条数,如果合法,接下来就循环处理每一条"请求"了。
我们可以看到在循环体内有一个switch语句,通过这个流程应用程序把msg中的每一个请求的具体类型进行了区分,
若请求类型(pam_message.msg_style)为 PAM_PROMPT_ECHO_OFF 对话函数调用了一个getpass(const char prompt)(可能在有界面的应用程序中会调用getguipass*之类的函数吧),意思是提示用输入一些内容,这些内容呢会被当做密码输入似的不会显示(或者以'*'显示)在屏幕上.这类函数通常允许有一个常字符串类型的入参,就是我们看到的输出到屏幕上的提示信息,如"请输入密码" "Password"...
若请求类型为 PAM_PROMPT_ECHO_ON 对话函数将提示信息直接输送到了标准错误输出设备上,通常这个标准错误输出设备与stdout是一致的,然后直接调用fgets来获得用户的输入,很明显用户输入的信息将显示在屏幕上.
若请求类型为 PAM_ERROR_MSG 则对话函数直接把msg[i]->msg中的内容放到了标准错误设备上,并进行了换行处理
若请求类型为 PAM_TEXT_INFO 则对话函数直接把msg[i]->msg中的内容放到了标准输出设备上,并进行换行处理
其他情况就是错误的情况了. 现在应该了解msg_style的每一个值的效果了吧,上面代码就是一个对话函数的大致流程了,复杂一些的对话函数的结构和这个事例是一致的.
其实我最想研究的是这个代码中内存开辟的一些细节,因为对话函数有一个特点,就是它内部产生的内存开辟需要由调用者进行释放,因此我们一起看下对话函数中各个环节的内存开辟情况.
事例代码中第一处开辟空间是 calloc(n, sizeof *aresp),这是为msg的响应开辟空间,如果开辟失败了返回PAM_BUF_ERR,接下来在PAM_PROMPT_ECHO_OFF 和PAM_PROMPT_ECHO_ON这两分支中都调用了strdup这个函数,这个函数内部要调用malloc进行内存开辟的,函数若调用失败会返回NULL.
因此如果一个对话函数如果成功返回在我们的PAM 模块中我们需要记得释放空间有*resp 和resp[i]->resp这些内存空间
若对话函数执行失败在我们的模块中不需要考虑空间的释放问题,一个好的对话函数在不能成功执行用户的请求的时候应该保证自己分配的空间会自己处理掉,但很遗憾上面的这个示例对话函数中还是有一些东西没有释放.仔细看一下我们发现这个对话函数调用了memset(aresp, 0, n*sizeof *aresp);(这个处理很好,把操作的痕迹从内存中抹去),但是很遗憾它应该在返回之前调用free(aresp)的,因为我们可以看到返回之前执行了*resp=NULL,因此如果respa不在返回前释放的话这块内存将永远释放不掉了.
上一次说了对话函数,对话函数是PAM模块与使用PAM进行认证的应用程序进行信息交互的桥梁,通过对话函数PAM模块可以获得用户的输入信息(明文、密文),
还有两套很重要的函数
pam_set_data(pam_handle_t *pamh, const char *pname, void *pdata, void (*freefunc)(pam_handle_t *pamh, void *pbuf, int status)),pam_get_data(pam_handle_t *pamh, const char *pname, (void **)precvbuf)
这两个函数用来通一个PAM模块中不同的PAM SPI之间进行信息交流
PAM SPI有这么几种(就是一个特定功能模块需要实现的接口)
关于认证的 pam_sm_authenticate pam_sm_setcred(),密码管理的 session操作的,就不一一说了,这是基本的内容,很多地方讲的,使用上面的两个函数实现不同接口之间的信息交互(当然只能按着顺序,如pam_sm_authenticate总是比pam_sm_setcred先调用,因此可以在pam_sm_authenticate 中设置一个数据内容用于标记认证的结果,在pam_sm_setcred中通过判断pam_sm_authenticate中设置的数据内容来判断认证结果),这里说一下freefunc这个函数,这个函数用来实现pdata的释放。
pam_set_item(pam_handle_t *pamh, int itemtype, (void *)pbuf);pam_get_item(pam_handle_t *pamh, int itemtype, (void **)pbuf);
这两个函数是用来进行模块之间信息交互的,itemtype的种类是约定好的,不需要自己定义.
有了这些知识就可以写一个最为简单的认证模块了,下一篇将给出一个简单但是完整的PAM模块的例子.
#define PAM_SM_AUTH#include <security/pam_modules.h>#include <security/pam_appl.h>#include <stdio.h>#include <string.h>#include <stdarg.h>#define MODULE_NAME "pam_sample"#define SAMPLE_PROMPT "Extra Password for root:"#define PAM_DEBUG_ARG 1#define DPRINT if (ctrl & PAM_DEBUG_ARG) sample_syslog#define PAM_RET_CHECK(ret) if(PAM_SUCCESS != ret) \{ \return ret; \}//if debug is setting this function can write log information /var/log/messagestatic void sample_syslog(int err, const char *format, ...){va_list args;char buffer[1024];va_start(args, format);vsprintf(buffer, format, args);/* don't do openlog or closelog, but put our name in to be friendly */syslog(err, "%s: %s", MODULE_NAME, buffer);va_end(args);}//get the paramtersstatic int sample_parse(int argc, const char **argv, char *szconf){int ctrl=0;/** If either is not there, then we can't parse anything.*/if ((argc == 0) || (argv == NULL)) {return ctrl;}/* step through arguments */for (ctrl=0; argc-- > 0; ++argv){/* generic options */if (!strcmp(*argv, "debug")){ctrl |= PAM_DEBUG_ARG;}else{sample_syslog(LOG_WARNING, "unrecognized option '%s'", *argv);}}return ctrl;}//callback function to release a buffervoid sample_pam_free(pam_handle_t *pamh, void *pbuf, int error_status){free(pbuf);}//conversation function//这个是做了封装的对话函数,每次只允许你向应用程序请求一个"问题"int sample_converse(pam_handle_t *pamh, int msg_style, char *message, char **password){const struct pam_conv *conv;struct pam_message resp_message;const struct pam_message *msg[1];struct pam_response *resp = NULL;int retval;resp_message.msg_style = msg_style;resp_message.msg = message;msg[0] = &resp_message;//之前老提到对话函数,我们说过对话函数由应用程序提供,这里可以看到在模块中怎么获得对话函数//通过pam_get_item 可以获得pam_conv这个结构的一个指针(第二个参数是PAM_CONV表示类型)//然后就想下面的调用方式,你可以在你的模块中调用这个对话函数,和应用程序交互retval = pam_get_item(pamh, PAM_CONV, (const void **)&conv);PAM_RET_CHECK(retval)retval = conv->conv(1, msg, &resp, conv->appdata_ptr);PAM_RET_CHECK(retval)if(password){*password = resp->resp;free(resp);}return PAM_SUCCESS;}#ifdef PAM_SM_AUTHPAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv){char *puser, *ppwd;int nret;int nloop;int nChallenge;int nValidrsp;char szbuf[256];char szconf[256];char *resp2challenge = NULL;int ctrl = 0;memset(szconf, 0, 256);//通过这个函数获得用户名nret = pam_get_user(pamh, &puser, "UserName:");if(PAM_SUCCESS != nret){int *pret = (int *)malloc(sizeof(int));//makelog("get username failed");DPRINT(LOG_DEBUG, "Get user name failed");*pret = nret;pam_set_data(pamh, "sample_setcred_return", (void *)pret, sample_pam_free);return PAM_SYSTEM_ERR;}else{//username//如果用户名为root,请用户输入附加密码 "123456"if(!strcasecmp("root", puser)){//如果为root 和应用程序交互,让用户输入密码//如果为root 和应用程序交互,让用户输入密码nret = sample_converse(pamh, PAM_PROMPT_ECHO_OFF, SAMPLE_PROMPT, &ppwd);if(PAM_SUCCESS != nret){int *pret = (int *)malloc(sizeof(int));*pret = nret;DPRINT(LOG_DEBUG, "Get extra password failed");pam_set_data(pamh, "sample_setcred_return", (void *)pret, sample_pam_free);return nret;}//you have get the extra passwordif(strcasecmp("123456", ppwd)){int *pret = (int *)malloc(sizeof(int)); *pret = PAM_AUTH_ERR; DPRINT(LOG_DEBUG, "Invalid extra password"); pam_set_data(pamh, "sample_setcred_return", (void *)pret, sample_pam_free); return PAM_AUTH_ERR;}}}return PAM_SUCCESS;}PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv){int nret = PAM_SUCCESS, *pret;pret = &nret;pam_get_data(pamh, "sample_setcred_return", (void **)&pret);return *pret;}#endif//PAM_SM_AUTH
编译 gcc -o pam_test.so -shared -fPIC ./pam_test.c -lpam
将pam_test.so 拷贝到/lib/security/下(平台不同有差异)
配置 vim /etc/pam.d/login (如果想为login使用这个模块)
在你希望的位置添加 auth sufficient pam_test.so
sufficient 还可以为 required 等具体请参照http://www.ibm.com/developerworks/cn/linux/l-pam/index.html?ca=drs-#resources
至此一个简单的PAM模块就写好了