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 Popov在OffensiveCon 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_driver
把vb2_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模块默认不会加载,利用的局限性比较大。