pwnrabb1t:~#

CVE-2019-18683 漏洞分析

发表于安全客https://www.anquanke.com/post/id/200029

(回过头来看之后发现自己很多话都写的很不通顺,有待改进)


快速预览: 1) linux v4l2 子系统(视频相关), vivid 模块 2) 对应设备 /dev/videoX 3) 锁竞争 造成的 uaf 4) 锁的内部释放锁


CVE-2019-18683是 linux v4l2 子系统上的一个竞争漏洞,潜伏时间长达5年,Alexander PopovOffensiveCon 2020上披露了漏洞细节。影响vivid驱动,最终造成uaf,有可能可以做本地提权。

在这篇文章,我们主要分析漏洞相关代码来找出漏洞的成因。

漏洞分析

调试环境搭建

v4l2 即video for linux version 2, 是和linux视频相关的子系统,当你在linux上用电脑的摄像头时就会使用到它,对应的驱动是/dev/videoX

漏洞影响的是 vivid 模块,但是这个模块默认情况下是不会加载的,需要我们手动加载,例如在ubuntu 1804 上可以用下面的命令加载它:

可以普通用户也有这个设备的访问权限,这就给攻击提供了可能性。

要调试分析这个驱动,可以直接就是装两个ubuntu 的vmware虚拟机,然后双机调试。但是自己测试的时候双机搞实在是慢的可以,而且还会出现各种问题十分蛋疼,所以就自己搞了个qemu 的调试环境,环境的搭建可能不太规范,但还是可以满足基本的调试需求的。

linux 内核编译

vivid 模块有很多的依赖,但是我不清楚具体内核需要用什么编译选项,所以就直接用了自己虚拟机上的编译配置,我用的是ubuntu 18.04 系统,内核用的linux-5.4版本,拷贝系统的config 文件,然后直接make 编译即可

cp /boot/config-4.15.0-76-generic .config
运行环境

文件系统的话随便找一个ctf的内核题拿一个就行,我这里用的是常用的 cpio格式的文件系统,但是这里还需要我们自己把模块加载进来,可以把驱动搞进/lib/modules 之类的,这里我的做法是把编译好的模块的ko文件找出来,在系统运行后自己insmod进去。

vivid模块在内核的/drivers/media/platform/vivid目录下,这个模块有很多的依赖,需要先把依赖的模块也加载才行, 查看ubuntu虚拟机的模块依赖/lib/modules/5.4.0/modules.dep可以找到它依赖的ko文件

➜ root@prbvv  ~/cve-2019-18683/linux/drivers/media/platform/vivid  cat /lib/modules/5.4.0/modules.dep |grep vivid
kernel/drivers/media/platform/vivid/vivid.ko: kernel/drivers/media/common/v4l2-tpg/v4l2-tpg.ko kernel/drivers/media/common/videobuf2/videobuf2-dma-contig.ko kernel/drivers/media/v4l2-core/v4l2-dv-timings.ko kernel/drivers/media/cec/cec.ko kernel/drivers/media/rc/rc-core.ko kernel/drivers/media/common/videobuf2/videobuf2-vmalloc.ko kernel/drivers/media/common/videobuf2/videobuf2-memops.ko kernel/drivers/media/common/videobuf2/videobuf2-v4l2.ko kernel/drivers/media/common/videobuf2/videobuf2-common.ko kernel/drivers/media/v4l2-core/videodev.ko kernel/drivers/media/mc/mc.ko

把这些模块全部都拷贝到文件系统里面:

#!/bin/bash
abs_dir=/home/prb/cve-2019-18683/linux-5.4
cp $abs_dir/drivers/media/platform/vivid/vivid.ko vivid.ko
cp $abs_dir/drivers/media/common/v4l2-tpg/v4l2-tpg.ko v4l2-tpg.ko
cp $abs_dir/drivers/media/common/videobuf2/videobuf2-dma-contig.ko videobuf2-dma-contig.ko
cp $abs_dir/drivers/media/v4l2-core/v4l2-dv-timings.ko v4l2-dv-timings.ko
cp $abs_dir/drivers/media/cec/cec.ko cec.ko
cp $abs_dir/drivers/media/rc/rc-core.ko rc-core.ko
cp $abs_dir/drivers/media/common/videobuf2/videobuf2-vmalloc.ko videobuf2-vmalloc.ko
cp $abs_dir/drivers/media/common/videobuf2/videobuf2-memops.ko videobuf2-memops.ko
cp $abs_dir/drivers/media/common/videobuf2/videobuf2-v4l2.ko videobuf2-v4l2.ko
cp $abs_dir/drivers/media/common/videobuf2/videobuf2-common.ko videobuf2-common.ko
cp $abs_dir/drivers/media/v4l2-core/videodev.ko videodev.ko
cp $abs_dir/drivers/media/mc/mc.ko mc.ko

然后在系统启动的时候insmod,注意加载的顺序, lsmod 查看是否成功加载。

#!/bin/sh
currdir=/mod
insmod $currdir/v4l2-tpg.ko             
insmod $currdir/v4l2-dv-timings.ko      
insmod $currdir/rc-core.ko              
insmod $currdir/videobuf2-memops.ko     
insmod $currdir/videobuf2-vmalloc.ko    
insmod $currdir/mc.ko                   
insmod $currdir/videodev.ko             
insmod $currdir/videobuf2-dma-contig.ko 
insmod $currdir/videobuf2-common.ko     
insmod $currdir/cec.ko                  
insmod $currdir/videobuf2-v4l2.ko       
insmod $currdir/vivid.ko             
gdb 调试

gdb 调试前还需要先把模块的符号加载好,每次模块的加载地址都不一样,这里我直接写了个脚本暴力找模块加载地址,然后每次系统启动的时候运行

#!/bin/sh
#modprobe vivid
linux=/home/prb/cve-2019-18683/linux-5.4
vivid=$linux/drivers/media/platform/vivid/vivid.ko
videodev=$linux/drivers/media/v4l2-core/videodev.ko
v4l2_tpg=$linux/drivers/media/common/v4l2-tpg/v4l2-tpg.ko
v4l2_dv_timings=$linux/drivers/media/v4l2-core/v4l2-dv-timings.ko
videobuf2_v4l2=$linux/drivers/media/common/videobuf2/videobuf2-v4l2.ko           
videobuf2_dma_contig=$linux/drivers/media/common/videobuf2/videobuf2-dma-contig.ko     
videobuf2_vmalloc=$linux/drivers/media/common/videobuf2/videobuf2-vmalloc.ko        
videobuf2_common=$linux/drivers/media/common/videobuf2/videobuf2-common.ko         
cec=$linux/drivers/media/cec/cec.ko
mc=$linux/drivers/media/mc/mc.ko

echo "add-symbol-file $vivid" `cat /proc/modules  |grep '^vivid' | awk -F ' ' '{print $6}'` 
echo "add-symbol-file $videodev" `cat /proc/modules  |grep '^videodev' | awk -F ' ' '{print $6}'` 
echo "add-symbol-file $v4l2_tpg" `cat /proc/modules  |grep '^v4l2_tpg' | awk -F ' ' '{print $6}'`
echo "add-symbol-file $v4l2_dv_timings" `cat /proc/modules  |grep '^v4l2_dv_timings' | awk -F ' ' '{print $6}'`
echo "add-symbol-file $videobuf2_v4l2" `cat /proc/modules  |grep '^videobuf2_v4l2' | awk -F ' ' '{print $6}'` 
echo "add-symbol-file $videobuf2_dma_contig" `cat /proc/modules  |grep '^videobuf2_dma_contig' | awk -F ' ' '{print $6}'` 
echo "add-symbol-file $videobuf2_vmalloc" `cat /proc/modules  |grep '^videobuf2_vmalloc' | awk -F ' ' '{print $6}'`
echo "add-symbol-file $videobuf2_common" `cat /proc/modules  |grep '^videobuf2_common' | awk -F ' ' '{print $6}'` 
echo "add-symbol-file $cec" `cat /proc/modules  |grep '^cec' | awk -F ' ' '{print $6}'` 
echo "add-symbol-file $mc" `cat /proc/modules  |grep '^mc' | awk -F ' ' '{print $6}'

例如我的 qemu 跑起来后是这样的

然后运行gdb(pwndbg插件),拷贝命令加载符号之后就可以正常调试了。

poc 测试

先下载好Alexander Popov提供的poc

poc 很简单,就开了两个线程,open("/dev/video0"),read(fd, buf, 0xfffded); 然后是close, 不断循环。

为了提高竞争的成功率,这里还指定了线程固定在哪个CPU上运行,比如所线程1就运行在CPU0,线程2运行在CPU1这样。

编译,然后跑一下,为了方便自己调试的时候都是使用的root权限

gcc -s --static  -o exp exp.c -lpthread

得到的 crash如下:

但是我没有办法得到和Alexander Popov一样的crash, 根据他的文章中的描述,漏洞是因为vivid_stop_generating_vid_cap函数在调用kthread_stop之前unlock 了 dev->mutex

	/* shutdown control thread */
	vivid_grab_controls(dev, false);
  	- mutex_unlock(&dev->mutex);
	kthread_stop(dev->kthread_vid_cap);
	dev->kthread_vid_cap = NULL;
  	- mutex_lock(&dev->mutex);

dev->kthread_vid_cap 保存的是函数vivid_thread_vid_cap,它是一个内核线程,kthread_stop 之后会结束这个线程。本来是打算vivid_stop_generating_vid_cap unlock dev->mutex 之后,这个锁就可以被这个内核线程拿到,然后break出循环。但是这个锁也是可以被vb2_fop_read 函数拿到,于是就有了竞争。

  	for (;;) {
  		try_to_freeze();
  		if (kthread_should_stop())
  			break;
  -		mutex_lock(&dev->mutex);
  +		if (!mutex_trylock(&dev->mutex)) {
  +			schedule_timeout_uninterruptible(1);
  +			continue;
  +		}
  		...
  	}

补丁是把vivid_stop_generating_vid_cap 解锁的过程去掉了,但是vivid_thread_vid_cap 还是会获取锁,文章中还写了补丁的修改过程挺有趣的。在close 的时候,会调用vivid_fop_release 函数,接着调用_vb2_fop_release

int _vb2_fop_release(struct file *file, struct mutex *lock)
{
	struct video_device *vdev = video_devdata(file);

	if (lock)
		mutex_lock(lock);
	if (file->private_data == vdev->queue->owner) {
		vb2_queue_release(vdev->queue);
		vdev->queue->owner = NULL;
	}
	if (lock)
		mutex_unlock(lock);
	return v4l2_fh_release(file);
}

这个函数也lock 了dev->mutex 后续会继续调用vivid_stop_generating_vid_cap, vb2_fop_read 函数也是差不多,在实际调用之前会加上锁。

总之,漏洞描述大概就是这样,我们知道这是一个竞争漏洞,最后是uaf,但是具体是怎么竞争的呢,又是哪里uaf呢,下面我们分析相关的代码。

代码分析

首先我们说明一下对vivid open read close 时的一些关键功能。

open : 这个关系不大,只是打开设备而已。

read 调用流程

基本调用流程如下(省略一些关系不大的调用)

- vfs_read
	- v4l2_read
		- vb2_fop_read (lock 设备)
			- vb2_read
				- __vb2_perform_fileio
					- __vb2_init_fileio
						- vb2_core_reqbufs
							- vb_queue_alloc(分配 vb2_buffer结构体)
                        - vb2_core_qbuf(vb2_buffer 加入 vb2_queue 队列)
                        - vb2_core_streamon
                            - vb2_start_streaming
                                - __enqueue_in_driver(当前要操作的vb2_buffer加入dev->vid_cap_active)
                                - vbi_cap_start_streaming
                       - vb2_core_dqbuf(vb2_queue中的 vb2_buffer出队)
						

首先需要注意几个结构体

vb2_queue 是一个队列,会保存已申请的 vb2_buffer信息(vb2_queue->bufs)

vb2_buffer保存要操作的视频流的一些信息,会在read开始的时候调用 vb_queue_alloc 来分配内存(最终分配kmalloc-1k的slub上的内存)

在做对数据流操作的时候,也就是读写buffer的时候,会首先将vb2_buffer加入到vb2_queue里面(vb2_queue->queued_list),默认会分配两个,后面要操作直接从队列里面拿。

还需要提的一点是 vb2_fop_read会上锁,上锁的点是dev->mutex,实际运行的时候和vb2_queue->lock的值相等,具体实现可以参考这里

vb2_buffer 申请完,加入好队列之后,会调用vb2_start_streaming来为vb2_buffer填充数据,它会先调用__enqueue_in_driver把要处理的buffer 加入到设备(vivid_dev)的vid_cap_active队列上,实际上调用的是vid_cap_buf_queue 函数

static void vid_cap_buf_queue(struct vb2_buffer *vb)
{
	struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
	struct vivid_dev *dev = vb2_get_drv_priv(vb->vb2_queue);
	struct vivid_buffer *buf = container_of(vbuf, struct vivid_buffer, vb);
	
	spin_lock(&dev->slock);
	list_add_tail(&buf->list, &dev->vid_cap_active);//
	spin_unlock(&dev->slock);
}

接着会调用vbi_cap_start_streaming 函数,它会调用vivid_start_generating_vid_cap 函数,他会启动一个内核线程,功能实现在vivid_thread_vid_cap 函数上

	dev->kthread_vid_cap = kthread_run(vivid_thread_vid_cap, dev,
			"%s-vid-cap", dev->v4l2_dev.name);

vivid_thread_vid_cap 是实际做数据填充的函数,启动后会进入一个无限循环,然后会等待锁(mutex_lock(&dev->mutex);),拿到锁之后就会进行实际对vb2_buffer的处理。

注意这里是处在 vb2_fop_read 函数的内部,也就是锁已经lock了,vivid_thread_vid_cap启动之后就是一直拿不到锁的,如果什么时候锁unlock掉,它就可以lock住然后继续执行。用gdb 调试发现在vb2_fop_read 函数执行期间,vivid_thread_vid_cap是会拿到锁的,也就是说vb2_fop_read运行的时候有什么地方unlock掉了dev->mutex

但是这里就很奇怪,为什么要在锁的内部unlock掉这个锁呢,感觉这样要控制竞争会很麻烦,可能是编程上的一些原因,whatever, 我们先找出这个unlock的地方, 具体我们需要先看vb2_core_dqbuf 函数,它的作用就是处理vb2_queue->queued_list上的vb2_buffer,然后出队。

vb2_core_dqbuf的函数调用情况如下:

vb2_core_dqbuf
	- __vb2_get_done_vb
		- __vb2_wait_for_done_vb
			- vb2_ops_wait_prepare
			- vb2_ops_wait_finish

vb2_ops_wait_prepare 就是unlock vb2_queue->lock 的地方,vb2_ops_wait_finish 是重新加上锁。

也就是说,vb2_core_dqbuf解锁,然后vivid_thread_vid_cap 会拿到这个锁,等它对vb2_buffer操作完成之后unlock,vb2_core_dqbuf又重新获得这个锁继续运行`。

这里也是我们的漏洞点所在,本来期望的是只有vivid_thread_vid_cap可以拿到这个锁,但是如果另外开一个进程,调用一个vb2_fop_read之类的函数,那么这个锁就有可能会被其他进程拿去了,锁就有可能变得乱七八糟的,但是被拿了也不一定会有问题,我们继续看看这里为什么会有漏洞。

下面是我给内核打patch之后输出的dmsg, 我们可以看到函数的执行顺序

从上面可以知道,vivid_thread_vid_cap执行完之后,会再次调用 vb2_core_qbuf(没有去看为什么会调用),然后函数内部判断buffer是不是已经streaming过了,是的话会调用__enqueue_in_driver 把这个vb2_buffer加入到dev->vid_cap_active队列里面,然后vb2_fop_read函数结束,继续后面 close 函数的流程。

也就是说read 完之后,dev->vid_cap_active 里面会保存一个vb2_buffer的地址。

我们查找vid_cap_active的引用点,发现它会被vivid_thread_vid_cap,vid_cap_start_streaming以及vivid_stop_generating_vid_cap 函数使用

vivid_thread_vid_cap 会把vid_cap_active队列的vb2_buffer都拿出来处理,运行完之后队列里面就没有buffer了

vid_cap_start_streaming 主要是判断buffer状态做一些变换之类的,buffer仍然在队列里面

vivid_stop_generating_vid_cap 函数会在 close(fd)的时候调用,会清空vid_cap_active 队列上的所有buffer

okay, read的时候的大概流程就是这样,接下来我们看 close 的时候做了什么操作。

close 函数

基本调用流程如下(省略一些关系不大的调用)

- vivid_fop_release
	- vb2_fop_release
		- vb2_queue_release
			- vb2_core_queue_release
				- __vb2_cleanup_fileio
					- vb2_core_streamoff
						- __vb2_queue_cancel
							- vivid_stop_generating_vid_cap
					- vb2_core_reqbufs
						- __vb2_queue_free

close 的流程比较简单,总的来说就是先调用vivid_stop_generating_vid_cap unlock 掉锁, 然后调用 kthread_stop通知vivid_thread_vid_cap内核线程结束, 这里也是漏洞的触发点, unlock的时候锁可以被其他进程抢占。

	mutex_unlock(&dev->mutex);
	kthread_stop(dev->kthread_vid_cap);
	dev->kthread_vid_cap = NULL;
	mutex_lock(&dev->mutex);

vivid_stop_generating_vid_cap 调用完了之后会调用__vb2_queue_free, 它会把vb2_queue->bufs 上的 vb2_buffer都 kfree 掉,然后 close 流程结束。

同样,给内核加printk之后可以看到这样的调用过程

竞争过程

okay,具体的实现大概清楚了,那么是怎么样触发的 uaf 呢 ? 运行poc之后可以看到触发漏洞时的函数调用如下:

因为两个线程运行在不同的cpu上,所以很容易可以看出来调用时属于哪个线程的。我们把红色部分叫线程A,蓝色叫线程B。

线程A: 运行到 vb2_core_dqbuf, unlock 锁(本来时要vivid_thread_vid_cap拿到的)

线程B: vb2_fop_read 拿到锁,一些检查不通过函数退出,释放锁

线程A: 其vivid_thread_vid_cap拿到锁,buffer操作后释放锁

线程B: 进入close 流程,调用vivid_stop_genrating_vid_cap 清空设备的vid_cap_active队列,unlock 锁(期望vivid_thread_vid_cap拿到锁)

线程A: vb2_core_dqbuf 重新拿到锁,调用__enqueue_in_drivervb2_buffer加入设备队列(这里地址是0xffff88800fb5f000), 线程A 结束 read 流程,释放锁

线程B: vivid_stop_genrating_vid_cap 重新拿到锁 接下来函数调用如下

线程B 接下来就和正常的 close 流程一样,kfree 掉队列里面的 vb2_buffer,但是注意,这个时候内存地址0xffff88800fb5f000的buffer还是在设备的vid_cap_active队列里面的,如果这个buffer下一次read可以被用到,那么就可能有uaf了。

线程B close 流程结束后进入新的一轮循环。等到再次调用 vivid_thread_vid_cap 的时候,前面队列中保存的已经被kfree掉的vb2_buffer 就会被传到vivid_fillbuff 后面做正常的流程,因为kfree的buffer有一些数据项不符合要求,所以后面函数执行的时候就会出问题,于是就crash了,竞争的流程大概就是这样。

漏洞利用

漏洞利用的话,因为这里是有一个 uaf, 可以修改 vb2_buffer结构体内部的信息来劫持控制流,但是因为没有地址泄露,所以劫持控制流也起不到很大的作用。

Alexander Popov的做法是漏洞会触发一个warnning, 也就是我们前面一张图里面的driver bug 那一段,具体实现在__vb2_queue_cancel里,输出有内存地址,可以打开/proc/kmsg 来获取这个地址。用 userfaultfd的控制vb2_buffer的生命周期,然后劫持控制流ROP。

但实际上,现在系统都会默认加上kernel.dmesg_restrict = 1 配置,kmsg 里面的地址是看不到的,vivid模块默认也是不加载的,利用就比较局限了,所以这里就不做利用的分析了, 可以看看Alexander Popov的文章,里面有他的具体利用过程。

总结

CVE-2019-18683 是 linux v4l2 子系统的一个竞争漏洞,最终导致uaf,可以利用这个来劫持执行流,可能可以结合其他的漏洞达到权限提升的目的,但因为 vivid模块默认不会加载,利用的局限性比较大。

reference

https://a13xp0p0v.github.io/2020/02/15/CVE-2019-18683.html