Introduction

今年5月份,Brad Spengler指出了Linux内核中的一个UAF漏洞. 受影响版本Linux 3.11-4.0.3,在Linux 4.0.4中被修复.

Vulnerability Analysis

在分析问题代码前,我们先看看内核是如何执行到path_openat().

当我们在用户态下调用open()/openat()系统调用时,Linux系统将产生如下过程:

open()/openat() --> [interrupt] --> sys_open()/sys_openat() --> do_sys_open() --> do_filp_open() --> path_openat()

从产生问题代码的问题文件(fs/namei.c)入手:

struct file *do_filp_open(int dfd, struct filename *pathname,
		                const struct open_flags *op)
{
	struct nameidata nd;
	int flags = op->lookup_flags;
	struct file *filp;

	filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_RCU);
	if (unlikely(filp == ERR_PTR(-ECHILD)))
		filp = path_openat(dfd, pathname, &nd, op, flags);
	if (unlikely(filp == ERR_PTR(-ESTALE)))
		filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_REVAL);
	return filp;
}

do_filp_open()函数比较简单,它会解析文件路径并创建一个file结构返回给上层函数do_sys_open.一般情况下path_openat()会被执行两次,两种不同的path walk策略:rcu-walkref-walk.最后的LOOKUP_REVAL只有在NFS文件系统中才用到.

跟随着代码路径,我们来到了path_open()函数.嗯,就是这个函数出的问题.:)

static struct file *path_openat(int dfd, struct filename *pathname,
		struct nameidata *nd, const struct open_flags *op, int flags)
{
	struct file *file;
	struct path path;
	int opened = 0;
	int error;

	file = get_empty_filp();
	if (IS_ERR(file))
		return file;

	file->f_flags = op->open_flag;

	if (unlikely(file->f_flags & __O_TMPFILE)) {
		error = do_tmpfile(dfd, pathname, nd, flags, op, file, &opened);
		goto out;
	}

[...]
out:
	path_cleanup(nd);   // --> second called
[...]
	return file;
}

do_tmpfile()函数调用path_lookupat()执行了一次fput(nd->base),随即path_openat()又一次调用path_cleanup()函数,fput(nd->base)再次被执行.

static int do_tmpfile(int dfd, struct filename *pathname,
		struct nameidata *nd, int flags,
		const struct open_flags *op,
		struct file *file, int *opened)
{
[...]
	int error = path_lookupat(dfd, pathname->name,
			flags | LOOKUP_DIRECTORY, nd);
	if (unlikely(error))
		return error;
[...]
	return error;
}

紧接着,我们看一下path_cleanup()函数,它传入一个名为nd的参数,该参数为strcut nameidata *类型, nd变量在整个查找过程中充当中间变量,它既可以为当前查找输入数据,又可以保存本次查找的结果.

static void path_cleanup(struct nameidata *nd)
{
	if (nd->root.mnt && !(nd->flags & LOOKUP_ROOT)) {
		path_put(&nd->root);
		nd->root.mnt = NULL;
	}
	if (unlikely(nd->base))
		fput(nd->base);   // --> double fput()
}

path_lookupat()函数中调用path_init()初始化nd.

/* Returns 0 and nd will be valid on success; Retuns error, otherwise. */
static int path_lookupat(int dfd, const char *name,
		unsigned int flags, struct nameidata *nd)
{
	[...]
	err = path_init(dfd, name, flags, nd);  // --> set nd
	[...]
	path_cleanup(nd);   // --> first called
	return err;
}

path_init()用于设置路径搜索的起始位置,即设置nd变量.

static int path_init(int dfd, const char *name, unsigned int flags,
		struct nameidata *nd)
{
	int retval = 0;

	nd->last_type = LAST_ROOT; /* if there are only slashes... */
	nd->flags = flags | LOOKUP_JUMPED | LOOKUP_PARENT;
	nd->depth = 0;
	nd->base = NULL;
	if (flags & LOOKUP_ROOT) {
		[...]
	}

	nd->root.mnt = NULL;

	nd->m_seq = read_seqbegin(&mount_lock);
	if (*name=='/') {
		[...]
			set_root(nd)
		[...]
		nd->path = nd->root;
	} else if (dfd == AT_FDCWD) {
		[...]
			get_fs_pwd(current->fs, &nd->path);
		[...]
	} else {
		/* Caller must check execute permissions on the starting path component */
		struct fd f = fdget_raw(dfd);
		struct dentry *dentry;

		if (!f.file)
			return -EBADF;

		dentry = f.file->f_path.dentry;

		if (*name) {
			if (!d_can_lookup(dentry)) {
				fdput(f);
				return -ENOTDIR;
			}
		}

		nd->path = f.file->f_path;
		if (flags & LOOKUP_RCU) {
			if (f.flags & FDPUT_FPUT)
				nd->base = f.file;      // init nd->base
			nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
			rcu_read_lock();
		} else {
			path_get(&nd->path);
			fdput(f);
		}
	}

	nd->inode = nd->path.dentry->d_inode;
	if (!(flags & LOOKUP_RCU))
		goto done;
	if (likely(!read_seqcount_retry(&nd->path.dentry->d_seq, nd->seq)))
		goto done;
	if (!(nd->flags & LOOKUP_ROOT))
		nd->root.mnt = NULL;
	rcu_read_unlock();
	return -ECHILD;
done:
	current->total_link_count = 0;
	return link_path_walk(name, nd);
}
  1. 如果路径名为以/开头,表示绝对路径,并将起始路径指向进程的根目录;否则,表示是一个相对路径.
  2. 如果目录描述符(dfd)AT_FDCWD,表示相对路径,并将起始路径指向当前工作目录,即pwd.
  3. 如果目录描述符(dfd)不是AT_FDCWD,表示是由用户指定的相对路径,并将起始路径指向该路径.

在设置起始搜索路径,为相对路径时,open()函数在内核里默认是步骤2的操作,而openat()函数为步骤3的操作.

PoC & Exploit

Come soon…

Reference