目录
前言
在 从 manual 中学习 seccomp 技术 这篇文章中,我谈及了使用 seccomp 让用户无法加载内核模块的功能,在本文中尝试写个简单的 demo 来验证下可行性。
环境信息与依赖安装
系统版本:debian 11
内核版本:Linux debian 5.15.0-0.bpo.2-amd64
执行 sudo apt install libseccomp-dev libpam0g-dev
命令安装依赖库。
使用 libseccomp 库编写 demo
#define _DEFAULT_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <errno.h>
#include <sys/prctl.h>
#include <fcntl.h>
#include <seccomp.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stddef.h>
#define PAM_SM_SESSION
#include <security/pam_modules.h>
#include <security/pam_ext.h>
static int seccomp_filter_install(void)
{
int rc = 0;
scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_KILL);
if (ctx == NULL)
goto out;
rc = seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(init_module), 0);
if (rc < 0)
goto out;
rc = seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(finit_module), 0);
if (rc < 0)
goto out;
rc = seccomp_load(ctx);
if (rc < 0)
goto out;
out:
seccomp_release(ctx);
return -rc;
}
PAM_EXTERN int pam_seccomp_open_session(pam_handle_t *pamh,
int flags,
int argc,
const char **argv)
{
int ret;
ret = seccomp_filter_install();
return ret != 0 ? PAM_SESSION_ERR:PAM_SUCCESS;
}
PAM_EXTERN int pam_seccomp_close_session(pam_handle_t *pamh,
int flags,
int argc,
const char **argv)
{
return PAM_SUCCESS;
}
将上述文件保存为 seccomp.c,使用如下编译命令编译之:
gcc -fPIC -shared -lpam -lseccomp ./seccomp.c -o pam_seccomp.so
编译完成后将 so 权限修改为 0644,然后将文件拷贝到 lib/x86_64-linux-gnu/security/ 目录中,示例命令如下:
chmod 644 ./pam_seccomp.so
sudo cp ./pam_seccomp.so /lib/x86_64-linux-gnu/security/
此后修改 /etc/pam.d/sshd 文件的内容,在 pam_loginuid.so 配置项目前增加一条 session required pam_seccomp.so
配置项目(仅用于测试)。
测试发现无法生效,使用 strace 跟踪发现在加载 /lib/x86_64-linux-gnu/security/
位置的 pam_seccomp.so
后,还去加载了 /lib/security/
目录中的 pam_seccomp.so
文件(此目录不存在),而其它的 pam 模块并不会访问 /lib/security 目录。
创建相关目录并拷贝文件后重试仍旧不能生效。
提问
1. 难道是编译过程中有机关?
获取 deb 源码包,手动编译发现一个 pam 模块的编译命令如下:
make[3]: Entering directory '/tmp/pam-1.4.0/modules/pam_limits'
/bin/bash ../../libtool --tag=CC --mode=compile gcc -DHAVE_CONFIG_H -I. -I../.. -I../../libpam/include -I../../libpamc/include -DLIMITS_FILE_DIR=\"/etc/security/limits.d/*.conf\" -DLIMITS_FILE=\"/etc/security/limits.conf\" -W -Wall -Wbad-function-cast -Wcast-align -Wcast-qual -Wmissing-declarations -Wmissing-prototypes -Wpointer-arith -Wreturn-type -Wstrict-prototypes -Wwrite-strings -Winline -Wshadow -g -O2 -MT pam_limits.lo -MD -MP -MF .deps/pam_limits.Tpo -c -o pam_limits.lo pam_limits.c
libtool: compile: gcc -DHAVE_CONFIG_H -I. -I../.. -I../../libpam/include -I../../libpamc/include "-DLIMITS_FILE_DIR=\"/etc/security/limits.d/*.conf\"" -DLIMITS_FILE=\"/etc/security/limits.conf\" -W -Wall -Wbad-function-cast -Wcast-align -Wcast-qual -Wmissing-declarations -Wmissing-prototypes -Wpointer-arith -Wreturn-type -Wstrict-prototypes -Wwrite-strings -Winline -Wshadow -g -O2 -MT pam_limits.lo -MD -MP -MF .deps/pam_limits.Tpo -c pam_limits.c -fPIC -DPIC -o .libs/pam_limits.o
mv -f .deps/pam_limits.Tpo .deps/pam_limits.Plo
/bin/bash ../../libtool --tag=CC --mode=link gcc -I../../libpam/include -I../../libpamc/include -DLIMITS_FILE_DIR=\"/etc/security/limits.d/*.conf\" -DLIMITS_FILE=\"/etc/security/limits.conf\" -W -Wall -Wbad-function-cast -Wcast-align -Wcast-qual -Wmissing-declarations -Wmissing-prototypes -Wpointer-arith -Wreturn-type -Wstrict-prototypes -Wwrite-strings -Winline -Wshadow -g -O2 -no-undefined -avoid-version -module -Wl,--version-script=./../modules.map -Wl,--as-needed -Wl,--no-undefined -Wl,-O1 -o pam_limits.la -rpath /lib64/security pam_limits.lo ../../libpam/libpam.la
libtool: link: rm -fr .libs/pam_limits.la .libs/pam_limits.lai .libs/pam_limits.so
libtool: link: gcc -shared -fPIC -DPIC .libs/pam_limits.o -Wl,-rpath -Wl,/tmp/pam-1.4.0/libpam/.libs ../../libpam/.libs/libpam.so -g -O2 -Wl,--version-script=./../modules.map -Wl,--as-needed -Wl,--no-undefined -Wl,-O1 -Wl,-soname -Wl,pam_limits.so -o .libs/pam_limits.so
libtool: link: ( cd ".libs" && rm -f "pam_limits.la" && ln -s "../pam_limits.la" "pam_limits.la" )
能够看到编译命令还是有些复杂,但是也没有看到啥怀疑点。同时网上搜索 pam 模块的 demo,没有找到需要特殊编译参数的说明。 一通折腾后发现在 libpam0g-dev 包的安装目录中有一个模块的示例代码的 Makefile 文件,使用的编译命令如下:
cc -g -fPIC -I"../../include" -c -o pam_secret.o pam_secret.c
ld -x --shared -o pam_secret.so pam_secret.o -l
使用上述编译命令修改 demo 开始的编译命令,测试发现仍旧不生效。
2. 是否代码实现存在问题?
获取 pam deb 源码包,阅读 modules/pam_limits/pam_limits.c
文件确定关键函数如下:
- pam_sm_open_session
- pam_sm_close_session
与上文的代码对比,确认没有异常。
3. 是否执行问题?
demo 代码的核心函数为 seccomp_filter_install,我怀疑可能是这个函数的执行出现异常,于是单独将这个函数拷贝出来写了一个测试程序进行测试,strace 跟踪发现有如下错误信息:
seccomp(SECCOMP_SET_MODE_STRICT, 1, NULL) = -1 EINVAL (Invalid argument)
seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_TSYNC, NULL) = -1 EFAULT (Bad address)
seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_LOG, NULL) = -1 EFAULT (Bad address)
seccomp(SECCOMP_GET_ACTION_AVAIL, 0, [SECCOMP_RET_LOG]) = 0
seccomp(SECCOMP_GET_ACTION_AVAIL, 0, [SECCOMP_RET_KILL_PROCESS]) = 0
seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_SPEC_ALLOW, NULL) = -1 EFAULT (Bad address)
seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, NULL) = -1 EFAULT (Bad address)
seccomp(SECCOMP_GET_NOTIF_SIZES, 0, 0x7fff5f826ee2) = 0
seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_TSYNC_ESRCH, NULL) = -1 EFAULT (Bad address)
看上去确实执行失败了,可查阅 manual 并没有发现怀疑点,想到可以跳过这个 libseccomp 库,于是暂时放弃。
不使用 libseccomp 库编写 demo
源码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <errno.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>
#define PAM_SM_SESSION
#include <security/pam_modules.h>
#include <security/pam_ext.h>
#include <linux/audit.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/unistd.h>
#include <stddef.h>
#include <unistd.h>
static int install_filter(int nr, int arch, int error) {
struct sock_filter filter[] = {
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, arch))),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arch, 0, 3),
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, nr))),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1),
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (error & SECCOMP_RET_DATA)),
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
.filter = filter,
};
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) {
perror("prctl(PR_SET_SECCOMP)");
return 1;
}
return 0;
}
int disable_module_init(void) {
int ret = 0;
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
perror("prctl(NO_NEW_PRIVS)");
return 1;
}
ret = install_filter(__NR_finit_module, AUDIT_ARCH_X86_64, EPERM);
if (ret != 0) {
perror("install filter failed\n");
}
ret = install_filter(__NR_init_module, AUDIT_ARCH_X86_64, EPERM);
if (ret != 0) {
perror("install filter failed\n");
}
return ret;
}
PAM_EXTERN int pam_sm_open_session(pam_handle_t *pamh,
int flags,
int argc,
const char **argv)
{
int ret = 0;
ret = disable_module_init();
return ret == 0 ? PAM_SUCCESS:PAM_SESSION_ERR;
}
PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh,
int flags,
int argc,
const char **argv)
{
return PAM_SUCCESS;
}
按照上文的描述进行编译配置,并修改 sshd 配置允许 root 登录,然后重启 sshd 服务,测试通过。测试示例如下:
[longyu@debian]$ ssh root@localhost
root@localhost's password:
....................................
root@debian:~# modprobe uio
modprobe: ERROR: could not insert 'uio': Operation not permitted
root@debian:~# strace modprobe uio 2>&1 | grep finit_module
finit_module(3, "", 0) = -1 EPERM (Operation not permitted)
root@debian:~#
能够看到,使用 root 用户通过 ssh 登录后加载 uio 模块报无权限,strace 跟踪并过滤 finit_module 系统调用,确定其返回 EPERM 错误,与下发的过滤规则一致。
扩展思考
将 pam 与 seccomp 能够实现让用户无法加载内核模块的功能,但是使用 seccomp 带来的一个负面作用是子进程都被设定了 no_now_privs 标志,suid、Linux capabilities 等提权行为也会被禁止,会影响到用户正常的使用过程。