参考:SSH 协议基本原理及 wireshark 抓包分析https://juejin.im/post/5baaf517e51d453df0442dce
参考:pam会话函数详解https://blog.csdn.net/wzsy/article/details/37725375
参考:PAM详解(一)PAM介绍http://blog.chinaunix.net/uid-29479952-id-5761558.html
参考:PAM详解(二)PAM开发http://blog.chinaunix.net/uid-29479952-id-5761564.html
使用卡认证、key认证方式进行ssh登陆,如需要Xshell、securecrt自定义交互提示信息。需要使用键盘交互鉴权方式。
使用键盘交互模式验证卡
键盘交互模式:
解释: xshell有键盘交互模式, 可以边给出自定义提示信息, 边输入密码。
0. 默认xshell的Keyboard Interactive单选按钮无效, 需要修改sshd_config文件, 使此按钮生效
1. 修改方式: 请修改配置为"/etc/ssh/sshd_config里ChallengeResponseAuthentication yes"
2. 同时关闭密码交互方式: 请修改配置为"/etc/ssh/sshd_config里PasswordAuthentication no"
3. 同时关闭公钥交互方式: 请修改配置为"/etc/ssh/sshd_config里PubkeyAuthentication no"
问题1:是否可以禁用记住用户名、禁用记住密码?
答:
SSH登录里面Keyboard Interactive认证方式
SSH 的认证协议
常见的 SSH 协议认证方式有如下几种:
基于口令的验证方式(password authentication method),通过输入用户名和密码的方式进行远程机器的登录验证。
基于公共密钥的安全验证方式(public key authentication method),通过生成一组密钥(public key/private key)来实现用户的登录验证。
基于键盘交互的验证方式(keyboard interactive authentication method),通过服务器向客户端发送提示信息,然后由客户端根据相应的信息通过手工输入的方式发还给服务器端。
//my_log.h #define g_is_MY_debug_level 4 #define MY_DEBUG_E_CONDITION ( 1 <= g_is_MY_debug_level) #define MY_DEBUG_W_CONDITION ( 2 <= g_is_MY_debug_level) #define MY_DEBUG_L_CONDITION ( 3 <= g_is_MY_debug_level) #define MY_DEBUG_I_CONDITION ( 4 <= g_is_MY_debug_level) #define MY_DEBUG_D_CONDITION ( 5 <= g_is_MY_debug_level) #define MY_print_ln_E( fmt, arg...) { if (MY_DEBUG_E_CONDITION) { syslog(LOG_ERR, "fthsm|||-> Time(%s), (%s|%d), "fmt"\r\n", getCurrentTime(), __func__, __LINE__, ##arg); } } #define MY_print_ln_W( fmt, arg...) { if (MY_DEBUG_W_CONDITION) { syslog(LOG_WARNING, "fthsm|||-> Time(%s), (%s|%d), "fmt"\r\n", getCurrentTime(), __func__, __LINE__, ##arg); } } #define MY_print_ln_L( fmt, arg...) { if (MY_DEBUG_L_CONDITION) { syslog(LOG_NOTICE, "fthsm|||-> Time(%s), (%s|%d), "fmt"\r\n", getCurrentTime(), __func__, __LINE__, ##arg); } } #define MY_print_ln_I( fmt, arg...) { if (MY_DEBUG_I_CONDITION) { syslog(LOG_INFO, "fthsm|||-> Time(%s), (%s|%d), "fmt"\r\n", getCurrentTime(), __func__, __LINE__, ##arg); } } #define MY_print_ln_D( fmt, arg...) { if (MY_DEBUG_D_CONDITION) { syslog(LOG_DEBUG, "fthsm|||-> Time(%s), (%s|%d), "fmt"\r\n", getCurrentTime(), __func__, __LINE__, ##arg); } }
// my_pam.main
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <pwd.h> #include <sys/stat.h> #include <fcntl.h> #include <dlfcn.h> #include <iconv.h> #include <security/pam_modules.h> #include <security/pam_ext.h> #include <dlfcn.h> /************************************* 日志查看方式: tail -f var/log/secure 默认使用配置: /etc/rsyslog.conf中的这个配置 authpriv.* var/log/secure *************************************/ #include "midcard_main.h" #include "pam_log.h" #define LIB_SECURITY_PATH "/lib64/security" typedef int (*PAMAuthFunc)(pam_handle_t *, int, int, const char **); #if 1 /* * 调用pam_unix.so.orig中的某个导出函数 */ static int call_pam_unix_foo(IN pam_handle_t *pamh, IN int flags, IN int argc, IN const char **argv, IN const char *foo_name) { //MY_print_ln_I("3333333333333333"); void *handler = dlopen(LIB_SECURITY_PATH"/pam_unix.so", RTLD_LAZY); // 如果将pam_unix.so重命名为pam_unix.so.orig则pam_sm_setcred会返回失败 if (!handler) { MY_print_ln_E("Failed to open the pam_unix.so"); return PAM_AUTH_ERR; } PAMAuthFunc auth_func = (PAMAuthFunc)dlsym(handler, foo_name); if (!auth_func) { MY_print_ln_E("There is no \"%s\" in symbol table.", foo_name); dlclose(handler); return PAM_AUTH_ERR; } int ret = auth_func(pamh, flags, argc, argv); /* BUGGY HERE!!! 如果这里将句柄释放掉,在某些机器上使用原有的pam_unix.so进行 * 验证时会出现段错误,导致界面无法登入。 * dlclose(handler); */ //MY_print_ln_I("ret=%d[PAM_SUCCESS=%d, PAM_AUTH_ERR=%d, PAM_CONV_ERR=%d]", ret, PAM_SUCCESS, PAM_AUTH_ERR, PAM_CONV_ERR); return ret; } /* * 使用系统原有的登录验证机制,即走pam_unix.so中的验证 * * 参数: * 同pam_sm_authenticate()的参数 * 返回值: * pam_unix.so中pam_sm_authenticate()函数的返回值。 */ static int pam_unix_authenticate(IN pam_handle_t *pamh, IN int flags, IN int argc, IN const char **argv) { return call_pam_unix_foo(pamh, flags, argc, argv, "pam_sm_authenticate"); } #endif #if 1 /*********************************** * 获取用户输入的数据 * * 使用此接口的前提是sshd启用键盘交互模式, (即使Keyboard Interactive单选按钮生效) * 请修改配置为"/etc/ssh/sshd_config里ChallengeResponseAuthentication yes" * * pamh: pam句柄 * keep_ms: 执行此操作后延时的ms, 不延时请传入0 * msg_style: PAM_PROMPT_ECHO_ON获取用户输入的用户名, * PAM_PROMPT_ECHO_OFF获取用户输入的密码 * hint_msg_str: 提示信息 * resp_str: 输出参数, 输出待读取的数据 * return : PAM_SUCCESS获取成功, PAM_AUTH_ERR获取失败 * * /usr/include/security/_pam_types.h * #define PAM_PROMPT_ECHO_OFF 1 * #define PAM_PROMPT_ECHO_ON 2 * #define PAM_ERROR_MSG 3 * #define PAM_TEXT_INFO 4 **********************************/ static int MY_hint(IN pam_handle_t *pamh, IN unsigned int keep_ms, IN int msg_style, IN const char *hint_msg_str, OUT const char **resp_str) { struct pam_message hint_msg = { .msg_style = msg_style, .msg = (NULL == hint_msg_str) ? "" : hint_msg_str, }; struct pam_conv *hint_conv = NULL; const struct pam_message *hint_msg_ptr = &hint_msg; struct pam_response *hint_resp = NULL; int ret = PAM_AUTH_ERR; // 更多参数请参考man page if (pam_get_item(pamh, PAM_CONV, (const void **)&hint_conv) != PAM_SUCCESS) { MY_print_ln_E("无法获取权限验证会话"); return PAM_AUTH_ERR; } MY_print_ln_I("msg_style=%d, hint=%s", msg_style, hint_msg_str); ret = (*hint_conv->conv)(1, &hint_msg_ptr, &hint_resp, hint_conv->appdata_ptr); if (PAM_SUCCESS != ret || NULL == hint_resp) { MY_print_ln_E("conversation_function ret Failed. ret=%d, resp=%s", ret, hint_resp->resp); return ret; } /* 请调用者释放 if (hint_resp->resp) { free(hint_resp->resp); hint_resp->resp = NULL; } */ if (NULL != resp_str){ *resp_str = hint_resp->resp; } free(hint_resp); hint_resp = NULL; if (0 < keep_ms) { pam_fail_delay(pamh, keep_ms * 1000); /* 使错误提示信息保持一定时间(第2个参数单位为微秒)(1秒==1000000微秒) */ } return PAM_SUCCESS; } /********************************** * 使用键盘交互模式验证卡 * * 键盘交互模式: * 解释: xshell有键盘交互模式, 可以边给出自定义提示信息, 边输入密码。 * 0. 默认xshell的Keyboard Interactive单选按钮无效, 需要修改sshd_config文件, 使此按钮生效 * 1. 修改方式: 请修改配置为"/etc/ssh/sshd_config里ChallengeResponseAuthentication yes" * 2. 同时关闭密码交互方式: 请修改配置为"/etc/ssh/sshd_config里PasswordAuthentication no" * 3. 同时关闭公钥交互方式: 请修改配置为"/etc/ssh/sshd_config里PubkeyAuthentication no" */ static int MYVerify(IN pam_handle_t *pamh) { const char *username = NULL; const char *password = NULL; int ret_user = PAM_AUTH_ERR; int ret_password = PAM_AUTH_ERR; int ret = -1; int index = 0; char username_in_card[128] = {'\0'}; char hint_buff[256] = {'\0'}; char err_hint_buff[256] = {'\0'}; static char *err_hint = ""; char *service_name = NULL; int MaxAuthTries = 1; // 1. 读取用户输入的用户名 ret_user = pam_get_user(pamh, &username, "Card Username:"); // username不需要释放 MY_print_ln_I("#######################user=%s, ret=%d", username, ret_user); if (PAM_SUCCESS != ret_user || NULL == username || 0 == strlen(username)) { MY_print_ln_E("无法获取用户输入的用户名, ret=%d", ret_user); return PAM_AUTH_ERR; } // 2. 读取服务名称 ret = pam_get_item(pamh, PAM_SERVICE, (const void **)&service_name); if (PAM_SUCCESS != ret || NULL == service_name) { MY_print_ln_E("无法获取服务名称"); return PAM_AUTH_ERR; } MY_print_ln_I("#######################user=%s, service_name=%s", username, service_name); // 3. 读取用户输入的密码 // sshd服务的sshpam_thread_cleanup线程结束方式: // 1. 获取用户输入密码, 如果点击取消 // 2. 用户长时间未鉴权成功, sshd主动关闭链接, 默认2分钟, 由sshd_config配置文件LoginGraceTime字段决定 // sshd无限循环即可: 可以不断的给出提示, 如果用户需要切换用户名, 点击取消即可 MaxAuthTries = (0 == strcmp("sshd", service_name)) ? 0x7fffffff : 1; for (index = 0; index < MaxAuthTries; index++) { memset((void *)hint_buff, 0, sizeof(hint_buff)); snprintf(hint_buff, sizeof(hint_buff) - 1, "###Please plugin MYCard for %s###\r\n%sUserCard PIN:", username, err_hint); // The pam_get_authtok function returns the cached authentication token, or prompts the user if no token is currently cached. // ret_password = pam_get_authtok(pamh, PAM_AUTHTOK, &password, "UserKey PIN:"); // 此函数会缓存: 与需求不符 // ret_password = MY_hint(pamh, 0, PAM_PROMPT_ECHO_OFF, hint_buff, &password); // ok1, password需要释放 ret_password = pam_prompt(pamh,PAM_PROMPT_ECHO_OFF, (char **)&password,"%s", hint_buff); // ok2, password需要释放 if (PAM_SUCCESS != ret_password || NULL == password) { MY_print_ln_E("无法获取用户输入的PIN码, ret=%d", ret_password); return PAM_AUTH_ERR; } // TODO: please close debug level (modify g_is_MY_debug_level in pam_log.h)!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! MY_print_ln_D("user=%s,pwd=%s\n", username, password); MY_COMMON_print_byte("password", (unsigned char *)password, strlen(password)); // 4. 读取卡里存储的用户名 ret = get_username_from_card(password, strlen(password), username_in_card, sizeof(username_in_card)); free((char *)password); password = NULL; if (0 == ret) { int username_cmp_ret = strcmp(username, username_in_card); if (0 == username_cmp_ret) { MY_print_ln_I("登陆成功"); return PAM_SUCCESS; } else { memset((void *)err_hint_buff, 0, sizeof(err_hint_buff)); snprintf(err_hint_buff, sizeof(err_hint_buff) - 1, "###Current Card is not for %s###\r\n", username); err_hint = err_hint_buff; MY_print_ln_I("卡的用户名与输入的用户名不一致:%s", err_hint); } } else { memset((void *)err_hint_buff, 0, sizeof(err_hint_buff)); snprintf(err_hint_buff, sizeof(err_hint_buff) - 1, "###%s###\r\n", get_strerr(ret)); err_hint = err_hint_buff; MY_print_ln_I("%s", err_hint); } } // end for MY_print_ln_I("########### MYVerify exit ##########"); return PAM_AUTH_ERR; } #endif PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) { MY_print_ln_I("#############################pam_sm_authenticate in"); return MYVerify(pamh); } PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) { MY_print_ln_I("#############################pam_sm_setcred in"); return call_pam_unix_foo(pamh, flags, argc, argv, "pam_sm_setcred"); } PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv) { MY_print_ln_I("#############################pam_sm_acct_mgmt in"); return call_pam_unix_foo(pamh, flags, argc, argv, "pam_sm_acct_mgmt"); } PAM_EXTERN int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, const char **argv) { MY_print_ln_I("#############################pam_sm_open_session in"); return call_pam_unix_foo(pamh, flags, argc, argv, "pam_sm_open_session"); } PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, const char **argv) { MY_print_ln_I("#############################pam_sm_close_session in"); return call_pam_unix_foo(pamh, flags, argc, argv, "pam_sm_close_session"); } PAM_EXTERN int pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, const char **argv) { MY_print_ln_I("#############################pam_sm_chauthtok in"); return call_pam_unix_foo(pamh, flags, argc, argv, "pam_sm_chauthtok"); } /* * 该结构为, 程序被编译成静态模块所需的一个模块数据结构 */ #ifdef PAM_STATIC struct pam_module _pam_pwdb_modstruct={ "pam_fthsm", pam_sm_authenticate, pam_sm_setcred, pam_sm_acct_mgmt, pam_sm_open_session, pam_sm_close_session, pam_sm_chauthtok }; #endif
// Makefile MIDCARD_DIR=./midcard srclist=pam_my.c \ pam_log.c \ all:pam_my.so pam_my.so: $(srclist) gcc -I. -I$(MIDCARD_DIR) -I/usr/include/ -I/usr/include/PCSC/ -lpcsclite -lcrypto $^ -fPIC -shared -o $@ strip $@ clean: rm -f *.so midcard/*.o copy: cp -f pam_fthsm.so /lib64/security/
/etc/ssh/sshd_config的关键配置
Port 22 Subsystem sftp internal-sftp AllowUsers root test PubkeyAuthentication no PasswordAuthentication no ChallengeResponseAuthentication yes UsePAM yes
/etc/pam.d/sshd 的关键配置 #%PAM-1.0 #auth required pam_sepermit.so #auth substack password-auth #auth include postlogin account required pam_nologin.so account include password-auth password include password-auth # pam_selinux.so close should be the first session rule session required pam_selinux.so close session required pam_loginuid.so # pam_selinux.so open should only be followed by sessions to be executed in the user context session required pam_selinux.so open env_params session optional pam_keyinit.so force revoke session include password-auth session include postlogin auth required pam_my.so