使用 pam module 与 seccomp 技术禁止用户加载内核模块

前言

从 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文件确定关键函数如下:

  1. pam_sm_open_session
  2. 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 等提权行为也会被禁止,会影响到用户正常的使用过程。

参考链接

pam_seccomp.c
Writing Your First PAM Module

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值