CVE-2019-2025

本文解析了一种针对Android 8.0内核的漏洞,涉及Binder驱动程序的内存管理,特别是binder_transaction函数中的竞态条件。通过精心构造的IPC操作,攻击者能导致未授权的内存释放,进而触发UAF并利用漏洞获取root权限。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

CVE-2019-2025

复现环境: Android 8.0 Linux kernel:Linux localhost 4.4.124+ #1 SMP

前提知识

binder是c/s结构 有services client 线程池 还有传输的数据

讲解binder相关的数据结构里面的主要成员,binder的相关函数以及binder的内核缓存区管理

相关数据结构

struct binder_proc {
   //每个进程调用open()打开binder驱动都会创建该结构体,用于管理IPC所需的各种信息
	struct hlist_node proc_node;
	struct rb_root threads;
	struct rb_root nodes;
	struct rb_root refs_by_desc;
	struct rb_root refs_by_node;
	struct list_head waiting_threads;
	int pid;
struct	struct task_struct *tsk;
	struct hlist_node deferred_work_node;
	int deferred_work;
	bool is_dead;

	struct list_head todo;
	struct binder_stats stats;
	struct list_head delivered_death;
	int max_threads;
	int requested_threads;
	int requested_threads_started;
	int tmp_ref;
	struct binder_priority default_priority;
	struct dentry *debugfs_entry;
	struct binder_alloc alloc;
	struct binder_context *context;
	spinlock_t inner_lock;
	spinlock_t outer_lock;
};

. threads : binder_thread红黑树的根节点 用于管理binder线程池的红黑树

. nodes : binder_node红黑树的根节点 用于管理binder实体对象(services组件)的红黑树

. refs_by_desc,refs_by_node: 都是binder_ref(client组件)红黑树的根节点,主要是key不同, 用于管理binder引用对象(client组件)的红黑树

. alloc : binder proc的地址空间(!!!),这个结构体极为重要,是漏洞实现的关键

struct binder_alloc {
   
	struct mutex mutex;
	struct vm_area_struct *vma;
	struct mm_struct *vma_vm_mm;
	void *buffer;
	ptrdiff_t user_buffer_offset;
	struct list_head buffers;
	struct rb_root free_buffers;
	struct rb_root allocated_buffers;
	size_t free_async_space;
	struct binder_lru_page *pages;
	size_t buffer_size;
	uint32_t buffer_free;
	int pid;
	size_t pages_high;
};

. buffer : 指向一块大的内核缓存区,Binder驱动程序为了方便它进行管理,会将它划分成若干个小块,这些小块的内核缓冲区使用结构体binder_buffer来描述

. buffers:list_head是一个双向链表 ,buffers是双向链表的头部 用来管理buffer里面的binder_buffer ,将 binder_buffer保存在这个列表中,按照地址值从小到大的顺序来排列

. buffer_size : 内核缓存区的大小

. buffer_free: 空闲内核缓存区的大小

. vma : 内核缓存区有两个地址,一个是内核空间地址,另一个是用户空间地址,内核地址是成员buffer,用户地址保存在vma

. user_buffer_offset : 内核缓存区两个地址的差值,也就是内核空间地址和用户空间地址的差值,是个固定的值

. pages : 内核缓存区的物理地址

. free_buffers : 保存空闲的,还没被分配物理页面的小块内核缓存区的红黑树

. allocated_buffers : 保存正在使用的,即已经分配物理页面的小块内核缓存区的红黑树

struct binder_buffer {
   
	struct list_head entry; /* free and allocated entries by address */
	struct rb_node rb_node; /* free entry by size or allocated entry */
				/* by address */
	unsigned free:1;
	unsigned allow_user_free:1;
	unsigned async_transaction:1;
	unsigned free_in_progress:1;
	unsigned debug_id:28;

	struct binder_transaction *transaction;

	struct binder_node *target_node;
	size_t data_size;
	size_t offsets_size;
	size_t extra_buffers_size;
	void *data;
};

位置用来描述一个内核内核缓存区,用来在进程间传输数据的

. entry : 作为在binder_alloc中free_buffers和allocated_buffers的节点

. free : 表示该内核缓存区是空闲的。

. allow_user_free : 如果为1,那么service组件会请求Binder驱动程序施放该内核缓存区

. transaction : 表示一个内核缓存区交的是哪一个事务

. target_node : 哪一个Binder实体对象使用

. data : 指向一块大小可变的数据缓存区,用来保存通信数据的,里面保存的数据分为两种类型,其中一种是普通数据,另一种是Binder对象,在数据的后面还跟着一个偏移数组,用来记录数据缓存区中每一个Binder对象在数据缓存区中的位置

. data_size : 前面数据缓存区的大小

. offsets_size : 后面偏移数组的大小

binder的相关函数

binder_open
static int binder_open(struct inode *nodp, struct file *filp)
{
   
	struct binder_proc *proc;
	struct binder_device *binder_dev;

	binder_debug(BINDER_DEBUG_OPEN_CLOSE, "binder_open: %d:%d\n",
		     current->group_leader->pid, current->pid);

	proc = kzalloc(sizeof(*proc), GFP_KERNEL); //为binder_proc结构体在分配kernel内存空间
	if (proc == NULL)
		return -ENOMEM;
	spin_lock_init(&proc->inner_lock);
	spin_lock_init(&proc->outer_lock);
	get_task_struct(current->group_leader);
	proc->tsk = current->group_leader; //将当前线程的task保存到binder进程的tsk
	INIT_LIST_HEAD(&proc->todo);//初始化todo列表
	if (binder_supported_policy(current->policy)) {
   //将当前进程的nice值转换为进程优先级
		proc->default_priority.sched_policy = current->policy;
		proc->default_priority.prio = current->normal_prio;
	} else {
   
		proc->default_priority.sched_policy = SCHED_NORMAL;
		proc->default_priority.prio = NICE_TO_PRIO(0);
	}

	binder_dev = container_of(filp->private_data, struct binder_device,
				  miscdev);
	proc->context = &binder_dev->context;
	binder_alloc_init(&proc->alloc); //初始化binder_alloc

	binder_stats_created(BINDER_STAT_PROC);//BINDER_PROC对象创建数加1
	proc->pid = current->group_leader->pid;//初始化pid
	INIT_LIST_HEAD(&proc->delivered_death);//初始化已分发的死亡通知列表
	INIT_LIST_HEAD(&proc->waiting_threads);
	filp->private_data = proc;//file文件指针的private_data变量指向binder_proc数据

	mutex_lock(&binder_procs_lock);
	hlist_add_head(&proc->proc_node, &binder_procs);//将proc_node节点添加到binder_procs为表头的队列
	mutex_unlock(&binder_procs_lock);

	if (binder_debugfs_dir_entry_proc) {
   
		char strbuf[11];

		snprintf(strbuf, sizeof(strbuf), "%u", proc->pid);
		/*
		 * proc debug entries are shared between contexts, so
		 * this will fail if the process tries to open the driver
		 * again with a different context. The priting code will
		 * anyway print all contexts that a given PID has, so this
		 * is not a problem.
		 */
		proc->debugfs_entry = debugfs_create_file(strbuf, S_IRUGO,
			binder_debugfs_dir_entry_proc,
			(void *)(unsigned long)proc->pid,
			&binder_proc_fops);
	}

	return 0;
}

创建binder_proc对象,并把当前进程等信息保存到binder_proc对象,然后初始化,该对象管理IPC所需的各种信息并拥有其他结构体的根结构体;再把binder_proc对象保存到文件指针filp,以及把binder_proc加入到全局链表binder_procs

binder_update_page_range
static int binder_update_page_range(struct binder_alloc *alloc, int allocate,
				    void *start, void *end)
{
   
	void *page_addr;
	unsigned long user_page_addr;
	struct binder_lru_page *page;
	struct vm_area_struct *vma = NULL;
	struct mm_struct *mm = NULL;
	bool need_mm = false;

	if (end <= start)//开头 > 结尾 返回
		return 0;

	trace_binder_update_page_range(alloc, allocate, start, end);

	if (allocate == 0)//此处allocate为1,代表分配过程。如果为0则代表释放过程
		goto free_range;

	for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {
   
		page = &alloc->pages[(page_addr - alloc->buffer) / PAGE_SIZE];  //分配一个page的物理内存
		if (!page->page_ptr) {
   
			need_mm = true;
			break;
		}
	}

	[...]
		page->page_ptr = alloc_page(GFP_KERNEL |
					    __GFP_HIGHMEM |
					    __GFP_ZERO);
		if (!page->page_ptr) {
   
			pr_err("%d: binder_alloc_buf failed for page at %pK\n",
				alloc->pid, page_addr);
			goto err_alloc_page_failed;
		}
		page->alloc = alloc;
		INIT_LIST_HEAD(&page->lru);

		ret = map_kernel_range_noflush((unsigned long)page_addr,
					       PAGE_SIZE, PAGE_KERNEL,
					       &page->page_ptr);//物理空间映射到虚拟内核空间
		flush_cache_vmap((unsigned long)page_addr,
				(unsigned long)page_addr + PAGE_SIZE);
		if (ret != 1) {
   
			pr_err("%d: binder_alloc_buf failed to map page at %pK in kernel\n",
			       alloc->pid, page_addr);
			goto err_map_kernel_failed;
		}
		user_page_addr =
			(uintptr_t)page_addr + alloc->user_buffer_offset;//虚拟用户空间
		ret = vm_insert_page(vma, user_page_addr, page[0].page_ptr);//物理空间映射到虚拟进程空间
		if (ret) {
   
			pr_err("%d: binder_alloc_buf failed to map page at %lx in userspace\n",
			       alloc->pid, user_page_addr);
			goto err_vm_insert_page_failed;
		}

		if (index + 1 > alloc->pages_high)
			alloc->pages_high = index + 1;

		trace_binder_alloc_page_end(alloc, index);
		/* vm_insert_page does not seem to increment the refcount */
	}
	if (mm) {
   
		up_write(&mm->mmap_sem);
		mmput(mm);
	}
	return 0;

free_range://释放内存,物理地址和内核虚拟地址,用户虚拟地址解除映射的过程
	[...]
}

工作如下:

在这里插入图片描述

binder_update_page_range主要完成工作:分配物理空间,将物理空间映射到内核空间,将物理空间映射到进程空间. 另外,不同参数下该方法也可以释放物理页面。

binder_ioctl

binder_ioctl()函数负责在两个进程间收发IPC数据和IPC reply数据。

ioctl(文件描述符,ioctl命令,数据类型)

(1) 文件描述符,是通过open()方法打开Binder Driver后返回值;

(2) ioctl命令和数据类型是一体的,不同的命令对应不同的数据类型

ioctl命令 数据类型 操作
BINDER_WRITE_READ struct binder_write_read 收发Binder IPC数据
BINDER_SET_MAX_THREADS __u32 设置Binder线程最大个数
BINDER_SET_CONTEXT_MGR __s32 设置Service Manager节点
BINDER_THREAD_EXIT __s32 释放Binder线程
BINDER_VERSION struct binder_version 获取Binder版本信息
BINDER_SET_IDLE_TIMEOUT __s64 没有使用
BINDER_SET_IDLE_PRIORITY __s32 没有使用

这些命令中**BINDER_WRITE_READ**命令是漏洞的核心

static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
   
	int ret;
	struct binder_proc *proc = filp->private_data;
	struct binder_thread *thread;
	unsigned int size = _IOC_SIZE(cmd);
	void __user *ubuf = (void __user *)arg;

	/*pr_info("binder_ioctl: %d:%d %x %lx\n",
			proc->pid, current->pid, cmd, arg);*/

	binder_selftest_alloc(&proc->alloc);

	trace_binder_ioctl(cmd, arg);

	ret = wait_event_interruptible(binder_user_error_wait, binder_stop_on_user_error < 2);
	if (ret)
		goto err_unlocked;

	thread = binder_get_thread(proc);
	if (thread == NULL) {
   
		ret = -ENOMEM;
		goto err;
	}

	switch (cmd) {
   
	case BINDER_WRITE_READ://进行binder的读写操作
		ret = binder_ioctl_write_read(filp, cmd, arg, thread);
		if (ret)
			goto err;
		break;
	case BINDER_SET_MAX_THREADS: {
    设置binder最大支持的线程数
		int max_threads;

		if (copy_from_user(&max_threads, ubuf,
				   sizeof(max_threads))) {
   
			ret = -EINVAL;
			goto err;
		}
		binder_inner_proc_lock(proc);
		proc->max_threads = max_threads;
		binder_inner_proc_unlock(proc);
		break;
	}
	case BINDER_SET_CONTEXT_MGR: //成为binder的上下文管理者,也就是ServiceManager成为守护进程
		ret = binder_ioctl_set_ctx_mgr(filp);
		if (ret)
			goto err;
		break;
	case BINDER_THREAD_EXIT:  //当binder线程退出,释放binder线程
		binder_debug(BINDER_DEBUG_THREADS, "%d:%d exit\n",
			     proc->pid, thread->pid);
		binder_thread_release(proc, thread);
		thread = NULL;
		break;
	case BINDER_VERSION: {
    //获取binder的版本号
		struct binder_version __user *ver = ubuf;

		if (size != sizeof(struct binder_version)) {
   
			ret = -EINVAL;
			goto err;
		}
[...]
err_unlocked:
	trace_binder_ioctl_done(ret);
	return ret;
}
binder_ioctl_write_read

对于ioctl()方法中,传递进来的命令是cmd = BINDER_WRITE_READ时执行该方法,arg是一个binder_write_read结构体

static int binder_ioctl_write_read(struct file *filp,
				unsigned int cmd, unsigned long arg,
				struct binder_thread *thread)
{
   
	int ret = 0;
	struct binder_proc *proc = filp->private_data;
	unsigned int size = _IOC_SIZE(cmd);
	void __user *ubuf = (void __user *)arg;
	struct binder_write_read bwr;

	if (size != sizeof(struct binder_write_read)) {
   
		ret = -EINVAL;
		goto out;
	}
	if (copy_from_user(&bwr, ubuf, sizeof(bwr))) {
    //把用户空间数据ubuf拷贝到bwr
		ret = -EFAULT;
		goto out;
	}
	binder_debug(BINDER_DEBUG_READ_WRITE,
		     "%d:%d write %lld at %016llx, read %lld at %016llx\n",
		     proc->pid, thread->pid,
		     (u64)bwr.write_size, (u64)bwr.write_buffer,
		     (u64)bwr.read_size, (u64)bwr.read_buffer);

	if (bwr.write_size > 0) {
    //当写缓存中有数据,则执行binder写操作
		ret = binder_thread_write(proc, thread,
					  bwr.write_buffer,
					  bwr.write_size,
					  &bwr.write_consumed);
		trace_binder_write_done(ret);
		if (ret < 0) {
     //当写失败,再将bwr数据写回用户空间,并返回
			bwr.read_consumed = 0;
			if (copy_to_user(ubuf, &bwr, sizeof(bwr)))
				ret = -EFAULT;
			goto out;
		}
	}
	if (bwr.read_size > 0) {
    //当读缓存中有数据,则执行binder读操作
		ret = binder_thread_read(proc, thread, bwr.read_buffer,
					 bwr.read_size,
					 &bwr.read_consumed,
					 filp->f_flags & O_NONBLOCK);
		trace_binder_read_done(ret);
		binder_inner_proc_lock(proc);
		if (!binder_worklist_empty_ilocked(&proc->todo))
			binder_wakeup_proc_ilocked(proc); //唤醒等待状态的线程
		binder_inner_proc_unlock(proc);
		if (ret < 0) {
   
			if (copy_to_user(ubuf, &bwr, sizeof(bwr))) //当读失败,再将bwr数据写回用户空间,并返回
				ret = -EFAULT;
			goto out;
		}
	}
	binder_debug(BINDER_DEBUG_READ_WRITE,
		     "%d:%d wrote %lld of %lld, read return %lld of %lld\n",
		     proc->pid, thread->pid,
		     (u6
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值