Linux kernel module autoloading

by Michael S


Posted on July 23, 2019 at 2:29PM


Linux kernel module autoloading can often significantly increase the overall attack surface. Depending on the distribution configuration, certain outdated / often unused modules can be loaded by non-privileged users. From the attacker's perspective, module autoloading is obviously useful if the vulnerability is present in the module that can be loaded by a non-privileged user to trigger the bug. It can also be useful as an exploitation technique such as triggering module loading after overwriting the modprobe path with a path to your controlled binary.

The following article describes the module autoloading process implemented by the Linux kernel. One of the common uses of this process is network modules when certain protocol implementations are compiled as modules and requested from user space.

For example, DCCP (Datagram Congestion Control Protocol) implementation is generally compiled as a module on most mainstream distributions. When a socket is created in user space:

int sock_fd = socket(AF_INET, SOCK_DCCP, IPPROTO_DCCP);

the first argument represents the protocol family and the last argument is the actual protocol to be used under the chosen protocol family. The following execution path is triggered on socket creation:

int __sock_create(struct net *net, int family, int type, int protocol,
                         struct socket **res, int kern)
{
...
        if (family < 0 || family >= NPROTO)
                return -EAFNOSUPPORT;
        if (type < 0 || type >= SOCK_MAX)
                return -EINVAL;
...
#ifdef CONFIG_MODULES
...
        if (rcu_access_pointer(net_families[family]) == NULL)
                request_module("net-pf-%d", family);          [1]
#endif
...

        pf = rcu_dereference(net_families[family]);
        err = -EAFNOSUPPORT;
        if (!pf)
                goto out_release;
...
        err = pf->create(net, sock, protocol, kern);          [2]
        if (err < 0)
                goto out_module_put;

The family input argument for the example above is AF_INET. If the entire family is not present, the kernel attempts to autoload the module in [1]. request_moduleis a macro for the following function:

int __request_module(bool wait, const char *fmt, ...)
{
	va_list args;
	char module_name[MODULE_NAME_LEN];
	int ret;

	...
	if (!modprobe_path[0])
		return 0;

	va_start(args, fmt);
	ret = vsnprintf(module_name, MODULE_NAME_LEN, fmt, args);
	va_end(args);
	if (ret >= MODULE_NAME_LEN)
		return -ENAMETOOLONG;

	ret = security_kernel_module_request(module_name);
	if (ret)
		return ret;

	...

	trace_module_request(module_name, wait, _RET_IP_);

	ret = call_modprobe(module_name, wait ? UMH_WAIT_PROC : UMH_WAIT_EXEC);  [3]

	atomic_inc(&kmod_concurrent_max);
	wake_up(&kmod_wq);

	return ret;
}

In [3], the kernel executes call_modprobe which creates a user-space process with root privileges. modprobe_path is a global variable (char [256]) pointing to /sbin/modprobe by default (and can be set to an arbitrary path via sysctl kernel.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);              [4]
...

The argument to modprobe is net-pf-n where n is the numeric value for the address family (i.e. AF_INET in our example). /sbin/modprobe resolves the net-pf-n alias and loads the requested module. All these module aliases are listed in /lib/modules/`uname -r`/modules.alias.

# grep net-pf- /lib/modules/`uname -r`/modules.alias
...
alias net-pf-28 mpls_router
alias net-pf-26 llc2
alias net-pf-15 af_key
alias net-pf-4 ipx
alias net-pf-5 appletalk
alias net-pf-9 x25
alias net-pf-6 netrom
alias net-pf-11 rose
alias net-pf-3 ax25
alias net-pf-29 can
alias net-pf-31 bluetooth
alias net-pf-33 rxrpc
alias net-pf-41 kcm
alias net-pf-20 atm
alias net-pf-8 atm
...

And all protocol address families supported by the kernel are listed in include/linux/socket.h:

/* Supported address families. */
#define AF_UNSPEC       0
#define AF_UNIX         1       /* Unix domain sockets          */
#define AF_LOCAL        1       /* POSIX name for AF_UNIX       */
#define AF_INET         2       /* Internet IP Protocol         */
#define AF_AX25         3       /* Amateur Radio AX.25          */
#define AF_IPX          4       /* Novell IPX                   */
#define AF_APPLETALK    5       /* AppleTalk DDP                */
#define AF_NETROM       6       /* Amateur Radio NET/ROM        */
#define AF_BRIDGE       7       /* Multiprotocol bridge         */
#define AF_ATMPVC       8       /* ATM PVCs                     */
#define AF_X25          9       /* Reserved for X.25 project    */
#define AF_INET6        10      /* IP version 6                 */
#define AF_ROSE         11      /* Amateur Radio X.25 PLP       */
#define AF_DECnet       12      /* Reserved for DECnet project  */
#define AF_NETBEUI      13      /* Reserved for 802.2LLC project*/
#define AF_SECURITY     14      /* Security callback pseudo AF */
#define AF_KEY          15      /* PF_KEY key management API */
#define AF_NETLINK      16
...

Once the network family is loaded (if required), __sock_create() then executes create() in [2] (provided by the address family) function with protocol and socket type parameters passed to sys_socket(). For example, if AF_INET is specified (as in our example above), __sock_create will execute inet_create():

static int inet_create(struct net *net, struct socket *sock, int protocol,
                       int kern)
{
...
lookup_protocol:
        err = -ESOCKTNOSUPPORT;
        rcu_read_lock();
        list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {

                err = 0;
                /* Check the non-wild match. */
                if (protocol == answer->protocol) {
                        if (protocol != IPPROTO_IP)
                                break;
                } else {
                        /* Check for the two wild cases. */
                        if (IPPROTO_IP == protocol) {
                                protocol = answer->protocol;
                                break;
                        }
                        if (IPPROTO_IP == answer->protocol)
                                break;
                }
                err = -EPROTONOSUPPORT;
        }

        if (unlikely(err)) {
                if (try_loading_module < 2) {
                        rcu_read_unlock();
                        /*
                         * Be more specific, e.g. net-pf-2-proto-132-type-1
                         * (net-pf-PF_INET-proto-IPPROTO_SCTP-type-SOCK_STREAM)
                         */
                        if (++try_loading_module == 1)
                                request_module("net-pf-%d-proto-%d-type-%d",     [5]
                                               PF_INET, protocol, sock->type);
                        /*
                         * Fall back to generic, e.g. net-pf-2-proto-132
                         * (net-pf-PF_INET-proto-IPPROTO_SCTP)
                         */
                        else
                                request_module("net-pf-%d-proto-%d",             [6]
                                               PF_INET, protocol);
                        goto lookup_protocol;
                } else
                        goto out_rcu_unlock;
        }
...

This function iterates over the available protocols and if the match is found, attempts to autoload the module in [5] or [6].

Depending on the distribution's configuration, certain protocol families might be blacklisted - cannot be autoloaded by non-privileged users; privileged users can still load them manually. For example, on Ubuntu Bionic (18.04) the following protocols are blacklisted:

for p in `grep -v "^#" /etc/modprobe.d/blacklist-rare-network.conf | cut -d" " -f2`; do grep "$p " /lib/modules/`uname -r`/modules.alias | cut -d" " -f3; done
ax25
decnet
ieee802154_socket
netrom
rds
rose
x25

Most mainstream distributions blacklist these network protocols which significantly reduces the attack surface. For example, CVE-2019-1181 is given a CVSS base score of 8.1 yet the actual risk is almost non-existent! On most distributions this protocol is either not compiled into the kernel at all or compiled as a module and then blacklisted (see above).

Furthermore, some protocols implement capability checks (either within the current or init user/network namespaces). For example, the AF_PACKET network family requires CAP_SYS_RAW capability but depending on the distribution's configuration, this capability can be obtained by a non-privileged user through a namespace. Namespaces is another feature that drastically increases the kernel attack surface but we will leave this for another post.