Loading... # 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 ```sh apt install libpam0g-dev ``` Centos ```sh 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"` 是引入一个本地自定义的库,后面会后详解 ```golang /* #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`里 *接口函数是需要开发人员自己实现的函数,在函数内可以实现各种自定义的功能* 函数签名为 ```c_cpp int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv); ``` 使用 Go 实现,因为这个函数是要被外部调用的,因此需要在上方添加 `//export` 的注释,`CGO` 转写成 C 的时候会转成 `extern` 关键字 ```golang //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`里,函数签名为 ```c_cpp int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv); ``` 使用 Go 实现 ```golang //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` 中,函数签名为 ```c_cpp 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 将该函数简单包装成一个固定参数的函数 ```c_cpp 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 ```c_cpp #include <security/pam_appl.h> int pam_prompt_wrapper(pam_handle_t *pamh, int style, char **response, const char *str); ``` pam_prompt_wrapper.c ```c_cpp #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 ```golang 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 的共享库 ```sh go build -buildmode=c-shared -o pam_example.so ``` # 0x3 使用 需要将模块放置到指定目录 ```sh 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` Last modification:October 20, 2023 © Allow specification reprint Support Appreciate the author AliPayWeChat Like 0 如果觉得我的文章对你有用,请随意赞赏
8 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