This MD describes how well-known modprobe_path technique works.
modprobe_path
overwriting technique is well-known technique to run your arbitrary shellscript as root privilege. This technique requires below conditions.
- You have AAW, to overwrite
modprobe_path
. - You can write arbitrary two files. One has mal-formed format such as
\xff\xff\xff\xff
. The other is a shellscript which do what you like as root. - You can execve the latter file of 2.
CONFIG_STATIC_USERMODEHELPER
is not enabled.
In the most cases, 2 and 3 are fulfilled in kernel-pwn challs. So virtually, the only condition is that you have AAW.
The sections below describes the principle of this tech.
When you type ./hoge
on your shell, it calls execve
systemcall.
SYSCALL_DEFINE3(execve,
const char __user *, filename,
const char __user *const __user *, argv,
const char __user *const __user *, envp)
{
return do_execve(getname(filename), argv, envp);
}
This calls do_execveat_common()
> bprm_execve()
> exec_binprm()
, then search_binary_handler()
. This func searches formats
for the appropriate loader function for the specified binary. formats
is just a list of struct linux_binfmt
.
pwndbg> delist formats
$22 = {
next = 0xffffffff8248d6e0 <script_format>,
prev = 0xffffffff824861c0 <formats>
}
$23 = {
next = 0xffffffff8248d720 <elf_format>,
prev = 0xffffffff8248d640 <misc_format>
}
$24 = {
next = 0xffffffff8248d760 <compat_elf_format>,
prev = 0xffffffff8248d6e0 <script_format>
}
$25 = {
next = 0xffffffff824861c0 <formats>,
prev = 0xffffffff8248d720 <elf_format>
}
$26 = {
next = 0xffffffff8248d640 <misc_format>,
prev = 0xffffffff8248d760 <compat_elf_format>
}
This list has four formats. elf_format
is a common format of ELF binary. compat_elf_format
seems to be identical with elf_format
. script_format
is for script files which begin with shebang(#!
). misc_format
is for misc binaries. I haven't checked it deeply, but binary types for misc can be registered separetely. Each formats have below structure. The most important member here is load_binary
.
pwndbg> p script_format
$27 = {
lh = {
next = 0xffffffff8248d720 <elf_format>,
prev = 0xffffffff8248d640 <misc_format>
},
module = 0x0,
load_binary = 0xffffffff81222e90 <load_script>,
load_shlib = 0x0,
core_dump = 0x0,
min_coredump = 0
}
pwndbg> p elf_format
$28 = {
lh = {
next = 0xffffffff8248d760 <compat_elf_format>,
prev = 0xffffffff8248d6e0 <script_format>
},
module = 0x0,
load_binary = 0xffffffff812236c0 <load_elf_binary>,
load_shlib = 0xffffffff81223210 <load_elf_library>,
core_dump = 0xffffffff81224d30 <elf_core_dump>,
min_coredump = 4096
}
pwndbg> p compat_elf_format
$29 = {
lh = {
next = 0xffffffff824861c0 <formats>,
prev = 0xffffffff8248d720 <elf_format>
},
module = 0x0,
load_binary = 0xffffffff812265b0 <load_elf_binary>,
load_shlib = 0xffffffff81226110 <load_elf_library>,
core_dump = 0xffffffff81227c60 <elf_core_dump>,
min_coredump = 4096
}
pwndbg> p misc_format
$30 = {
lh = {
next = 0xffffffff8248d6e0 <script_format>,
prev = 0xffffffff824861c0 <formats>
},
module = 0x0,
load_binary = 0xffffffff812223e0 <load_misc_binary>,
load_shlib = 0x0,
core_dump = 0x0,
min_coredump = 0
}
In search_binary_handler()
, formats
list is for-looped and each load_binary()
is called for the binary.
retry:
read_lock(&binfmt_lock);
list_for_each_entry(fmt, &formats, lh) {
if (!try_module_get(fmt->module))
continue;
read_unlock(&binfmt_lock);
retval = fmt->load_binary(bprm);
read_lock(&binfmt_lock);
put_binfmt(fmt);
if (bprm->point_of_no_return || (retval != -ENOEXEC)) {
read_unlock(&binfmt_lock);
return retval;
}
}
read_unlock(&binfmt_lock);
For example, load_binary
of elf_format
is load_elf_binary()
. It just checks the magic(\x7FELF
) for ELF header of the binary.
if (memcmp(elf_ex->e_ident, ELFMAG, SELFMAG) != 0)
goto out;
load_binary
of script_format
is load_script()
. It just checks shebang. If shebang is vaild, bprm->interpreter
is set to the specified interpreter and the original binary name becomes the one of argv, then re-search the appropriate handler in search_binary_handler()
.
if ((bprm->buf[0] != '#') || (bprm->buf[1] != '!'))
return -ENOEXEC;
If no handler is found for the specified binary format, it reaches the below path in search_binary_handler()
.
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;
}
This checks if the specified binary's first 4bytes are not printable(non-ASCIIs w/o tab or newline). Then, it calls request_module()
with the format name binfmt-<first 4bytes of the binary>
. It just calls __request_module()
, then call_modprobe()
.
static int call_modprobe(char *module_name, int wait)
{
struct subprocess_info *info;
static char *envp[] = {
"HOME=/",
"TERM=linux",
"PATH=/sbin:/usr/sbin:/bin:/usr/bin",
NULL
};
char **argv = kmalloc(sizeof(char *[5]), GFP_KERNEL);
if (!argv)
goto out;
module_name = kstrdup(module_name, GFP_KERNEL);
if (!module_name)
goto free_argv;
argv[0] = modprobe_path;
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;
return call_usermodehelper_exec(info, wait | UMH_KILLABLE);
free_module_name:
kfree(module_name);
free_argv:
kfree(argv);
out:
return -ENOMEM;
}
It tries to load the specified binary as a module. Default helper name is defined as char-array in kernel/kmod.c
as /sbin/modprobe
. It is run as root priviledge. And It is not const.
char modprobe_path[KMOD_PATH_LEN] = "/sbin/modprobe";
So you can overwrite it with your evil shellscript to get shell or to cat a flag.
In call_usermodehelper_setup()
, sub_info->path
is set as below.
#ifdef CONFIG_STATIC_USERMODEHELPER
sub_info->path = CONFIG_STATIC_USERMODEHELPER_PATH;
#else
sub_info->path = path;
#endif
Here, path
is modprobe_path
. If CONFIG_STATIC_USERMODEHELPER
is defined, usermode helper is set to CONFIG_STATIC_USERMODEHELPER_PATH
, which is defined as a constant value on build-time. Therefore, overwriting modprobe_path
is useless when this config is enabled.
This patch is introduced int below commit for mitigation of this kind of exploit.
https://lore.kernel.org/lkml/[email protected]/