本文不对PAM模块机制做深入讲解,网上一堆文章已经把这个事儿做了,这里仅仅是PAM的实现示例以记录自己的学习过程和成果。
本文仅仅实现一个PAM示例,即一个用户程序通过PAM机制进行密码认证,编写一个用户程序,编写一个pam模块动态库即可完成这个功能。
一。PAM介绍
Linux-PAM(即linux可插入认证模块)是一套共享库,使本地系统管理员可以随意选择程序的认证方式。换句话说,不用(重新编写)重新编译一个包含PAM功能的应用程序,就可以改变它使用的认证机制,这种方式下,就算升级本地认证机制,也不用修改程序。
PAM使用配置/etc/pam.d/下的文件,来管理对程序的认证方式.应用程序 调用相应的配置文件,从而调用本地的认证模块.模块放置在/lib/security下,以加载动态库的形式加载到内存,系统程序sudo,su,login等都是通过调用PAM模块实现的口令验证。
二。PAM组成
PAM是一种系统安全认证机制:
1.PAM API,即PAM库提供的API函数,用户程序使用这些api完成口令认证。
2.PAM SPI,即PAM的接口函数,这些函数由用户在PAM 模块动态库中实现,完成认证,账户管理,密码管理,会话管理等功能,被PAM API通过dlopen方式调用。
3.应用程序,即使用PAM模块进行口令认证的用户程序,例如:su,login,sudo等,应用程序调用PAM API完成口令等相关认证功能。
4.配置文件,/etc/pam.conf这个文件一般不使用了,因为使用的是/etc/pam.d/*的配置文件,每个文件表示一个用户程序的pam认证配置文件,决定使用哪个模块动态库进行认证,用户程序调用PAM API时,PAM API会查找/etc/pam.d/目录下的配置文件以加pam模块载动态库。
如su程序使用的pam配置文件/etc/pam.d/su的配置如下:
#
# The PAM configuration file for the Shadow `su’ service
#
# This allows root to su without passwords (normal operation)
auth sufficient pam_rootok.so
简单解释:su 切换root用户时执行pam_rootok.so的验证,效果是从su切换到普通用户时不用输入密码,但是从普通用户su到root时则需要密码,这就是pam_rootok.so的作用。
该目录下配置文件名称要与自己编写的PAM模块的服务名称一致,即要与用户程序里pam_start函数的第一个参数一致。
5. PAM模块动态库
/lib/x86_64-linux-gnu/security/
自己编写的PAM动态库也放到这个目录下。
三。编写示例
1.ubuntu16.04安装pam库
sudo apt install libpam0g-dev
2.编写pam模块动态库
vim pam_test2.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <security/pam_appl.h>
#include <security/pam_modules.h>
#include <security/pam_ext.h>
//认证管理,设置用户证书
PAM_EXTERN int pam_sm_setcred( pam_handle_t *pamh, int flags, int argc, const char **argv )
{
printf("TestPam Setcred\n");
return PAM_SUCCESS;
}
//账号管理
PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv)
{
printf("TestPam Acct mgmt\n");
return PAM_SUCCESS;
}
//编写PAM SPI接口代码,由PAM API pam_authenticate通过dlopen方式调用
PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv)
{
//这里密码写死abc,只有输入abc才能验证成功
int ret = 0;
char *pass = NULL;
char default_pass[] = "abc";
char *tip = "输入TestPam密码:"; //输入密码的提示信息,传递到会话函数,在其中显示
//存储会话函数的变量
struct pam_conv *conv = NULL;
ret = pam_get_item(pamh, PAM_CONV, (const void **)&conv);
if(ret != PAM_SUCCESS || conv == NULL)
{
printf("pam_get_item PAM_CONV failed:%d\n", ret);
return PAM_SYSTEM_ERR;
}
//获取密码可以在会话函数中完成,包括与用户程序交互数据的操作都应该在用户实现的
//会话函数中完成,也可以直接getpass在模块动态库完成(可能不符合PAM设计规范)
//调用会话函数,获取密码方式1,一般交互数据操作都在这个会话函数中完成
struct pam_message msg;
const struct pam_message *pmsg;
struct pam_response *presp;
msg.msg_style = PAM_PROMPT_ECHO_OFF;
msg.msg = tip;
pmsg = &msg;
//conv的第2,3参数为指针数组,即可以传递多个消息
//这里只是获取密码一个数据,所以传递的数组仅包含一个元素表示从会话函数中获取密码
ret = conv->conv(1, &pmsg, &presp, conv->appdata_ptr);
if(ret != PAM_SUCCESS || presp == NULL)
{
printf("conv failed:%d\n", ret);
return PAM_CONV_ERR;
}
pass = presp->resp;
free(presp);
//获取密码方式2
//pass = getpass(tip);
printf("password is:%s\n", pass);
//密码不相等,认证失败
if(strcmp(default_pass, pass) != 0)
{
free(pass);
printf("password failed\n");
return PAM_AUTH_ERR;
}
free(pass);
printf("login success\n");
return PAM_SUCCESS;
}
编译:gcc -fPIC -shared -o pam_test2.so pam_test2.c -lpam
生成pam_test2.so,并将该文件复制到pam模块目录/lib/x86_64-linux-gnu/security/
生成配置文件/etc/pam.d/test2,内容如下:
auth required pam_test2.so
2.编写用户程序
vim test2.c
#include <stdio.h>
#include <security/pam_appl.h>
#include <security/pam_misc.h>
//释放所有分配的响应数组变量
void free_resp(int num, struct pam_response **response)
{
int i;
struct pam_response *tmp;
for(i=0; i<num; i++)
{
tmp = response[i];
if(tmp)
{
if(tmp->resp)
free(tmp->resp);
free(tmp);
}
}
}
//会话回调函数,在应用程序中实现,在PAM模块动态库中回调
//正常的流程是,获取密码等与用户交互的操作应该在这个函数里完成,这个函数传递到PAM模块
//动态库,由动态库调用,这里为了简单演示仅仅实现一个空函数
extern int myconv(int num_msg, const struct pam_message **msgm,
struct pam_response **response, void *appdata_ptr)
{
printf("conv 会话空函数\n");
printf("num_msg:%d\n", num_msg);
int i;
const struct pam_message *pmsg;
struct pam_response *presp;
if(num_msg <= 0 || num_msg >= PAM_MAX_NUM_MSG)
{
printf("bad number of messages %d <= 0 || >= %d\n", num_msg, PAM_MAX_NUM_MSG);
*response = NULL;
return PAM_CONV_ERR;
}
//分配响应用的数组变量,个数与请求保持一致
presp = calloc(num_msg, sizeof(struct pam_response));
if(presp == NULL)
return PAM_BUF_ERR;
//设置response数组的每个元素为指针
for(i = 0; i < num_msg; i++)
response[i] = presp++;
//循环每个消息
for(i = 0; i < num_msg; i++)
{
//第i个消息
pmsg = msgm[i];
presp = response[i];
if(pmsg->msg == NULL)
{
printf("message[%d]: %d NULL\n", i, pmsg->msg_style);
goto err;
}
//初始化响应变量
presp->resp = NULL;
presp->resp_retcode = 0;
//根据消息类型处理消息
switch(pmsg->msg_style)
{
//从标准输入获取不回显数据,一般是输入密码
case PAM_PROMPT_ECHO_OFF:
{
//printf("%s\n", pmsg->msg);
char *p = (char *)malloc(100);
p = getpass(pmsg->msg); //获取密码
presp->resp = p;
break;
}
//回显消息,从标准输入获取数据并显示在屏幕上,一般是交互的名为信息,比如用户名称等
case PAM_PROMPT_ECHO_ON:
printf("msg[%d]:%s\n", i, pmsg->msg);
//presp->resp = gets(NULL); //获取输入消息赋值resp
break;
//回显PAM模块传递的错误消息
case PAM_ERROR_MSG:
printf("%s\n", pmsg->msg);
break;
//回显PAM模块传递的错误消息
case PAM_TEXT_INFO:
printf("%s\n", pmsg->msg);
break;
default:
printf("message[%d] type error %d : %s\n", i, pmsg->msg_style, pmsg->msg);
goto err;
}
}
return PAM_SUCCESS;
err:
free_resp(num_msg, response);
*response = NULL;
return PAM_CONV_ERR;
}
struct pam_conv conv;
int main(int argc, char *argv[])
{
int ret = 0;
pam_handle_t *pamh = NULL;
const char *user = NULL;
//会话函数传递到PAM模块中,在模块中通过pam_get_item获取并调用
conv.conv = myconv;
conv.appdata_ptr = NULL;
//初始化PAM,服务名称设置为test2,所以/etc/pam.d/的配置文件也要是这个名称
if((pam_start("test2", user, &conv, &pamh)) != PAM_SUCCESS)
{
return 0;
}
//设置数据,用户程序通过这两个函数与模块共享数据,在模块中可以pam_get_item获取
//pam_set_item()
//认证用户
ret = pam_authenticate(pamh, 0);
if(ret == PAM_SUCCESS)
printf("认证成功\n");
else
printf("认证失败\n");
//结束PAM
ret = pam_end(pamh, ret);
return ret;
}
编译:gcc -g test2.c -o test2 -lpam
记得放置配置文件/etc/pam.d/test2,
内容:auth required pam_test2.so
运行程序:
密码写死的abc
认证成功截图:
认证失败截图,输入的不是abc就失败:
示例完成,这样用户程序test2就可以在自己的程序中使用PAM模块的认证功能,以后需要修改认证逻辑的时候修改PAM模块pam_test2.so就可以了,用户程序不需要做改动。
系统的程序比如我们登录linux的时候,login程序使用PAM模块是pam_unix.so,我们可以把pam_test2.so在login的配置文件/etc/pam.d/login中配置一行认证条目就可以实现命令行登录系统时使用我们自己的认证方式,也可以配置到/etc/pam.d/lightdm中,这样图形界面登录时也使用我们自己的认证方式,这个功能在示例二中实现。