Linux Kernel Exploit with CVE-2019-18683
Intro
Alexander Popov가 발표한 CVE-2019-18683에 대해 분석해보자.
Environment Setting
이번에는 qemu + virt-manager를 이용해서 조금 더 간단히 테스트 환경을 먼저 구축하도록 한다. 이 방법은 참고할 수 있는 문서들이 많으므로 간단하게 정리해본다.
먼저 아래의 명령을 이용하여 필요한 패키지를 설치한다.
sudo apt install virt-manager qemu-kvm libvirt-daemon-system
그리고 qemu를 이용해서 ubuntu 18.04.3 server 버전을 설치한다. 자세한 설치 방법은 아래 링크에서 확인하도록 한다.
Ubuntu KVM 설치
설치가 완료되면 아래의 명령을 통해 kernel과 module 모두 업그레이드를 한다.
sudo apt install linux-image-4.15.0-66-generic linux-modules-extra-4.15.0-66-generic
원활한 kernel dubugging을 위해 grub을 수정하여 kaslr을 해제한다.
GRUB_CMDLINE_LINUX_DEFAULT="quiet nokaslr"
cve-2019-18683은 vivid 드라이버에서 발생하는데, 이 드라이버는 자동으로 로드되지 않기 때문에 /etc/modules 파일을 수정하여 부팅 시마다 해당 드라이버가 로딩되도록 설정해야 한다.
재부팅 후 아래와 같이 해당 드라이버가 제대로 로딩되었는지 확인한다.
KASLR이 해제되었는지는 dmesg를 통해 알 수 있다.
dmesg | grep command
마지막으로 해당 장치에 대한 사용자의 권한을 확인해보면, 사용자 lucid7이 user에 포함되어 있음을 볼 수 있다.
위의 권한들을 콘솔에서 로컬로 로그인을 수행해야만 활성화 되며, 단순히 ssh로 로그인 할 경우에는 활성화 되지 않으므로 주의해야 한다.
이전 분석 문서의 테스트 환경 구축하기를 참고하여 kernel binary, source, debug symbol을 host에 준비한다.
그리고 kernel debugging을 위해 아래와 같이 virt-manager에서 xml을 수정한다.
자세한 내용은 Kernel GDB live Debugging with KVM를 참고하도록 한다.
<domain xmlns:qemu="http://libvirt.org/schemas/domain/qemu/1.0" type="kvm">
...
<qemu:commandline>
<qemu:arg value="-gdb"/>
<qemu:arg value="tcp::1235"/>
</qemu:commandline>
</domain>
guest를 재부팅 하고 아래 debug.dbg를 이용하여 gdb를 실행한다.
set print pretty on
target remote localhost:1235
add-auto-load-safe-path source/linux-4.15/scripts/gdb/vmlinux-gdb.py
add-symbol-file symbol/vmlinux-4.15.0-66-generic 0
source /home/lucid7/Downloads/pwndbg/gdbinit.py
continue 후 ctrl+c로 interrupt를 걸어보면 소스를 찾을 수 없다는 에러 화면이 아래와 같이 발생하는데, 미리 준비했던 kernel source를 해당 디렉토리에 복사해 놓으면 된다.
sudo mkdir /build/linux-WKYm23
sudo cp -r linux-4.15 /build/linux-WKYm23/linux-4.15.0
CVE-2019-18683
이 취약점에 대한 원문은 CVE-2019-18683: Exploiting a Linux kernel vulnerabiity in the V4L2 subsystem에서 찾아볼 수 있다.
이 취약점은 2020년 Alexander Popov가 발표한 local privilege escalation 취약점이다.
원문의 제목에 나타나 있는 것처럼 공격자는 V4L2라는 subsystem의 취약점을 이용해 KASLR/SMEP/SMAP을 우회하여 root 권한을 획득할 수 있다. 해당 취약점은 race condition 상태에서 발생하는 Use-After-Free 취약점이다.
1. Overview
V4L2는 Video for Linux 2의 약자로써 Linux에서 video device를 다루기 위해 사용되는 framework의 version 2를 말한다.
V4L2 (Video for Linux) 와 Video Buffer 에 대해 간단하게 설명하면, V4L2는 Video streaming I/O 를 지원하기 위한 프레임워크이다. 스트리밍 API이므로 성능이 중요하고 userspace와 kernel 간의 메모리 교환에서 반드시 zero-copy가 이뤄져야 한다. 이 때문에 구현해야 하는 API들이 꽤 복잡하다. 복잡성을 조금이라도 줄이기 위해, 스트리밍에 사용하는 버퍼에 관련된 코드의 일관성을 유지하고자 나온 것이 현재의 Video Buffer 프레임워크이다(현재 버전은 2이다).
https://seokbeomkim.github.io/posts/v4l2-dmabuf/
device는 /dev/video0, /dev/video1 이라는 character device를 생성하기 때문에 사용자는 file descriptor을 이용하여 open(), read(), write(), close() 등의 API를 이용할 수 있다.
취약점은 V4L2를 기반으로 하는 virtual video kernel module 인 VIVI 에서 발견되었다. 이 모듈은 virtual video input device에 대한 장치 드라이버 역할을 하며, 시스템에 물리적 하드웨어 장치가 없는 상태에서도 V4L2 API를 사용하여 video device를 에뮬레이트 하는 기능을 가지고 있다.
ubuntu에서는 일반 사용자도 vivi 드라이버에 의해 생성된 장치에 대해 rw 권한을 가지고 있다.
vivi가 스트리밍을 다룰 때 잘못된 mutex에 대한 lock으로 인해 buffer queue에 대해 user-after-free 취약점이 발생하게 된다.
아래는 문제가 발생하는 소스 부분이다.
/* 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);
원문의 저자는 mutex가 unlock 되었을 때, 동시에 vb2_for_read() 함수에서 다시 mutex를 lock한 후 buffer queue를 조작하게 되면 스트리밍이 다시 시작되었을 때 use-after-free의 기회가 생길 수 있다고 설명하고 있다.
사실 V4L2가 어떻게 동작하는지 모르는 상황에서는 이 말이 무엇인지 알 수 없기 때문에, 먼저 V4L2에 관해 먼저 알아보는 것이 필요하다.
2. V4L2
아래의 내용은 Video for Linux Ver.2(V4L2) 문서의 내용을 간략화 한 것이며, 자세한 정보는 해당 문서를 참고하기 바란다.
일반적으로 V4L2를 이용한 스트리밍 처리의 순서는 아래와 같다. (IOCTL 기준)
위의 그림은 Application에서 V4L2 드라이버로 보낼 명령(IOCTL)의 순서도를 간략히 나타낸 것이다. QBUF, DQBUF, STREAMON을 반복적으로 수행할 경우 영상을 얻을 수 있으며, 그렇지 않을 경우에는 스틸컷(1프레임)만 얻을 수 있다.
QUERYCAP
연결된 Device의 이름 및 수행 가능한 동작 등 장치 정보를 사용자 영역에 알려주는 역할을 한다. 이 명령이 수행되고 나면 아래와 같이 디바이스에 대한 정보를 출력할 수 있다.
S_FMT_VID_CAP
포맷이나 해상도 등의 설정을 하기 위해 수행되는 명령이다.
REQBUF
Device로부터 받아온 데이터를 저장하기 위한 buffer를 할당하는 역할을 하는 명령이다. 할당된 buffer는 queue에 저장된다.
QUERYBUF & MMAP
특정 buffer의 정보를 요청할 때 사용되며, 사용자 영역에서 넘어온 buffer의 index 값을 이용하여 얻어온 buffer의 offset 정보를 바탕으로 mmap()을 호출하게 된다. mmap() 호출이 정상적으로 수행되고 난 후 해당 buffer는 사용자 영역의 buffer와 매핑되게 된다.
QBUF
Device에게 새로운 frame를 요청한다.
STREAMON
queue에 buffer를 연결한 후 Device의 스트림 기능을 활성화한 다음 가져온 이미지 정보를 decode 하고 buffer에 저장하는 역할을 한다. 이 명령 전후에 QBUF와 DQBUF를 요청해야만 Application이 이미지 정보를 얻어올 수 있다.
DQBUF
QBUF 수행으로 얻어진 frame의 index를 이용해 매핑된 메모리 영역으로부터 실제 이미지 데이터를 가져오는 기능을 수행한다. 이 명령으로 인해 이미지 데이터가 사용자 영역으로 넘어가게 된다.
지금까지 V4L2 드라이버로 보낼 명령(IOCTL)의 순서도를 통해 해당 기능들을 아주 간략히 알아보았다. 이 지식을 바탕으로 kernel source를 분석해 보자.
3. READ 과정 소스 분석
vivid 드라이버는 디바이스 타입에 따라 has_vid_cap, has_vbi_cap 속성을 가진다. TV나 SVID 타입일 경우 has_vbi_cap 속성이, video capture device 의 경우 has_vid_cap 속성이 설정되며 이 속성에 따라 동작하는 드라이버가 구분된다.
여기서는 webcam 디바이스를 제어한다는 가정하에 has_vid_cap 속성을 가지는 것으로 간주하고 관련 소스를 살펴볼 것이다.
3.1 v4l2_read()
어플리케이션에서 device의 file descriptor에 대한 read() 호출은 v4l2_read() 함수를 호출하도록 되어 있다.
// https://elixir.bootlin.com/linux/v4.15/source/drivers/media/v4l2-core/v4l2-dev.c#L459
static const struct file_operations v4l2_fops = {
.owner = THIS_MODULE,
.read = v4l2_read,
.write = v4l2_write,
.open = v4l2_open,
.get_unmapped_area = v4l2_get_unmapped_area,
.mmap = v4l2_mmap,
.unlocked_ioctl = v4l2_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = v4l2_compat_ioctl32,
#endif
.release = v4l2_release,
.poll = v4l2_poll,
.llseek = no_llseek,
};
그리고 이 함수는 v4l2_file_operations 의 read() 함수 호출로 이어진다.
// https://elixir.bootlin.com/linux/v4.15/source/drivers/media/v4l2-core/v4l2-dev.c#L300
static ssize_t v4l2_read(struct file *filp, char __user *buf, size_t sz, loff_t *off)
{
...
if (video_is_registered(vdev))
ret = vdev->fops->read(filp, buf, sz, off);
...
}
3.2 vb2_fop_read()
vdev->fops->read() 함수는 v4l2_file_operations 의 vb2_fop_read() 함수로 이어진다.
// https://elixir.bootlin.com/linux/v4.15/source/drivers/media/platform/vivid/vivid-core.c#L494
static const struct v4l2_file_operations vivid_fops = {
.owner = THIS_MODULE,
.open = v4l2_fh_open,
.release = vivid_fop_release,
.read = vb2_fop_read,
.write = vb2_fop_write,
.poll = vb2_fop_poll,
.unlocked_ioctl = video_ioctl2,
.mmap = vb2_fop_mmap,
};
vb2_fop_read() 함수는 적절한 권한 체크를 하고 lock을 걸고난 후 vb2_read()를 호출한다. vb2_read() 함수가 완료된 후 lock이 걸려 있으면 unlock을 하고 종료한다.
// https://elixir.bootlin.com/linux/v4.15/source/drivers/media/v4l2-core/videobuf2-v4l2.c#L883
ssize_t vb2_fop_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
struct video_device *vdev = video_devdata(file);
struct mutex *lock = vdev->queue->lock ? vdev->queue->lock : vdev->lock; // vdev->queue->lock == vdev->lock
...
if (lock && mutex_lock_interruptible(lock)) // lock(vdev->queue->lock) == lock(vivid_dev->mutex)
return -ERESTARTSYS;
...
err = vb2_read(vdev->queue, buf, count, ppos, file->f_flags & O_NONBLOCK);
...
exit:
if (lock)
mutex_unlock(lock);
return err;
}
취약점과 관련이 있는 mutex의 경우 좀 복잡하게 여러 이름으로 설정되어 있으나 create_instance() 함수를 보면 아래와 같이 정리가 가능하다.
// https://elixir.bootlin.com/linux/v4.15/source/drivers/media/platform/vivid/vivid-core.c#L642
struct vivid_dev *dev;
struct video_device *vfd;
struct vb2_queue *q;
mutex_init(&dev->mutex);
q = &dev->vb_vid_cap_q;
q->lock = &dev->mutex;
q->dev = dev->v4l2_dev.dev;
vfd->queue = &dev->vb_vid_cap_q;
vfd->lock = &dev->mutex;
따라서 아래의 값은 모두 동일한 mutex를 가리킨다.
- vivid_dev->mutex
- video_device->lock
- vivid_dev->vb_vid_cap_q->lock
- video_device->queue->lock
위에서 정리한 동일한 mutex를 가리키는 각 변수들을 염두에 두고 소스 분석을 계속 진행해보자.
3.3 vb2_read()
vb2_read() 함수는 __vb2_perform_fileio() 함수를 호출한다.
// https://elixir.bootlin.com/linux/v4.15/source/drivers/media/v4l2-core/videobuf2-core.c#L2460
size_t vb2_read(struct vb2_queue *q, char __user *data, size_t count, loff_t *ppos, int nonblocking)
{
return __vb2_perform_fileio(q, data, count, ppos, nonblocking, 1);
}
3.4 __vb2_perform_fileio()
이 함수는 아래와 같이 크게 3가지의 함수로 구분될 수 있다.
- __vb2_init_fileio() : 스트리밍을 위해 필요한 준비 및 실제로 스트리밍을 하는 kernel thread를 생성시킨다.
- vb2_core_dqbuf() : 메모리로부터 데이터를 가져온다. 앞에서 살펴본 DQBUF에 해당한다.
- copy_to_user() : 데이터를 사용자 영역으로 넘겨주는 kernel api
// https://elixir.bootlin.com/linux/v4.15/source/drivers/media/v4l2-core/videobuf2-core.c#L2305
static size_t __vb2_perform_fileio(struct vb2_queue *q, char __user *data, size_t count, loff_t *ppos, int nonblock, int read)
{
...
if (!vb2_fileio_is_active(q)) { // q->fileio 검사
ret = __vb2_init_fileio(q, read); // <== 진입
...
}
fileio = q->fileio;
// __vb2_init_fileio() 함수에서 cur_index = q->num_buffers 로 설정되며 값은 2가 된다.
index = fileio->cur_index;
if (index >= q->num_buffers) { // <== 진입
struct vb2_buffer *b;
// vb2_core_dqbuf() 실행
ret = vb2_core_dqbuf(q, &index, NULL, nonblock);
...
}
...
if (read)
ret = copy_to_user(data, buf->vaddr + buf->pos, count);
else
ret = copy_from_user(buf->vaddr + buf->pos, data, count);
...
}
...
}
3.4.1 __vb2_init_fileio()
이 함수도 아래와 같이 크게 3가지 함수로 구분할 수 있다.
- vb2_core_reqbufs()
- vb2_core_qbuf()
- vb2_core_streamon()
// https://elixir.bootlin.com/linux/v4.15/source/drivers/media/v4l2-core/videobuf2-core.c#L2163
static int __vb2_init_fileio(struct vb2_queue *q, int read)
{
struct vb2_fileio_data *fileio;
...
// vb2_fileio_data 구조체에 메모리 할당
fileio = kzalloc(sizeof(*fileio), GFP_KERNEL);
if (fileio == NULL)
return -ENOMEM;
// fileio 구조체 내용 설정
fileio->read_once = q->fileio_read_once;
fileio->write_immediately = q->fileio_write_immediately;
fileio->count = count; // 1로 설정되어 있음
fileio->memory = VB2_MEMORY_MMAP; // mmap 방식의 버퍼 요청을 위한 서정
fileio->type = q->type;
q->fileio = fileio; // 설정한 fileio를 queue->fileio에 설정
// 버퍼에 video memory를 할당하고, 할당된 vb2_buffer 버퍼의 갯수를 반환하는데,num_buffers에 의존하므로 이 값은 2임
// 할당된 버퍼는 q->bufs[vb->index] 에 설정되고, 버퍼에 할당된 메모리 주소는 vb->planes[plane].mem_priv 에 설정
ret = vb2_core_reqbufs(q, fileio->memory, &fileio->count);
...
// plane는 오직 1개만 지원함
if (q->bufs[0]->num_planes != 1) {
...
}
for (i = 0; i < q->num_buffers; i++) {
// mmap 주소의 매핑된 kernel virtual address 를 반환
fileio->bufs[i].vaddr = vb2_plane_vaddr(q->bufs[i], 0);
...
// vb->planes[0].length 의 값이 저장됨
fileio->bufs[i].size = vb2_plane_size(q->bufs[i], 0);
}
if (read) { // <== 진입
for (i = 0; i < q->num_buffers; i++) {
// 버퍼를 준비하는 과정을 실행하고 해당 버퍼를 vivid_dev->vbi_cap_active 리스트에 추가
ret = vb2_core_qbuf(q, i, NULL);
if (ret)
goto err_reqbufs;
fileio->bufs[i].queued = 1;
}
fileio->initial_index = q->num_buffers;
fileio->cur_index = q->num_buffers;
}
ret = vb2_core_streamon(q, q->type);
...
}
3.4.1.1 vb2_core_reqbufs()
이 함수는 vid_cap_queue_setup() 함수를 호출하여 필요한 버퍼 수와 버퍼 당 plane 수를 요청한 후, __vb2_queue_alloc() 함수를 통해 2개의 buffer를 할당한다.
int vb2_core_reqbufs( struct vb2_queue *q,
enum vb2_memory memory, // VB2_MEMORY_MMAP
unsigned int *count) // 1
{
...
// 만약 스트리밍 중일 경우에는 리턴
if (q->streaming) {
dprintk(1, "streaming active\n");
return -EBUSY;
}
// count는 1로 강제 설정되어 넘어오므로 이 if문은 실행되지 않음
if (*count == 0 || q->num_buffers != 0 || q->memory != memory) {
...
}
// #define VB2_MAX_FRAME (32)
// #define VB2_MAX_PLANES (8)
num_buffers = min_t(unsigned int, *count, VB2_MAX_FRAME); // 1
// q->min_buffers_needed 는 vivid_create_instance 함수에서 2로 설정되어 있음. 따라서 2개의 버퍼가 생성됨
// https://elixir.bootlin.com/linux/v4.15/source/drivers/media/platform/vivid/vivid-core.c#L642
num_buffers = max_t(unsigned int, num_buffers, q->min_buffers_needed); // 2
memset(q->alloc_devs, 0, sizeof(q->alloc_devs));
q->memory = memory; // VB2_MEMORY_MMAP
// Driver에게 필요한 버퍼 수와 버퍼 당 plane 수를 요청
// Driver는 크기를 설정 및 각 plane 에 대한 컨텍스트 할당
ret = call_qop( q,
queue_setup, // vid_cap_queue_setup() 호출
q,
&num_buffers, // 2
&num_planes,
plane_sizes,
q->alloc_devs);
...
// 버퍼에 video memory(vb2_buffer)를 할당하고, 할당된 vb2_buffer 버퍼의 갯수를 반환하는데, num_buffers에 의존하므로 2임
// 할당된 버퍼는 q->bufs[vb->index] 에 설정되고, 버퍼에 할당된 메모리 주소는 vb->planes[plane].mem_priv 에 설정
allocated_buffers = __vb2_queue_alloc( q,
memory, // VB2_MEMORY_MMAP
num_buffers, // 2
num_planes, // 1
plane_sizes); // plane_sizes[0] = size
...
// 요청한 갯수보다 작은 갯수의 버퍼가 할당되었을 경우에 재시도
if (!ret && allocated_buffers < num_buffers) {
...
}
...
// 할당된 버퍼의 갯수를 설정
q->num_buffers = allocated_buffers; // 2
...
*count = allocated_buffers; // 할당된 버퍼의 갯수를 설정. 2
...
}
3.4.1.1.1 __vb2_queue_alloc()
이 함수는 2개의 버퍼에 vb2_buffer 구초제(video memory)를 위한 메모리를 할당하여 내용을 채우고, 해당 버퍼를 q->bufs 배열에 삽입한다. 그리고 vb->planes[plane].length 크기만큼 메모리를 할당하여 vb->planes[plane].mem_priv 에 설정한다.
해당 버퍼의 state는 VB2_BUF_STATE_DEQUEUED 로 설정된다.
static int __vb2_queue_alloc(struct vb2_queue *q,
enum vb2_memory memory, // VB2_MEMORY_MMAP
unsigned int num_buffers, // 2
unsigned int num_planes, // 1
const unsigned plane_sizes[VB2_MAX_PLANES]) // plane_sizes[0] = size
{
...
// 버퍼 2개에 대해 vb2_buffer 구조체 메모리 할당
for (buffer = 0; buffer < num_buffers; ++buffer) {
vb = kzalloc(q->buf_struct_size, GFP_KERNEL);
...
// vb2_buffer 구조체 채우기
vb->state = VB2_BUF_STATE_DEQUEUED;
vb->vb2_queue = q;
vb->num_planes = num_planes;
vb->index = q->num_buffers + buffer;
vb->type = q->type;
vb->memory = memory; // VB2_MEMORY_MMAP
for (plane = 0; plane < num_planes; ++plane) {
vb->planes[plane].length = plane_sizes[plane];
vb->planes[plane].min_length = plane_sizes[plane];
}
// queue->bufs 배열에 할당된 videovb2_bufferbuf 구조체 설정
q->bufs[vb->index] = vb;
// 버퍼는 q->bufs[vb->indx] 에 할당됨
// mmap 주소는 vb->planes[plane].mem_priv 에 설정됨
// plane 1개에 buffer 2개인 상태임
if (memory == VB2_MEMORY_MMAP) {
// 전달된 버퍼에 대해 video memory(vb2_buffer) 할당
// 할당된 메모리 주소는 vb->planes[plane].mem_priv 에 설정
ret = __vb2_buf_mem_alloc(vb);
...
// vb->planes[plane].m.offset 에 vb->planes[plane].length 만큼 오프셋을 설정
__setup_offsets(vb);
// vb2_buffer 에 대해 Driver에서 제공하는 buf_init 함수 실행하여 추가적인 버퍼 초기화
ret = call_vb_qop(vb, buf_init, vb);
...
}
}
...
}
3.4.1.1.2 __vb2_buf_mem_alloc()
이 함수는 vb->planes[plane].length 변수에 저장된 크기만큼 메모리를 할당한 후 그 값을 vb->planes[plane].mem_priv 변수에 저장한다.
static int __vb2_buf_mem_alloc(struct vb2_buffer *vb)
{
...
for (plane = 0; plane < vb->num_planes; ++plane) {
unsigned long size = PAGE_ALIGN(vb->planes[plane].length);
mem_priv = call_ptr_memop(vb, alloc, q->alloc_devs[plane] ? : q->dev, q->dma_attrs, size, q->dma_dir, q->gfp_flags);
...
vb->planes[plane].mem_priv = mem_priv;
}
...
}
3.4.1.2 vb2_core_qbuf()
이 함수는 먼저 __buf_prepare() -> vid_cap_buf_prepare() 순서로 함수를 호출하여 전달된 버퍼의 type에 적절한 크기를 구해 vb->planes[plane_no].bytesused 에 설정하고, state를 VB2_BUF_STATE_PREPARED 로 변경한다.
이후 해당 버퍼의 vb->queued_entry를 q->queued_list 리스트에 추가하고, q->waiting_for_buffers 를 false 로 설정하여 해당 버퍼가 queueed_list에 추가되었음을 설정한 후, state 를 VB2_BUF_STATE_QUEUED 로 변경한다.
// https://elixir.bootlin.com/linux/v4.15/source/drivers/media/v4l2-core/videobuf2-core.c#L653
int vb2_core_qbuf(struct vb2_queue *q,
unsigned int index, // 0 ~ 1
void *pb) // null
{
...
vb = q->bufs[index]; // 이전에 할당되어 q->bufs 배열에 저장되어 있던 vb2_buffer 버퍼
// __vb2_queue_alloc() 함수에서 state는 VB2_BUF_STATE_DEQUEUED로 설정되어 있음
switch (vb->state) {
// state를 VB2_BUF_STATE_PREPARING 로 변경, vb->planes[plane_no].bytesused = size 설정됨
case VB2_BUF_STATE_DEQUEUED:
ret = __buf_prepare(vb, pb); // vbi_cap_buf_prepare() 함수 호출
if (ret)
return ret;
break;
...
}
// vb->queued_entry 를 q->queued_list 에 삽입
list_add_tail(&vb->queued_entry, &q->queued_list);
q->queued_count++;
q->waiting_for_buffers = false; // queue 에 추가되었음을 설정
// state 를 VB2_BUF_STATE_QUEUED 로 변경
vb->state = VB2_BUF_STATE_QUEUED;
...
}
3.4.1.2.1 vid_cap_buf_prepare()
이 함수는 전달된 버퍼의 type에 맞는 크기를 vb->planes[plane_no].bytesused 에 설정하는 역할을 한다.
static int vid_cap_buf_prepare(struct vb2_buffer *vb)
{
...
for (p = 0; p < buffers; p++) {
size = tpg_g_line_width(&dev->tpg, p) * dev->fmt_cap_rect.height + dev->fmt_cap->data_offset[p];
...
vb2_set_plane_payload(vb, p, size); // vb->planes[plane_no].bytesused = size
vb->planes[p].data_offset = dev->fmt_cap->data_offset[p];
}
...
}
3.4.1.2.2 __enqueue_in_driver()
이 함수는 state 를 VB2_BUF_STATE_ACTIVE 로 변경하고, vid_cap_buf_queue() 함수를 호출한다.
static void __enqueue_in_driver(struct vb2_buffer *vb)
{
struct vb2_queue *q = vb->vb2_queue;
vb->state = VB2_BUF_STATE_ACTIVE;
...
// vid_cap_buf_queue() 호출
call_void_vb_qop(vb, buf_queue, vb);
}
3.4.1.2.3 vid_cap_buf_queue()
이 함수는 전달된 버퍼를 vivid_dev->vid_cap_active 리스트에 추가한다.
static void vbi_cap_buf_queue(struct vb2_buffer *vb)
{
...
// vivid_buffer 를 vivid_dev->vid_cap_active 리스트에 추가
list_add_tail(&buf->list, &dev->vid_cap_active);
...
}
3.4.1.3 vb2_core_streamon()
할당된 buffer가 vid_cap_active 에 추가되면 vb2_core_streamon() 함수가 호출되며, 이 함수는 다시vb2_start_streaming() 함수를 호출한다.
// https://elixir.bootlin.com/linux/v4.15/source/drivers/media/v4l2-core/videobuf2-core.c#L1696
int vb2_core_streamon(struct vb2_queue *q, unsigned int type)
{
...
if (q->queued_count >= q->min_buffers_needed) {
...
ret = vb2_start_streaming(q);
...
}
...
}
3.4.1.3.1 vb2_start_streaming()
이 함수는 먼저 queued_list 에 있는 각 vb2_buffer를 __enqueue_in_driver() 함수 호출을 통해 vivid_dev->vid_cap_active 리스트에 추가한 후, streaming이 시작되었음을 설정한다. 그리고 start_streaming() 함수를 호출하는데 이 함수는 vid_cap_start_streaming() 함수와 연결되어 있다.
// https://elixir.bootlin.com/linux/v4.15/source/drivers/media/platform/vivid/vivid-vbi-cap.c#L235
const struct vb2_ops vivid_vbi_cap_qops = {
.queue_setup = vbi_cap_queue_setup,
.buf_prepare = vbi_cap_buf_prepare,
.buf_queue = vbi_cap_buf_queue,
.start_streaming = vbi_cap_start_streaming,
.stop_streaming = vbi_cap_stop_streaming,
.wait_prepare = vb2_ops_wait_prepare,
.wait_finish = vb2_ops_wait_finish,
};
static int vb2_start_streaming(struct vb2_queue *q)
{
...
// q->queued_list 에 들어있는 vb2_buffer를 vivid_dev->vid_cap_active 리스트에 추가
list_for_each_entry(vb, &q->queued_list, queued_entry)
__enqueue_in_driver(vb);
// 스트리밍 시작 설정
q->start_streaming_called = 1;
// vid_cap_start_streaming() 호출
ret = call_qop(q, start_streaming, q, atomic_read(&q->owned_by_drv_count));
// start_streaming 이 정상적으로 종료되면 return
if (!ret)
return 0;
...
}
3.4.1.3.2 vid_cap_start_streaming()
이 함수는 vivid_start_generating_vid_cap() 함수를 호출한다. 만약 에러가 발생할 경우 해당 버퍼를 vid_cap_active 리스트에서 제거하고 vb2_buffer_done() 함수를 호출한다.
static int vid_cap_start_streaming(struct vb2_queue *vq, unsigned count)
{
...
if (dev->start_streaming_error) {
...
} else {
err = vivid_start_generating_vid_cap(dev, &dev->vid_cap_streaming);
}
if (err) {
...
list_for_each_entry_safe(buf, tmp, &dev->vid_cap_active, list) {
list_del(&buf->list);
vb2_buffer_done(&buf->vb.vb2_buf, VB2_BUF_STATE_QUEUED);
}
}
...
}
3.4.1.3.3 vivid_start_generating_vid_cap()
이 함수는 kernel thread를 생성해 vivid_thread_vid_cap() 함수를 실행한다. 실행된 스레드는 lock을 획득할 때 까지 대기하다가, lock을 획득하면 vivid_thread_vid_cap_tick() 함수를 통해 데이터 저장을 시작한다. 여기서 lock에 사용되는 dev->mutex 는 dev->queue->vivid_dev->mutex 와 동일하다.
// https://elixir.bootlin.com/linux/v4.15/source/drivers/media/platform/vivid/vivid-kthread-cap.c#L855
int vivid_start_generating_vid_cap(struct vivid_dev *dev, bool *pstreaming)
{
...
dev->kthread_vid_cap = kthread_run(vivid_thread_vid_cap, dev, "%s-vid-cap", dev->v4l2_dev.name);
...
}
// https://elixir.bootlin.com/linux/v4.15/source/drivers/media/platform/vivid/vivid-kthread-cap.c#L752
static int vivid_thread_vid_cap(void *data)
{
...
for (;;) {
...
mutex_lock(&dev->mutex); // lock(dev->queue->vivid_dev->mutex)
...
// vivid_buffer vid_cap_buf 과 vbi_cap_buf 를 채운다.
vivid_thread_vid_cap_tick(dev, dropped_bufs);
...
mutex_unlock(&dev->mutex); // unlock(dev->queue->vivid_dev->mutex)
...
}
}
위에서 생성된 kernel thread는 데이터를 저장하기 위해 vivid_dev->mutex의 lock을 획득하려고 시도한다. 하지만 이 mutex는 이미 vb2_fop_read() 함수에서 lock이 되어 있는 상태이므로 어디에선가 unlock이 될 때까지 계속 대기하게 된다.
3.4.1.3.4 vivid_thread_vid_cap_tick()
이 함수는 vivid_fillbuff() 함수를 호출하여 vid_cap_active 리스트로부터 얻어온 버퍼를 해당 리스트에서 삭제한 뒤 vivid_fillbuff() 함수를 호출하여 실제로 채우고 이 작업이 완료되면 vb2_buffer_done() 함수를 호출한다.
static void vivid_thread_vid_cap_tick(struct vivid_dev *dev, int dropped_bufs)
{
...
// dev->vid_cap_active 에서 vivid_buffer vid_cap_buf 1개 가져오고 리스트에서 제거
if (!list_empty(&dev->vid_cap_active)) {
vid_cap_buf = list_entry(dev->vid_cap_active.next, struct vivid_buffer, list);
list_del(&vid_cap_buf->list);
}
...
if (vid_cap_buf) {
vivid_fillbuff(dev, vid_cap_buf);
...
vb2_buffer_done(&vid_cap_buf->vb.vb2_buf, dev->dqbuf_error ? VB2_BUF_STATE_ERROR : VB2_BUF_STATE_DONE);
...
}
...
}
3.4.1.3.5 vb2_buffer_done()
버퍼를 채우는 작업이 완료되면 호출되는 함수이다. 이 함수는 q->done_list에 해당 버퍼를 추가하고 wake_up() 함수를 호출하여 이벤트를 발생시킨다.
void vb2_buffer_done(struct vb2_buffer *vb,
enum vb2_buffer_state state) // VB2_BUF_STATE_DONE
{
struct vb2_queue *q = vb->vb2_queue;
unsigned long flags;
unsigned int plane;
// __enqueue_in_driver() 에서 VB2_BUF_STATE_ACTIVE 로 설정됨
if (WARN_ON(vb->state != VB2_BUF_STATE_ACTIVE))
return;
...
if (state != VB2_BUF_STATE_QUEUED && state != VB2_BUF_STATE_REQUEUEING) {
for (plane = 0; plane < vb->num_planes; ++plane)
call_void_memop(vb, finish, vb->planes[plane].mem_priv); // 잘 모르겠음
}
...
else {
list_add_tail(&vb->done_entry, &q->done_list); // q->done_list 에 해당 버퍼를 추가함
vb->state = state; // state 는 VB2_BUF_STATE_DONE 로 변경
}
...
switch (state) {
...
default:
wake_up(&q->done_wq); // 이벤트 발생
break;
}
}
이제 위에서 생성한 kernel thread는 lock을 획득할 때까지 대기하게 되고, 실행 흐름은 다시 __vb2_perform_fileio() 함수로 돌아와서 vb2_core_dqbuf() 함수가 실행된다. 이 부분은 앞에서 살펴본 DQBUF에 해당한다.
3.4.2 vb2_core_dqbuf()
이 함수는 __vb2_get_done_vb() 함수를 호출해 kernel thread 가 삽입한 버퍼를 q->done_list 로부터 가져와 vb2_buffer 구조체 변수에 저장한다. 그리고 vid_cap_buf_finish() 함수를 호출하여 time code를 설정한 후 해당 버퍼를 queued_entry 로부터 제거하고 state를 VB2_BUF_STATE_DEQUEUED로 변경한다.
int vb2_core_dqbuf( struct vb2_queue *q,
unsigned int *pindex, // 2
void *pb, // null
bool nonblocking)
{
struct vb2_buffer *vb = NULL;
int ret;
ret = __vb2_get_done_vb(q, &vb, pb, nonblocking);
if (ret < 0)
return ret;
switch (vb->state) { // VB2_BUF_STATE_DONE
case VB2_BUF_STATE_DONE:
dprintk(3, "returning done buffer\n");
break;
...
}
call_void_vb_qop(vb, buf_finish, vb); // vid_cap_buf_finish() 호출, 타임코드 설정
if (pindex)
*pindex = vb->index;
// pb 가 null 이므로 호출되지 않음
if (pb)
call_void_bufop(q, fill_user_buffer, vb, pb);
// 해당 버퍼를 queued_entry 에서 제거한다
list_del(&vb->queued_entry);
q->queued_count--;
trace_vb2_dqbuf(q, vb);
// state 를 VB2_BUF_STATE_DEQUEUED 로 변경
__vb2_dqbuf(vb);
...
}
3.4.2.1 __vb2_get_done_vb()
이 함수는 __vb2_wait_for_done_vb()를 호출하여 사용 가능한 버퍼가 있을 때까지 대기하다가 q->done_list 로부터 버퍼를 획득한다.
static int __vb2_get_done_vb( struct vb2_queue *q,
struct vb2_buffer **vb, // NULL, __out, get buffer from q->done_list
void *pb, // null
int nonblocking)
{
...
// 버퍼를 사용할 수 있을 때까지 대기함
ret = __vb2_wait_for_done_vb(q, nonblocking);
...
// done_list 로부터 버퍼 획득
*vb = list_first_entry(&q->done_list, struct vb2_buffer, done_entry);
...
}
3.4.2.1.1 __vb2_wait_for_done_vb()
이 함수는 unlock 후 wait_event_interruptible() 함수를 호출하여 해당 버퍼를 사용할 수 있을 때까지 대기한 후 다시 lock을 한다. 해당 함수가 대기하는 q->done_wq 는 kernel thread 가 lock을 획득하여 버퍼를 채운 후 호출하는 vb2_buffer_done() 함수에서 발생시키는 이벤트이다.
static int __vb2_wait_for_done_vb(struct vb2_queue *q, int nonblocking)
{
for (;;) {
...
call_void_qop(q, wait_prepare, q); // mutex_unlock(vq->lock);
// wake_up 이 되면 condition 체크 후 만족하지 않으면 다시 sleep
ret = wait_event_interruptible(q->done_wq, !list_empty(&q->done_list) || !q->streaming || q->error);
call_void_qop(q, wait_finish, q); // mutex_lock(vq->lock);
...
}
return 0;
}
3.4.3 copy_to_user()
이제 버퍼에 저장된 내용을 사용자 영역으로 복사한다.
4. CLOSE 과정 소스 분석
close()는 vivid_fop_release() 함수로부터 시작한다.
4.1 vivid_fop_release()
vb2_fop_release() 함수를 호출한다. 내부적으로 vb2_fop_release() 함수를 호출하며, 내부에서 다시 _vb2_fop_release() 함수를 호출한다.
int vb2_fop_release(struct file *file)
{
struct video_device *vdev = video_devdata(file);
struct mutex *lock = vdev->queue->lock ? vdev->queue->lock : vdev->lock; // vdev->queue->lock == vdev->lock
return _vb2_fop_release(file, lock);
}
static int vivid_fop_release(struct file *file)
{
...
if (vdev->queue)
return vb2_fop_release(file);
...
}
4.2 _vb2_fop_release
이 함수는 파라미터로 전달된 lock 변수에 mutex_lock() 을 걸고 vb2_queue_release() 함수를 호출한다. 해당 함수는 다시 vb2_core_queue_release() 함수를 호출하며, 해당 함수가 완료되면 획득한 lock을 다시 unlock 한다.
void vb2_queue_release(struct vb2_queue *q)
{
vb2_core_queue_release(q);
}
int _vb2_fop_release(struct file *file, struct mutex *lock)
{
...
if (lock) // lock(vdev->queue->lock) == lock(vivid_dev->mutex)
mutex_lock(lock);
if (file->private_data == vdev->queue->owner) {
vb2_queue_release(vdev->queue);
vdev->queue->owner = NULL;
}
if (lock)
mutex_unlock(lock);
...
}
4.3 vb2_core_queue_release()
이 함수는 아래와 같이 세 부분으로 이루어져 있다.
- __vb2_cleanup_fileio()
- vb2_queue_cancel()
- __vb2_queue_free()
void vb2_core_queue_release(struct vb2_queue *q)
{
__vb2_cleanup_fileio(q); // fileio 관련 리소스 해제, wake_up_all(&q->done_wq);
__vb2_queue_cancel(q);
mutex_lock(&q->mmap_lock); // lock(vdev->queue->lock) == lock(vivid_dev->mutex)
__vb2_queue_free(q, q->num_buffers);
mutex_unlock(&q->mmap_lock); // unlock(vdev->queue->lock) == lock(vivid_dev->mutex)
}
4.3.1 __vb2_cleanup_fileio()
이 함수는 fileio 에서 사용되었던 리소스들을 해제하는 역할을 한다. 먼저 vb2_core_streamoff() -> __vb2_queue_cancel() 함수를 호출하여, streaming을 일시 중지하고 드라이버와 videobuf에서 모든 버퍼를 제거한다.
int vb2_core_streamoff(struct vb2_queue *q, unsigned int type)
{
...
__vb2_queue_cancel(q);
...
}
static int __vb2_cleanup_fileio(struct vb2_queue *q)
{
...
if (fileio) {
vb2_core_streamoff(q, q->type);
...
vb2_core_reqbufs(q, fileio->memory, &fileio->count);
kfree(fileio);
...
}
...
}
4.3.1.1 __vb2_queue_cancel()
이 함수는 vid_cap_stop_streaming() -> vivid_stop_generating_vid_cap() 함수를 차례대로 호출하여 vid_cap_active 리스트 내 모든 버퍼들을 해당 리스트에서 제거하고 vb2_buffer_done() 함수를 호출하여 해당 버퍼를 done_list에 추가한다. 그리고 kthread_stop() 함수를 호출한다.
위의 작업이 완료되면 q->done_wq 이벤트를 발생시키고 모든 버퍼들에 대해 vid_cap_buf_finish() 함수 및 __vb2_dqbuf() 함수를 호출한다.
static void vid_cap_stop_streaming(struct vb2_queue *vq)
{
...
vivid_stop_generating_vid_cap(dev, &dev->vid_cap_streaming);
...
}
static void __vb2_queue_cancel(struct vb2_queue *q)
{
...
// 이 값은 vb2_start_streaming() 함수에서 1로 설정된 상태이다.
if (q->start_streaming_called)
call_void_qop(q, stop_streaming, q); // vid_cap_stop_streaming() 호출
...
wake_up_all(&q->done_wq);
for (i = 0; i < q->num_buffers; ++i) {
struct vb2_buffer *vb = q->bufs[i];
if (vb->state == VB2_BUF_STATE_PREPARED || vb->state == VB2_BUF_STATE_QUEUED) {
unsigned int plane;
for (plane = 0; plane < vb->num_planes; ++plane)
call_void_memop(vb, finish, vb->planes[plane].mem_priv); // vid_cap_buf_finish() 호출, 타임코드 설정
}
if (vb->state != VB2_BUF_STATE_DEQUEUED) {
vb->state = VB2_BUF_STATE_PREPARED;
call_void_vb_qop(vb, buf_finish, vb); // vid_cap_buf_finish() 호출, 타임코드 설정
}
__vb2_dqbuf(vb); // state 를 VB2_BUF_STATE_DEQUEUED 로 변경
}
}
4.3.1.1.1 vivid_stop_generating_vid_cap()
이 함수는 vid_cap_active 리스트로부터 모든 버퍼들을 제거하고, vb2_buffer_done() 함수를 호출하여 done_list에 추가한다. 모든 버퍼에 대해 위의 작업이 완료되면, unlock 후 kthread_stop() 함수를 호출해 kernel thread 가 실행 중이던 vivid_thread_vid_cap() 함수를 종료하고 다시 lock을 한다.
void vivid_stop_generating_vid_cap(struct vivid_dev *dev, bool *pstreaming)
{
...
if (pstreaming == &dev->vid_cap_streaming) {
/* Release all active buffers */
while (!list_empty(&dev->vid_cap_active)) {
struct vivid_buffer *buf;
buf = list_entry(dev->vid_cap_active.next, struct vivid_buffer, list);
list_del(&buf->list);
vb2_buffer_done(&buf->vb.vb2_buf, VB2_BUF_STATE_ERROR);
dprintk(dev, 2, "vid_cap buffer %d done\n", buf->vb.vb2_buf.index);
}
}
...
mutex_unlock(&dev->mutex);
kthread_stop(dev->kthread_vid_cap);
dev->kthread_vid_cap = NULL;
mutex_lock(&dev->mutex);
}
4.3.1.2 vb2_core_reqbufs()
close 될 때의 이 함수는 아래 if 문을 실행하게 되는데, 이 함수 호출 전에 실행된 __vb2_queue_cancel() 함수를 한번 더 실행한 후 __vb2_queue_free() 함수를 실행하여 해당 버퍼의 vb->planes[plane].mem_priv 메모리와 videobuf 메모리를 해제한다.
static int __vb2_queue_free(struct vb2_queue *q, unsigned int buffers)
{
...
__vb2_free_mem(q, buffers);
...
for (buffer = q->num_buffers - buffers; buffer < q->num_buffers; ++buffer) {
kfree(q->bufs[buffer]);
q->bufs[buffer] = NULL;
}
...
}
int vb2_core_reqbufs(struct vb2_queue *q, enum vb2_memory memory, unsigned int *count)
{
...
if (*count == 0 || q->num_buffers != 0 || q->memory != memory) {
...
__vb2_queue_cancel(q); // 혹시 모를 PREPARED 또는 QUEUED 상태 버퍼 정리
ret = __vb2_queue_free(q, q->num_buffers);
...
}
...
}
4.3.2 __vb2_queue_cancel()
__vb2_cleanup_fileio() 함수에서 에러가 발생했을 경우를 대비하여 한번 더 호출하는 것으로 보인다.
4.3.3 __vb2_queue_free()
__vb2_cleanup_fileio() 함수에서 에러가 발생했을 경우를 대비하여 한번 더 호출하는 것으로 보인다.
5. 취약점 분석
위에서 소스를 분석한 결과를 보면 개발자가 원래 의도했던 기능을 위한 함수 호출 순서는 다음이었던 것으로 보인다. (소스로만 분석한 거라 실제 실행 순서와는 차이가 있을 수 있음)
Read
Function | Action |
---|---|
vb2_fop_read | lock |
vb2_start_streaming | add buffer |
vb2_core_dqbuf | unlock |
vivid_thread_vid_cap | lock |
vivid_thread_vid_cap_tick | remove buffer |
vivid_thread_vid_cap | unlock |
vb2_core_dqbuf | lock |
vb2_fop_read | unlock |
Close
Function | Action |
---|---|
_vb2_fop_release | lock |
vivid_stop_generating_vid_cap | remove buffer |
vivid_stop_generating_vid_cap | unlock |
vivid_stop_generating_vid_cap | lock |
vb2_core_reqbufs | release buffer memory |
_vb2_fop_release | unlock |
위의 그림에서 볼 수 있듯이 open과 close는 모두 동일한 mutex를 바라보고 동기화를 수행한다. 따라서 multi processor 환경에서 동시에 open과 close를 반복하게 될 경우 race condition이 발생하게 된다.
아래는 원문 저자가 올린 poc 코드를 실행했을 때 crash가 일어나기 전까지 출력된 결과이다.
CPUID | Function | Action | Caller
0 [vb2_fop_read] (lock), caller:(v4l2_read+0x71/0x90)</div>
0 [vb2_core_reqbufs], caller:(__vb2_init_fileio+0xfe/0x2c0)
0 [__vb2_queue_alloc] (alloc buffer) vb:0000000031fe575c, caller:(vb2_core_reqbufs+0x197/0x400)
0 [__vb2_buf_mem_alloc] (alloc mem_priv) mem_priv:000000004bcd5f2e, caller:(vb2_core_reqbufs+0x197/0x400)
0 [__vb2_queue_alloc] (alloc buffer) vb:0000000060c02281, caller:(vb2_core_reqbufs+0x197/0x400)
0 [__vb2_buf_mem_alloc] (alloc mem_priv) mem_priv:00000000e61115ec, caller:(vb2_core_reqbufs+0x197/0x400)
0 [vb2_core_qbuf] (add queued_list) vb:0000000031fe575c, caller:(__vb2_init_fileio+0x253/0x2c0)
0 [vb2_core_qbuf] (add queued_list) vb:0000000060c02281, caller:(__vb2_init_fileio+0x253/0x2c0)
0 [__enqueue_in_driver] caller:(vb2_start_streaming+0x33/0xd0)
0 [vid_cap_buf_queue] (add vid_cap_active) vb:0000000031fe575c, caller:(__enqueue_in_driver+0x65/0xb0)
0 [__enqueue_in_driver] caller:(vb2_start_streaming+0x33/0xd0)
0 [vid_cap_buf_queue] (add vid_cap_active) vb:0000000060c02281, caller:(__enqueue_in_driver+0x65/0xb0)
0 [vid_cap_start_streaming], caller:(vb2_start_streaming+0x6d/0xd0)
0 [vivid_start_generating_vid_cap], caller:(vid_cap_start_streaming+0x163/0x180)
0 [__vb2_wait_for_done_vb] (unlock), caller:(__vb2_perform_fileio+0x294/0x410)
0 [vivid_thread_vid_cap] (lock), caller:(kthread+0x122/0x140)
0 [vivid_thread_vid_cap_tick], caller:(kthread+0x122/0x140)
0 [vivid_fillbuff] (fill buffer) buf:0000000031fe575c, caller:(vivid_thread_vid_cap+0x2f2/0x850)
1 [vb2_fop_read] caller:(v4l2_read+0x71/0x90)
0 [vb2_buffer_done], caller:(vivid_thread_vid_cap+0x33d/0x850)
0 [vb2_buffer_done] (add done_list) vb:0000000031fe575c, caller:(vivid_thread_vid_cap+0x33d/0x850)
0 [vivid_thread_vid_cap] (unlock), caller:(kthread+0x122/0x140)
1 [vb2_fop_read] (lock), caller:(v4l2_read+0x71/0x90)
1 [__vb2_perform_fileio], caller:(vb2_read+0x14/0x20)
1 [__vb2_get_done_vb] (remove q->done_list) vb:00000000607e3ca1, caller:(__vb2_perform_fileio+0x294/0x410)
1 [__vb2_dqbuf], caller:(vb2_core_dqbuf+0x20f/0x630)
1 [vb2_core_qbuf] (add queued_list) vb:0000000031fe575c, caller:(__vb2_perform_fileio+0x18a/0x410)
1 [__enqueue_in_driver] caller:(vb2_core_qbuf+0x1b4/0x1e0)
1 [vid_cap_buf_queue] (add vid_cap_active) vb:0000000031fe575c, caller:(__enqueue_in_driver+0x65/0xb0)
1 [vb2_fop_read] (unlock), caller:(v4l2_read+0x71/0x90)
0 [__vb2_wait_for_done_vb] (lock), caller:(__vb2_perform_fileio+0x294/0x410)
0 [__vb2_wait_for_done_vb] (unlock), caller:(__vb2_perform_fileio+0x294/0x410)
1 [vb2_fop_release], caller:(vivid_fop_release+0x8d/0x1c0)
1 [_vb2_fop_release] (lock), caller:(vb2_fop_release+0x4c/0x70)
1 [vb2_core_queue_release], caller:(_vb2_fop_release+0xd3/0xf0)
1 [__vb2_cleanup_fileio], caller:(vb2_core_queue_release+0x3b/0x70)
1 [vb2_core_streamoff], caller:(__vb2_cleanup_fileio+0x4f/0xb0)
1 [__vb2_queue_cancel], caller:(vb2_core_streamoff+0x3d/0x90)
1 [vid_cap_stop_streaming], caller:(__vb2_queue_cancel+0x4d/0x120)
1 [vivid_stop_generating_vid_cap] remove vid_cap_active vb:0000000060c02281, caller:(vid_cap_stop_streaming+0x4c/0x60)
1 [vb2_buffer_done], caller:(vivid_stop_generating_vid_cap+0x15d/0x200)
1 [vb2_buffer_done] (add done_list) vb:0000000060c02281, caller:(vivid_stop_generating_vid_cap+0x15d/0x200)
1 [vivid_stop_generating_vid_cap] remove vid_cap_active vb:0000000031fe575c, caller:(vid_cap_stop_streaming+0x4c/0x60)
1 [vb2_buffer_done], caller:(vivid_stop_generating_vid_cap+0x15d/0x200)
1 [vb2_buffer_done] (add done_list) vb:0000000031fe575c, caller:(vivid_stop_generating_vid_cap+0x15d/0x200)
1 [vivid_stop_generating_vid_cap] (unlock), caller:(vid_cap_stop_streaming+0x4c/0x60)
0 [__vb2_wait_for_done_vb] (lock), caller:(__vb2_perform_fileio+0x294/0x410)
0 [__vb2_get_done_vb] (remove q->done_list) vb:000000002d2e911c, caller:(__vb2_perform_fileio+0x294/0x410)
0 [__vb2_dqbuf], caller:(vb2_core_dqbuf+0x20f/0x630)
0 [vb2_core_qbuf] (add queued_list) vb:0000000060c02281, caller:(__vb2_perform_fileio+0x18a/0x410)
0 [__enqueue_in_driver] caller:(vb2_core_qbuf+0x1b4/0x1e0)
0 [vid_cap_buf_queue] (add vid_cap_active) vb:0000000060c02281, caller:(__enqueue_in_driver+0x65/0xb0)
0 [vb2_fop_read] (unlock), caller:(v4l2_read+0x71/0x90)
1 [vivid_stop_generating_vid_cap] (lock), caller:(vid_cap_stop_streaming+0x4c/0x60)
1 [vb2_buffer_done], caller:(__vb2_queue_cancel.cold+0x63/0x65)
1 [vb2_buffer_done] (add done_list) vb:0000000060c02281, caller:(__vb2_queue_cancel.cold+0x63/0x65)
1 [__vb2_dqbuf], caller:(__vb2_queue_cancel+0x107/0x120)
1 [__vb2_dqbuf], caller:(__vb2_queue_cancel+0x107/0x120)
1 [vb2_core_reqbufs], caller:(__vb2_cleanup_fileio+0x72/0xb0)
1 [__vb2_queue_cancel], caller:(vb2_core_reqbufs+0xbd/0x400)
1 [__vb2_dqbuf], caller:(__vb2_queue_cancel+0x107/0x120)
1 [__vb2_dqbuf], caller:(__vb2_queue_cancel+0x107/0x120)
1 [__vb2_queue_free], caller:(vb2_core_reqbufs+0xcd/0x400)
1 [__vb2_queue_free] (clean mem_priv) mem_priv:000000004bcd5f2e, caller:(vb2_core_reqbufs+0xcd/0x400)
1 [__vb2_queue_free] (clean mem_priv) mem_priv:00000000e61115ec, caller:(vb2_core_reqbufs+0xcd/0x400)
1 [__vb2_free_mem] (__vb2_buf_mem_free buffers) buffer:0000000031fe575c, caller:(vb2_core_reqbufs+0xcd/0x400)
1 [__vb2_buf_mem_free] (null to mem_priv) mem_priv:000000004bcd5f2e, caller:(__vb2_queue_free+0x295/0x2b0)
1 [__vb2_free_mem] (__vb2_buf_mem_free buffers) buffer:0000000060c02281, caller:(vb2_core_reqbufs+0xcd/0x400)
1 [__vb2_buf_mem_free] (null to mem_priv) mem_priv:00000000e61115ec, caller:(__vb2_queue_free+0x295/0x2b0)
1 [__vb2_queue_free] (kfree) buffer:0000000031fe575c, caller:(vb2_core_reqbufs+0xcd/0x400)
1 [__vb2_queue_free] (kfree) buffer:0000000060c02281, caller:(vb2_core_reqbufs+0xcd/0x400)
1 [__vb2_cleanup_fileio] (kfree), caller:(vb2_core_queue_release+0x3b/0x70)
1 [__vb2_queue_cancel], caller:(vb2_core_queue_release+0x43/0x70)
1 [__vb2_queue_free], caller:(vb2_core_queue_release+0x5b/0x70)
1 [_vb2_fop_release] (unlock), caller:(vb2_fop_release+0x4c/0x70)
0 [vb2_fop_release], caller:(vivid_fop_release+0x8d/0x1c0)
1 [vb2_fop_read] caller:(v4l2_read+0x71/0x90)
0 [_vb2_fop_release] (lock), caller:(vb2_fop_release+0x4c/0x70)
0 [_vb2_fop_release] (unlock), caller:(vb2_fop_release+0x4c/0x70)
1 [vb2_fop_read] (lock), caller:(v4l2_read+0x71/0x90)
0 [vb2_fop_read] caller:(v4l2_read+0x71/0x90)
1 [__vb2_perform_fileio], caller:(vb2_read+0x14/0x20)
1 [__vb2_init_fileio], caller:(__vb2_perform_fileio+0x373/0x410)
1 [vb2_core_reqbufs], caller:(__vb2_init_fileio+0xfe/0x2c0)
1 [__vb2_queue_cancel], caller:(vb2_core_reqbufs+0xbd/0x400)
1 [__vb2_queue_free], caller:(vb2_core_reqbufs+0xcd/0x400)
1 [__vb2_queue_alloc] (alloc buffer) vb:000000006503affd, caller:(vb2_core_reqbufs+0x197/0x400)
1 [__vb2_buf_mem_alloc] (alloc mem_priv) mem_priv:00000000e61115ec, caller:(vb2_core_reqbufs+0x197/0x400)
1 [__vb2_queue_alloc] (alloc buffer) vb:00000000786bfc16, caller:(vb2_core_reqbufs+0x197/0x400)
1 [__vb2_buf_mem_alloc] (alloc mem_priv) mem_priv:000000006a469467, caller:(vb2_core_reqbufs+0x197/0x400)
1 [vb2_core_qbuf] (add queued_list) vb:000000006503affd, caller:(__vb2_init_fileio+0x253/0x2c0)
1 [vb2_core_qbuf] (add queued_list) vb:00000000786bfc16, caller:(__vb2_init_fileio+0x253/0x2c0)
1 [__enqueue_in_driver] caller:(vb2_start_streaming+0x33/0xd0)
1 [vid_cap_buf_queue] (add vid_cap_active) vb:000000006503affd, caller:(__enqueue_in_driver+0x65/0xb0)
1 [__enqueue_in_driver] caller:(vb2_start_streaming+0x33/0xd0)
1 [vid_cap_buf_queue] (add vid_cap_active) vb:00000000786bfc16, caller:(__enqueue_in_driver+0x65/0xb0)
1 [vid_cap_start_streaming], caller:(vb2_start_streaming+0x6d/0xd0)
1 [vivid_start_generating_vid_cap], caller:(vid_cap_start_streaming+0x163/0x180)
1 [__vb2_wait_for_done_vb] (unlock), caller:(__vb2_perform_fileio+0x294/0x410)
0 [vb2_fop_read] (lock), caller:(v4l2_read+0x71/0x90)
0 [__vb2_perform_fileio], caller:(vb2_read+0x14/0x20)
0 [__vb2_wait_for_done_vb] (unlock), caller:(__vb2_perform_fileio+0x294/0x410)
1 [vivid_thread_vid_cap] (lock), caller:(kthread+0x122/0x140)
1 [vivid_thread_vid_cap_tick], caller:(kthread+0x122/0x140)
1 [vivid_fillbuff] (fill buffer) buf:0000000060c02281, caller:(vivid_thread_vid_cap+0x2f2/0x850)
아래는 위의 출력 결과를 조금 더 가독성 높게 정리한 결과이다. CPU 0과 1 두 개가 동시에 open(), close()를 실행하면서 수행될 때 호출되는 함수들의 call graph 를 나타낸 것이다.
CPU 0 (READ)
vb2_fop_read (lock)
vb2_read
__vb2_perform_fileio
__vb2_init_fileio
vb2_core_reqbufs
__vb2_queue_alloc (alloc buffer) vb:0000000031fe575c
__vb2_buf_mem_alloc (alloc mem_priv) mem_priv:000000004bcd5f2e
__vb2_queue_alloc (alloc buffer) vb:0000000060c02281
__vb2_buf_mem_alloc (alloc mem_priv) mem_priv:00000000e61115ec
vb2_core_qbuf (add queued_list) vb:0000000031fe575c
vb2_core_qbuf (add queued_list) vb:0000000060c02281
vb2_core_streamon
vb2_start_streaming
__enqueue_in_driver
vid_cap_buf_queue (add vid_cap_active) vb:0000000031fe575c
__enqueue_in_driver
vid_cap_buf_queue (add vid_cap_active) vb:0000000060c02281
vid_cap_start_streaming
vivid_start_generating_vid_cap
__vb2_wait_for_done_vb (unlock)
vivid_thread_vid_cap (lock)
vivid_thread_vid_cap_tick
vivid_fillbuff (fill buffer) buf:0000000031fe575c
vb2_buffer_done (add done_list) vb:0000000031fe575c
vivid_thread_vid_cap (unlock)
CPU 1 (READ)
vb2_fop_read (lock)
vb2_read
__vb2_perform_fileio
__vb2_get_done_vb (remove q->done_list) vb:00000000607e3ca1
vb2_core_dqbuf
__vb2_dqbuf
vb2_core_qbuf (add queued_list) vb:0000000031fe575c
__enqueue_in_driver
vid_cap_buf_queue (add vid_cap_active) vb:0000000031fe575c
vb2_fop_read (unlock)
CPU 0 (READ)
__vb2_wait_for_done_vb (lock)
__vb2_wait_for_done_vb (unlock)
CPU 1 (CLOSE)
vb2_fop_release
_vb2_fop_release (lock)
vb2_core_queue_release
__vb2_cleanup_fileio
vb2_core_streamoff
__vb2_queue_cancel
vid_cap_stop_streaming
vivid_stop_generating_vid_cap (remove vid_cap_active) vb:0000000060c02281
vb2_buffer_done (add done_list) vb:0000000060c02281
vivid_stop_generating_vid_cap (remove vid_cap_active) vb:0000000031fe575c
vb2_buffer_done (add done_list) vb:0000000031fe575c
vivid_stop_generating_vid_cap (unlock)
CPU 0 (READ)
__vb2_wait_for_done_vb (lock)
__vb2_get_done_vb (remove q->done_list) vb:000000002d2e911c
vb2_core_dqbuf
__vb2_dqbuf
vb2_core_qbuf (add queued_list) vb:0000000060c02281
__enqueue_in_driver
vid_cap_buf_queue (add vid_cap_active) vb:0000000060c02281
vb2_fop_read (unlock)
CPU 1 (CLOSE)
vivid_stop_generating_vid_cap (lock)
vb2_buffer_done (add done_list) vb:0000000060c02281
__vb2_dqbuf
__vb2_dqbuf
vb2_core_reqbufs
__vb2_queue_cancel
__vb2_dqbuf
__vb2_dqbuf
__vb2_queue_free (clean mem_priv) vb->planes[0].mem_priv:000000004bcd5f2e
__vb2_queue_free (clean mem_priv) vb->planes[0].mem_priv:00000000e61115ec
__vb2_free_mem (__vb2_buf_mem_free buffers) buffer:0000000031fe575c
__vb2_buf_mem_free (null to mem_priv) mem_priv:000000004bcd5f2e
__vb2_free_mem (__vb2_buf_mem_free buffers) buffer:0000000060c02281
__vb2_buf_mem_free (null to mem_priv) mem_priv:00000000e61115ec
__vb2_queue_free (kfree) buffer:0000000031fe575c
__vb2_queue_free (kfree) buffer:0000000060c02281
__vb2_cleanup_fileio (kfree)
__vb2_queue_cancel
__vb2_queue_free
_vb2_fop_release (unlock)
CPU 0 (CLOSE)
vb2_fop_release
_vb2_fop_release (lock)
_vb2_fop_release (unlock)
CPU 1 (READ)
vb2_fop_read (lock)
vb2_read
__vb2_perform_fileio
__vb2_init_fileio
vb2_core_reqbufs
__vb2_queue_cancel
__vb2_queue_free
__vb2_queue_alloc (alloc buffer) vb:000000006503affd
__vb2_buf_mem_alloc (alloc mem_priv) mem_priv:00000000e61115ec
__vb2_queue_alloc (alloc buffer) vb:00000000786bfc16
__vb2_buf_mem_alloc (alloc mem_priv) mem_priv:000000006a469467
vb2_core_qbuf (add queued_list) vb:000000006503affd
vb2_core_qbuf (add queued_list) vb:00000000786bfc16
vb2_core_streamon
vb2_start_streaming
__enqueue_in_driver
vid_cap_buf_queue (add vid_cap_active) vb:000000006503affd
__enqueue_in_driver
vid_cap_buf_queue (add vid_cap_active) vb:00000000786bfc16
vid_cap_start_streaming
vivid_start_generating_vid_cap
__vb2_wait_for_done_vb (unlock)
CPU 0 (READ)
vb2_fop_read (lock)
vb2_read
__vb2_perform_fileio
__vb2_wait_for_done_vb (unlock)
CPU 1 (READ)
vivid_thread_vid_cap (lock)
vivid_thread_vid_cap_tick
vivid_fillbuff (fill buffer) buf:0000000060c02281
crash는 136라인의 0x0000000060c02281 버퍼에 대해 vivid_fillbuff() 함수에서 어떤 작업을 하면서 발생한다. 이 buf 주소를 잘 살펴보면, 해당 주소는 89 라인의 __vb2_queue_free() 함수에서 kfree() 이후 NULL 로 초기화되는 주소이다.
해당 buf의 주소인 0x0000000060c02281 에 대해 다시 정리해보면 아래와 같다.
- CPU 0 : __vb2_queue_alloc() 함수에서 buf 할당 (9라인)
- CPU 0 : vid_cap_buf_queue() 함수에서 vid_cap_active 에 추가 (18 라인)
- CPU 1 : vivid_stop_generating_vid_cap() 함수에서 vid_cap_active 에서 제거 (55 라인)
- CPU 0 : vid_cap_buf_queue() 함수에서 vid_cap_active 에 추가 (69 라인)
- __vb2_queue_free() 함수에서 kfree 후 NULL 로 초기화 (89 라인)
- vivid_fillbuff() 함수에서 buf 주소에 접근 시 NULL pointer dereference 로 crash 발생
Exploit 분석
현재 공개되어 있는 익스플로잇 코드는 없으나 참고할 수 있을만한 코드를 아래에서 다운로드 할 수 있다.
https://github.com/Limesss/cve-2019-18683
Reference
CVE-2019-18683: Exploiting a Linux kernel vulnerability in the V4L2 subsystem