0x0 前言
PAM (Pluggable Authentication Modules) 系统的一部分,它是一个用于 Linux 系统认证的模块化框架。
如果给 ssh 登录添加 TOTP
验证一般会用到 google-authenticator
模块,这个模块就是实现和使用 PAM API 实现的。
PAM 不光可以在 ssh 登录时使用在其他大多数需要验证的时候也可以使用,甚至可以在自己程序中调用,总之 PAM 是一个非常好用的系统认证框架。
本篇文章将使用 Go 实现相关功能,而 PAM 的接口是用 C 语言实现的。因此将会使用 Go 的 CGO
进行编译,并且会尽量减少 C 语言的部分。
0x1 准备
因为 PAM 是 Linux 系统的认证框架,所以在 Linux 上开发最方便
首先安装依赖库:
Debian/Ubuntu
apt install libpam0g-dev
Centos
yum install pam-devel
安装后 include 文件在 /usr/include/security/
目录下
0x2 开发 PAM 认证模块
首先在 Go 中引入 C 的库, Go 是在 import "C"
上方使用注释的方式引入 C 的内容
注意 import "C"
上方不能有空行
#cgo LDFLAGS: -lpam
是 CGO
的命令,LDFLAGS: -lpam
是表示链接 PAM 静态库
typedef const char cchar_t;
是将 cchar_t
自定义为 const char
关键字,因为后面讲 C 转写成 GO 的时候 const xx
无法转写,因此需要提前定义一个关键字
#include "pam_prompt_wrapper.h"
是引入一个本地自定义的库,后面会后详解
/*
#cgo LDFLAGS: -lpam
#include <stdio.h>
#include <stdlib.h>
#include <security/pam_appl.h>
#include <security/pam_modules.h>
#include <security/pam_ext.h>
#include "pam_prompt_wrapper.h"
typedef const char cchar_t;
*/
import "C"
函数说明
pam_sm_authenticate
是认证逻辑的接口函数,也是认证的核心,函数定义在pam_modules.h
里
接口函数是需要开发人员自己实现的函数,在函数内可以实现各种自定义的功能
函数签名为
int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv);
使用 Go 实现,因为这个函数是要被外部调用的,因此需要在上方添加 //export
的注释,CGO
转写成 C 的时候会转成 extern
关键字
//export pam_sm_authenticate
func pam_sm_authenticate(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C.cchar_t) C.int {
return C.PAM_SUCCESS
}
pam_sm_acct_mgmt
是用户识别管理接口函数,用户判断用户否有权限,ssh登录中需要,函数定义也在pam_modules.h
里,函数签名为
int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv);
使用 Go 实现
//export pam_sm_acct_mgmt
func pam_sm_acct_mgmt(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C.cchar_t) C.int {
return C.PAM_SUCCESS
}
pam_prompt
是认证过程中用于交互的函数,改函数直接调用,函数定义在 pam_ext.h
中,函数签名为
extern int PAM_FORMAT((printf, 4, 5)) PAM_NONNULL((1,4)) pam_prompt (pam_handle_t *pamh, int style, char **response, const char *fmt, ...);
注意 该函数中带有 ...
表示可以接受可变参数,而 CGO
中则不支持调用可变参数函数。因此需要使用 C 将该函数简单包装成一个固定参数的函数
int pam_prompt_wrapper (pam_handle_t *pamh, int style, char **response,const char *str) { return pam_prompt(pamh, style, response, "%s", str) }
并且该函数要额外使用 C 文件存储,并在 Go 中引入
代码文件
pam_prompt_wrapper.h
#include <security/pam_appl.h> int pam_prompt_wrapper(pam_handle_t *pamh, int style, char **response, const char *str);
pam_prompt_wrapper.c
#include <security/pam_appl.h> #include <security/pam_ext.h> #include <security/pam_modules.h> #include <stdarg.h> int pam_prompt_wrapper(pam_handle_t *pamh, int style, char **response, const char *str) { return pam_prompt(pamh, style, response, "%s", fmt); }
main.go
package main
/*
#cgo LDFLAGS: -lpam
#include <stdio.h>
#include <stdlib.h>
#include <security/pam_appl.h>
#include <security/pam_modules.h>
#include <security/pam_ext.h>
#include "pam_prompt_wrapper.h"
typedef const char cchar_t;
*/
import "C"
import (
"fmt"
"os"
"unsafe"
)
//export pam_sm_authenticate
func pam_sm_authenticate(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C.cchar_t) C.int {
// 实现认证功能
// 例如:
// 认证时会显示 input >
// 输入 123 后认证成功,否则失败
a, _ := qa(pamh, false, "input >")
if a == "123" {
return C.PAM_SUCCESS
}
return C.PAM_AUTH_ERR
}
//export pam_sm_acct_mgmt
func pam_sm_acct_mgmt(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C.cchar_t) C.int {
// 这里直接返回成功,也可以根据自己需求添加更多功能
return C.PAM_SUCCESS
}
// 简单封装了交互函数
func qa(pamh *C.pam_handle_t, echo bool, query string) (string, C.int) {
var f C.int
f = C.PAM_PROMPT_ECHO_OFF
if echo {
f = C.PAM_PROMPT_ECHO_ON
}
input := C.CString("")
defer C.free(unsafe.Pointer(input))
prompt := C.CString(query)
defer C.free(unsafe.Pointer(prompt))
code := C.pam_prompt_wrapper(pamh, f, &input, prompt)
return C.GoString(input), code
}
func main() {
}
编译
需要将 Go 编译成 C 的共享库
go build -buildmode=c-shared -o pam_example.so
0x3 使用
需要将模块放置到指定目录
sudo cp pam_example.so /lib/x86_64-linux-gnu/security
然后启动,这里展示在 ssh 登录验证中使用我们的模块
首先编辑 /etc/ssh/sshd_config
,修改 ChallengeResponseAuthentication
或 KbdInteractiveAuthentication
选项的值将其设为 yes
重启 sshd
服务 sudo service sshd restart
编辑 /etc/pam.d/sshd
,在文件添加 auth required pam_example.so
9 comments
哈哈哈,写的太好了
請問博主, 我照您的範例設定完後想測試連線時都會失敗, 查看 service ssh status 時會看到
: fatal: PAM: pam_setcred(): Module is unknown
: PAM unable to resolve symbol: pam_sm_setcred
等錯誤訊息, 上網查不太到原因...請問有遇過嗎?
尝试去实现pam_sm_setcred函数试试
//export pam_sm_setcred
func pam_sm_setcred(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C.char) C.int {
// 将C字符串数组转换为Go字符串切片
goArgv := make([]string, int(argc))
for i := 0; i < int(argc); i++ {
goArgv[i] = C.GoString(C.char_at(argv, C.int(i)))
}
// 打印调试信息
log.Printf("pam_sm_setcred called with flags: %d, argc: %d, argv: %v\n", flags, argc, goArgv)
// 实现你的逻辑
if flags&C.PAM_ESTABLISH_CRED != 0 {
log.Println("Setting credentials")
// 实现设置凭证的逻辑
} else if flags&C.PAM_DELETE_CRED != 0 {
log.Println("Deleting credentials")
// 实现删除凭证的逻辑
}
return C.PAM_SUCCESS
}
謝謝博主,這個問題我已經透過實作pam_sm_setcred解決了,但現在卡在另一個問題,我用終端機都可以正常進入到input:123的環節後正常登入,但是用MobaXterm或PuTTY都會在輸入帳密後卡住,不顯示input。
神奇的是如果我故意把密碼打錯,然後在嘗試登入就會成功正常顯示input:並且可以正常輸入123後登入,google了很久不知道為什麼用MobaXterm或PuTTY就會有這種問題...
pam_prompt_wrapper.c中最后应该是str而非fmt吧
请教一下博主,main.go的作用是什么呢,编译的so库里面似乎没用到这个文件
main.go是Go的文件,也是这篇使用Go开发编译实现PAM的主文件,go build 命令就是编译该目录下的*.go文件
哦,我最开始以为main.go是测试代码,先入为主了
hhh