CVE-2019-18683 linux v4l2 漏洞分析

阅读量852430

|评论5

|

发布时间 : 2020-03-06 16:30:02

 

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);
}

接着会调用vid_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

本文由rtfingc原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/200029

安全客 - 有思想的安全新媒体

分享到:微信
+12赞
收藏
rtfingc
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66